-
Notifications
You must be signed in to change notification settings - Fork 4
/
SolidlyV2AMO.sol
405 lines (347 loc) · 16 KB
/
SolidlyV2AMO.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
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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "./MasterAMO.sol";
import {IGauge} from "./interfaces/v2/IGauge.sol";
import {ISolidlyRouter} from "./interfaces/v2/ISolidlyRouter.sol";
import {IPair} from "./interfaces/v2/IPair.sol";
import {ISolidlyV2AMO} from "./interfaces/v2/ISolidlyV2AMO.sol";
contract SolidlyV2AMO is ISolidlyV2AMO, MasterAMO {
using SafeERC20Upgradeable for IERC20Upgradeable;
/* ========== ERRORS ========== */
error TokenNotWhitelisted(address token);
error UsdAmountOutMismatch(uint256 routerOutput, uint256 balanceChange);
error LpAmountOutMismatch(uint256 routerOutput, uint256 balanceChange);
error InvalidReserveRatio(uint256 ratio);
/* ========== EVENTS ========== */
event AddLiquidityAndDeposit(uint256 boostSpent, uint256 usdSpent, uint256 liquidity, uint256 indexed tokenId);
event UnfarmBuyBurn(uint256 boostRemoved, uint256 usdRemoved, uint256 liquidity, uint256 boostAmountOut);
event GetReward(address[] tokens, uint256[] amounts);
event VaultSet(address rewardVault);
event TokenIdSet(uint256 tokenId, bool useTokenId);
event ParamsSet(
uint256 boostMultiplier,
uint24 validRangeWidth,
uint24 validRemovingRatio,
uint256 boostLowerPriceSell,
uint256 boostUpperPriceBuy,
uint256 boostSellRatio,
uint256 usdBuyRatio
);
event RewardTokensSet(address[] tokens, bool isWhitelisted);
/* ========== ROLES ========== */
/// @inheritdoc ISolidlyV2AMO
bytes32 public constant override REWARD_COLLECTOR_ROLE = keccak256("REWARD_COLLECTOR_ROLE");
/* ========== VARIABLES ========== */
/// @inheritdoc ISolidlyV2AMO
address public override router;
/// @inheritdoc ISolidlyV2AMO
address public override gauge;
/// @inheritdoc ISolidlyV2AMO
address public override rewardVault;
/// @inheritdoc ISolidlyV2AMO
mapping(address => bool) public override whitelistedRewardTokens;
/// @inheritdoc ISolidlyV2AMO
uint256 public override boostSellRatio;
/// @inheritdoc ISolidlyV2AMO
uint256 public override usdBuyRatio;
/// @inheritdoc ISolidlyV2AMO
uint256 public override tokenId;
/// @inheritdoc ISolidlyV2AMO
bool public override useTokenId;
/* ========== FUNCTIONS ========== */
function initialize(
address admin,
address boost_,
address usd_,
address boostMinter_,
address router_,
address gauge_,
address rewardVault_,
uint256 tokenId_,
bool useTokenId_,
uint256 boostMultiplier_,
uint24 validRangeWidth_,
uint24 validRemovingRatio_,
uint256 boostLowerPriceSell_,
uint256 boostUpperPriceBuy_,
uint256 boostSellRatio_,
uint256 usdBuyRatio_
) public initializer {
if (router_ == address(0) || gauge_ == address(0)) revert ZeroAddress();
address pool_ = ISolidlyRouter(router_).pairFor(usd_, boost_, true);
super.initialize(admin, boost_, usd_, pool_, boostMinter_);
router = router_;
gauge = gauge_;
_grantRole(SETTER_ROLE, msg.sender);
setVault(rewardVault_);
setTokenId(tokenId_, useTokenId_);
setParams(
boostMultiplier_,
validRangeWidth_,
validRemovingRatio_,
boostLowerPriceSell_,
boostUpperPriceBuy_,
boostSellRatio_,
usdBuyRatio_
);
_revokeRole(SETTER_ROLE, msg.sender);
}
////////////////////////// SETTER_ROLE ACTIONS //////////////////////////
/// @inheritdoc ISolidlyV2AMO
function setVault(address rewardVault_) public override onlyRole(SETTER_ROLE) {
if (rewardVault_ == address(0)) revert ZeroAddress();
rewardVault = rewardVault_;
emit VaultSet(rewardVault);
}
/// @inheritdoc ISolidlyV2AMO
function setTokenId(uint256 tokenId_, bool useTokenId_) public override onlyRole(SETTER_ROLE) {
tokenId = tokenId_;
useTokenId = useTokenId_;
emit TokenIdSet(tokenId, useTokenId);
}
/// @inheritdoc ISolidlyV2AMO
function setParams(
uint256 boostMultiplier_,
uint24 validRangeWidth_,
uint24 validRemovingRatio_,
uint256 boostLowerPriceSell_,
uint256 boostUpperPriceBuy_,
uint256 boostSellRatio_,
uint256 usdBuyRatio_
) public override onlyRole(SETTER_ROLE) {
if (validRangeWidth_ > FACTOR || validRemovingRatio_ < FACTOR) revert InvalidRatioValue(); // validRangeWidth is a few percentage points (scaled with factor). So it needs to be lower than 1 (scaled with FACTOR)
// validRemovingRatio nedds to be greater than 1 (we remove more BOOST than USD otherwise the pool is balanced )
boostMultiplier = boostMultiplier_;
validRangeWidth = validRangeWidth_;
validRemovingRatio = validRemovingRatio_;
boostLowerPriceSell = boostLowerPriceSell_;
boostUpperPriceBuy = boostUpperPriceBuy_;
boostSellRatio = boostSellRatio_;
usdBuyRatio = usdBuyRatio_;
emit ParamsSet(
boostMultiplier,
validRangeWidth,
validRemovingRatio,
boostLowerPriceSell,
boostUpperPriceBuy,
boostSellRatio,
usdBuyRatio
);
}
/// @inheritdoc ISolidlyV2AMO
function setWhitelistedTokens(address[] memory tokens, bool isWhitelisted) external override onlyRole(SETTER_ROLE) {
for (uint i = 0; i < tokens.length; i++) {
whitelistedRewardTokens[tokens[i]] = isWhitelisted;
}
emit RewardTokensSet(tokens, isWhitelisted);
}
////////////////////////// AMO_ROLE ACTIONS //////////////////////////
function _mintAndSellBoost(
uint256 boostAmount,
uint256 minUsdAmountOut,
uint256 deadline
) internal override returns (uint256 boostAmountIn, uint256 usdAmountOut) {
// Mint the specified amount of BOOST tokens
IMinter(boostMinter).protocolMint(address(this), boostAmount);
// Approve the transfer of BOOST tokens to the router
IERC20Upgradeable(boost).approve(router, boostAmount);
// Define the route to swap BOOST tokens for USD tokens
ISolidlyRouter.route[] memory routes = new ISolidlyRouter.route[](1);
routes[0] = ISolidlyRouter.route(boost, usd, true);
if (minUsdAmountOut < toUsdAmount(boostAmount)) minUsdAmountOut = toUsdAmount(boostAmount);
uint256 usdBalanceBefore = balanceOfToken(usd);
// Execute the swap and store the amounts of tokens involved
uint256[] memory amounts = ISolidlyRouter(router).swapExactTokensForTokens(
boostAmount,
minUsdAmountOut,
routes,
address(this),
deadline
);
uint256 usdBalanceAfter = balanceOfToken(usd);
boostAmountIn = amounts[0];
usdAmountOut = amounts[1];
// we check that selling BOOST yields proportionally more USD
if (usdAmountOut != usdBalanceAfter - usdBalanceBefore)
revert UsdAmountOutMismatch(usdAmountOut, usdBalanceAfter - usdBalanceBefore);
if (usdAmountOut < minUsdAmountOut) revert InsufficientOutputAmount(usdAmountOut, minUsdAmountOut);
emit MintSell(boostAmount, usdAmountOut);
}
function _addLiquidity(
uint256 usdAmount,
uint256 minBoostSpend,
uint256 minUsdSpend,
uint256 deadline
) internal override returns (uint256 boostSpent, uint256 usdSpent, uint256 liquidity) {
// We only add liquidity when price is withing range (close to $1)
// Price needs to be in range: 1 +- validRangeRatio / 1e6 == factor +- validRangeRatio
// if price is too high, we need to mint and sell more before we add liqudiity
uint256 price = boostPrice();
if (price <= FACTOR - validRangeWidth || price >= FACTOR + validRangeWidth) revert InvalidRatioToAddLiquidity();
// Mint the specified amount of BOOST tokens
uint256 boostAmount = (toBoostAmount(usdAmount) * boostMultiplier) / FACTOR;
IMinter(boostMinter).protocolMint(address(this), boostAmount);
// Approve the transfer of BOOST and USD tokens to the router
IERC20Upgradeable(boost).approve(router, boostAmount);
IERC20Upgradeable(usd).approve(router, usdAmount);
uint256 lpBalanceBefore = balanceOfToken(pool);
// Add liquidity to the BOOST-USD pool
(boostSpent, usdSpent, liquidity) = ISolidlyRouter(router).addLiquidity(
boost,
usd,
true,
boostAmount,
usdAmount,
minBoostSpend,
minUsdSpend,
address(this),
deadline
);
uint256 lpBalanceAfter = balanceOfToken(pool);
if (liquidity != lpBalanceAfter - lpBalanceBefore)
revert LpAmountOutMismatch(liquidity, lpBalanceAfter - lpBalanceBefore);
// Revoke approval from the router
IERC20Upgradeable(boost).approve(router, 0);
IERC20Upgradeable(usd).approve(router, 0);
// Approve the transfer of liquidity tokens to the gauge and deposit them
IERC20Upgradeable(pool).approve(gauge, liquidity);
if (useTokenId) {
IGauge(gauge).deposit(liquidity, tokenId);
} else {
IGauge(gauge).deposit(liquidity);
}
// Burn excessive boosts
if (boostAmount > boostSpent) IBoostStablecoin(boost).burn(boostAmount - boostSpent);
emit AddLiquidityAndDeposit(boostSpent, usdSpent, liquidity, tokenId);
}
function _unfarmBuyBurn(
uint256 liquidity,
uint256 minBoostRemove,
uint256 minUsdRemove,
uint256 minBoostAmountOut,
uint256 deadline
)
internal
override
returns (uint256 boostRemoved, uint256 usdRemoved, uint256 usdAmountIn, uint256 boostAmountOut)
{
// Withdraw the specified amount of liquidity tokens from the gauge
IGauge(gauge).withdraw(liquidity);
// Approve the transfer of liquidity tokens to the router for removal
IERC20Upgradeable(pool).approve(router, liquidity);
uint256 usdBalanceBefore = balanceOfToken(usd);
// Remove liquidity and store the amounts of USD and BOOST tokens received
(boostRemoved, usdRemoved) = ISolidlyRouter(router).removeLiquidity(
boost,
usd,
true,
liquidity,
minBoostRemove,
minUsdRemove,
address(this),
deadline
);
uint256 usdBalanceAfter = balanceOfToken(usd);
// we check that each USDC buys more than 1 BOOST (repegging is not an expense for the protocol)
if (usdRemoved != usdBalanceAfter - usdBalanceBefore)
revert UsdAmountOutMismatch(usdRemoved, usdBalanceAfter - usdBalanceBefore);
// Ensure the BOOST amount is greater than or equal to the USD amount
if ((boostRemoved * validRemovingRatio) / FACTOR < toBoostAmount(usdRemoved))
revert InvalidRatioToRemoveLiquidity();
// Define the route to swap USD tokens for BOOST tokens
ISolidlyRouter.route[] memory routes = new ISolidlyRouter.route[](1);
routes[0] = ISolidlyRouter.route(usd, boost, true);
// Approve the transfer of usd tokens to the router
IERC20Upgradeable(usd).approve(router, usdRemoved);
if (minBoostAmountOut < toBoostAmount(usdRemoved)) minBoostAmountOut = toBoostAmount(usdRemoved);
// Execute the swap and store the amounts of tokens involved
uint256[] memory amounts = ISolidlyRouter(router).swapExactTokensForTokens(
usdRemoved,
minBoostAmountOut,
routes,
address(this),
deadline
);
// Burn the BOOST tokens received from the liquidity
// Burn the BOOST tokens received from the swap
usdAmountIn = amounts[0];
boostAmountOut = amounts[1];
IBoostStablecoin(boost).burn(boostRemoved + boostAmountOut);
emit UnfarmBuyBurn(boostRemoved, usdRemoved, liquidity, boostAmountOut);
}
////////////////////////// REWARD_COLLECTOR_ROLE ACTIONS //////////////////////////
/// @inheritdoc ISolidlyV2AMO
function getReward(
address[] memory tokens,
bool passTokens
) external override onlyRole(REWARD_COLLECTOR_ROLE) whenNotPaused nonReentrant {
uint256[] memory rewardsAmounts = new uint256[](tokens.length);
// Collect the rewards
if (passTokens) {
IGauge(gauge).getReward(address(this), tokens);
} else {
IGauge(gauge).getReward();
}
// Calculate the reward amounts and transfer them to the reward vault
for (uint i = 0; i < tokens.length; i++) {
if (!whitelistedRewardTokens[tokens[i]]) revert TokenNotWhitelisted(tokens[i]);
rewardsAmounts[i] = IERC20Upgradeable(tokens[i]).balanceOf(address(this));
IERC20Upgradeable(tokens[i]).safeTransfer(rewardVault, rewardsAmounts[i]);
}
// Emit an event for collecting rewards
emit GetReward(tokens, rewardsAmounts);
}
////////////////////////// PUBLIC FUNCTIONS //////////////////////////
function _mintSellFarm() internal override returns (uint256 liquidity, uint256 newBoostPrice) {
(uint256 boostReserve, uint256 usdReserve) = getReserves();
uint256 boostAmountIn = (((usdReserve - boostReserve) / 2) * boostSellRatio) / FACTOR;
(, , , , liquidity) = _mintSellFarm(
boostAmountIn,
toUsdAmount(boostAmountIn), // minUsdAmountOut
1, // minBoostSpend
1, // minUsdSpend
block.timestamp + 1 // deadline
);
newBoostPrice = boostPrice();
}
function _unfarmBuyBurn() internal override returns (uint256 liquidity, uint256 newBoostPrice) {
(uint256 boostReserve, uint256 usdReserve) = getReserves();
uint256 usdNeeded = (((boostReserve - usdReserve) / 2) * usdBuyRatio) / FACTOR;
uint256 totalLp = IERC20Upgradeable(pool).totalSupply();
liquidity = (usdNeeded * totalLp) / usdReserve;
// Readjust the LP amount and USD needed to balance price before removing LP
// ( rationale: we first compute the amount of USD needed to rebalance the price in the pool; then first-order adjust for the fact that removing liquidity/totalLP fraction of the pool increases price impact —— less liquidity needs to be removed )
// liquidity -= liquidity ** 2 / totalLp;
_unfarmBuyBurn(
liquidity,
(liquidity * boostReserve) / totalLp, // the minBoostRemove argument
toUsdAmount(usdNeeded), // the minUsdRemove argument
usdNeeded, // the minBoostAmountOut argument
block.timestamp + 1 // deadline is next block as the computation is valid instantly
);
newBoostPrice = boostPrice();
}
function _validateSwap(bool boostForUsd) internal view override {
(uint256 boostReserve, uint256 usdReserve) = getReserves();
if (boostForUsd && boostReserve >= usdReserve)
revert InvalidReserveRatio({ratio: (FACTOR * usdReserve) / boostReserve});
if (!boostForUsd && usdReserve >= boostReserve)
revert InvalidReserveRatio({ratio: (FACTOR * usdReserve) / boostReserve});
}
////////////////////////// VIEW FUNCTIONS //////////////////////////
/// @inheritdoc IMasterAMO
function boostPrice() public view override returns (uint256 price) {
uint256 amountOut = IPair(pool).getAmountOut(10 ** boostDecimals, boost);
price = amountOut / 10 ** (usdDecimals - PRICE_DECIMALS);
}
function getReserves() public view returns (uint256 boostReserve, uint256 usdReserve) {
(uint256 reserve0, uint256 reserve1, ) = IPair(pool).getReserves();
if (boost < usd) {
boostReserve = reserve0;
usdReserve = toBoostAmount(reserve1); // scaled
} else {
boostReserve = reserve1;
usdReserve = toBoostAmount(reserve0); // scaled
}
}
}