代币锁是一种限制代币提取的一种合约。它可以把合约中的代币先锁定一段时间,受益人在锁仓期满后才能发起提现取出代币。时间锁是一种限制合约的行为的特殊合约。它通过给合约的重要函数(如转账、提现、交易等)加上一个锁定期,用于这个操作的延期执行。
代币锁是一种限制代币提取的一种合约。它可以把合约中的代币先锁定一段时间,受益人在锁仓期满后才能发起提现取出代币(有点类似线性释放,但代币锁只能在锁定期满后提取,而线性释放一直可以提取)。代币锁常用于质押项目、流动性提供项目(如 Uniswap
中的流动性锁仓)。
时间锁是一种限制合约的行为的特殊合约。它通过给合约的重要函数(如转账、提现、交易等)加上一个锁定期,用于这个操作的延期执行。例如,对合约的转账调用加上一个
2
天的时间锁,假如资金被盗了,项目方起码还有两天的时间趁代币还未被转走进行补救操作。
代币锁是一种特殊的时间锁
。代币锁和时间锁的最核心区别就是:代币锁控制的是代币的转移、时间锁控制的是合约本身行为(当然可以是代币,也可以不是代币)。
代币锁和时间锁都用到了同一个概念,那就是时间。两者的相同点是:都是通过对行为加上时间 锁
的概念来达到锁定代币、锁定行为函数的目的。
代币锁的实现非常简单,和线性释放类似 线性释放跳转链接。都是通过设定一个 beneficiary
受益人 一个 locktime
锁仓期(类似于线性释放的 duration
持续时间),一个 startTime
开始时间戳来进行管理代币的释放。相关核心状态变量如下:
token
:锁仓的代币的地址beneficiary
:受益人(代码中采用合约 owner
实现)locktime
:锁仓期starttime
:锁仓开始时间 /*被锁仓的代币*/
address public token;
/*锁仓期*/
uint256 public immutable lockTime;
/*开始时间*/
uint256 public immutable startTime;
constructor()
: 构造函数,初始化 token
地址、 beneficiary受益人
、lockTime
锁仓期、startTime
锁仓起始期。release()
: 受益人主动调用领取代币,在此中检查当前时间戳是否已经过锁仓,如果过了则可以领取代币。 /*构造函数*/
constructor(address _token,address beneficiary,uint256 _startTime, uint256 _lockTime) Ownable(beneficiary){
require(_lockTime>0,"LockTime must be greater than zero");
token = _token;
startTime = _startTime;
lockTime = _lockTime;
}
/*请求释放,检查锁仓期,然后释放*/
function release() public {
/*检查时间戳是否已过锁仓期*/
require(block.timestamp >= startTime + lockTime,"you should wait for release");
uint256 amount = IERC20(token).balanceOf(address(this));
require(amount > 0, "not enough tokens for this release");
IERC20(token).transfer(owner(),amount);
}
时间锁的实现逻辑稍微复杂一丢丢。但时间锁的构思非常巧妙,相信你看了后也会赞叹。代码基于 OZ
代码库的 TimeLockController
简化而成。
时间锁的核心思想是,将要执行的操作用 hash
函数抽象成一个 id
,存放在一个 mapping
的结构中,值为这个操作任务的锁定时间戳。在判断任务是否能执行,使用当前区块时间戳和任务时间戳判断大小即可(区块时间戳 > 任务时间戳
说明可以执行,否则仍在锁定期)。
如果一个用户需要调用合约执行操作,那他需要调用两次合约,第一次调用将这个操作任务缓存在合约中,这时这个任务的时间戳就固定好了,第二次调用需要等到这个任务锁定期结束再发起调用,否则调用失败。
optToTimestamp
这个 mapping
结构, 用于缓存我们抽象出来的任务。key
为任务的调用数据 hash
,value
用于存储每个任务的锁定时间戳,只有当调用时的区块时间戳大于这个值,任务才能被成功执行。
minDelay
用于存储当前这个合约所有操作函数的最小延迟时间,每个任务都必须大于这个值,否则不会存储到 mapping
缓存中。
/*任务 id => 任务锁定的时间戳*/
mapping(bytes32 => uint256) private optToTimestamp;
/*最小延迟时间,每个任务都必须大于这个值*/
uint256 private minDelay;
scheduleOpt
:
缓存任务到 mapping
结构中。做到类似延迟队列的效果。 首先判断传入的 delay
是否大于合约规定的 minDelay
,小于则拒绝。使用 keccak256()
来计算这个调用的 hash
值,作为 mapping
的 key
,value
值为:当前时间戳 + 传入的 delay
。这个 mapping
结构是关键,用于缓存起来任务和时间戳供后面判断任务是否到期了。 /*将执行的某个操作存放到延迟队列中*/
function scheduleOpt(
address target,
uint value,
bytes calldata data,
uint256 delay
) external {
/*计算这次操作的 id */
bytes32 optId = calculateOpt(target, value, data, delay);
require(optToTimestamp[optId] == 0, "option already in optToTimestamp");
require(delay >= minDelay,"not enough delay time");
/*将本次操作 id 放入 map 中,值为锁定的时间戳*/
optToTimestamp[optId] = block.timestamp + delay;
}
cancelOpt
:
这是一个取消 mapping
结构中的任务的函数,如果我们在锁定过程中,不想执行该任务,可以使用该函数进行打断。这个函数非常简单,只需要在 mapping
结构中移除任务即可。 /*将延迟队列中的某个操作移除*/
function cancelOpt(
address target,
uint value,
bytes calldata data,
uint256 delay
) external {
/*计算这次操作的 id */
bytes32 optId = calculateOpt(target, value, data, delay);
require(optToTimestamp[optId] != 0, "option not in optToTimestamp");
delete optToTimestamp[optId];
}
executeOpt
这个函数是我们核心执行函数,其逻辑是:先将调用的参数用 hash
函数恢复出任务的 id
。然后使用这个 id
在上面那个缓存任务的 mapping
中寻找出这个任务对应的锁定期。然后使用当前时间戳
和任务的时间戳
对比,如果当前时间戳大于任务的时间戳,则说明可以正常执行。然后删除缓存的任务,调用 call
即可。 /*执行队列中的某个交易*/
function executeOpt(
address target,
uint value,
bytes calldata data,
uint256 delay
) external {
/*计算这次操作的 id */
bytes32 optId = calculateOpt(target, value, data, delay);
require(optToTimestamp[optId] != 0, "option not in optToTimestamp");
/*检查操作是否过了锁定期*/
uint256 executeTimestamp = optToTimestamp[optId];
/*当前时间戳大于 map 缓存中的时间戳*/
require(block.timestamp > executeTimestamp,"option not ready");
delete optToTimestamp[optId];
/*转发执行*/
(bool success, bytes memory returndata) = target.call{value: value}(data);
Address.verifyCallResult(success, returndata);
}
代币锁合约:TokenLock.sol
contract TokenLock is Ownable {
/*被锁仓的代币*/
address public token;
/*锁仓期*/
uint256 public immutable lockTime;
/*开始时间*/
uint256 public immutable startTime;
/*构造函数*/
constructor(address _token,address beneficiary,uint256 _startTime, uint256 _lockTime) Ownable(beneficiary){
require(_lockTime>0,"LockTime must be greater than zero");
token = _token;
startTime = _startTime;
lockTime = _lockTime;
}
/*请求释放,检查锁仓期,然后释放*/
function release() public {
/*检查时间戳是否已过锁仓期*/
require(block.timestamp >= startTime + lockTime,"you should wait for release");
uint256 amount = IERC20(token).balanceOf(address(this));
require(amount > 0, "not enough tokens for this release");
IERC20(token).transfer(owner(),amount);
}
}
TokenLockTest.t.sol
contract TokenLockTest is Test {
MockERC20 public token;
TokenLock public lock;
address public beneficiary = address(0xBEEF);
uint256 public start;
uint256 public lockDuration = 7 days;
uint256 public amount = 1_000 ether;
/*初始化*/
function setUp() public {
token = new MockERC20();
start = block.timestamp + 1 days;
// 创建锁仓合约
lock = new TokenLock(address(token), beneficiary, start, lockDuration);
// 铸币并转入锁仓合约
token.mint(address(this), amount);
token.transfer(address(lock), amount);
}
/*锁仓期内,资金为 0*/
function test_RevertIfNoTokens() public {
// 清空代币
vm.warp(start + lockDuration);
console.logUint(token.balanceOf(address(lock)));
/*先烧掉 lock 里面的所有资金*/
token.burn(address(lock), amount);
vm.prank(beneficiary);
vm.expectRevert("not enough tokens for this release");
lock.release();
}
/*锁仓期内,不能释放*/
function test_CannotReleaseBeforeUnlockTime() public {
vm.warp(start + lockDuration / 2);
vm.prank(beneficiary);
vm.expectRevert("you should wait for release");
lock.release();
}
/*锁仓期后,可以释放*/
function test_CanReleaseAfterUnlockTime() public {
vm.warp(start + lockDuration);
vm.prank(beneficiary);
lock.release();
assertEq(token.balanceOf(beneficiary), amount);
}
}
forge test --match-path "./test/tokenLock/TokenLockTest.t.sol" -vvv
时间锁 TimeLock.sol
contract TimeLocker {
/*任务 id => 任务锁定的时间戳*/
mapping(bytes32 => uint256) private optToTimestamp;
/*最小延迟时间,每个任务都必须大于这个值*/
uint256 private minDelay;
constructor(uint256 _minDelay){
minDelay = _minDelay;
}
/*将执行的某个操作存放到延迟队列中*/
function scheduleOpt(
address target,
uint value,
bytes calldata data,
uint256 delay
) external {
/*计算这次操作的 id */
bytes32 optId = calculateOpt(target, value, data, delay);
require(optToTimestamp[optId] == 0, "option already in optToTimestamp");
require(delay >= minDelay, "not enough delay time");
/*将本次操作 id 放入 map 中,值为锁定的时间戳*/
optToTimestamp[optId] = block.timestamp + delay;
}
/*将延迟队列中的某个操作移除*/
function cancelOpt(
address target,
uint value,
bytes calldata data,
uint256 delay
) external {
/*计算这次操作的 id */
bytes32 optId = calculateOpt(target, value, data, delay);
require(optToTimestamp[optId] != 0, "option not in optToTimestamp");
delete optToTimestamp[optId];
}
/*执行队列中的某个交易*/
function executeOpt(
address target,
uint value,
bytes calldata data,
uint256 delay
) external {
/*计算这次操作的 id */
bytes32 optId = calculateOpt(target, value, data, delay);
require(optToTimestamp[optId] != 0, "option not in optToTimestamp");
/*检查操作是否过了锁定期*/
uint256 executeTimestamp = optToTimestamp[optId];
/*当前时间戳大于 map 缓存中的时间戳*/
require(block.timestamp > executeTimestamp, "option not ready");
delete optToTimestamp[optId];
/*转发执行*/
(bool success, bytes memory returndata) = target.call{value: value}(data);
Address.verifyCallResult(success, returndata);
}
/*计算调用的交易 hash*/
function calculateOpt(
address target,
uint value,
bytes calldata data,
uint256 delay
) public pure returns (bytes32){
return keccak256(abi.encode(target, value, data, delay));
}
}
测试合约 TimeLockerTest.t.sol
contract Target {
uint256 public value;
function setValue(uint256 _value) external {
value = _value;
}
}
contract TimeLockerTest is Test { TimeLocker locker; Target target;
address user = address(0xBEEF);
uint256 minDelay = 1 days;
function setUp() public {
locker = new TimeLocker(minDelay);
target = new Target();
vm.deal(user, 1 ether);
}
/*测试操作锁定、执行*/
function testScheduleAndExecute() public {
// 构造 calldata
bytes memory data = abi.encodeWithSignature("setValue(uint256)", 42);
uint256 valueToSend = 0;
uint256 delay = 2 days;
// 调用 scheduleOpt
locker.scheduleOpt(address(target), valueToSend, data, delay);
// 确保还未到执行时间不能执行
vm.expectRevert("option not ready");
locker.executeOpt(address(target), valueToSend, data, delay);
// 跳过时间
vm.warp(block.timestamp + delay + 1);
// 执行操作
locker.executeOpt(address(target), valueToSend, data, delay);
// 验证目标合约状态已更新
assertEq(target.value(), 42);
}
/*测试打断锁定,取消执行*/
function testCancel() public {
bytes memory data = abi.encodeWithSignature("setValue(uint256)", 100);
uint256 valueToSend = 0;
uint256 delay = 1 days;
// 调用 scheduleOpt
locker.scheduleOpt(address(target), valueToSend, data, delay);
// 取消
locker.cancelOpt(address(target), valueToSend, data, delay);
// 再次执行应 revert
vm.expectRevert("option not in optToTimestamp");
locker.executeOpt(address(target), valueToSend, data, delay);
}
}
- **测试命令**
```js
forge test --match-path "./test/timeLocker/TimeLockerTest.t.sol" -vvv
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!