ERC20合约项目实战(下)

956 阅读3分钟

一.实验内容

  1. 充值时的币种使用LETH,领取奖励和提取本金时的币种为ETH

  2. 每充值5个LETH,经过一个区块高度后,可以领取1个ETH的利息

  3. 在构造函数中设置活动起止区块高度,只有在起止区块高度内的充值才可以计算利息。起止区块高度之间的区块也称为活动期。活动期开始前的区块以及活动期结束后的区块,不计算利息

  4. 编写modifier,使得充值操作只能在活动期进行

  5. 在活动期内,用户可以随时充值、提取利息、提取本金,利息不参与复投

二. 参考算法

  1. 使用depositAmount记录用户在活动期内的存款数量

  2. 对每个用户记录一个checkPoint,在每次用户存款或者提取本金时更新;checkPoint = depositAmount *currentBlockNumber

  3. 对每个用户记录一个calculatedReward,在每次用户存款/提取本金时更新

  4. 对每个用户记录一个claimedReward,在每次用户提取利息时记录

  5. 用户的待领取利息 = calculatedReward - claimedReward +从上次操作到当前区块高度新产生的利息

  6. 用户在提取本金时,先返还之前的所有利息

  7. 所有对uint256的算数运算,使用SafeMath

三.核心代码

此合约中导入了上次ERC20合约项目实践(上)的合约以及safemath合约。

  • 映射
    mapping(address => uint256) public depositAmount; // 用户的存款总量
    mapping(address => uint256) public checkPoint; // 每次存款或提取本金时,更新这个值
    mapping(address => uint256) public calculatedReward; // 已经计算的利息
    mapping(address => uint256) public claimedReward; // 已经提取的利息
  • 事件
    event Deposit(address indexed sender, uint256 amount); //地址=> 存款(存钱)
    event Claim(address indexed sender, address recipient, uint256 amount);
    event Withdraw(address indexed sender, uint256 amount);//地址=> 钱(取钱)
  • 构造函数

在构造函数中设置活动起止区块高度,只有在起止区块高度内的充值才可以计算利息。起止区块高度之间的区块也称为活动期。活动期开始前的区块以及活动期结束后的区块,不计算利息

    constructor(address payable _wethAddress, uint256 _period) {
        // period 为从当前开始,延续多少个区块
        startBlock = block.number;
        endBlock = block.number + _period + 1;
        _weth = _wethAddress;
    }
  • 函数修饰器

编写modifier,使得充值操作只能在活动期进行

    // 修饰符,充值时只允许在设定的区块范围内
    modifier onlyValidTime() {
        require(block.number < endBlock);
        _;
    }
  • 主要方法

1.存钱到合约

步骤:

1)更新存钱之前的利息

2)调用LETH合约的deposit方法存在本地账户中,在此过程中,将ETH转换为LETH

3)将存在账户中的钱转到合约

4)更新checkpoint

    // 存钱到合约
    function deposit(uint256 _amount) public onlyValidTime returns (bool) {
        // 此处编写业务逻辑
        if(depositAmount[msg.sender] != 0){
             calculatedReward[msg.sender] = (block.number - checkPoint[msg.sender].div(depositAmount[msg.sender])).mul(depositAmount[msg.sender].div(rewardBase)).add(calculatedReward[msg.sender]);
        }
        WETH(_weth).deposit();
        WETH(_weth).transferFrom(msg.sender,address(this),_amount);
        depositAmount[msg.sender] = depositAmount[msg.sender].add(_amount);
        checkPoint[msg.sender] = depositAmount[msg.sender].mul(block.number);

        emit Deposit(msg.sender, _amount);
        return true;
    }

2.查询利息

利息=已经计算的利息+存钱之后的利息=calculatesReward+(block.number-原来的block.number)*余额/区块

 // 查询利息
    function getPendingReward(address _account)public view returns (uint256 pendingReward) {
        // 此处编写业务逻辑
        if(checkPoint[_account] == 0){
            return 0;
        }
        pendingReward = calculatedReward[_account].add(block.number.sub(checkPoint[_account].div(depositAmount[_account]))).mul(depositAmount[_account]).div(rewardBase);        
    }

3.领取利息

1)算出目前账户拥有的利息

2)合约将利息转给账户(合约就相当于银行)

最重要的步骤就是以上两个,但是在该方法里面还要去维护其他属性变量。如更新calculateReward(已经计算的利息),claimedReward(已经提取的利息)

    function claimReward(address payable _toAddress) public returns (bool) {
        uint pendingReward = getPendingReward(msg.sender);
        //总的利息=账户目前的利息+已经提取的利息
        calculatedReward[msg.sender] = pendingReward.add(claimedReward[msg.sender]);
        WETH(_weth).withdrawTo(_toAddress,pendingReward);
        // 对每个用户记录一个claimedReward,在每次用户提取利息时记录;
        claimedReward[msg.sender] = claimedReward[msg.sender].add(pendingReward);
        emit Claim(msg.sender, _toAddress, pendingReward);
        return true;
    }

4.提取一定数量的本金

  1. 在提取本金之前,需要返还之前所有的利息
  2. 账户的余额大于提取的金额
  3. 合约将本金转给账户
  4. 更新checkpoint(因为depositAmount在变化)

提问:为什么要给msg.senderpayable

    function withdraw(uint256 _amount) public returns (bool) {
        bool flag = claimReward(payable(msg.sender));
        require(flag,"failed claimReward");
        require(depositAmount[msg.sender] >= _amount);
        WETH(_weth).withdrawTo(payable(msg.sender),_amount);
        checkPoint[msg.sender] = depositAmount[msg.sender].mul(block.number);

        emit Withdraw(msg.sender, _amount);
        return true;
    }

5.增加区块高度,获取区块高度

    // 用于在Remix本地环境中增加区块高度
    uint256 counter;

    function addBlockNumber() public {
        counter++;
    }

    // 获取当前区块高度
    function getBlockNumber() public view returns (uint256) {
        return block.number;
    }
}

四.总结

本次实验主要是涉及单个用户和平台的功能,如:用户存钱,取钱,查询利息;平台计算利息,保存钱等。是一个简单的defi项目的功能。实验目的主要是理解现实defi项目的业务,合约该如何设计。只学这么一点是不够的,可以去找复杂的defi项目合约学习。