forked from timeless-fi/options-token
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathOptionsCompounder.sol
More file actions
434 lines (376 loc) · 16.8 KB
/
OptionsCompounder.sol
File metadata and controls
434 lines (376 loc) · 16.8 KB
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
/* Imports */
import {IFlashLoanReceiver} from "./interfaces/IFlashLoanReceiver.sol";
import {ILendingPoolAddressesProvider} from "./interfaces/ILendingPoolAddressesProvider.sol";
import {ILendingPool} from "./interfaces/ILendingPool.sol";
import {IOracle} from "./interfaces/IOracle.sol";
import {IOptionsToken} from "./interfaces/IOptionsToken.sol";
import {DiscountExerciseParams, DiscountExercise} from "./exercise/DiscountExercise.sol";
import {IERC20} from "oz/token/ERC20/IERC20.sol";
import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol";
import {OwnableUpgradeable} from "oz-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "oz-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {SafeERC20} from "oz/token/ERC20/utils/SafeERC20.sol";
import {ExchangeType, SwapProps, SwapHelper} from "./helpers/SwapHelper.sol";
import "./interfaces/IOptionsCompounder.sol";
/**
* @title Consumes options tokens, exercise them with flashloaned asset and converts gain to strategy want token
* @author Eidolon, xRave110
* @dev Abstract contract which shall be inherited by the strategy
*/
contract OptionsCompounder is IFlashLoanReceiver, OwnableUpgradeable, UUPSUpgradeable, SwapHelper {
using FixedPointMathLib for uint256;
using SafeERC20 for IERC20;
/* Internal struct */
struct FlashloanParams {
uint256 optionsAmount;
address exerciserContract;
address sender;
uint256 initialBalance;
uint256 minPaymentAmount;
}
/* Modifier */
modifier onlyStrat() {
if (!_isStratAvailable(msg.sender)) {
revert OptionsCompounder__OnlyStratAllowed();
}
_;
}
/* Constants */
uint8 constant MAX_NR_OF_FLASHLOAN_ASSETS = 1;
uint256 public constant UPGRADE_TIMELOCK = 48 hours;
uint256 public constant FUTURE_NEXT_PROPOSAL_TIME = 365 days * 100;
/* Storages */
address public swapper;
ILendingPoolAddressesProvider private addressProvider;
ILendingPool private lendingPool;
bool private flashloanFinished;
IOracle private oracle;
IOptionsToken public optionsToken;
uint256 public upgradeProposalTime;
address public nextImplementation;
address[] private strats;
/* Events */
event OTokenCompounded(uint256 indexed gainInPayment, uint256 indexed returned);
/* Modifiers */
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
/**
* @notice Initializes params
* @dev Replaces constructor due to upgradeable nature of the contract. Can be executed only once at init.
* @param _optionsToken - option token address which allows to redeem underlying token via operation "exercise"
* @param _addressProvider - address lending pool address provider - necessary for flashloan operations
* @param _swapProps - swap properites for all swaps in the contract
* @param _oracle - oracles used in all swaps in the contract
* @param _strats - list of strategies used to call harvestOTokens()
*
*/
function initialize(
address _optionsToken,
address _addressProvider,
address _swapper,
SwapProps memory _swapProps,
IOracle _oracle,
address[] memory _strats
) public initializer {
__Ownable_init();
_setOptionsToken(_optionsToken);
_setSwapProps(_swapProps);
_setOracle(_oracle);
_setSwapper(_swapper);
_setStrats(_strats);
flashloanFinished = true;
_setAddressProvider(_addressProvider);
__UUPSUpgradeable_init();
_clearUpgradeCooldown();
}
/**
* Setters **********************************
*/
/**
* @notice Sets option token address
* @dev Can be executed only by admins
* @param _optionsToken - address of option token contract
*/
function setOptionsToken(address _optionsToken) external onlyOwner {
_setOptionsToken(_optionsToken);
}
function _setOptionsToken(address _optionsToken) internal {
if (_optionsToken == address(0)) {
revert OptionsCompounder__ParamHasAddressZero();
}
optionsToken = IOptionsToken(_optionsToken);
}
function setSwapProps(SwapProps memory _swapProps) external override onlyOwner {
_setSwapProps(_swapProps);
}
function setOracle(IOracle _oracle) external onlyOwner {
_setOracle(_oracle);
}
function _setOracle(IOracle _oracle) internal {
if (address(_oracle) == address(0)) {
revert OptionsCompounder__ParamHasAddressZero();
}
oracle = _oracle;
}
function setSwapper(address _swapper) external onlyOwner {
_setSwapper(_swapper);
}
function _setSwapper(address _swapper) internal {
if (_swapper == address(0)) {
revert OptionsCompounder__ParamHasAddressZero();
}
swapper = _swapper;
}
function setAddressProvider(address _addressProvider) external onlyOwner {
_setAddressProvider(_addressProvider);
}
function _setAddressProvider(address _addressProvider) internal {
if (_addressProvider == address(0)) {
revert OptionsCompounder__ParamHasAddressZero();
}
addressProvider = ILendingPoolAddressesProvider(_addressProvider);
lendingPool = ILendingPool(addressProvider.getLendingPool());
}
function setStrats(address[] memory _strats) external onlyOwner {
_setStrats(_strats);
}
function _setStrats(address[] memory _strats) internal {
_deleteStrats();
for (uint256 idx = 0; idx < _strats.length; idx++) {
_addStrat(_strats[idx]);
}
}
function addStrat(address _strat) external onlyOwner {
_addStrat(_strat);
}
/**
* @dev Function will be used sporadically with number of strategies less than 10, so no need to add any gas optimization
*/
function _addStrat(address _strat) internal {
if (_strat == address(0)) {
revert OptionsCompounder__ParamHasAddressZero();
}
if (!_isStratAvailable(_strat)) {
strats.push(_strat);
}
}
function _deleteStrats() internal {
if (strats.length != 0) {
delete strats;
}
}
/**
* @notice Function initiates flashloan to get assets for exercising options.
* @dev Can be executed only by keeper role. Reentrance protected.
* @param amount - amount of option tokens to exercise
* @param exerciseContract - address of exercise contract (DiscountContract)
* @param minWantAmount - minimal amount of want when the flashloan is considered as profitable
*/
function harvestOTokens(uint256 amount, address exerciseContract, uint256 minWantAmount) external onlyStrat {
_harvestOTokens(amount, exerciseContract, minWantAmount);
}
/**
* @notice Function initiates flashloan to get assets for exercising options.
* @dev Can be executed only by keeper role. Reentrance protected.
* @param amount - amount of option tokens to exercise
* @param exerciseContract - address of exercise contract (DiscountContract)
* @param minPaymentAmount - minimal amount of want when the flashloan is considered as profitable
*/
function _harvestOTokens(uint256 amount, address exerciseContract, uint256 minPaymentAmount) private {
/* Check exercise contract validity */
if (optionsToken.isExerciseContract(exerciseContract) == false) {
revert OptionsCompounder__NotExerciseContract();
}
/* Reentrance protection */
if (flashloanFinished == false) {
revert OptionsCompounder__FlashloanNotFinished();
}
if (minPaymentAmount == 0) {
revert OptionsCompounder__WrongMinPaymentAmount();
}
/* Locals */
IERC20 paymentToken = DiscountExercise(exerciseContract).paymentToken();
address[] memory assets = new address[](1);
assets[0] = address(paymentToken);
uint256[] memory amounts = new uint256[](1);
amounts[0] = DiscountExercise(exerciseContract).getPaymentAmount(amount);
// 0 = no debt, 1 = stable, 2 = variable
uint256[] memory modes = new uint256[](1);
modes[0] = 0;
/* necesary params used during flashloan execution */
bytes memory params =
abi.encode(FlashloanParams(amount, exerciseContract, msg.sender, paymentToken.balanceOf(address(this)), minPaymentAmount));
flashloanFinished = false;
lendingPool.flashLoan(
address(this), // receiver
assets,
amounts,
modes,
address(this), // onBehalf
params,
0 // referal code
);
}
/**
* @notice Exercise option tokens with flash loaned token and compound rewards
* in underlying tokens to stratefy want token
* @dev Function is called after this contract has received the flash loaned amount
* @param assets - list of assets flash loaned (only one asset allowed in this case)
* @param amounts - list of amounts flash loaned (only one amount allowed in this case)
* @param premiums - list of premiums for flash loaned assets (only one premium allowed in this case)
* @param params - encoded data about options amount, exercise contract address, initial balance and minimal want amount
* @return bool - value that returns whether flashloan operation went well
*/
function executeOperation(address[] calldata assets, uint256[] calldata amounts, uint256[] calldata premiums, address, bytes calldata params)
external
override
returns (bool)
{
if (flashloanFinished != false || msg.sender != address(lendingPool)) {
revert OptionsCompounder__FlashloanNotTriggered();
}
if (assets.length > MAX_NR_OF_FLASHLOAN_ASSETS || amounts.length > MAX_NR_OF_FLASHLOAN_ASSETS || premiums.length > MAX_NR_OF_FLASHLOAN_ASSETS)
{
revert OptionsCompounder__TooMuchAssetsLoaned();
}
/* Later the gain can be local variable */
_exerciseOptionAndReturnDebt(assets[0], amounts[0], premiums[0], params);
flashloanFinished = true;
return true;
}
/**
* @dev Private function that helps to execute flashloan and makes it more modular
* Emits event with gain from the option exercise after repayment of all debt from flashloan
* and amount of repaid assets
* @param asset - list of assets flash loaned (only one asset allowed in this case)
* @param amount - list of amounts flash loaned (only one amount allowed in this case)
* @param premium - list of premiums for flash loaned assets (only one premium allowed in this case)
* @param params - encoded data about options amount, exercise contract address, initial balance and minimal want amount
*/
function _exerciseOptionAndReturnDebt(address asset, uint256 amount, uint256 premium, bytes calldata params) private {
FlashloanParams memory flashloanParams = abi.decode(params, (FlashloanParams));
uint256 assetBalance = 0;
uint256 minAmountOut = 0;
/* Get underlying and payment tokens to make sure there is no change between
harvest and excersice */
IERC20 underlyingToken = DiscountExercise(flashloanParams.exerciserContract).underlyingToken();
{
IERC20 paymentToken = DiscountExercise(flashloanParams.exerciserContract).paymentToken();
/* Asset and paymentToken should be the same addresses */
if (asset != address(paymentToken)) {
revert OptionsCompounder__AssetNotEqualToPaymentToken();
}
}
{
IERC20(address(optionsToken)).safeTransferFrom(flashloanParams.sender, address(this), flashloanParams.optionsAmount);
bytes memory exerciseParams =
abi.encode(DiscountExerciseParams({maxPaymentAmount: amount, deadline: block.timestamp, isInstantExit: false}));
if (underlyingToken.balanceOf(flashloanParams.exerciserContract) < flashloanParams.optionsAmount) {
revert OptionsCompounder__NotEnoughUnderlyingTokens();
}
/* Approve spending payment token */
IERC20(asset).approve(flashloanParams.exerciserContract, amount);
/* Exercise in order to get underlying token */
optionsToken.exercise(flashloanParams.optionsAmount, address(this), flashloanParams.exerciserContract, exerciseParams);
/* Approve spending payment token to 0 for safety */
IERC20(asset).approve(flashloanParams.exerciserContract, 0);
}
{
uint256 balanceOfUnderlyingToken = 0;
uint256 swapAmountOut = 0;
balanceOfUnderlyingToken = underlyingToken.balanceOf(address(this));
minAmountOut = _getMinAmountOutData(balanceOfUnderlyingToken, swapProps.maxSwapSlippage, address(oracle));
/* Approve the underlying token to make swap */
underlyingToken.approve(swapper, balanceOfUnderlyingToken);
/* Swap underlying token to payment token (asset) */
swapAmountOut = _generalSwap(
swapProps.exchangeTypes, address(underlyingToken), asset, balanceOfUnderlyingToken, minAmountOut, swapProps.exchangeAddress
);
if (swapAmountOut == 0) {
revert OptionsCompounder__AmountOutIsZero();
}
/* Approve the underlying token to 0 for safety */
underlyingToken.approve(swapper, 0);
}
/* Calculate profit and revert if it is not profitable */
{
uint256 gainInPaymentToken = 0;
uint256 totalAmountToPay = amount + premium;
assetBalance = IERC20(asset).balanceOf(address(this));
if (
(
(assetBalance < flashloanParams.initialBalance)
|| (assetBalance - flashloanParams.initialBalance) < (totalAmountToPay + flashloanParams.minPaymentAmount)
)
) {
revert OptionsCompounder__FlashloanNotProfitableEnough();
}
/* Protected against underflows by statement above */
gainInPaymentToken = assetBalance - totalAmountToPay - flashloanParams.initialBalance;
/* Approve lending pool to spend borrowed tokens + premium */
IERC20(asset).approve(address(lendingPool), totalAmountToPay);
IERC20(asset).safeTransfer(flashloanParams.sender, gainInPaymentToken);
emit OTokenCompounded(gainInPaymentToken, totalAmountToPay);
}
}
/**
* @dev This function must be called prior to upgrading the implementation.
* It's required to wait UPGRADE_TIMELOCK seconds before executing the upgrade.
*/
function initiateUpgradeCooldown(address _nextImplementation) external onlyOwner {
upgradeProposalTime = block.timestamp;
nextImplementation = _nextImplementation;
}
/**
* @dev This function is called:
* - in initialize()
* - as part of a successful upgrade
* - manually to clear the upgrade cooldown.
*/
function _clearUpgradeCooldown() internal {
upgradeProposalTime = block.timestamp + FUTURE_NEXT_PROPOSAL_TIME;
}
function clearUpgradeCooldown() external onlyOwner {
_clearUpgradeCooldown();
}
/**
* @dev This function must be overriden simply for access control purposes.
* Only the owner can upgrade the implementation once the timelock
* has passed.
*/
function _authorizeUpgrade(address _nextImplementation) internal override onlyOwner {
require(upgradeProposalTime + UPGRADE_TIMELOCK < block.timestamp, "Upgrade cooldown not initiated or still ongoing");
require(_nextImplementation == nextImplementation, "Incorrect implementation");
_clearUpgradeCooldown();
}
/**
* Getters **********************************
*/
function isStratAvailable(address strat) external view returns (bool) {
return _isStratAvailable(strat);
}
function _isStratAvailable(address strat) internal view returns (bool) {
bool isStrat = false;
address[] memory _strats = strats;
for (uint256 idx = 0; idx < _strats.length; idx++) {
if (strat == _strats[idx]) {
isStrat = true;
break;
}
}
return isStrat;
}
function getStrats() external view returns (address[] memory) {
return strats;
}
function ADDRESSES_PROVIDER() external view returns (ILendingPoolAddressesProvider) {
return addressProvider;
}
function LENDING_POOL() external view returns (ILendingPool) {
return lendingPool;
}
}