Solidity学习之时间锁
什么是时间锁
在合约中有一种时间锁设计,它的作用是延迟执行某个操作。比如在金库合约中,转出的方法必须要通过时间锁去调用,那么在转账发起之后,会经过一段指定时间才能执行。
假设合约owner的私钥被盗,那么即使黑客想要转出资金,也必须等待一定的时间,这时合约持有者就可以采取一定的措施去减少损失。
实现逻辑
首先为了保证合约内部的敏感方法无法被直接调用,而是必须经过时间锁,那么就需要设计一个修饰器,这个修饰器下的函数的调用方必须是合约本身,这就避免了外部调用。
然后是时间锁的实现,时间锁的结构是一个map,第一次使用时间锁是将某个调用放入到map中,第二次使用则将map中的调用取出并执行。
那么map的key是什么呢,它由target、value、signature、data、executeTime几个字段组成,代表的含义是
在executeTime的时候,由合约去调用target的方法,方法的签名为signature,参数为data,并携带value的native token
这代表调用方和合约达成了约定,当executeTime时间到的时候,合约就要允许调用方用约定好的参数去执行约定好的方法,所以这是一个提前约定的机制。
因为有executeTime的存在,可以保证key是唯一的。
因此时间锁有两个方法:addTransaction2Queue和executeTransaction。
具体实现
成员变量
contract TimeClock {
address public admin;
uint public delay;
uint public constant GRACE_PERIOD = 7 days; // 交易过期时间
mapping (bytes32=>bool) public queuedTransactions;
constructor(uint delay_) {
admin = msg.sender;
delay = delay_;
}
}
admin是合约的控制方,保证了其他人无法使用时间锁,但无法避免admin地址被盗。
delay是时间锁的锁定时间,交易的执行时间必须大于当前时间+delay。
GRACE_PERIOD是过期机制,为了避免任务长期不执行带来的风险,属于防御机制,如果超过约定好的executeTime+GRACE_PERIOD,任务失效无法再被执行。
queuedTransactions就是存放任务的队列。
事件
event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
event NewTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
修饰器
modifier onlyOwner {
require(msg.sender == admin, "caller not admin");
_;
}
modifier onlyTimeclock {
require(msg.sender == address(this), "caller not timeclock");
_;
}
onlyOwner用来控制队列方法,保证只有合约的owner才能调用。
onlyTimeclock则用来控制实际要执行的逻辑,保证这些逻辑都在时间锁的控制之下。
函数
控制队列有三个函数:add,cancel和execute
function addTransaction2Queue(address target, uint value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner returns (bytes32) {
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
require(!queuedTransactions[txHash], "transaction already queued");
require(executeTime > getBlockTimestamp() + delay, "estimated transaction must satisfy delay");
require(msg.value > value, "value not enough");
queuedTransactions[txHash] = true;
emit NewTransaction(txHash, target, value, signature, data, executeTime);
return txHash;
}
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);
}
add和cancel方法都比较简单,主要就是条件的检查,然后做了一个map的插入移除操作。
考虑到复用性,就把getTxHash和getBlockTimestamp方法都抽出来了。
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));
}
function getBlockTimestamp() public view returns (uint256) {
return block.timestamp;
}
最后是execute方法:
function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint256 executeTime) external payable onlyOwner returns (bool) {
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
require(queuedTransactions[txHash], "transaction not valid");
queuedTransactions[txHash] = false;
require(getBlockTimestamp() > executeTime, "executed time not reached");
require(getBlockTimestamp() < executeTime + GRACE_PERIOD, "grace period over");
bytes memory callData;
if (bytes(signature).length == 0) {
callData = data;
} else {
callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
}
(bool success, ) = target.call{value:msg.value}(callData);
require(success, "execution failed");
emit ExecuteTransaction(txHash, target, value, signature, data, executeTime);
return true;
}
其中的逻辑判断表示:如果没有指定函数的话,那直接用data,会触发目标合约的fallback()或receive()逻辑;如果有的话则发起函数调用。
此时callData的封装使用了abi.encodePacked而非abi.encodeWithSignature或是abi.encodeWithSelector,这是因为调用方法的参数是不定的,已经被封装成bytes的data了,所以selector也只能手动封装。
函数选择器 =
keccak256("函数名(参数类型列表)")的前 4 个字节,即bytes4(keccak256(bytes(signature)))
最后写一个方法用来模拟:
function changeAdmin(address newAdmin) public onlyTimeclock {
admin = newAdmin;
emit ChangeOwner(newAdmin);
}
调用的时候需要手动做一下data的encode,比如我传入的是地址
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,那么data就是
0x000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2

浙公网安备 33010602011771号