I'm writing a contract and I want to shield it from front-running as best as it can possible be done (obviously, fully preventing front running would be ideal). My implementation is very similar to the Raffle implementation from @Ismael here so I will be using that:
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));
}
}
This contract is vulnerable to the following front running scenario:
- Honest user makes a commit. Commit is a hash so there is nothing the attacker can do at this point.
- Eventually honest user will make transaction to use the
reveal
method. At this point both thevalue
andnonce
are made public. - Before the honest user's transaction is picked from the mempool, the
attacker get's the now revealed parameters
value
andnonce
, sends a high gas transaction to make his commit. - In sequence frontruns the reveal transaction from honest user. (3 and 4 may even be done in the same block)
What measures can I take to prevent/mitigate this behavior?
- Just like in the contract above I need both
commit
andreveal
methods to be permanently open for interactions (Meaning I can't separate the methods in commit phase and reveal phase, as it would be done in a sealed bid auction)
My current idea for mitigation is to register the block.number
with the commit, and in the reveal
method only reserve the value if an arbitrary number of blocks has been mined since the commit was made. E.g: You made the commit on block 100, you need to wait to block 110 to call reval (otherwise the transaction is reverted). This gives a 10 blocks "advantage" to the honest user, as attacker will need to wait 10 blocks to try to frontrun.
The cons:
- It doesn't solve the problem, the
reveal
transaction may stay X blocks (X being the arbitrary number of blocks to wait) on the mempool, due to clogged blockchain, low gas, or other reasons. Long enough for the attacker to frontrun. - If the honest user mistakenly calls the reveal function before the number of blocks is mined he will have a failed transaction and the parameters will be revealed, cutting his advantage short.
- Hard to determine a reasonable number of blocks.
commit
transaction with the samevalue
(nonce
is irrelevant once revealed) and his address, followed by areveal
transaction. The point is that the attacker can frontrun the honest user'sreveal
transaction by making both thecommit
andreveal
transactions before the honestreveal
is processed.