July 4, 2022

Как написать смарт-контракт для стейкинга токена

🔹Cоздадим токен Sample ERC-20 Apple (APL).
🔹Период стейкинга в примере будет 3 месяца.
🔹APR будет 128%, то есть по истечению 3х месяцев реварды составят 32%.


Создание токена APL

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract AppleToken is ERC20 {

    uint8 constant _decimals = 18;
    uint256 constant _totalSupply = 100 * (10**6) * 10**_decimals;  // 100m tokens for distribution

    constructor() ERC20("Apple", "APL") {        
        _mint(msg.sender, _totalSupply);
    }
}

Мы создадим контракт StakeAPL с двумя важными методами:
🔸stakeToken
🔸claimReward

Метод StakeToken: Пользователь передает сумму в параметре (в формате Wei). Эта функция проверяет все допустимые условия, затем переводит сумму стейкинга в контракт, сохраняет эту запись в структуре и выдает событие Staked.


require(stakeAmount >0, “Stake amount should be correct”); 

require(block.timestamp < planExpired , “Plan Expired”); 
require(addressStaked[_msgSender()] == false, “You already participated”); 

require(aplToken.balanceOf(_msgSender()) >= stakeAmount, “Insufficient Balance”);

aplToken.transferFrom(_msgSender(), address(this), stakeAmount);

stakeInfos[_msgSender()] = StakeInfo({      
      startTS: block.timestamp,          
      endTS: block.timestamp + planDuration,            
     amount: stakeAmount,            
     claimed: 0 });

emit Staked(_msgSender(), stakeAmount);

Метод claimReward: Пользователь получает вознаграждение после завершения срока cтейкинга. Сначала мы получаем сумму, которую юзер застейкал, затем рассчитываем процентное вознаграждение и прибавляем его. Переводим эту сумму на адрес пользователя и выдаем событие Claimed.

uint256 stakeAmount = stakeInfos[_msgSender()].amount; 

uint256 totalTokens = stakeAmount + (stakeAmount * interestRate / 100); 

stakeInfos[_msgSender()].claimed == totalTokens; 

aplToken.transfer(_msgSender(), totalTokens); 

emit Claimed(_msgSender(), totalTokens);

Как работает процесс стейкинга?

  • У нас уже есть контракт ERC-20 (токен APL), и адрес этого контракта передается в конструктор StakeAPL во время развертывания.
  • Переведите несколько токенов APL на контракт StakeAPL.
    Второй пользователь (владелец токена APL) вызывает метод APL Token approve и дает одобрение адресу контракта StakeAPL с указанием суммы стейкинга.
  • Вызовите метод stakeToken контракта Staking с суммой в качестве параметра, и все готово. После истечения срока действия стейкинга вы можете вызвать метод claimReward, чтобы получить сумму с процентами.

Развертывание в тестовой сети Ринкеби

Вы можете скопировать приведенный ниже код и развернуть его с помощью remix. Как работать с remix, можно прочитать в этой статье.

// SPDX-License-Identifier: GPL-3.0-only

pragma solidity ^0.8.11;

import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Context.sol";

interface Token {
    function transfer(address recipient, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (uint256);    
}

contract StakeAPL is Pausable, Ownable, ReentrancyGuard {

    Token aplToken;

    // 30 Days (30 * 24 * 60 * 60)
    uint256 public planDuration = 2592000;

    // 180 Days (180 * 24 * 60 * 60)
    uint256 _planExpired = 15552000;

    uint8 public interestRate = 32;
    uint256 public planExpired;
    uint8 public totalStakers;

    struct StakeInfo {        
        uint256 startTS;
        uint256 endTS;        
        uint256 amount; 
        uint256 claimed;       
    }
    
    event Staked(address indexed from, uint256 amount);
    event Claimed(address indexed from, uint256 amount);
    
    mapping(address => StakeInfo) public stakeInfos;
    mapping(address => bool) public addressStaked;


    constructor(Token _tokenAddress) {
        require(address(_tokenAddress) != address(0),"Token Address cannot be address 0");                
        aplToken = _tokenAddress;        
        planExpired = block.timestamp + _planExpired;
        totalStakers = 0;
    }    

    function transferToken(address to,uint256 amount) external onlyOwner{
        require(aplToken.transfer(to, amount), "Token transfer failed!");  
    }

    function claimReward() external returns (bool){
        require(addressStaked[_msgSender()] == true, "You are not participated");
        require(stakeInfos[_msgSender()].endTS < block.timestamp, "Stake Time is not over yet");
        require(stakeInfos[_msgSender()].claimed == 0, "Already claimed");

        uint256 stakeAmount = stakeInfos[_msgSender()].amount;
        uint256 totalTokens = stakeAmount + (stakeAmount * interestRate / 100);
        stakeInfos[_msgSender()].claimed == totalTokens;
        aplToken.transfer(_msgSender(), totalTokens);

        emit Claimed(_msgSender(), totalTokens);

        return true;
    }

    function getTokenExpiry() external view returns (uint256) {
        require(addressStaked[_msgSender()] == true, "You are not participated");
        return stakeInfos[_msgSender()].endTS;
    }

    function stakeToken(uint256 stakeAmount) external payable whenNotPaused {
        require(stakeAmount >0, "Stake amount should be correct");
        require(block.timestamp < planExpired , "Plan Expired");
        require(addressStaked[_msgSender()] == false, "You already participated");
        require(aplToken.balanceOf(_msgSender()) >= stakeAmount, "Insufficient Balance");
        
           aplToken.transferFrom(_msgSender(), address(this), stakeAmount);
            totalStakers++;
            addressStaked[_msgSender()] = true;

            stakeInfos[_msgSender()] = StakeInfo({                
                startTS: block.timestamp,
                endTS: block.timestamp + planDuration,
                amount: stakeAmount,
                claimed: 0
            });
        
        emit Staked(_msgSender(), stakeAmount);
    }    


    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }
}