Skip to content

Commit

Permalink
Improve BoostManager precision and add test (#199)
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenshively authored Oct 25, 2023
1 parent 08533b9 commit 6fb1d96
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 253 deletions.
205 changes: 116 additions & 89 deletions periphery/src/managers/BoostManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {ERC20, SafeTransferLib} from "solmate/utils/SafeTransferLib.sol";

import {IUniswapV3SwapCallback} from "v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol";

import {LiquidityAmounts} from "aloe-ii-core/libraries/LiquidityAmounts.sol";
import {zip} from "aloe-ii-core/libraries/Positions.sol";
import {TickMath} from "aloe-ii-core/libraries/TickMath.sol";
import {Borrower, IManager} from "aloe-ii-core/Borrower.sol";
Expand Down Expand Up @@ -33,7 +34,7 @@ contract BoostManager is IManager, IUniswapV3SwapCallback {
borrower.transfer(amount0 > 0 ? uint256(amount0) : 0, amount1 > 0 ? uint256(amount1) : 0, msg.sender);
}

function callback(bytes calldata data, address owner, uint208) external override returns (uint208) {
function callback(bytes calldata data, address owner, uint208 positions) external override returns (uint208) {
// We cast `msg.sender` as a `Borrower`, but it could really be anything. DO NOT TRUST!
Borrower borrower = Borrower(payable(msg.sender));

Expand All @@ -48,107 +49,133 @@ contract BoostManager is IManager, IUniswapV3SwapCallback {

// Add liquidity (import funds from Uniswap NFT and borrow enough for specified boost factor)
if (action == 0) {
// The ID of the Uniswap NFT to import
uint256 tokenId;
// The position's lower tick
int24 lower;
// The position's upper tick
int24 upper;
// Amount of liquidity in the position
uint128 liquidity;
// Leverage factor
uint24 boost;
(tokenId, lower, upper, liquidity, boost) = abi.decode(args, (uint256, int24, int24, uint128, uint24));

require(owner == UNISWAP_NFT.ownerOf(tokenId), "Aloe: owners must match to import");

unchecked {
(uint256 amount0, uint256 amount1) = _withdrawFromUniswapNFT(tokenId, liquidity, msg.sender);
// Add 0.1% extra to account for rounding in Uniswap's math. This is more gas-efficient than
// computing exact amounts needed with LiquidityAmounts library, and has negligible impact on
// interest rates and liquidation thresholds.
borrower.borrow((amount0 * (boost - 9990)) / 10000, (amount1 * (boost - 9990)) / 10000, msg.sender);
borrower.uniswapDeposit(lower, upper, uint128((uint256(liquidity) * boost) / 10000));
}

return zip([lower, upper, 0, 0, 0, 0]);
return _action0Mint(borrower, owner, args);
}

// Collect earned fees
if (action == 1) {
// The position's lower and upper ticks
(int24 lower, int24 upper) = abi.decode(args, (int24, int24));
borrower.uniswapWithdraw(lower, upper, 0, owner);
borrower.uniswapWithdraw(int24(uint24(positions)), int24(uint24(positions >> 24)), 0, owner);
}

// Burn liquidity
if (action == 2) {
// The position's lower tick
int24 lower;
// The position's upper tick
int24 upper;
// Amount of liquidity in the position
uint128 liquidity;
// Maximum amount of token0 or token1 to swap in order to repay debts
uint128 maxSpend;
// Whether to swap token0 for token1 or vice versa
bool zeroForOne;
(lower, upper, liquidity, maxSpend, zeroForOne) = abi.decode(args, (int24, int24, uint128, uint128, bool));

// Burn liquidity and collect fees
borrower.uniswapWithdraw(lower, upper, liquidity, msg.sender);

// Collect metadata from `borrower`
Lender lender0 = borrower.LENDER0();
Lender lender1 = borrower.LENDER1();
ERC20 token0 = borrower.TOKEN0();
ERC20 token1 = borrower.TOKEN1();

// Balance sheet computations
lender0.accrueInterest();
lender1.accrueInterest();
uint256 liabilities0 = lender0.borrowBalanceStored(msg.sender);
uint256 liabilities1 = lender1.borrowBalanceStored(msg.sender);
uint256 assets0 = token0.balanceOf(msg.sender);
uint256 assets1 = token1.balanceOf(msg.sender);
int256 surplus0 = int256(assets0) - int256(liabilities0);
int256 surplus1 = int256(assets1) - int256(liabilities1);

// Swap iff (it's necessary) AND (direction matches user's intent)
if (surplus0 < 0 && !zeroForOne) {
(, int256 spent1) = borrower.UNISWAP_POOL().swap({
recipient: msg.sender,
zeroForOne: false,
amountSpecified: surplus0, // negative implies "exact amount out"
sqrtPriceLimitX96: TickMath.MAX_SQRT_RATIO - 1,
data: abi.encode(borrower, token0, token1)
});
require(uint256(spent1) <= maxSpend, "slippage");
assets0 = liabilities0;
assets1 -= uint256(spent1);
} else if (surplus1 < 0 && zeroForOne) {
(int256 spent0, ) = borrower.UNISWAP_POOL().swap({
recipient: msg.sender,
zeroForOne: true,
amountSpecified: surplus1, // negative implies "exact amount out"
sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1,
data: abi.encode(borrower, token0, token1)
});
require(uint256(spent0) <= maxSpend, "slippage");
assets0 -= uint256(spent0);
assets1 = liabilities1;
}
return _action2Burn(borrower, owner, args, positions);
}

// Repay
borrower.repay(liabilities0, liabilities1);
return 0;
}

unchecked {
borrower.transfer(assets0 - liabilities0, assets1 - liabilities1, owner);
borrower.withdrawAnte(payable(owner));
function _action0Mint(Borrower borrower, address owner, bytes memory args) private returns (uint208) {
// The ID of the Uniswap NFT to import
uint256 tokenId;
// The position's lower tick
int24 lower;
// The position's upper tick
int24 upper;
// Amount of liquidity in the position
uint128 liquidity;
// Leverage factor
uint24 boost;
(tokenId, lower, upper, liquidity, boost) = abi.decode(args, (uint256, int24, int24, uint128, uint24));

require(owner == UNISWAP_NFT.ownerOf(tokenId), "Aloe: owners must match to import");

unchecked {
(uint256 amount0, uint256 amount1) = _withdrawFromUniswapNFT(tokenId, liquidity, msg.sender);

liquidity = uint128((uint256(liquidity) * boost) / 10_000);
{
(uint160 sqrtPriceX96, , , , , , ) = borrower.UNISWAP_POOL().slot0();
(uint256 needs0, uint256 needs1) = LiquidityAmounts.getAmountsForLiquidity(
sqrtPriceX96,
TickMath.getSqrtRatioAtTick(lower),
TickMath.getSqrtRatioAtTick(upper),
liquidity
);
amount0 = needs0 > amount0 ? needs0 - amount0 : 0;
amount1 = needs1 > amount1 ? needs1 - amount1 : 0;
}

borrower.borrow(amount0, amount1, msg.sender);
borrower.uniswapDeposit(lower, upper, liquidity);
}

return 0;
return zip([lower, upper, 0, 0, 0, 0]);
}

function _action2Burn(
Borrower borrower,
address owner,
bytes memory args,
uint208 positions
) private returns (uint208) {
// The position's lower tick
int24 lower = int24(uint24(positions));
// The position's upper tick
int24 upper = int24(uint24(positions >> 24));
// Amount of liquidity in the position
uint128 liquidity;
// Maximum amount of token0 or token1 to swap in order to repay debts
uint128 maxSpend;
// Whether to swap token0 for token1 or vice versa
bool zeroForOne;

(maxSpend, zeroForOne) = abi.decode(args, (uint128, bool));
(liquidity, , , , ) = borrower.UNISWAP_POOL().positions(keccak256(abi.encodePacked(msg.sender, lower, upper)));

// Burn liquidity and collect fees
if (liquidity > 0) borrower.uniswapWithdraw(lower, upper, liquidity, msg.sender);

// Collect metadata from `borrower`
Lender lender0 = borrower.LENDER0();
Lender lender1 = borrower.LENDER1();
ERC20 token0 = borrower.TOKEN0();
ERC20 token1 = borrower.TOKEN1();

// Balance sheet computations
lender0.accrueInterest();
lender1.accrueInterest();
uint256 liabilities0 = lender0.borrowBalanceStored(msg.sender);
uint256 liabilities1 = lender1.borrowBalanceStored(msg.sender);
uint256 assets0 = token0.balanceOf(msg.sender);
uint256 assets1 = token1.balanceOf(msg.sender);
int256 surplus0 = int256(assets0) - int256(liabilities0);
int256 surplus1 = int256(assets1) - int256(liabilities1);

// Swap iff (it's necessary) AND (direction matches user's intent)
if (surplus0 < 0 && !zeroForOne) {
(, int256 spent1) = borrower.UNISWAP_POOL().swap({
recipient: msg.sender,
zeroForOne: false,
amountSpecified: surplus0, // negative implies "exact amount out"
sqrtPriceLimitX96: TickMath.MAX_SQRT_RATIO - 1,
data: abi.encode(borrower, token0, token1)
});
require(uint256(spent1) <= maxSpend, "slippage");
assets0 = liabilities0;
assets1 -= uint256(spent1);
} else if (surplus1 < 0 && zeroForOne) {
(int256 spent0, ) = borrower.UNISWAP_POOL().swap({
recipient: msg.sender,
zeroForOne: true,
amountSpecified: surplus1, // negative implies "exact amount out"
sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1,
data: abi.encode(borrower, token0, token1)
});
require(uint256(spent0) <= maxSpend, "slippage");
assets0 -= uint256(spent0);
assets1 = liabilities1;
}

// Repay
borrower.repay(liabilities0, liabilities1);

unchecked {
borrower.transfer(assets0 - liabilities0, assets1 - liabilities1, owner);
borrower.withdrawAnte(payable(owner));
}

return zip([int24(1), 1, 0, 0, 0, 0]);
}

function _withdrawFromUniswapNFT(
Expand Down
Loading

0 comments on commit 6fb1d96

Please sign in to comment.