本技術教程將教您什麼是時間鎖智能合約以及如何構建它們。您將製作一個智能合約,允許將ERC-20 代幣鑄造排隊到基於時間的窗口中。
本教程將使用:
本教程的代碼可以在我們的 示例GitHub
什麼是Timelock 智能合約?
時間鎖的核心是一段額外的代碼,它將智能合約中的功能限制在特定的時間窗口內。最簡單的形式可能類似於這個簡單的“if”語句:
if (block.timestamp < _timelockTime) { revert ErrorNotReady(block.timestamp, _timelockTime); }
時間鎖合約用例
時間鎖合約有許多潛在的用例。它們通常用於首次代幣發行(ICO),以幫助推動代幣的眾籌銷售。它們還可以用於實施歸屬計劃,用戶可以在設定的時間段過去後提取資金。
另一種可能的用途是作為一種基於智能合約的意志形式。使用Chainlink Keepers,您可以定期檢查遺囑的所有者,一旦提交了死亡證明,遺囑的智能合約就可以解鎖。
這些只是幾個例子,但可能性是無窮無盡的。在這個例子中,我們將專注於創建一個 ERC-20 強制時間鎖定隊列來鑄造硬幣的合約。
如何創建Timelock 智能合約
在本教程中,我們將使用Foundry 來構建和測試我們的Solidity 合約。您可以在 代工GitHub.
初始化項目
您將使用初始化項目 forge init
. 項目初始化後, forge test
將作為健全性檢查,以確保一切設置正確。
❯ forge init timelocked-contract Initializing /Users/rg/Development/timelocked-contract... Installing ds-test in "/Users/rg/Development/timelocked-contract/lib/ds-test", (url: https://github.com/dapphub/ds-test, tag: None) Installed ds-test Initialized forge project. ❯ cd timelocked-contract ❯ forge test [⠒] Compiling... [⠰] Compiling 3 files with 0.8.10 [⠔] Solc finished in 143.06ms Compiler run successful Running 1 test for src/test/Contract.t.sol:ContractTest [PASS] testExample() (gas: 190) Test result: ok. 1 passed; 0 failed; finished in 469.71µs
創建您的測試
您將創建測試以確保合約滿足時間鎖定的所有要求。您將要檢查的主要功能是:
- 排隊代幣鑄造
- 到達窗口後薄荷
- 取消排隊的鑄幣廠
除了這些功能之外,您還將確保合約不允許在沒有先排隊的情況下進行雙重排隊或鑄幣等濫用。
項目初始化後,您將能夠運行測試。您將使用測試來確保您的項目按預期工作。測試屬於 src/test/Contract.t.sol
. Foundry 使用測試名稱來指示它們應該成功還是失敗。 testThisShouldWork
預計會通過而 testFailShouldNotWork
如果測試恢復,則通過。這使我們能夠測試應該和不應該通過的案例。
還有一些約定要先解釋一下。時間鎖基於一個隊列,該隊列將使用 _toAddress
, _amount
, 和 time
參數. 這些值將使用散列 keccak256
.
// Create hash of transaction data for use in the queue function generateTxnHash( address _to, uint256 _amount, uint256 _timestamp ) public pure returns (bytes32) { return keccak256(abi.encode(_to, _amount, _timestamp)); }
此外,您將需要操縱測試區塊鏈的時間來模擬時間流逝。這是通過Foundry 實現的 CheatCodes
.
interface CheatCodes { function warp(uint256) external; }
Warp 允許你操縱當前區塊的時間戳。該函數接受新時間戳的uint。我們將使用它為當前時間“添加時間”,模擬時間的流逝。這將使我們能夠創建一套測試,以確保我們的時間鎖按預期運行。
替換內容 src/test/Contract.t.sol
具有以下內容:
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.10; import "ds-test/test.sol"; import "../Contract.sol"; interface CheatCodes { function warp(uint256) external; } contract ContractTest is DSTest { // HEVM_ADDRESS is the pre-defined contract that contains the cheatcodes CheatCodes constant cheats = CheatCodes(HEVM_ADDRESS); Contract public c; address toAddr = 0x1234567890123456789012345678901234567890; function setUp() public { c = new Contract(); c.queueMint( toAddr, 100, block.timestamp + 600 ); } // Ensure you can't double queue function testFailDoubleQueue() public { c.queueMint( toAddr, 100, block.timestamp + 600 ); } // Ensure you can't queue in the past function testFailPastQueue() public { c.queueMint( toAddr, 100, block.timestamp - 600 ); } // Minting should work after the time has passed function testMintAfterTen() public { uint256 targetTime = block.timestamp + 600; cheats.warp(targetTime); c.executeMint( toAddr, 100, targetTime ); } // Minting should fail if you mint too soon function testFailMintNow() public { c.executeMint( toAddr, 100, block.timestamp + 600 ); } // Minting should fail if you didn't queue function testFailMintNonQueued() public { c.executeMint( toAddr, 999, block.timestamp + 600 ); } // Minting should fail if try to mint twice function testFailDoubleMint() public { uint256 targetTime = block.timestamp + 600; cheats.warp(block.timestamp + 600); c.executeMint( toAddr, 100, targetTime ); c.executeMint( toAddr, 100, block.timestamp + 600 ); } // Minting should fail if you try to mint too late function testFailLateMint() public { uint256 targetTime = block.timestamp + 600; cheats.warp(block.timestamp + 600 + 1801); emit log_uint(block.timestamp); c.executeMint( toAddr, 100, targetTime ); } // you should be able to cancel a mint function testCancelMint() public { bytes32 txnHash = c.generateTxnHash( toAddr, 100, block.timestamp + 600 ); c.cancelMint(txnHash); } // you should be able to cancel a mint once but not twice function testFailCancelMint() public { bytes32 txnHash = c.generateTxnHash( toAddr, 999, block.timestamp + 600 ); c.cancelMint(txnHash); c.cancelMint(txnHash); } // you shouldn't be able to cancel a mint that doesn't exist function testFailCancelMintNonQueued() public { bytes32 txnHash = c.generateTxnHash( toAddr, 999, block.timestamp + 600 ); c.cancelMint(txnHash); } }
建立合同
你現在應該可以運行了 forge test
並看到許多錯誤。現在是時候讓你的測試通過了。
我們將從一個基本的ERC-20 合約開始。所有這些工作都屬於 src/Contract.sol
.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract Contract is ERC20, Ownable { constructor() ERC20("TimeLock Token", "TLT") {} }
要使用OpenZeppelin 合約,您需要安裝它們並將Foundry 指向它們。
要安裝合同,請運行
❯ forge install openzeppelin/openzeppelin-contracts
您還需要通過創建映射導入 remappings.txt
.
@openzeppelin/=lib/openzeppelin-contracts/ ds-test/=lib/ds-test/src/
這個重新映射文件允許您使用OpenZeppelin 合約之類的東西,並以您通常使用其他工具(例如Hardhat 或Remix)的方式導入它們。此文件將導入重新映射到它們所在的目錄。我還通過以下方式安裝了OpenZeppelin 合同 forge install openzeppelin/openzeppelin-contracts
. 這些將用於創建ERC-721 合約。
如果一切正常,您可以運行 forge build
編譯合同。
此時,您可以構建下面的合同。該合同將允許您將鑄幣廠排隊並在適當的窗口期間返回以執行該鑄幣廠。
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.10; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract Contract is ERC20, Ownable { // Error Messages for the contract error ErrorAlreadyQueued(bytes32 txnHash); error ErrorNotQueued(bytes32 txnHash); error ErrorTimeNotInRange(uint256 blockTimestmap, uint256 timestamp); error ErrorNotReady(uint256 blockTimestmap, uint256 timestamp); error ErrorTimeExpired(uint256 blockTimestamp, uint256 expiresAt); // Queue Minting Event event QueueMint( bytes32 indexed txnHash, address indexed to, uint256 amount, uint256 timestamp ); // Mint Event event ExecuteMint( bytes32 indexed txnHash, address indexed to, uint256 amount, uint256 timestamp ); // Cancel Mint Event event CancelMint(bytes32 indexed txnHash); // Constants for minting window uint256 public constant MIN_DELAY = 60; // 1 minute uint256 public constant MAX_DELAY = 3600; // 1 hour uint256 public constant GRACE_PERIOD = 1800; // 30 minutes // Minting Queue mapping(bytes32 => bool) public mintQueue; constructor() ERC20("TimeLock Token", "TLT") {} // Create hash of transaction data for use in the queue function generateTxnHash( address _to, uint256 _amount, uint256 _timestamp ) public pure returns (bytes32) { return keccak256(abi.encode(_to, _amount, _timestamp)); } // Queue a mint for a given address amount, and timestamp function queueMint( address _to, uint256 _amount, uint256 _timestamp ) public onlyOwner { // Generate the transaction hash bytes32 txnHash = generateTxnHash(_to, _amount, _timestamp); // Check if the transaction is already in the queue if (mintQueue[txnHash]) { revert ErrorAlreadyQueued(txnHash); } // Check if the time is in the range if ( _timestamp < block.timestamp + MIN_DELAY || _timestamp > block.timestamp + MAX_DELAY ) { revert ErrorTimeNotInRange(_timestamp, block.timestamp); } // Queue the transaction mintQueue[txnHash] = true; // Emit the QueueMint event emit QueueMint(txnHash, _to, _amount, _timestamp); } // Execute a mint for a given address, amount, and timestamp function executeMint( address _to, uint256 _amount, uint256 _timestamp ) external onlyOwner { // Generate the transaction hash bytes32 txnHash = generateTxnHash(_to, _amount, _timestamp); // Check if the transaction is in the queue if (!mintQueue[txnHash]) { revert ErrorNotQueued(txnHash); } // Check if the time has passed if (block.timestamp < _timestamp) { revert ErrorNotReady(block.timestamp, _timestamp); } // Check if the window has expired if (block.timestamp > _timestamp + GRACE_PERIOD) { revert ErrorTimeExpired(block.timestamp, _timestamp); } // Remove the transaction from the queue mintQueue[txnHash] = false; // Execute the mint mint(_to, _amount); // Emit the ExecuteMint event emit ExecuteMint(txnHash, _to, _amount, _timestamp); } // Cancel a mint for a given transaction hash function cancelMint(bytes32 _txnHash) external onlyOwner { // Check if the transaction is in the queue if (!mintQueue[_txnHash]) { revert ErrorNotQueued(_txnHash); } // Remove the transaction from the queue mintQueue[_txnHash] = false; // Emit the CancelMint event emit CancelMint(_txnHash); } // Mint tokens to a given address function mint(address to, uint256 amount) internal { _mint(to, amount); } }
從這往哪兒走
Timelock 合約本身就很強大。它們為智能合約中的交易提供安全措施和透明度。但是,它們不是靠自己工作的。您將需要返回並在適當的窗口內執行功能- 除非您自動執行合同。
鏈環守護者 允許您在智能合約中自動調用函數。這將使您能夠創建排隊的函數以在預定義的窗口內自動執行,從而消除錯過執行窗口的風險。要了解更多信息,請前往 守護者文檔
如果您是開發人員並希望將Chainlink 集成到您的智能合約應用程序中,請查看 區塊鏈教育中心, 開發者文檔 或者 聯繫專家. 你也可以 直接通過去中心化的預言機將您的智能合約與現實世界的數據聯繫起來。