Solidity
Commit-Reveal 合約仍然容易受到搶先交易的影響。如何改進?
我正在寫一份契約,我想盡可能地保護它免於搶先執行(顯然,完全防止搶先執行將是理想的)。我的實現與@Ismael here的 Raffle 實現非常相似,所以我將使用它:
contract Raffle { mapping(address => bytes32) commitments; mapping(uint256 => address) reserved; event Reserved(uint256 value, address owner); event Committed(bytes32 hash); function commit(bytes32 hash) public { require(commitments[msg.sender] == bytes32(0), "Already committed"); commitments[msg.sender] = hash; emit Committed(hash); // Added this event for similarity } function reveal(uint256 nonce, uint256 value) public { bytes32 d = digest(nonce, value, msg.sender); require(commitments[msg.sender] == d, "Invalid data"); require(reserved[value] == address(0), "Already reserved"); reserved[value] = msg.sender; emit Reserved(value, msg.sender); } function digest(uint256 nonce, uint256 value, address sender) public pure returns (bytes32) { return keccak256(abi.encodePacked(nonce, value, sender)); } }
該合約易受以下前端執行情況的影響:
- 誠實的使用者做出承諾。送出是一個雜湊,因此攻擊者此時無能為力。
- 最終,誠實的使用者將使用該
reveal
方法進行交易。此時value
和nonce
都已公開。- 在從記憶體池中挑選出誠實使用者的交易之前,攻擊者獲得了現在公開的參數
value
,並nonce
發送一個高氣體交易以進行他的送出。- 依次從誠實使用者處搶先交易。(3 和 4 甚至可以在同一個塊中完成)
我可以採取哪些措施來防止/減輕這種行為?
- 就像在上面的契約中一樣,我需要
commit
和reveal
方法都永久開放以進行互動(這意味著我不能將送出階段和揭示階段的方法分開,因為它將在密封投標拍賣中完成)我目前的緩解想法是
block.number
在送出中註冊 ,並且在該reveal
方法中僅在自送出後已探勘任意數量的塊時才保留該值。例如:您在塊 100 上送出,您需要等待塊 110 呼叫 reval(否則事務被還原)。這為誠實的使用者提供了 10 個區塊的“優勢”,因為攻擊者需要等待 10 個區塊才能嘗試搶先。缺點:
- 這並不能解決問題,由於
reveal
區塊鏈阻塞、gas 低或其他原因,交易可能會在記憶體池中停留 X 個塊(X 是要等待的任意數量的塊)。足夠長的時間讓攻擊者先行。- 如果誠實的使用者在挖出塊數之前錯誤地呼叫了揭示函式,他將有一個失敗的交易並且參數將被揭示,從而縮短他的優勢。
- 很難確定合理數量的塊。
我還提出了一個基於 的解決方案
block.number
,它比使用更安全block.timestamp
,但方法不同。與其在幾個區塊之後使用它來確保預訂,我會使用它作為排序標準,以防多個使用者使用相同的號碼。當使用者送出雜湊時,目前塊號被擷取在送出結構中。之後,當使用者顯示他/她的號碼時,顯示功能將確定該號碼是否為:
a)免費=> 將分配給目前使用者
b)不免費=> 它將比較目前使用者和之前分配的使用者之間的塊編號,並更新到具有最舊塊的使用者(第一個送出該編號的使用者)
更新後的程式碼如下所示:
// SPDX-License-Identifier: MIT pragma solidity 0.8.0; contract Raffle { struct Commitments { bytes32 commitment; uint256 blockNumber; } mapping(address => Commitments) commitments; mapping(uint256 => address) reserved; function commit(bytes32 hash) external { require(commitments[msg.sender].commitment == bytes32(0), "Already committed"); commitments[msg.sender] = Commitments(hash, block.number); } function reveal(uint256 nonce, uint256 value) external { bytes32 d = digest(nonce, value, msg.sender); require(commitments[msg.sender].commitment == d, "Invalid data"); if (reserved[value] == address(0)) { reserved[value] = msg.sender; } else if (commitments[reserved[value]].blockNumber > commitments[msg.sender].blockNumber) { reserved[value] = msg.sender; } else { revert('Already reserved'); } } function digest(uint256 nonce, uint256 value, address sender) public pure returns (bytes32) { return keccak256(abi.encodePacked(nonce, value, sender)); } }