Solidity Staking Mechanics
Staking is locking tokens in a contract to earn rewards over time. Rewards grow with the time the tokens stay locked, at a fixed rate per second.
Staking records the locked tokens
Staking 100 tokens records a stake of 100. The staking contract holds those tokens.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ToyToken {
string public name;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(string memory tokenName, address holder, uint256 supply) {
name = tokenName;
totalSupply = supply;
balanceOf[holder] = supply;
}
function transfer(address to, uint256 amount) public returns (bool) {
require(to != address(0), "zero address");
require(balanceOf[msg.sender] >= amount, "low balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(to != address(0), "zero address");
require(balanceOf[from] >= amount, "low balance");
require(allowance[from][msg.sender] >= amount, "low allowance");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}
contract Staking {
ToyToken public token;
mapping(address => uint256) public staked;
mapping(address => uint256) public pending;
mapping(address => uint256) public lastUpdate;
constructor(address tokenAddr) {
token = ToyToken(tokenAddr);
}
function earned(address user) public view returns (uint256) {
if (staked[user] == 0) {
return 0;
}
return pending[user] + staked[user] * (block.timestamp - lastUpdate[user]) / 100;
}
function _update(address user) internal {
pending[user] = earned(user);
lastUpdate[user] = block.timestamp;
}
function stake(uint256 amount) public {
require(amount > 0, "empty stake");
_update(msg.sender);
token.transferFrom(msg.sender, address(this), amount);
staked[msg.sender] += amount;
}
function withdraw() public {
_update(msg.sender);
uint256 amount = staked[msg.sender];
uint256 reward = pending[msg.sender];
require(amount > 0, "nothing staked");
staked[msg.sender] = 0;
pending[msg.sender] = 0;
token.transfer(msg.sender, amount + reward);
}
}Rewards grow over a fixed time gap
After 100 seconds pass, 100 staked tokens have earned 100 reward tokens. The rate is fixed, so the reward is exact.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ToyToken {
string public name;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(string memory tokenName, address holder, uint256 supply) {
name = tokenName;
totalSupply = supply;
balanceOf[holder] = supply;
}
function transfer(address to, uint256 amount) public returns (bool) {
require(to != address(0), "zero address");
require(balanceOf[msg.sender] >= amount, "low balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(to != address(0), "zero address");
require(balanceOf[from] >= amount, "low balance");
require(allowance[from][msg.sender] >= amount, "low allowance");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}
contract Staking {
ToyToken public token;
mapping(address => uint256) public staked;
mapping(address => uint256) public pending;
mapping(address => uint256) public lastUpdate;
constructor(address tokenAddr) {
token = ToyToken(tokenAddr);
}
function earned(address user) public view returns (uint256) {
if (staked[user] == 0) {
return 0;
}
return pending[user] + staked[user] * (block.timestamp - lastUpdate[user]) / 100;
}
function _update(address user) internal {
pending[user] = earned(user);
lastUpdate[user] = block.timestamp;
}
function stake(uint256 amount) public {
require(amount > 0, "empty stake");
_update(msg.sender);
token.transferFrom(msg.sender, address(this), amount);
staked[msg.sender] += amount;
}
function withdraw() public {
_update(msg.sender);
uint256 amount = staked[msg.sender];
uint256 reward = pending[msg.sender];
require(amount > 0, "nothing staked");
staked[msg.sender] = 0;
pending[msg.sender] = 0;
token.transfer(msg.sender, amount + reward);
}
}Withdrawing pays the stake plus its reward
Withdrawing after 100 seconds pays 100 staked tokens plus 100 reward tokens. The stake reads 0 after the withdrawal.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ToyToken {
string public name;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(string memory tokenName, address holder, uint256 supply) {
name = tokenName;
totalSupply = supply;
balanceOf[holder] = supply;
}
function transfer(address to, uint256 amount) public returns (bool) {
require(to != address(0), "zero address");
require(balanceOf[msg.sender] >= amount, "low balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(to != address(0), "zero address");
require(balanceOf[from] >= amount, "low balance");
require(allowance[from][msg.sender] >= amount, "low allowance");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}
contract Staking {
ToyToken public token;
mapping(address => uint256) public staked;
mapping(address => uint256) public pending;
mapping(address => uint256) public lastUpdate;
constructor(address tokenAddr) {
token = ToyToken(tokenAddr);
}
function earned(address user) public view returns (uint256) {
if (staked[user] == 0) {
return 0;
}
return pending[user] + staked[user] * (block.timestamp - lastUpdate[user]) / 100;
}
function _update(address user) internal {
pending[user] = earned(user);
lastUpdate[user] = block.timestamp;
}
function stake(uint256 amount) public {
require(amount > 0, "empty stake");
_update(msg.sender);
token.transferFrom(msg.sender, address(this), amount);
staked[msg.sender] += amount;
}
function withdraw() public {
_update(msg.sender);
uint256 amount = staked[msg.sender];
uint256 reward = pending[msg.sender];
require(amount > 0, "nothing staked");
staked[msg.sender] = 0;
pending[msg.sender] = 0;
token.transfer(msg.sender, amount + reward);
}
}Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ToyToken {
string public name;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(string memory tokenName, address holder, uint256 supply) {
name = tokenName;
totalSupply = supply;
balanceOf[holder] = supply;
}
function transfer(address to, uint256 amount) public returns (bool) {
require(to != address(0), "zero address");
require(balanceOf[msg.sender] >= amount, "low balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(to != address(0), "zero address");
require(balanceOf[from] >= amount, "low balance");
require(allowance[from][msg.sender] >= amount, "low allowance");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}
contract Staking {
ToyToken public token;
mapping(address => uint256) public staked;
mapping(address => uint256) public pending;
mapping(address => uint256) public lastUpdate;
constructor(address tokenAddr) {
token = ToyToken(tokenAddr);
}
function earned(address user) public view returns (uint256) {
if (staked[user] == 0) {
return 0;
}
return pending[user] + staked[user] * (block.timestamp - lastUpdate[user]) / 100;
}
function _update(address user) internal {
pending[user] = earned(user);
lastUpdate[user] = block.timestamp;
}
function stake(uint256 amount) public {
require(amount > 0, "empty stake");
_update(msg.sender);
token.transferFrom(msg.sender, address(this), amount);
staked[msg.sender] += amount;
}
function withdraw() public {
_update(msg.sender);
uint256 amount = staked[msg.sender];
uint256 reward = pending[msg.sender];
require(amount > 0, "nothing staked");
staked[msg.sender] = 0;
pending[msg.sender] = 0;
token.transfer(msg.sender, amount + reward);
}
}Key points
- Staking: locking tokens in a contract for a time.
- Reward rate: one reward token grows for every 100 staked tokens each second.
- Accrual: rewards build up while tokens stay staked.
- Withdrawal: leaving the stake pays back the stake plus its rewards.
- Reward fund: the staking contract must already hold reward tokens to pay out.
In this example, Ana stakes 200 tokens and waits 50 seconds at the fixed rate. She withdraws 300 tokens, which is her stake plus 100 reward tokens.
Note
this staking contract pays rewards in the same toy token that is staked.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
How many reward tokens do 200 staked tokens earn in 50 seconds at this rate?