# WTF Solidity极简入门: 45. 时间锁

By [0xAA](https://paragraph.com/@wtfacademy) · 2022-08-13

---

我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。

推特：[@0xAA\_Science](https://twitter.com/0xAA_Science)

社区：[Discord](https://discord.wtf.academy)｜[微信群](https://wechat.wtf.academy)｜[官网 wtf.academy](https://wtf.academy)

所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

* * *

这一讲，我们就将介绍时间锁和时间锁合约。代码由Compound的[Timelock合约](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol)简化而来。

时间锁
---

![](https://storage.googleapis.com/papyrus_images/05e8e69258b65517e42b9a6bda35ae1d612103f2d13ce988d2e2a7b2bd3e9bd5.jpg)

时间锁（Timelock）是银行金库和其他高安全性容器中常见的锁定机制。它是一种计时器，旨在防止保险箱或保险库在预设时间之前被打开，即便开锁的人知道正确密码。

在区块链，时间锁被`DeFi`和`DAO`大量采用。它是一段代码，他可以将智能合约的某些功能锁定一段时间。它可以大大改善智能合约的安全性，举个例子，假如一个黑客黑了`Uniswap`的多签，准备提走金库的钱，但金库合约加了2天锁定期的时间锁，那么黑客从创建提钱的交易，到实际把钱提走，需要2天的等待期。在这一段时间，项目方可以找应对办法，投资者可以提前抛售代币减少损失。

时间锁合约
-----

下面，我们介绍一下时间锁`Timelock`合约。它的逻辑并不复杂：

*   在创建`Timelock`合约时，项目方可以设定锁定期，并把合约的管理员设为自己。
    
*   时间锁主要有三个功能：
    
    *   创建交易，并加入到时间锁队列。
        
    *   在交易的锁定期满后，执行交易。
        
    *   后悔了，取消时间锁队列中的某些交易。
        
*   项目方一般会把时间锁合约设为重要合约的管理员，例如金库合约，再通过时间锁操作他们。
    
*   时间锁合约的管理员一般为项目的多签钱包，保证去中心化。
    

### 事件

`Timelock`合约中共有`4`个事件。

*   `QueueTransaction`：交易创建并进入时间锁队列的事件。
    
*   `ExecuteTransaction`：锁定期满后交易执行的事件。
    
*   `CancelTransaction`：交易取消事件。
    
*   `NewAdmin`：修改管理员地址的事件。
    

        // 事件
        // 交易取消事件
        event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature,  bytes data, uint executeTime);
        // 交易执行事件
        event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature,  bytes data, uint executeTime);
        // 交易创建并进入队列 事件
        event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
        // 修改管理员地址的事件
        event NewAdmin(address indexed newAdmin);
    

### 状态变量

`Timelock`合约中共有`4`个状态变量。

*   `admin`：管理员地址。
    
*   `delay`：锁定期。
    
*   `GRACE_PERIOD`：交易过期时间。如果交易到了执行的时间点，但在`GRACE_PERIOD`没有被执行，就会过期。
    
*   `queuedTransactions`：进入时间锁队列交易的标识符`txHash`到`bool`的映射，记录所有在时间锁队列中的交易。
    

        // 状态变量
        address public admin; // 管理员地址
        uint public constant GRACE_PERIOD = 7 days; // 交易有效期，过期的交易作废
        uint public delay; // 交易锁定时间 （秒）
        mapping (bytes32 => bool) public queuedTransactions; // txHash到bool，记录所有在时间锁队列中的交易
    

### 修饰器

`Timelock`合约中共有`2`个`modifier`。

*   `onlyOwner()`：被修饰的函数只能被管理员执行。
    
*   `onlyTimelock()`：被修饰的函数只能被时间锁合约执行。
    

        // onlyOwner modifier
        modifier onlyOwner() {
            require(msg.sender == admin, "Timelock: Caller not admin");
            _;
        }
    
        // onlyTimelock modifier
        modifier onlyTimelock() {
            require(msg.sender == address(this), "Timelock: Caller not Timelock");
            _;
        }
    

### 函数

`Timelock`合约中共有`7`个函数。

*   构造函数：初始化交易锁定时间（秒）和管理员地址。
    
*   `queueTransaction()`：创建交易并添加到时间锁队列中。参数比较复杂，因为要描述一个完整的交易：
    
    *   `target`：目标合约地址
        
    *   `value`：发送ETH数额
        
    *   `signature`：调用的函数签名（function signature）
        
    *   `data`：交易的call data
        
    *   `executeTime`：交易执行的区块链时间戳。
        
    
    调用这个函数时，要保证交易预计执行时间`executeTime`大于当前区块链时间戳+锁定时间`delay`。交易的唯一标识符为所有参数的哈希值，利用`getTxHash()`函数计算。进入队列的交易会更新在`queuedTransactions`变量中，并释放`QueueTransaction`事件。
    
*   `executeTransaction()`：执行交易。它的参数与`queueTransaction()`相同。要求被执行的交易在时间锁队列中，达到交易的执行时间，且没有过期。执行交易时用到了`solidity`的低级成员函数`call`，在[第22讲](https://github.com/AmazingAng/WTFSolidity/blob/main/22_Call/readme.md)中有介绍。
    
*   `cancelTransaction()`：取消交易。它的参数与`queueTransaction()`相同。它要求被取消的交易在队列中，会更新`queuedTransactions`并释放`CancelTransaction`事件。
    
*   `changeAdmin()`：修改管理员地址，只能被`Timelock`合约调用。
    
*   `getBlockTimestamp()`：获取当前区块链时间戳。
    
*   `getTxHash()`：返回交易的标识符，为很多交易参数的`hash`。
    

        /**
         * @dev 构造函数，初始化交易锁定时间 （秒）和管理员地址
         */
        constructor(uint delay_) {
            delay = delay_;
            admin = msg.sender;
        }
    
        /**
         * @dev 改变管理员地址，调用者必须是Timelock合约。
         */
        function changeAdmin(address newAdmin) public onlyTimelock {
            admin = newAdmin;
    
            emit NewAdmin(newAdmin);
        }
    
        /**
         * @dev 创建交易并添加到时间锁队列中。
         * @param target: 目标合约地址
         * @param value: 发送eth数额
         * @param signature: 要调用的函数签名（function signature）
         * @param data: call data，里面是一些参数
         * @param executeTime: 交易执行的区块链时间戳
         *
         * 要求：executeTime 大于 当前区块链时间戳+delay
         */
        function queueTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner returns (bytes32) {
            // 检查：交易执行时间满足锁定时间
            require(executeTime >= getBlockTimestamp() + delay, "Timelock::queueTransaction: Estimated execution block must satisfy delay.");
            // 计算交易的唯一识别符：一堆东西的hash
            bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
            // 将交易添加到队列
            queuedTransactions[txHash] = true;
    
            emit QueueTransaction(txHash, target, value, signature, data, executeTime);
            return txHash;
        }
    
        /**
         * @dev 取消特定交易。
         *
         * 要求：交易在时间锁队列中
         */
        function cancelTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner{
            // 计算交易的唯一识别符：一堆东西的hash
            bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
            // 检查：交易在时间锁队列中
            require(queuedTransactions[txHash], "Timelock::cancelTransaction: Transaction hasn't been queued.");
            // 将交易移出队列
            queuedTransactions[txHash] = false;
    
            emit CancelTransaction(txHash, target, value, signature, data, executeTime);
        }
    
        /**
         * @dev 执行特定交易。
         *
         * 要求：
         * 1. 交易在时间锁队列中
         * 2. 达到交易的执行时间
         * 3. 交易没过期
         */
        function executeTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public payable onlyOwner returns (bytes memory) {
            bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
            // 检查：交易是否在时间锁队列中
            require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued.");
            // 检查：达到交易的执行时间
            require(getBlockTimestamp() >= executeTime, "Timelock::executeTransaction: Transaction hasn't surpassed time lock.");
            // 检查：交易没过期
           require(getBlockTimestamp() <= executeTime + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale.");
            // 将交易移出队列
            queuedTransactions[txHash] = false;
    
            // 获取call data
            bytes memory callData;
            if (bytes(signature).length == 0) {
                callData = data;
            } else {
                callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
            }
            // 利用call执行交易
            (bool success, bytes memory returnData) = target.call{value: value}(callData);
            require(success, "Timelock::executeTransaction: Transaction execution reverted.");
    
            emit ExecuteTransaction(txHash, target, value, signature, data, executeTime);
    
            return returnData;
        }
    
        /**
         * @dev 获取当前区块链时间戳
         */
        function getBlockTimestamp() public view returns (uint) {
            return block.timestamp;
        }
    
        /**
         * @dev 将一堆东西拼成交易的标识符
         */
        function getTxHash(
            address target,
            uint value,
            string memory signature,
            bytes memory data,
            uint executeTime
        ) public pure returns (bytes32) {
            return keccak256(abi.encode(target, value, signature, data, executeTime));
        }
    

`Remix`演示
---------

### 1\. 部署`Timelock`合约，锁定期设为`120`秒。

### 2\. 直接调用`changeAdmin()`将报错。

### 3\. 构造更改管理员的交易。

为了构造交易，我们要分别填入以下参数： address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime

*   `target`：因为调用的是`Timelock`自己的函数，填入合约地址。
    
*   `value`：不用转入ETH，这里填`0`。
    
*   `signature`：`changeAdmin()`的函数签名为：`"changeAdmin(address)"`。
    
*   `data`：这里填要传入的参数，也就是新管理员的地址。但是要把地址填充为32字节的数据，以满足[以太坊ABI编码标准](https://github.com/AmazingAng/WTFSolidity/blob/main/27_ABIEncode/readme.md)。可以使用[hashex](https://abi.hashex.org/)网站进行参数的ABI编码。例子：
    
*   `executeTime`：先调用`getBlockTimestamp()`得到当前区块链时间，再在它的基础上加个150秒填入。
    

### 4\. 调用`queueTransaction`，将交易放入时间锁队列。

### 5\. 在锁定期内调用`executeTransaction`，调用失败。

### 6\. 在锁定期满调用`executeTransaction`，交易成功。

### 7\. 查看新的`admin`地址。

总结
--

时间锁可以将智能合约的某些功能锁定一段时间，大大减少项目方`rug pull`和黑客攻击的机会，增加去中心化应用的安全性。它被`DeFi`和`DAO`大量采用，其中包括`Uniswap`和`Compound`。你投资的项目有使用时间锁吗？

---

*Originally published on [0xAA](https://paragraph.com/@wtfacademy/wtf-solidity-45)*
