今天,我們介紹時間鎖和時間鎖合約。程式碼由Compound的Timelock合約簡化而來。
時間鎖
時間鎖(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 dataexecuteTime
:交易執行的區塊鏈時間戳。呼叫這個函式時,要保證交易預計執行時間
executeTime
大於當前區塊鏈時間戳+鎖定時間delay
。交易的唯一識別符號為所有引數的雜湊值,利用getTxHash()
函式計算。進入佇列的交易會更新在queuedTransactions
變數中,並釋放QueueTransaction
事件。executeTransaction()
:執行交易。它的引數與queueTransaction()
相同。要求被執行的交易在時間鎖佇列中,達到交易的執行時間,且沒有過期。執行交易時用到了solidity
的低階成員函式call
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)); }
總結
時間鎖可以將智慧合約的某些功能鎖定一段時間,大大減少專案方rug pull
和駭客攻擊的機會,增加去中心化應用的安全性。它被DeFi
和DAO
大量採用,其中包括Uniswap
和Compound