Solidity學習之時間鎖
什么是時間鎖
在合約中有一種時間鎖設計,它的作用是延遲執(zhí)行某個操作。比如在金庫合約中,轉(zhuǎn)出的方法必須要通過時間鎖去調(diào)用,那么在轉(zhuǎn)賬發(fā)起之后,會經(jīng)過一段指定時間才能執(zhí)行。
假設合約owner的私鑰被盜,那么即使黑客想要轉(zhuǎn)出資金,也必須等待一定的時間,這時合約持有者就可以采取一定的措施去減少損失。
實現(xiàn)邏輯
首先為了保證合約內(nèi)部的敏感方法無法被直接調(diào)用,而是必須經(jīng)過時間鎖,那么就需要設計一個修飾器,這個修飾器下的函數(shù)的調(diào)用方必須是合約本身,這就避免了外部調(diào)用。
然后是時間鎖的實現(xiàn),時間鎖的結(jié)構(gòu)是一個map,第一次使用時間鎖是將某個調(diào)用放入到map中,第二次使用則將map中的調(diào)用取出并執(zhí)行。
那么map的key是什么呢,它由target、value、signature、data、executeTime幾個字段組成,代表的含義是
在executeTime的時候,由合約去調(diào)用target的方法,方法的簽名為signature,參數(shù)為data,并攜帶value的native token
這代表調(diào)用方和合約達成了約定,當executeTime時間到的時候,合約就要允許調(diào)用方用約定好的參數(shù)去執(zhí)行約定好的方法,所以這是一個提前約定的機制。
因為有executeTime的存在,可以保證key是唯一的。
因此時間鎖有兩個方法:addTransaction2Queue和executeTransaction。
具體實現(xiàn)
成員變量
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是時間鎖的鎖定時間,交易的執(zhí)行時間必須大于當前時間+delay。
GRACE_PERIOD是過期機制,為了避免任務長期不執(zhí)行帶來的風險,屬于防御機制,如果超過約定好的executeTime+GRACE_PERIOD,任務失效無法再被執(zhí)行。
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才能調(diào)用。
onlyTimeclock則用來控制實際要執(zhí)行的邏輯,保證這些邏輯都在時間鎖的控制之下。
函數(shù)
控制隊列有三個函數(shù):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;
}
其中的邏輯判斷表示:如果沒有指定函數(shù)的話,那直接用data,會觸發(fā)目標合約的fallback()或receive()邏輯;如果有的話則發(fā)起函數(shù)調(diào)用。
此時callData的封裝使用了abi.encodePacked而非abi.encodeWithSignature或是abi.encodeWithSelector,這是因為調(diào)用方法的參數(shù)是不定的,已經(jīng)被封裝成bytes的data了,所以selector也只能手動封裝。
函數(shù)選擇器 =
keccak256("函數(shù)名(參數(shù)類型列表)")的前 4 個字節(jié),即bytes4(keccak256(bytes(signature)))
最后寫一個方法用來模擬:
function changeAdmin(address newAdmin) public onlyTimeclock {
admin = newAdmin;
emit ChangeOwner(newAdmin);
}
調(diào)用的時候需要手動做一下data的encode,比如我傳入的是地址
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,那么data就是
0x000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2

浙公網(wǎng)安備 33010602011771號