你也不想你的代币被盗吧? - 手搓实现代币锁和时间锁

代币锁是一种限制代币提取的一种合约。它可以把合约中的代币先锁定一段时间,受益人在锁仓期满后才能发起提现取出代币。时间锁是一种限制合约的行为的特殊合约。它通过给合约的重要函数(如转账、提现、交易等)加上一个锁定期,用于这个操作的延期执行。

代币锁和时间锁

代币锁是一种限制代币提取的一种合约。它可以把合约中的代币先锁定一段时间,受益人在锁仓期满后才能发起提现取出代币(有点类似线性释放,但代币锁只能在锁定期满后提取,而线性释放一直可以提取)。代币锁常用于质押项目、流动性提供项目(如 Uniswap 中的流动性锁仓)。

image.png 时间锁是一种限制合约的行为的特殊合约。它通过给合约的重要函数(如转账、提现、交易等)加上一个锁定期,用于这个操作的延期执行。例如,对合约的转账调用加上一个 2 天的时间锁,假如资金被盗了,项目方起码还有两天的时间趁代币还未被转走进行补救操作。

image.png

代币锁和时间锁的相同和区别

  • 代币锁:控制的是代币的转移和释放,用于防止投资人抛售代币(和线性释放类似功能,但只能锁仓期满才能执行代币操作)。可以说,代币锁是一种特殊的时间锁
  • 时间锁:时间锁控制的是合约的某个操作(函数)的执行时间,例如控制转账、升级等函数延期执行。

代币锁和时间锁的最核心区别就是:代币锁控制的是代币的转移、时间锁控制的是合约本身行为(当然可以是代币,也可以不是代币)。

代币锁和时间锁都用到了同一个概念,那就是时间。两者的相同点是:都是通过对行为加上时间 的概念来达到锁定代币、锁定行为函数的目的。

代币锁的实现逻辑核心

代币锁的实现非常简单,和线性释放类似 线性释放跳转链接。都是通过设定一个 beneficiary 受益人 一个 locktime 锁仓期(类似于线性释放的 duration 持续时间),一个 startTime 开始时间戳来进行管理代币的释放。相关核心状态变量如下:

  • 核心状态变量
    1. token:锁仓的代币的地址
    2. beneficiary:受益人(代码中采用合约 owner 实现)
    3. locktime:锁仓期
    4. starttime:锁仓开始时间
    /*被锁仓的代币*/
    address public token;
     /*锁仓期*/
    uint256 public immutable lockTime;
    /*开始时间*/
    uint256 public immutable startTime;
  • 核心函数
    1. constructor(): 构造函数,初始化 token 地址、 beneficiary受益人lockTime 锁仓期、startTime 锁仓起始期。
    2. 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 为任务的调用数据 hashvalue 用于存储每个任务的锁定时间戳,只有当调用时的区块时间戳大于这个值,任务才能被成功执行。 minDelay 用于存储当前这个合约所有操作函数的最小延迟时间,每个任务都必须大于这个值,否则不会存储到 mapping 缓存中。
    /*任务 id => 任务锁定的时间戳*/
    mapping(bytes32 => uint256) private optToTimestamp;
    /*最小延迟时间,每个任务都必须大于这个值*/
    uint256 private minDelay;
  • 核心函数
    1. scheduleOpt: 缓存任务到 mapping 结构中。做到类似延迟队列的效果。 首先判断传入的 delay 是否大于合约规定的 minDelay,小于则拒绝。使用 keccak256() 来计算这个调用的 hash 值,作为 mappingkeyvalue 值为:当前时间戳 + 传入的 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;
    }
  1. 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];
    }
  1. 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);

    }

手搓一个最小的代币锁

  1. 代币锁合约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);
    }
    }
  2. 测试合约 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);
    }
}
  1. 测试命令
forge test --match-path "./test/tokenLock/TokenLockTest.t.sol"  -vvv

image.png

手搓一个最小的时间锁

  • 时间锁 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

image.png

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
shawn_shaw
shawn_shaw
web3潜水员、技术爱好者、web3钱包开发工程师、欢迎交流工作机会。欢迎闲聊、交流技术、交流工作:vx:cola_ocean