/
BalancerFlashloanDirectMintHandler.sol
217 lines (179 loc) · 9.4 KB
/
BalancerFlashloanDirectMintHandler.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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;
import { IonHandlerBase } from "./IonHandlerBase.sol";
import { IVault, IERC20 as IERC20Balancer } from "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
import { IFlashLoanRecipient } from "@balancer-labs/v2-interfaces/contracts/vault/IFlashLoanRecipient.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
IVault constant VAULT = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
/**
* @dev There are a couple things to consider here from a security perspective. The
* first one is that the flashloan callback must only be callable from the
* Balancer vault. This ensures that nobody can pass arbitrary data to the
* callback. The second one is that the flashloan must only be initialized from
* this contract. This is a trickier one to enforce since Balancer flashloans
* are not EIP-3156 compliant and do not pass on the initiator through the
* callback. To get around this, an inverse reentrancy lock of sorts is used.
* The lock is set to 2 when a flashloan is initiated and set to 1 once the
* callback execution terminates. If the lock is not 2 when the callback is
* called, then the flashloan was not initiated by this contract and the tx is
* reverted.
*
* This contract currently deposits directly into LST contract 1:1. It should be
* noted that a more favorable trade could be possible via DEXs.
*/
abstract contract BalancerFlashloanDirectMintHandler is IonHandlerBase, IFlashLoanRecipient {
using SafeERC20 for IERC20;
error ReceiveCallerNotVault(address unauthorizedCaller);
error FlashLoanedTooManyTokens(uint256 amountTokens);
error FlashloanedInvalidToken(address tokenAddress);
error ExternalBalancerFlashloanNotAllowed();
uint256 private flashloanInitiated = 1;
/**
* @notice Code assumes Balancer flashloans remain free
* @param initialDeposit in collateral terms
* @param resultingAdditionalCollateral in collateral terms
* @param maxResultingDebt in WETH terms. While it is unlikely that the
* exchange rate changes from when a transaction is submitted versus when it
* is executed, it is still possible so we want to allow for a bound here,
* even though it doesn't pose the same level of threat as slippage.
*/
function flashLeverageCollateral(
uint256 initialDeposit,
uint256 resultingAdditionalCollateral,
uint256 maxResultingDebt
)
external
{
LST_TOKEN.safeTransferFrom(msg.sender, address(this), initialDeposit);
uint256 amountToLeverage = resultingAdditionalCollateral - initialDeposit; // in collateral terms
IERC20Balancer[] memory addresses = new IERC20Balancer[](1);
addresses[0] = IERC20Balancer(address(LST_TOKEN));
uint256[] memory amounts = new uint256[](1);
amounts[0] = amountToLeverage;
if (amounts[0] == 0) {
// AmountToBorrow.IS_MAX because we don't want to create any new debt here
_depositAndBorrow(msg.sender, address(this), resultingAdditionalCollateral, 0, AmountToBorrow.IS_MAX);
return;
}
uint256 wethRequiredForRepayment = _getEthAmountInForLstAmountOut(amountToLeverage);
if (wethRequiredForRepayment > maxResultingDebt) {
revert FlashloanRepaymentTooExpensive(wethRequiredForRepayment, maxResultingDebt);
}
// Prevents attackers from initiating flashloan and passing malicious data through callback
flashloanInitiated = 2;
VAULT.flashLoan(
IFlashLoanRecipient(address(this)),
addresses,
amounts,
abi.encode(msg.sender, initialDeposit, resultingAdditionalCollateral, maxResultingDebt)
);
flashloanInitiated = 1;
}
/**
* @notice Code assumes Balancer flashloans remain free
* @param initialDeposit in collateral terms
* @param resultingAdditionalCollateral in collateral terms
* @param maxResultingDebt in WETH terms. While it is unlikely that the
* exchange rate changes from when a transaction is submitted versus when it
* is executed, it is still possible so we want to allow for a bound here,
* even though it doesn't pose the same level of threat as slippage.
*/
function flashLeverageWeth(
uint256 initialDeposit,
uint256 resultingAdditionalCollateral,
uint256 maxResultingDebt
)
external
payable
{
LST_TOKEN.safeTransferFrom(msg.sender, address(this), initialDeposit);
IERC20Balancer[] memory addresses = new IERC20Balancer[](1);
addresses[0] = IERC20Balancer(address(WETH));
uint256 amountLst = resultingAdditionalCollateral - initialDeposit; // in collateral terms
uint256 amountWethToFlashloan = _getEthAmountInForLstAmountOut(amountLst);
if (amountWethToFlashloan == 0) {
// AmountToBorrow.IS_MAX because we don't want to create any new debt here
_depositAndBorrow(msg.sender, address(this), resultingAdditionalCollateral, 0, AmountToBorrow.IS_MAX);
return;
}
// It is technically possible to accrue slight dust amounts more of debt
// than maxResultingDebt because you may need to borrow slightly more at
// the IonPool level to receieve the desired amount of WETH. This is
// because the IonPool will round in its favor and always gives out dust
// amounts less of WETH than the debt accrued to the position. However,
// this will always be bounded by the rate of the ilk at the time
// divided by RAY and will NEVER be subject to slippage, which is what
// we really want to protect against.
if (amountWethToFlashloan > maxResultingDebt) {
revert FlashloanRepaymentTooExpensive(amountWethToFlashloan, maxResultingDebt);
}
uint256[] memory amounts = new uint256[](1);
amounts[0] = amountWethToFlashloan;
flashloanInitiated = 2;
VAULT.flashLoan(
IFlashLoanRecipient(address(this)),
addresses,
amounts,
abi.encode(msg.sender, initialDeposit, resultingAdditionalCollateral, maxResultingDebt)
);
flashloanInitiated = 1;
}
/**
* @notice Code assumes Balancer flashloans remain free.
* @dev This function is intended to never be called directly. It should
* only be called by the Balancer VAULT during a flashloan initiated by this
* contract. This callback logic only handles the creation of leverage
* positions by minting. Since not all tokens have withdrawable liquidity
* via the LST protocol directly, deleverage through the protocol will need
* to be implemented in the inheriting contract.
*
* @param tokens Array of tokens flash loaned
* @param amounts amounts flash loaned
* @param userData arbitrary data passed from initiator of flash loan
*/
function receiveFlashLoan(
IERC20Balancer[] memory tokens,
uint256[] memory amounts,
uint256[] memory,
bytes memory userData
)
external
override
{
if (tokens.length > 1) revert FlashLoanedTooManyTokens(tokens.length);
if (msg.sender != address(VAULT)) revert ReceiveCallerNotVault(msg.sender);
if (flashloanInitiated != 2) revert ExternalBalancerFlashloanNotAllowed();
IERC20Balancer token = tokens[0];
(address user, uint256 initialDeposit, uint256 resultingAdditionalCollateral, uint256 maxResultingDebt) =
abi.decode(userData, (address, uint256, uint256, uint256));
// Flashloaned WETH needs to be converted into collateral asset
if (address(token) == address(WETH)) {
uint256 collateralFromDeposit = _depositWethForLst(amounts[0]);
// Sanity checks
assert(collateralFromDeposit + initialDeposit == resultingAdditionalCollateral);
assert(collateralFromDeposit <= maxResultingDebt);
// AmountToBorrow.IS_MIN because we want to make sure enough is borrowed to cover flashloan
_depositAndBorrow(user, address(this), resultingAdditionalCollateral, amounts[0], AmountToBorrow.IS_MIN);
WETH.transfer(address(VAULT), amounts[0]);
} else {
if (address(LST_TOKEN) != address(token)) revert FlashloanedInvalidToken(address(token));
uint256 wethToBorrow = _getEthAmountInForLstAmountOut(amounts[0]);
// Sanity checks
assert(amounts[0] + initialDeposit == resultingAdditionalCollateral);
assert(wethToBorrow <= maxResultingDebt);
// AmountToBorrow.IS_MIN because we want to make sure enough is borrowed to cover flashloan
_depositAndBorrow(user, address(this), resultingAdditionalCollateral, wethToBorrow, AmountToBorrow.IS_MIN);
// Convert borrowed WETH back to collateral token
uint256 tokenAmountReceived = _depositWethForLst(wethToBorrow);
LST_TOKEN.safeTransfer(address(VAULT), tokenAmountReceived);
}
}
/**
* @dev Unwraps weth into eth and deposits into lst contract
* @param amountWeth to deposit
* @return amountLst received
*/
function _depositWethForLst(uint256 amountWeth) internal virtual returns (uint256);
function _getEthAmountInForLstAmountOut(uint256 amountLst) internal view virtual returns (uint256);
}