Skip to content

Commit

Permalink
Enable pool fees when rebalancing Aerodrome AMO (#2276)
Browse files Browse the repository at this point in the history
* make aerodrome AMO leave swap fees when rebalancing

* minor adjustements and test fixes

* fix bug

* burn OETHb on the contract and minor renames

* fix various issues with quoter and the tests

* space

* undo removal of default values

* remove comment

* add view to _checkForExpectedPoolPrice function

* burn all the Super OETHb after performing the swap

* put back burning OETHb after the swap function

* add comment

* minor test correction

* OETHb on the contract after rebalance should be 0

* simplify nr of functions

* Aerodrome AMO remove liquidity proper fix (#2290)

* enhance the way we withdraw liquidity

* better location

* fix comment

* update tests and simplify liquidity calculation

* small fix

* ignore dust in burning oeth as well

* Update deployment ID

---------

Co-authored-by: Shahul Hameed <10547529+shahthepro@users.noreply.github.com>
  • Loading branch information
sparrowDom and shahthepro authored Oct 28, 2024
1 parent 16d7435 commit dfde2d8
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 108 deletions.
5 changes: 5 additions & 0 deletions contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,9 @@ interface IAMOStrategy {
function claimGovernance() external;

function transferGovernance(address _governor) external;

function getPositionPrincipal()
external
view
returns (uint256 _amountWeth, uint256 _amountOethb);
}
2 changes: 1 addition & 1 deletion contracts/contracts/interfaces/aerodrome/ISugarHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ interface ISugarHelper {
uint160 sqrtRatioX96,
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96
) external pure returns (uint256 liquidity);
) external pure returns (uint128 liquidity);

/// @notice Computes the amount of token0 for a given amount of token1 and price range
/// @param amount1 Amount of token1 to estimate liquidity
Expand Down
84 changes: 55 additions & 29 deletions contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {
*/
function depositAll() external override onlyVault nonReentrant {
uint256 _wethBalance = IERC20(WETH).balanceOf(address(this));
if (_wethBalance > 0) {
if (_wethBalance > 1e12) {
_deposit(WETH, _wethBalance);
}
}
Expand Down Expand Up @@ -426,11 +426,13 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {

/**
* When rebalance is called for the first time there is no strategy
* liquidity in the pool yet. The full liquidity removal is thus skipped.
* liquidity in the pool yet. The liquidity removal is thus skipped.
* Also execute this function when WETH is required for the swap.
*/
if (tokenId != 0) {
_removeLiquidity(1e18);
if (tokenId != 0 && _swapWeth && _amountToSwap > 0) {
_ensureWETHBalance(_amountToSwap);
}

// in some cases we will just want to add liquidity and not issue a swap to move the
// active trading position within the pool
if (_amountToSwap > 0) {
Expand Down Expand Up @@ -531,6 +533,8 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {
_amountOethbCollected,
underlyingAssets
);

_burnOethbOnTheContract();
}

/**
Expand All @@ -545,6 +549,8 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {
uint256 _balance = _tokenToSwap.balanceOf(address(this));

if (_balance < _amountToSwap) {
// This should never trigger since _ensureWETHBalance will already
// throw an error if there is not enough WETH
if (_swapWeth) {
revert NotEnoughWethForSwap(_balance, _amountToSwap);
}
Expand Down Expand Up @@ -576,6 +582,14 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {
: sqrtRatioX96TickHigher
})
);

/**
* In the interest of each function in _rebalance to leave the contract state as
* clean as possible the OETHb tokens here are burned. This decreases the
* dependence where `_swapToDesiredPosition` function relies on later functions
* (`addLiquidity`) to burn the OETHb. Reducing the risk of error introduction.
*/
_burnOethbOnTheContract();
}

/**
Expand All @@ -590,7 +604,10 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {
function _addLiquidity() internal gaugeUnstakeAndRestake {
uint256 _wethBalance = IERC20(WETH).balanceOf(address(this));
uint256 _oethbBalance = IERC20(OETHb).balanceOf(address(this));
require(_wethBalance > 0, "Must add some WETH");
// don't deposit small liquidity amounts
if (_wethBalance <= 1e12) {
return;
}

uint160 _currentPrice = getPoolX96Price();
/**
Expand Down Expand Up @@ -709,6 +726,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {
*/
function _checkForExpectedPoolPrice(bool throwException)
internal
view
returns (bool _isExpectedRange, uint256 _wethSharePct)
{
require(
Expand Down Expand Up @@ -759,7 +777,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {
*/
function _burnOethbOnTheContract() internal {
uint256 _oethbBalance = IERC20(OETHb).balanceOf(address(this));
if (_oethbBalance > 0) {
if (_oethbBalance > 1e12) {
IVault(vaultAddress).burnForStrategy(_oethbBalance);
}
}
Expand Down Expand Up @@ -802,6 +820,36 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {
emit UnderlyingAssetsUpdated(underlyingAssets);
}

/**
* @dev This function removes the appropriate amount of liquidity to assure that the required
* amount of WETH is available on the contract
*
* @param _amount WETH balance required on the contract
*/
function _ensureWETHBalance(uint256 _amount) internal {
uint256 _wethBalance = IERC20(WETH).balanceOf(address(this));
if (_wethBalance >= _amount) {
return;
}

require(tokenId != 0, "No liquidity available");
uint256 _additionalWethRequired = _amount - _wethBalance;
(uint256 _wethInThePool, ) = getPositionPrincipal();

if (_wethInThePool < _additionalWethRequired) {
revert NotEnoughWethLiquidity(
_wethInThePool,
_additionalWethRequired
);
}

uint256 shareOfWethToRemove = Math.min(
_additionalWethRequired.divPrecisely(_wethInThePool) + 1,
1e18
);
_removeLiquidity(shareOfWethToRemove);
}

/**
* @notice Withdraw an `amount` of assets from the platform and
* send to the `_recipient`.
Expand All @@ -817,28 +865,8 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {
require(_asset == WETH, "Unsupported asset");
require(_recipient == vaultAddress, "Only withdraw to vault allowed");

uint256 _wethBalance = IERC20(WETH).balanceOf(address(this));
if (_wethBalance < _amount) {
require(tokenId != 0, "No liquidity available");
uint256 _additionalWethRequired = _amount - _wethBalance;
(uint256 _wethInThePool, ) = getPositionPrincipal();

if (_wethInThePool < _additionalWethRequired) {
revert NotEnoughWethLiquidity(
_wethInThePool,
_additionalWethRequired
);
}

uint256 shareOfWethToRemove = Math.min(
_additionalWethRequired.divPrecisely(_wethInThePool) + 1,
1e18
);
_removeLiquidity(shareOfWethToRemove);
}
_ensureWETHBalance(_amount);

// burn remaining OETHb
_burnOethbOnTheContract();
_withdraw(_recipient, _amount);
}

Expand All @@ -854,8 +882,6 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy {
if (_balance > 0) {
_withdraw(vaultAddress, _balance);
}
// burn remaining OETHb
_burnOethbOnTheContract();
}

function _withdraw(address _recipient, uint256 _amount) internal {
Expand Down
70 changes: 48 additions & 22 deletions contracts/contracts/utils/AerodromeAMOQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,10 @@ contract QuoterHelper {
/// --- CONSTANT & IMMUTABLE
////////////////////////////////////////////////////////////////
uint256 public constant BINARY_MIN_AMOUNT = 1 wei;
uint256 public constant BINARY_MAX_AMOUNT_FOR_REBALANCE = 3_000 ether;
uint256 public constant BINARY_MAX_AMOUNT_FOR_PUSH_PRICE = 5_000_000 ether;

uint256 public constant BINARY_MAX_ITERATIONS = 100;
uint256 public constant BINARY_MAX_ITERATIONS = 40;
uint256 public constant PERCENTAGE_BASE = 1e18; // 100%
uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e12; // 0.0001%
uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e15; // 0.1%

////////////////////////////////////////////////////////////////
/// --- VARIABLES STORAGE
Expand Down Expand Up @@ -101,9 +99,11 @@ contract QuoterHelper {

strategy.setAllowedPoolWethShareInterval(shareStart, shareEnd);
}

uint256 iterations = 0;
uint256 low = BINARY_MIN_AMOUNT;
uint256 high = BINARY_MAX_AMOUNT_FOR_REBALANCE;
uint256 high;
(high, ) = strategy.getPositionPrincipal();
int24 lowerTick = strategy.lowerTick();
int24 upperTick = strategy.upperTick();
bool swapWETHForOETHB = getSwapDirectionForRebalance();
Expand Down Expand Up @@ -338,6 +338,14 @@ contract QuoterHelper {
return currentPrice > targetPrice;
}

// returns total amount in the position principal of the Aerodrome AMO strategy. Needed as a
// separate function because of the limitation in local variable count in getAmountToSwapToReachPrice
function getTotalStrategyPosition() internal returns (uint256) {
(uint256 wethAmount, uint256 oethBalance) = strategy
.getPositionPrincipal();
return wethAmount + oethBalance;
}

/// @notice Get the amount of tokens to swap to reach the target price.
/// @dev This act like a quoter, i.e. the transaction is not performed.
/// @dev Because the amount to swap can be largely overestimated, because CLAMM alow partial orders,
Expand All @@ -359,34 +367,53 @@ contract QuoterHelper {
{
uint256 iterations = 0;
uint256 low = BINARY_MIN_AMOUNT;
uint256 high = BINARY_MAX_AMOUNT_FOR_PUSH_PRICE;
// high search start is twice the position principle of Aerodrome AMO strategy.
// should be more than enough
uint256 high = getTotalStrategyPosition() * 2;
bool swapWETHForOETHB = getSwapDirection(sqrtPriceTargetX96);

while (low <= high && iterations < BINARY_MAX_ITERATIONS) {
uint256 mid = (low + high) / 2;

// Call QuoterV2 from SugarHelper
(, uint160 sqrtPriceX96After, , ) = quoterV2.quoteExactInputSingle(
IQuoterV2.QuoteExactInputSingleParams({
tokenIn: swapWETHForOETHB
? clPool.token0()
: clPool.token1(),
tokenOut: swapWETHForOETHB
? clPool.token1()
: clPool.token0(),
amountIn: mid,
tickSpacing: strategy.tickSpacing(),
sqrtPriceLimitX96: sqrtPriceTargetX96
})
);
(uint256 amountOut, uint160 sqrtPriceX96After, , ) = quoterV2
.quoteExactInputSingle(
IQuoterV2.QuoteExactInputSingleParams({
tokenIn: swapWETHForOETHB
? clPool.token0()
: clPool.token1(),
tokenOut: swapWETHForOETHB
? clPool.token1()
: clPool.token0(),
amountIn: mid,
tickSpacing: strategy.tickSpacing(),
sqrtPriceLimitX96: sqrtPriceTargetX96
})
);

if (
isWithinAllowedVariance(sqrtPriceX96After, sqrtPriceTargetX96)
) {
return (mid, iterations, swapWETHForOETHB, sqrtPriceX96After);
/** Very important to return `amountOut` instead of `mid` as the first return parameter.
* The issues was that when quoting we impose a swap price limit (sqrtPriceLimitX96: sqrtPriceTargetX96)
* and in that case the `amountIn` acts like a maximum amount to swap. And we don't know how much
* of that amount was actually consumed. For that reason we "estimate" it by returning the
* amountOut since that is only going to be a couple of basis point away from amountIn in the
* worst cases.
*
* Note: we could be returning mid instead of amountOut in cases when those values are only basis
* points apart (assuming that complete balance of amountIn has been consumed) but that might increase
* complexity too much in an already complex contract.
*/
return (
amountOut,
iterations,
swapWETHForOETHB,
sqrtPriceX96After
);
} else if (low == high) {
// target swap amount not found.
// try increasing BINARY_MAX_AMOUNT_FOR_PUSH_PRICE
// might be that "high" amount is too low on start
revert("SwapAmountNotFound");
} else if (
swapWETHForOETHB
Expand Down Expand Up @@ -539,7 +566,6 @@ contract AerodromeAMOQuoter {
revert("Previous call should only revert, it cannot succeed");
} catch (bytes memory reason) {
bytes4 receivedSelector = bytes4(reason);

if (receivedSelector == QuoterHelper.ValidAmount.selector) {
uint256 value;
uint256 iterations;
Expand Down
28 changes: 28 additions & 0 deletions contracts/deploy/base/020_upgrade_amo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const { deployOnBaseWithGuardian } = require("../../utils/deploy-l2");
const {
deployBaseAerodromeAMOStrategyImplementation,
} = require("../deployActions");

module.exports = deployOnBaseWithGuardian(
{
deployName: "020_upgrade_amo",
},
async ({ ethers }) => {
const cAMOStrategyProxy = await ethers.getContract(
"AerodromeAMOStrategyProxy"
);
const cAMOStrategyImpl =
await deployBaseAerodromeAMOStrategyImplementation();

return {
actions: [
{
// 1. Upgrade AMO
contract: cAMOStrategyProxy,
signature: "upgradeTo(address)",
args: [cAMOStrategyImpl.address],
},
],
};
}
);
Loading

0 comments on commit dfde2d8

Please sign in to comment.