🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity Project: Staking Token

A staking token project is a token plus a pool where holders lock tokens and earn rewards over time. The pool pays rewards at a fixed rate for every second tokens stay locked.

Staking locks tokens in the pool

The holder approves the pool and stakes tokens. The holder balance falls and the pool balance rises by the same amount.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract StakeToken {
    string public name = "Stake Token";
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    address public owner;

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Approval(address indexed owner, address indexed spender, uint256 amount);

    constructor() {
        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner mints");
        require(to != address(0), "no zero address");
        totalSupply += amount;
        balanceOf[to] += amount;
        emit Transfer(address(0), to, amount);
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "no zero address");
        require(balanceOf[msg.sender] >= amount, "not enough 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;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(allowance[from][msg.sender] >= amount, "allowance too low");
        require(balanceOf[from] >= amount, "not enough balance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

contract StakingPool {
    StakeToken public token;
    mapping(address => uint256) public staked;

    constructor(StakeToken tokenAddress) {
        token = tokenAddress;
    }

    function stake(uint256 amount) public {
        require(amount > 0, "stake more than zero");
        token.transferFrom(msg.sender, address(this), amount);
        staked[msg.sender] += amount;
    }
}

Rewards grow with warped time

Staking 100 tokens for 10 seconds earns 10 reward tokens at the fixed rate. Warping the clock moves time forward to a fixed timestamp.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract StakeToken {
    string public name = "Stake Token";
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    address public owner;

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Approval(address indexed owner, address indexed spender, uint256 amount);

    constructor() {
        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner mints");
        require(to != address(0), "no zero address");
        totalSupply += amount;
        balanceOf[to] += amount;
        emit Transfer(address(0), to, amount);
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "no zero address");
        require(balanceOf[msg.sender] >= amount, "not enough 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;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(allowance[from][msg.sender] >= amount, "allowance too low");
        require(balanceOf[from] >= amount, "not enough balance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

contract StakingPool {
    StakeToken public token;
    mapping(address => uint256) public staked;
    mapping(address => uint256) public rewardOwed;
    mapping(address => uint256) public lastUpdate;

    constructor(StakeToken tokenAddress) {
        token = tokenAddress;
    }

    function updateReward(address user) internal {
        rewardOwed[user] += staked[user] * (block.timestamp - lastUpdate[user]) / 100;
        lastUpdate[user] = block.timestamp;
    }

    function stake(uint256 amount) public {
        require(amount > 0, "stake more than zero");
        updateReward(msg.sender);
        token.transferFrom(msg.sender, address(this), amount);
        staked[msg.sender] += amount;
    }

    function pendingReward(address user) public view returns (uint256) {
        return rewardOwed[user] + staked[user] * (block.timestamp - lastUpdate[user]) / 100;
    }

    function claim() public {
        updateReward(msg.sender);
        uint256 reward = rewardOwed[msg.sender];
        require(reward > 0, "no reward to claim");
        rewardOwed[msg.sender] = 0;
        token.transfer(msg.sender, reward);
    }

    function withdraw(uint256 amount) public {
        require(amount <= staked[msg.sender], "withdraw more than staked");
        updateReward(msg.sender);
        staked[msg.sender] -= amount;
        token.transfer(msg.sender, amount);
        uint256 reward = rewardOwed[msg.sender];
        rewardOwed[msg.sender] = 0;
        token.transfer(msg.sender, reward);
    }
}

Withdrawing pays stake plus reward

Withdrawing after 10 seconds returns the 100 staked tokens and 10 reward tokens. The balance rises by 110 in total.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract StakeToken {
    string public name = "Stake Token";
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    address public owner;

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Approval(address indexed owner, address indexed spender, uint256 amount);

    constructor() {
        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner mints");
        require(to != address(0), "no zero address");
        totalSupply += amount;
        balanceOf[to] += amount;
        emit Transfer(address(0), to, amount);
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "no zero address");
        require(balanceOf[msg.sender] >= amount, "not enough 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;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(allowance[from][msg.sender] >= amount, "allowance too low");
        require(balanceOf[from] >= amount, "not enough balance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

contract StakingPool {
    StakeToken public token;
    mapping(address => uint256) public staked;
    mapping(address => uint256) public rewardOwed;
    mapping(address => uint256) public lastUpdate;

    constructor(StakeToken tokenAddress) {
        token = tokenAddress;
    }

    function updateReward(address user) internal {
        rewardOwed[user] += staked[user] * (block.timestamp - lastUpdate[user]) / 100;
        lastUpdate[user] = block.timestamp;
    }

    function stake(uint256 amount) public {
        require(amount > 0, "stake more than zero");
        updateReward(msg.sender);
        token.transferFrom(msg.sender, address(this), amount);
        staked[msg.sender] += amount;
    }

    function pendingReward(address user) public view returns (uint256) {
        return rewardOwed[user] + staked[user] * (block.timestamp - lastUpdate[user]) / 100;
    }

    function claim() public {
        updateReward(msg.sender);
        uint256 reward = rewardOwed[msg.sender];
        require(reward > 0, "no reward to claim");
        rewardOwed[msg.sender] = 0;
        token.transfer(msg.sender, reward);
    }

    function withdraw(uint256 amount) public {
        require(amount <= staked[msg.sender], "withdraw more than staked");
        updateReward(msg.sender);
        staked[msg.sender] -= amount;
        token.transfer(msg.sender, amount);
        uint256 reward = rewardOwed[msg.sender];
        rewardOwed[msg.sender] = 0;
        token.transfer(msg.sender, reward);
    }
}

Claiming pays only the reward

Claiming after 10 seconds pays 10 reward tokens and leaves the stake locked. The staked amount stays at 100.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract StakeToken {
    string public name = "Stake Token";
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    address public owner;

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Approval(address indexed owner, address indexed spender, uint256 amount);

    constructor() {
        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner mints");
        require(to != address(0), "no zero address");
        totalSupply += amount;
        balanceOf[to] += amount;
        emit Transfer(address(0), to, amount);
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "no zero address");
        require(balanceOf[msg.sender] >= amount, "not enough 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;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(allowance[from][msg.sender] >= amount, "allowance too low");
        require(balanceOf[from] >= amount, "not enough balance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

contract StakingPool {
    StakeToken public token;
    mapping(address => uint256) public staked;
    mapping(address => uint256) public rewardOwed;
    mapping(address => uint256) public lastUpdate;

    constructor(StakeToken tokenAddress) {
        token = tokenAddress;
    }

    function updateReward(address user) internal {
        rewardOwed[user] += staked[user] * (block.timestamp - lastUpdate[user]) / 100;
        lastUpdate[user] = block.timestamp;
    }

    function stake(uint256 amount) public {
        require(amount > 0, "stake more than zero");
        updateReward(msg.sender);
        token.transferFrom(msg.sender, address(this), amount);
        staked[msg.sender] += amount;
    }

    function pendingReward(address user) public view returns (uint256) {
        return rewardOwed[user] + staked[user] * (block.timestamp - lastUpdate[user]) / 100;
    }

    function claim() public {
        updateReward(msg.sender);
        uint256 reward = rewardOwed[msg.sender];
        require(reward > 0, "no reward to claim");
        rewardOwed[msg.sender] = 0;
        token.transfer(msg.sender, reward);
    }

    function withdraw(uint256 amount) public {
        require(amount <= staked[msg.sender], "withdraw more than staked");
        updateReward(msg.sender);
        staked[msg.sender] -= amount;
        token.transfer(msg.sender, amount);
        uint256 reward = rewardOwed[msg.sender];
        rewardOwed[msg.sender] = 0;
        token.transfer(msg.sender, reward);
    }
}

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract StakeToken {
    string public name = "Stake Token";
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    address public owner;

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Approval(address indexed owner, address indexed spender, uint256 amount);

    constructor() {
        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner mints");
        require(to != address(0), "no zero address");
        totalSupply += amount;
        balanceOf[to] += amount;
        emit Transfer(address(0), to, amount);
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "no zero address");
        require(balanceOf[msg.sender] >= amount, "not enough 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;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(allowance[from][msg.sender] >= amount, "allowance too low");
        require(balanceOf[from] >= amount, "not enough balance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

contract StakingPool {
    StakeToken public token;
    mapping(address => uint256) public staked;
    mapping(address => uint256) public rewardOwed;
    mapping(address => uint256) public lastUpdate;

    constructor(StakeToken tokenAddress) {
        token = tokenAddress;
    }

    function updateReward(address user) internal {
        rewardOwed[user] += staked[user] * (block.timestamp - lastUpdate[user]) / 100;
        lastUpdate[user] = block.timestamp;
    }

    function stake(uint256 amount) public {
        require(amount > 0, "stake more than zero");
        updateReward(msg.sender);
        token.transferFrom(msg.sender, address(this), amount);
        staked[msg.sender] += amount;
    }

    function pendingReward(address user) public view returns (uint256) {
        return rewardOwed[user] + staked[user] * (block.timestamp - lastUpdate[user]) / 100;
    }

    function claim() public {
        updateReward(msg.sender);
        uint256 reward = rewardOwed[msg.sender];
        require(reward > 0, "no reward to claim");
        rewardOwed[msg.sender] = 0;
        token.transfer(msg.sender, reward);
    }

    function withdraw(uint256 amount) public {
        require(amount <= staked[msg.sender], "withdraw more than staked");
        updateReward(msg.sender);
        staked[msg.sender] -= amount;
        token.transfer(msg.sender, amount);
        uint256 reward = rewardOwed[msg.sender];
        rewardOwed[msg.sender] = 0;
        token.transfer(msg.sender, reward);
    }
}

Key points

  • Staking: locking tokens in a pool for a time instead of holding them freely.
  • Stake: the amount a holder has locked in the pool.
  • Fixed rate: rewards grow by the same amount per second for every token staked.
  • Reward: tokens earned for time locked, paid on top of the returned stake.
  • Claim: taking earned rewards while the stake stays locked.
  • Withdraw: taking the stake back, which also pays any reward earned so far.

In this example, a token and a staking pool work together, with the pool tracking each holder stake, reward owed, and last update time. Two holders stake different amounts and earn rewards in proportion, and claiming then withdrawing pays the reward for each period plus the stake itself.

Note

the toy pool pays rewards from tokens set aside for it, so it cannot pay more than it holds.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

A holder stakes 100 tokens, waits 10 seconds, then withdraws everything. How many tokens does the holder receive in total?