Solidity

Commit-Reveal 合約仍然容易受到搶先交易的影響。如何改進?

  • April 9, 2021

我正在寫一份契約,我想盡可能地保護它免於搶先執行(顯然,完全防止搶先執行將是理想的)。我的實現與@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));
   }
}

該合約易受以下前端執行情況的影響

  1. 誠實的使用者做出承諾。送出是一個雜湊,因此攻擊者此時無能為力。
  2. 最終,誠實的使用者將使用該reveal方法進行交易。此時valuenonce都已公開。
  3. 在從記憶體池中挑選出誠實使用者的交易之前,攻擊者獲得了現在公開的參數value,並nonce發送一個高氣體交易以進行他的送出。
  4. 依次從誠實使用者處搶先交易。(3 和 4 甚至可以在同一個塊中完成)

我可以採取哪些措施來防止/減輕這種行為?

  • 就像在上面的契約中一樣,我需要commitreveal方法都永久開放以進行互動(這意味著我不能將送出階段和揭示階段的方法分開,因為它將在密封投標拍賣中完成)

我目前的緩解想法是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));
   }
}

引用自:https://ethereum.stackexchange.com/questions/96429