Nomad Bridge
Total Losses
$190.0M+
Date
Network
Categories
bridges data validationStep-by-step
- Call
processwith an arbitrary message to the bridge.
Detailed Description
The root of the problem lies in the initialize method. In the bad initialization tx the _commitedRoot was sent as 0x00. This causes the confirmAt[0x00] value to be 1.
function initialize(uint32 _remoteDomain, address _updater, bytes32 _committedRoot, uint256 _optimisticSeconds) public initializer {
__NomadBase_initialize(_updater);
// set storage variables
entered = 1;
remoteDomain = _remoteDomain;
committedRoot = _committedRoot;
// pre-approve the committed root.
confirmAt[_committedRoot] = 1;
_setOptimisticTimeout(_optimisticSeconds);
}
So far, there are no obvious issues. The problem is apparent when you check the process and acceptableRoot methods: sending an arbitrary message results in a call to acceptableRoot(messages[_messageHash]). If the message is not in the messages map (ie: it has not been processed before), this triggers a call with to acceptableRoot(0x00).
Because the update set confirmAt[0x00] at 1, this will end up giving true for all messages! So anyone can send any message to process and get it approved by the contract.
function process(bytes memory _message) public returns (bool _success) {
// ensure message was meant for this domain
bytes29 _m = _message.ref(0);
require(_m.destination() == localDomain, "!destination");
// ensure message has been proven
bytes32 _messageHash = _m.keccak();
require(acceptableRoot(messages[_messageHash]), "!proven");
// check re-entrancy guard
require(entered == 1, "!reentrant");
entered = 0;
// update message status as processed
messages[_messageHash] = LEGACY_STATUS_PROCESSED;
// call handle function
IMessageRecipient(_m.recipientAddress()).handle(
_m.origin(),
_m.nonce(),
_m.sender(),
_m.body().clone()
);
// emit process results
emit Process(_messageHash, true, "");
// reset re-entrancy guard
entered = 1;
// return true
return true;
}
function acceptableRoot(bytes32 _root) public view returns (bool) {
// this is backwards-compatibility for messages proven/processed
// under previous versions
if (_root == LEGACY_STATUS_PROVEN) return true;
if (_root == LEGACY_STATUS_PROCESSED) return false;
uint256 _time = confirmAt[_root];
if (_time == 0) {
return false;
}
return block.timestamp >= _time;
}
Possible mitigations
- Make sure that initializers uphold invariants. In this case, a
require(_committedRoot != 0)would have prevented the attack.