这篇文章将会介绍智能合约中的工夫锁是什么,并且解说如何开发它。你将会开发一个智能合约,这个合约能够将 ERC-20 通证的铸造申请按工夫排列。
这个教程将会应用到:
- Foundry
- Solidity
- Ethereum
教程的代码能够在这个 GitHub Repo 中找到。
什么是智能合约的工夫锁
实质上,工夫锁是用来将智能合约中的某个函数限度在一段时间内的代码。“if”语句就能够实现最简略的工夫锁:
if (block.timestamp < _timelockTime) { revert ErrorNotReady(block.timestamp, _timelockTime);}
工夫锁的利用场景
智能合约中的工夫锁有很多潜在的利用场景,它们通常会被用在通证首次公开发行中,用于实现通证销售的一些性能。工夫锁也能够被用来依照时间表受权投资资金应用,即用户只有在一段时间当前才能够取出资金。
另一个可能的场景就是通过智能合约去实现遗嘱。应用 Chainlink Keepers,你能够周期性的查看遗嘱的客人是否还在,一旦死亡证实被公布,这个遗嘱的智能合约就会解锁。
以上只是很少的一些利用案例,智能合约工夫锁有很多种场景去应用。在这个案例中,咱们会聚焦于一个 ERC-20 合约,用工夫锁实现一个队列来铸造它。
怎么创立一个智能合约工夫锁
在这个教程中,咱们会应用 Foundry 来开发和测试 Solidity 合约。对于 Foundry 这个框架,你能够它的 GitHub 中找到更多的信息。
初始化我的项目
你能够应用 forge init
初始化我的项目。我的项目初始化实现后,forge test
命令会进行一次查看确保我的项目初始化的过程没有问题。
❯ forge init timelocked-contractInitializing /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.06msCompiler run successfulRunning 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
示意只有这个测试被 revert 的时候才会通过。
还有一些其余的应用常规。工夫锁会基于一个队列,这个队列会应用 _toAddress
, _amount
, 和 time
这几个参数的哈希值,而 keccak256
会被用来计算它们的哈希值。
// Create hash of transaction data for use in the queuefunction generateTxnHash( address _to, uint256 _amount, uint256 _timestamp) public pure returns (bytes32) { return keccak256(abi.encode(_to, _amount, _timestamp));}
另外,你须要本人设置测试环境中的工夫来模仿有多少工夫过来。这点能够通过 Foundry 的 CheatCode
实现。
interface CheatCodes { function warp(uint256) external;}
wrap 函数能够让你设置以后的区块的工夫戳。这个函数承受一个 uint 参数来生成新的工夫戳。咱们须要应用它给以后工夫来“减少工夫”,模仿工夫的流逝。这个能够让咱们在测试中依照预期提供工夫锁性能所须要的变量。
把 src/test/Contract.t.sol
中的内容替换为上面的代码:
// SPDX-License-Identifier: UNLICENSEDpragma 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: MITpragma 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
来映射这些 imports
@openzeppelin/=lib/openzeppelin-contracts/ds-test/=lib/ds-test/src/
这个 remapping 文件能够让你像在 Hardhat 或者 Remix 中一样应用像OpenZeppelin 合约这样的内部库,因为这个文件将 import 从新映射到了它们所在的文件夹。我通过 forge install openzeppelin/openzeppelin-contracts
装置了 OpenZeppelin 合约,它们在这里会被用来创立 ERC-721 合约。
如果一切顺利的话,你能够运行 forge build
来编译合约。
实现上述操作当前,就能够写上面的智能合约。这个合约能够让你将一个铸造操作退出队列,而后在某个工夫窗口执行这个铸造操作。
// SPDX-License-Identifier: UNLICENSEDpragma 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); }}
接下来能够做什么
智能合约的工夫锁十分有用,它们能够让合约内的交易变得更加平安和通明。然而工夫锁无奈主动触发,所以你须要在某个工夫节点回来而后执行这个函数。想要让它们本人执行的话,就须要自动化你的合约。
Chainlink Keepers 能够让你自动化智能合约中的函数。通过应用 Chainlink Keepers,你能够在某一些提前定义好的工夫节点,让你智能合约中的函数主动执行。想要理解更多对于 Chainlink Keepers 的信息,请查看 Keepers 文档。
您能够关注 Chainlink 预言机并且私信退出开发者社区,有大量对于智能合约的学习材料以及对于区块链的话题!