Nomad is an implementation and extension of the Optics protocol (OPTimistic Interchain Communication), i.e. an optimistic cross-chain bridge.
It was hacked due to wrong initialisation parameters.
Attack Vectors & Details
1)In this tx, the hacker just called process() in Replica.sol. Once you passed these three requires, the specified operations will be processed by NomadBridge.handle().
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;
}
All the requires passed. The first and third ones are obvious, so check the second one: acceptableRoot(messages[_messageHash]). messages[_messageHash] = 0x0, because the message was forged by the hacker(non-existent in this contractâs history).
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;
}
In a mapping, it will be 0 by default. LEGACY_XXXX = 1 or 2, irrelevant here. Next is confirmAt[_root], as long as it != 0 and < current block time then the check will pass. So whatâs the value of confirmAt[0x0] ?
Here is the wrong initialisation param: confirmAt[_committedRoot] = 1. They passed _committedRoot = 0x0 while initialising the contract. So confirmAt[0x0] = 1. Check passed.
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);
}
Public Loot
Everyone can copy&paste the hacker's tx data and modify the receiver's address to benefit their own. It was reported there were at least 70 addresses did this, the public loot.