Skip to content

Commit

Permalink
✨ debt-manager: support cross-leverage
Browse files Browse the repository at this point in the history
Co-authored-by: danilo neves cruz <cruzdanilo@gmail.com>
  • Loading branch information
santichez and cruzdanilo committed Jun 26, 2023
1 parent 000693d commit ba05342
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 77 deletions.
5 changes: 5 additions & 0 deletions .changeset/swift-oranges-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/protocol": patch
---

✨ debt-manager: support cross-leverage
94 changes: 49 additions & 45 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,55 @@ AuditorTest:testEnableMarketAuditorMismatch() (gas: 24805)
AuditorTest:testEnableMarketShouldRevertWithInvalidPriceFeed() (gas: 149281)
AuditorTest:testEnterExitMarket() (gas: 178761)
AuditorTest:testExitMarketOwning() (gas: 177445)
DebtManagerTest:testApproveMaliciousMarket() (gas: 29065)
DebtManagerTest:testApproveMarket() (gas: 61556)
DebtManagerTest:testAvailableLiquidity() (gas: 100522)
DebtManagerTest:testBalancerFlashloanCallFromDifferentOrigin() (gas: 65744)
DebtManagerTest:testCallReceiveFlashLoanFromAnyAddress() (gas: 33553)
DebtManagerTest:testDeleverage() (gas: 463698)
DebtManagerTest:testDeleverageHalfBorrowPosition() (gas: 502876)
DebtManagerTest:testDeleverageWithWithdraw() (gas: 496289)
DebtManagerTest:testFixedDeleverage() (gas: 452060)
DebtManagerTest:testFixedRoll() (gas: 525951)
DebtManagerTest:testFixedRollSameMaturityWithThreeLoops() (gas: 400560)
DebtManagerTest:testFixedRollWithAccurateBorrowSlippage() (gas: 796447)
DebtManagerTest:testFixedRollWithAccurateBorrowSlippageWithThreeLoops() (gas: 1130759)
DebtManagerTest:testFixedRollWithAccurateRepaySlippage() (gas: 796385)
DebtManagerTest:testFixedRollWithAccurateRepaySlippageWithThreeLoops() (gas: 1125497)
DebtManagerTest:testFixedToFloatingRoll() (gas: 480375)
DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidity() (gas: 543110)
DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidityWithSlippage() (gas: 833500)
DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidityWithSlippageWithThreeLoops() (gas: 1005251)
DebtManagerTest:testFixedToFloatingRollWithAccurateSlippage() (gas: 680969)
DebtManagerTest:testFlashloanFeeGreaterThanZero() (gas: 431305)
DebtManagerTest:testFloatingToFixedRoll() (gas: 516351)
DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidity() (gas: 602076)
DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippage() (gas: 1000204)
DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippageWithThreePools() (gas: 1208787)
DebtManagerTest:testFloatingToFixedRollWithAccurateSlippage() (gas: 806911)
DebtManagerTest:testFloatingToFixedRollWithAccurateSlippageWithPreviousPosition() (gas: 757786)
DebtManagerTest:testLateFixedDeleverage() (gas: 485172)
DebtManagerTest:testLateFixedRoll() (gas: 536631)
DebtManagerTest:testLateFixedRollWithThreeLoops() (gas: 720302)
DebtManagerTest:testLateFixedToFloatingRoll() (gas: 487764)
DebtManagerTest:testLateFixedToFloatingRollWithThreeLoops() (gas: 649360)
DebtManagerTest:testLeverage() (gas: 377213)
DebtManagerTest:testLeverageShouldFailWhenHealthFactorNearOne() (gas: 755217)
DebtManagerTest:testLeverageWithAlreadyDepositedAmount() (gas: 403958)
DebtManagerTest:testLeverageWithInvalidBalancerVault() (gas: 3209983)
DebtManagerTest:testMockBalancerVault() (gas: 4563902)
DebtManagerTest:testPartialFixedDeleverage() (gas: 526046)
DebtManagerTest:testPartialFixedRoll() (gas: 589206)
DebtManagerTest:testPartialFixedToFloatingRoll() (gas: 553249)
DebtManagerTest:testPartialLateFixedRoll() (gas: 581585)
DebtManagerTest:testPartialLateFixedToFloatingRoll() (gas: 551947)
DebtManagerTest:testPermit2AndLeverage() (gas: 530215)
DebtManagerTest:testPermitAndDeleverage() (gas: 532307)
DebtManagerTest:testPermitAndRollFloatingToFixed() (gas: 600065)
DebtManagerTest:testApproveMaliciousMarket() (gas: 29094)
DebtManagerTest:testApproveMarket() (gas: 61541)
DebtManagerTest:testAvailableLiquidity() (gas: 127236)
DebtManagerTest:testBalancerFlashloanCallFromDifferentOrigin() (gas: 65836)
DebtManagerTest:testCallReceiveFlashLoanFromAnyAddress() (gas: 33615)
DebtManagerTest:testCrossLeverageFromUSDCToWETH() (gas: 972581)
DebtManagerTest:testCrossLeverageFromwstETHtoWETH() (gas: 934255)
DebtManagerTest:testCrossLeverageWithDeposit() (gas: 924350)
DebtManagerTest:testCrossLeverageWithInvalidFee() (gas: 154892)
DebtManagerTest:testDeleverage() (gas: 760044)
DebtManagerTest:testDeleverageHalfBorrowPosition() (gas: 800712)
DebtManagerTest:testDeleverageWithWithdraw() (gas: 794237)
DebtManagerTest:testFixedDeleverage() (gas: 732125)
DebtManagerTest:testFixedRoll() (gas: 832061)
DebtManagerTest:testFixedRollSameMaturityWithThreeLoops() (gas: 690162)
DebtManagerTest:testFixedRollWithAccurateBorrowSlippage() (gas: 1156577)
DebtManagerTest:testFixedRollWithAccurateBorrowSlippageWithThreeLoops() (gas: 2080139)
DebtManagerTest:testFixedRollWithAccurateRepaySlippage() (gas: 1156537)
DebtManagerTest:testFixedRollWithAccurateRepaySlippageWithThreeLoops() (gas: 2074877)
DebtManagerTest:testFixedToFloatingRoll() (gas: 787156)
DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidity() (gas: 917251)
DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidityWithSlippage() (gas: 1349570)
DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidityWithSlippageWithThreeLoops() (gas: 1948971)
DebtManagerTest:testFixedToFloatingRollWithAccurateSlippage() (gas: 1053097)
DebtManagerTest:testFlashloanFeeGreaterThanZero() (gas: 692539)
DebtManagerTest:testFloatingToFixedRoll() (gas: 808584)
DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidity() (gas: 956240)
DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippage() (gas: 1452843)
DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippageWithThreePools() (gas: 2218100)
DebtManagerTest:testFloatingToFixedRollWithAccurateSlippage() (gas: 1133688)
DebtManagerTest:testFloatingToFixedRollWithAccurateSlippageWithPreviousPosition() (gas: 1141687)
DebtManagerTest:testLateFixedDeleverage() (gas: 857112)
DebtManagerTest:testLateFixedRoll() (gas: 915668)
DebtManagerTest:testLateFixedRollWithThreeLoops() (gas: 1432856)
DebtManagerTest:testLateFixedToFloatingRoll() (gas: 869320)
DebtManagerTest:testLateFixedToFloatingRollWithThreeLoops() (gas: 1359083)
DebtManagerTest:testLeverage() (gas: 634676)
DebtManagerTest:testLeverageShouldFailWhenHealthFactorNearOne() (gas: 1270622)
DebtManagerTest:testLeverageWithAlreadyDepositedAmount() (gas: 668330)
DebtManagerTest:testLeverageWithInvalidBalancerVault() (gas: 3757470)
DebtManagerTest:testMockBalancerVault() (gas: 5443352)
DebtManagerTest:testPartialFixedDeleverage() (gas: 798591)
DebtManagerTest:testPartialFixedRoll() (gas: 875325)
DebtManagerTest:testPartialFixedToFloatingRoll() (gas: 845410)
DebtManagerTest:testPartialLateFixedRoll() (gas: 960501)
DebtManagerTest:testPartialLateFixedToFloatingRoll() (gas: 936905)
DebtManagerTest:testPermit2AndLeverage() (gas: 781144)
DebtManagerTest:testPermitAndDeleverage() (gas: 835432)
DebtManagerTest:testPermitAndRollFloatingToFixed() (gas: 893978)
InterestRateModelTest:testFixedBorrowRate() (gas: 8089)
InterestRateModelTest:testFloatingBorrowRate() (gas: 6236)
InterestRateModelTest:testMinFixedRate() (gas: 7610)
Expand Down
134 changes: 133 additions & 1 deletion contracts/periphery/DebtManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ contract DebtManager is Initializable {
using FixedLib for FixedLib.Pool;
using Address for address;

/// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK)
uint160 internal constant MIN_SQRT_RATIO = 4295128739;
/// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK)
uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;

/// @notice Auditor contract that lists the markets that can be leveraged.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
Auditor public immutable auditor;
Expand All @@ -26,12 +31,16 @@ contract DebtManager is Initializable {
/// @notice Balancer's vault contract that is used to take flash loans.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IBalancerVault public immutable balancerVault;
/// @notice Factory contract to be used to compute the address of the Uniswap V3 pool.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable uniswapV3Factory;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor(Auditor auditor_, IPermit2 permit2_, IBalancerVault balancerVault_) {
constructor(Auditor auditor_, IPermit2 permit2_, IBalancerVault balancerVault_, address uniswapV3Factory_) {
auditor = auditor_;
permit2 = permit2_;
balancerVault = balancerVault_;
uniswapV3Factory = uniswapV3Factory_;

_disableInitializers();
}
Expand Down Expand Up @@ -78,6 +87,54 @@ contract DebtManager is Initializable {
balancerVault.flashLoan(address(this), tokens, amounts, call(abi.encode(market, calls)));
}

/// @notice Cross-leverages the floating position of `msg.sender` to match `targetHealthFactor` by taking a flash loan
/// from Balancer's vault.
/// @param inMarket The Market to deposit the leveraged position.
/// @param outMarket The Market to borrow the leveraged position.
/// @param fee The fee of the pool that will be used to swap the assets.
/// @param principal The amount of `inMarket` assets to leverage.
/// @param targetHealthFactor The desired target health factor that the account will be leveraged to.
/// @param deposit Whether the `principal` should be deposited or not.
function crossLeverage(
Market inMarket,
Market outMarket,
uint24 fee,
uint256 principal,
uint256 targetHealthFactor,
bool deposit
) external {
LeverageVars memory v;
v.inAsset = address(inMarket.asset());
v.outAsset = address(outMarket.asset());
if (deposit) ERC20(v.inAsset).safeTransferFrom(msg.sender, address(this), principal);

{
(uint256 depositAdjustFactor, , , , ) = auditor.markets(inMarket);
(uint256 borrowAdjustFactor, , , , ) = auditor.markets(outMarket);
uint256 factor = depositAdjustFactor.mulWadDown(borrowAdjustFactor).divWadDown(targetHealthFactor);
v.amount = factor.mulWadDown(principal).divWadDown(1e18 - factor);
}

PoolKey memory poolKey = PoolAddress.getPoolKey(v.inAsset, v.outAsset, fee);
IUniswapV3Pool(PoolAddress.computeAddress(uniswapV3Factory, poolKey)).swap(
address(this),
v.outAsset == poolKey.token0,
-int256(v.amount),
v.outAsset == poolKey.token0 ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,
abi.encode(
SwapCallbackData({
inMarket: inMarket,
outMarket: outMarket,
inAsset: v.inAsset,
outAsset: v.outAsset,
principal: deposit ? principal : 0,
account: msg.sender,
fee: fee
})
)
);
}

/// @notice Deleverages the position of `msg.sender` a certain `percentage` by taking a flash loan from
/// Balancer's vault to repay the borrow.
/// @param market The Market to deleverage the position out.
Expand Down Expand Up @@ -337,6 +394,22 @@ contract DebtManager is Initializable {
}
}

/// @notice Callback function called by the Uniswap V3 pool contract when a swap is initiated.
/// @dev Only the Uniswap V3 pool contract is allowed to call this function.
/// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by
/// the end of the swap. If positive, the callback must send that amount of token0 to the pool.
/// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by
/// the end of the swap. If positive, the callback must send that amount of token1 to the pool.
/// @param data Any data passed through by the caller via the IUniswapV3PoolActions#swap call
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
SwapCallbackData memory s = abi.decode(data, (SwapCallbackData));
PoolKey memory poolKey = PoolAddress.getPoolKey(s.inAsset, s.outAsset, s.fee);
assert(msg.sender == PoolAddress.computeAddress(uniswapV3Factory, poolKey));

s.inMarket.deposit(s.principal + uint256(-(s.inAsset == poolKey.token0 ? amount0Delta : amount1Delta)), s.account);
s.outMarket.borrow(uint256(s.inAsset == poolKey.token1 ? amount0Delta : amount1Delta), msg.sender, s.account);
}

