什么是线性释放
线性释放(Linear Vesting
)一种常见的代币释放机制,指得是:代币按照固定的速率、持续均匀地在设定时间内逐渐释放。而不是一次性就释放完。
为什么需要线性释放
我们来思考一下,假如没有线性释放的场景。就比如,我们作为项目方,将要发行一个叫做 ShawnCoin
的 ERC20
代币,总量为 10
亿。为了奖励投资者(风投机构、私募),我们需要为他们发放 5
亿的代币,如果是一次性释放给他们的话,假如他们马上就抛售代币,会导致币价下跌非常迅猛,直接砸穿币价,散户成为接盘侠。
而有了线性释放的话,我们将代币均匀地按时间分摊释放给到投资者,将降低投资者抛售代币的压力,防止投资者过早跑路。
线性释放的核心流程
为了实现代币线程释放的要求,我们必须要设定一个释放的起始时间 start
,一个释放持续时间 duration
。在合约内,投资者主动调用 release
函数来获取代币,而在这个 release
函数中,我们通过一套数学公式来确定出每个时间片中该释放多少代币。
这么讲可能有点抽象,我们通过一个具体例子来展示流程:
- 我们的
ShawnCoin
代币,将要释放给一个叫Steve
的投资者,释放给他的总量为10
个。 - 释放的开始时间为 12798910(假设的时间戳),持续时间为
10
s。(即释放到 12798920 这个时间戳后停止释放),那么,每秒的释放个数为1
个代币。 - 拥有了这个关系之后,那么我们在这个
release
函数中,就可以通过数学关系来确定投资者可收到的代币数量了。具体看以下代码。
核心状态变量
线性释放合约中的核心状态变量有 4
个
- beneficiary
:受益人地址,在合约中,我们设定为合约的 owner
。规定了这个合约是专供此人释放代币使用的。
- start
:即我们上面讲的 start
时间。释放的开始时间。
- duration
:释放的持续时间,在这个持续时间内,代币均匀释放。
- erc20Released
: 这是一个 mapping
的结构,记录了受益人已经领取的代币数量。供在计算可领取数量的时候减去已经领取的数量。
核心函数
- 构造函数:初始化受益人地址、开始时间、持续时间。
release()
: 投资者提币的函数,投资者主动触发该函数,在次函数内,根据线性释放的关系,计算可提币的数量,将代币发送到受益人地址中。vestedAmount()
: 计算可提币数量的函数,可供外部查询以及release()
提币中计算使用。
手搓一个线性释放的合约
手搓最小代码实现,参考 OZ
代码库 VestingWallet.sol
- 链上合约 LinearVesting.sol
```js
contract LinearVesting is Ownable {
/*记录已领取数量*/
mapping(address => uint256) public erc20Released;
uint256 public immutable start;
uint256 public immutable duration;
/*构造函数,初始化合约参数*/
constructor(uint256 _start, uint256 _duration, address beneficiary) Ownable(beneficiary){
start = _start;
duration = _duration;
}
/*受益人提币*/
function release(address token) external {
/*计算可提币数量 = 已释放数量 - 已提取数量*/
uint256 releasable = vestedAmount(token, uint256(block.timestamp)) - erc20Released[token];
/*更新已提取数量*/
erc20Released[token] += releasable;
/*转出*/
IERC20(token).transfer(owner(), releasable);
}
/*计算已释放数量*/
function vestedAmount(address token, uint256 timestamp) public view returns (uint256){
/*合约中有多少币(总共释放多少)*/
uint256 totalAllocation = IERC20(token).balanceOf(address(this)) + erc20Released[token];
/*根据线性释放公式,计算已释放的数量*/
if (timestamp < start) {
/*未到释放时间*/
return 0;
} else if (timestamp >= start + duration) {
/*超时全部释放*/
return totalAllocation;
} else {
/* (总量 x 已过时长)/ 总时长*/
return (totalAllocation * (timestamp - start)) / duration;
}
}
}
```
-
线性释放测试
LinearVestingTest.t.sol
```js contract MockToken is OZERC20 { constructor() OZERC20("MockToken", "MTK") {} function mint(address to, uint256 amount) external { _mint(to, amount); } }
contract LinearVestingTest is Test { LinearVesting vesting; MockToken token; address beneficiary;
uint256 start = uint256(keccak256(abi.encodePacked(block.timestamp))); uint256 duration = 30 minutes; uint256 constant TOTAL_AMOUNT = 1_000 ether;
function setUp() public { /受益人/ beneficiary = address(0xBEEF); /构建合约/ vesting = new LinearVesting(start, duration, beneficiary); token = new MockToken(); /铸币/ token.mint(address(this), TOTAL_AMOUNT); /转入合约中,待释放/ token.transfer(address(vesting), TOTAL_AMOUNT); }
/释放之前、之间、之后/ function test_Releases() public { /之前/ console.log("==== start ====="); console.logUint(start); console.log("===== duration ===="); console.logUint(duration); vm.warp(start - duration); vm.prank(beneficiary); vesting.release(address(token));
uint256 expectedBefore = 0; /*释放 0 */ assertEq(token.balanceOf(beneficiary), expectedBefore); /*之间*/ vm.warp(start + duration / 2); vm.prank(beneficiary); vesting.release(address(token)); uint256 expected = TOTAL_AMOUNT / 2; /*一半时间提取,应该释放一半代币*/ assertEq(token.balanceOf(beneficiary), expected);
} } ``` - 测试命令
forge test --match-path "./test/linearVesting/LinearVestingTest.t.sol" -vvv