线性释放

本章讲解在 Solidity 中,什么是线性释放,以及线性释放智能合约的原理和实现。

视频Bilibili  |  Youtube
官网binschool.app
推特@BinSchool    DiscordBinDAO   微信:bkra50 

线性释放,英文名称为 “Vesting”,是指代币按照一定速度在一段时间内匀速释放的过程,这种机制通常用于激励机制。

线性释放,既可以限制被激励人员在短时间内大量抛售代币,造成代币价格的不稳定,又可以保证他们具有长期的动力。

我们举个线性释放的使用场景,来帮助您理解线性释放的机制。比如:

某项目方为了激励团队成员,为每个人发放了 10000 个代币,并限制为 4 年内线性释放这些代币。

那么,团队成员在第一年,就可以提取 2500 个代币;第二年,依然可以再提取 2500 个代币,依次类推,4 年后就已经提取了全部代币。

当然,也可以每两年提取一次,每次提取 5000 个代币。

这个具体提取时间,由团队成员自己掌握。团队成员能够提取代币的比例,就是按照“已过去的时间”占“锁定期”的比例。

线性释放过程,既可以直接写入在 ERC20 代币合约中,也可以编写一个独立的智能合约。

OpenZeppelin 库中的 VestingWallet 合约,就是一个通用的线性释放合约。

我们将其做了简化,单独编写成一个简单的线性释放合约。

实现步骤和算法

我们首先编写一个 ERC20 代币合约,作为锁仓使用的代币,您可以参考本教程的前面章节,直接复制一份代码。

然后,我们再编写一个锁仓合约 TokenVesting,用于存放锁定的代币。

我们将一定数量的需要锁定的代币,转入线性释放合约 TokenVesting 中,并设定锁定期和受益人。

在锁定期内,受益人可以调用 release 函数,按照“已过去的时间”占“锁定期”的比例,提走已经释放的代币。

1. ERC20 代币合约

代币名称为 BinSchool Coin,符号为 BC,为合约部署者发行 100 个代币:

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

// 引入代币合约需要继承的 openzeppelin 的 ERC-20 合约
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyCoin is ERC20{
  // 构造函数,调用了openzeppelin的ERC-20合约的构造函数,传入代币名称和符号
  constructor() ERC20("BinSchool Coin", "BC") {
    // 铸造 100 个 BC 给合约部署者
    _mint(msg.sender, 100*10**18);
  }
}

2. TokenVesting 线性释放合约 

线性释放合约 TokenVesting 共有3个函数:构造函数、释放函数和计算释放量函数。

构造函数用于指定锁定时间和受益人。

释放函数是提供给受益人提取代币使用的函数。

计算释放量函数用于计算当前可以释放的代币数量。

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

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

contract TokenVesting {
  IERC20 public immutable token; // 代币合约地址
  address public immutable beneficiary; // 受益人地址
  uint256 public immutable startTime; // 起始时间
  uint256 public immutable lockTime;  // 锁定时间,单位为秒
  uint256 public releasedAmount;// 已经释放的数量

  // 释放事件
  event Released(address indexed beneficiary, 
    address indexed token, 
    uint256 releaseTime, 
    uint256 amount);
    
  // 构造函数,参数分别为代币合约地址、受益人地址和锁定时间
  constructor(IERC20 _token, address _beneficiary, uint256 _lockTime) {
    // 校验锁定时间必须大于0
    require(_lockTime > 0, "lock time should greater than 0");
        
    token = _token; 
    beneficiary = _beneficiary; 
    lockTime = _lockTime; 
    startTime = block.timestamp; // 从合约部署开始计时
  }

  // 将代币按规则释放给受益人
  function release() external {
    // 获取当前应该释放的数量
    uint256 amount = vestingCount();
    // 如果余额为0,就终止转账
    require(amount > 0, "no tokens to release");

    // 将合约中的代币转账给受益人
    token.transfer(beneficiary, amount);
    // 触发释放代币事件
    emit Released(msg.sender, address(token), block.timestamp, amount);
  } 

  // 计算当前应该释放的数量
  function vestingCount() internal view returns(uint256) {
    // 总量为当前合约余额,再加上已经释放的数量
    uint256 totalAmount = IERC20(token).balanceOf(address(this)) + releasedAmount;
    // 获取当前时间
    uint256 currentTime = block.timestamp;
    // 如果当前时间已经超过锁定期,那么当前可释放全部代币
    if(currentTime - startTime >= lockTime ) {
        return totalAmount;
    }
    // 如果在锁定期内,可释放量为总量 * 已过去时间的占比
    return totalAmount * (currentTime - startTime) / lockTime;
  }
}

测试和验证

第一步:我们首先将 ERC20 代币合约部署在 Remix 上。在后续的锁仓合约中,需要用到它部署后的合约地址。

 

第二步:将 TokenVesting 锁仓合约部署在 Remix 上。

部署时需要提供3个参数:

_token 需要填入上面部署的代币合约的地址。

_beneficiary 是受益人的地址,可以填入当前用来部署合约的账户地址。

_lockTime 是锁定时间,填入 120,它的单位是秒,表示锁定 2 分钟。

 

第三步:向当前的 TokenVesting 锁仓合约转入 100BC 代币,注意默认的单位是 wei,也就是 100 后面需要跟着 18 个零。

在代币合约中,调用方法 transfer,分别填入 TokenVesting 锁仓合约的地址、转账金额 100BC

转账完成后,您可以通过代币合约的方法 balanceOf,填入 TokenVesting 锁仓合约的地址,查看它的余额。

 

第四步:等待一段时间后,调用接口 release,执行成功,代币被部分提取。您可以通过代币合约的方法 balanceOf,查看锁仓合约的余额减少了一部分,而受益人的地址里多了相应的数量。