/// @notice Calls `token.permit` on behalf of `permit.account`.
/// @param token The `ERC20` to call `permit`.
/// @param p Arguments for the permit call.
Expand Down Expand Up @@ -493,6 +566,15 @@ contract DebtManager is Initializable {
ERC20 asset;
uint256 liquidity;
}
struct SwapCallbackData {
Market inMarket;
Market outMarket;
address inAsset;
address outAsset;
address account;
uint256 principal;
uint24 fee;
}
}

error InvalidOperation();
Expand All @@ -515,6 +597,12 @@ struct RollVars {
uint256 i;
}

struct LeverageVars {
address inAsset;
address outAsset;
uint256 amount;
}

interface IBalancerVault {
function flashLoan(
address recipient,
Expand Down Expand Up @@ -551,3 +639,47 @@ interface IPermit2 {
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view returns (bytes32);
}

interface IUniswapV3Pool {
function swap(
address recipient,
bool zeroForOne,
int256 amountSpecified,
uint160 sqrtPriceLimitX96,
bytes calldata data
) external returns (int256 amount0, int256 amount1);
}

// https://github.com/Uniswap/v3-periphery/pull/271
library PoolAddress {
bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;

function getPoolKey(address tokenA, address tokenB, uint24 fee) internal pure returns (PoolKey memory) {
if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA);
return PoolKey({ token0: tokenA, token1: tokenB, fee: fee });
}

function computeAddress(address uniswapV3Factory, PoolKey memory key) internal pure returns (address pool) {
assert(key.token0 < key.token1);
pool = address(
uint160(
uint256(
keccak256(
abi.encodePacked(
hex"ff",
uniswapV3Factory,
keccak256(abi.encode(key.token0, key.token1, key.fee)),
POOL_INIT_CODE_HASH
)
)
)
)
);
}
}

struct PoolKey {
address token0;
address token1;
uint24 fee;
}
27 changes: 17 additions & 10 deletions deploy/DebtManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ import type { DeployFunction } from "hardhat-deploy/types";
import validateUpgrade from "./.utils/validateUpgrade";

const func: DeployFunction = async ({ deployments: { deploy, get }, getNamedAccounts }) => {
const [{ address: auditor }, { address: permit2 }, { address: balancerVault }, { address: timelock }, { deployer }] =
await Promise.all([
get("Auditor"),
get("Permit2"),
get("BalancerVault"),
get("TimelockController"),
getNamedAccounts(),
]);
const [
{ address: auditor },
{ address: permit2 },
{ address: balancerVault },
{ address: uniswapV3Factory },
{ address: timelock },
{ deployer },
] = await Promise.all([
get("Auditor"),
get("Permit2"),
get("BalancerVault"),
get("UniswapV3Factory"),
get("TimelockController"),
getNamedAccounts(),
]);

await validateUpgrade(
"DebtManager",
{ args: [auditor, permit2, balancerVault], envKey: "DEBT_MANAGER" },
{ args: [auditor, permit2, balancerVault, uniswapV3Factory], envKey: "DEBT_MANAGER" },
async (name, opts) =>
deploy(name, {
...opts,
Expand All @@ -32,6 +39,6 @@ const func: DeployFunction = async ({ deployments: { deploy, get }, getNamedAcco
};

func.tags = ["DebtManager"];
func.dependencies = ["TimelockController", "Auditor", "Markets", "BalancerVault", "Permit2"];
func.dependencies = ["TimelockController", "Auditor", "Markets", "UniswapV3Factory", "BalancerVault", "Permit2"];

export default func;
17 changes: 17 additions & 0 deletions deploy/mocks/UniswapV3Factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { DeployFunction } from "hardhat-deploy/types";

const func: DeployFunction = async ({
ethers: {
constants: { AddressZero },
},
deployments: { getOrNull, save },
network: { live },
}) => {
if (!(await getOrNull("UniswapV3Factory")) && !live) {
await save("UniswapV3Factory", { address: AddressZero, abi: [] });
}
};

func.tags = ["UniswapV3Factory"];

export default func;
Loading

0 comments on commit ba05342

Please sign in to comment.