From ba053426b6c05015667f1953d9c499418a4faaa0 Mon Sep 17 00:00:00 2001 From: Santiago Sanchez Avalos Date: Wed, 7 Jun 2023 16:43:51 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20debt-manager:=20support=20cross-lev?= =?UTF-8?q?erage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: danilo neves cruz --- .changeset/swift-oranges-provide.md | 5 ++ .gas-snapshot | 94 +++++++++---------- contracts/periphery/DebtManager.sol | 134 +++++++++++++++++++++++++++- deploy/DebtManager.ts | 27 +++--- deploy/mocks/UniswapV3Factory.ts | 17 ++++ test/solidity/DebtManager.t.sol | 118 +++++++++++++++++++----- 6 files changed, 318 insertions(+), 77 deletions(-) create mode 100644 .changeset/swift-oranges-provide.md create mode 100644 deploy/mocks/UniswapV3Factory.ts diff --git a/.changeset/swift-oranges-provide.md b/.changeset/swift-oranges-provide.md new file mode 100644 index 000000000..7f9c71ea1 --- /dev/null +++ b/.changeset/swift-oranges-provide.md @@ -0,0 +1,5 @@ +--- +"@exactly/protocol": patch +--- + +✨ debt-manager: support cross-leverage diff --git a/.gas-snapshot b/.gas-snapshot index fba125368..4bd2111f7 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -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) diff --git a/contracts/periphery/DebtManager.sol b/contracts/periphery/DebtManager.sol index 5a3233892..3aa7f0243 100644 --- a/contracts/periphery/DebtManager.sol +++ b/contracts/periphery/DebtManager.sol @@ -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; @@ -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(); } @@ -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. @@ -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. @@ -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(); @@ -515,6 +597,12 @@ struct RollVars { uint256 i; } +struct LeverageVars { + address inAsset; + address outAsset; + uint256 amount; +} + interface IBalancerVault { function flashLoan( address recipient, @@ -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; +} diff --git a/deploy/DebtManager.ts b/deploy/DebtManager.ts index f4c2dc54f..1b1ef1422 100644 --- a/deploy/DebtManager.ts +++ b/deploy/DebtManager.ts @@ -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, @@ -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; diff --git a/deploy/mocks/UniswapV3Factory.ts b/deploy/mocks/UniswapV3Factory.ts new file mode 100644 index 000000000..b6b16f225 --- /dev/null +++ b/deploy/mocks/UniswapV3Factory.ts @@ -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; diff --git a/test/solidity/DebtManager.t.sol b/test/solidity/DebtManager.t.sol index cedbcef20..398fe7c99 100644 --- a/test/solidity/DebtManager.t.sol +++ b/test/solidity/DebtManager.t.sol @@ -27,15 +27,21 @@ contract DebtManagerTest is Test { address internal bob; DebtManager internal debtManager; Market internal marketUSDC; + Market internal marketWETH; + Market internal marketwstETH; ERC20 internal usdc; + ERC20 internal wstETH; uint256 internal maturity; uint256 internal targetMaturity; function setUp() external { - vm.createSelectFork(vm.envString("OPTIMISM_NODE"), 84_666_000); + vm.createSelectFork(vm.envString("OPTIMISM_NODE"), 99_811_375); usdc = ERC20(deployment("USDC")); + wstETH = ERC20(deployment("wstETH")); marketUSDC = Market(deployment("MarketUSDC")); + marketWETH = Market(deployment("MarketWETH")); + marketwstETH = Market(deployment("MarketwstETH")); debtManager = DebtManager( address( new ERC1967Proxy( @@ -43,7 +49,8 @@ contract DebtManagerTest is Test { new DebtManager( Auditor(deployment("Auditor")), IPermit2(deployment("Permit2")), - IBalancerVault(deployment("BalancerVault")) + IBalancerVault(deployment("BalancerVault")), + deployment("UniswapV3Factory") ) ), abi.encodeCall(DebtManager.initialize, ()) @@ -54,9 +61,15 @@ contract DebtManagerTest is Test { assertLt(usdc.balanceOf(address(debtManager.balancerVault())), 1_000_000e6); deal(address(usdc), address(this), 22_000_000e6); + deal(address(marketWETH.asset()), address(this), 1_000e18); + deal(address(wstETH), address(this), 1_000e18); marketUSDC.approve(address(debtManager), type(uint256).max); + marketWETH.approve(address(debtManager), type(uint256).max); + marketwstETH.approve(address(debtManager), type(uint256).max); usdc.approve(address(marketUSDC), type(uint256).max); usdc.approve(address(debtManager), type(uint256).max); + wstETH.approve(address(marketwstETH), type(uint256).max); + wstETH.approve(address(debtManager), type(uint256).max); maturity = block.timestamp - (block.timestamp % FixedLib.INTERVAL) + FixedLib.INTERVAL; targetMaturity = maturity + FixedLib.INTERVAL; @@ -164,7 +177,7 @@ contract DebtManagerTest is Test { debtManager.leverage(marketUSDC, 100_000e6, 0, 1.03e18); (, , uint256 floatingBorrowShares) = marketUSDC.accounts(address(this)); - assertEq(marketUSDC.maxWithdraw(address(this)), 510153541352); + assertEq(marketUSDC.maxWithdraw(address(this)), 510153541353); assertEq(marketUSDC.previewRefund(floatingBorrowShares), 410153541355); } @@ -184,13 +197,13 @@ contract DebtManagerTest is Test { (, , uint256 floatingBorrowShares) = marketUSDC.accounts(address(this)); // precision loss (2) - assertEq(marketUSDC.maxWithdraw(address(this)), 100_000e6 - 2); + assertEq(marketUSDC.maxWithdraw(address(this)), 100_000e6 - 3); assertEq(marketUSDC.previewRefund(floatingBorrowShares), 0); } function testDeleverageWithWithdraw() external _checkBalances { debtManager.leverage(marketUSDC, 100_000e6, 100_000e6, 1.03e18); - debtManager.deleverage(marketUSDC, 0, 0, 1e18, 100_000e6 - 2); + debtManager.deleverage(marketUSDC, 0, 0, 1e18, 100_000e6 - 3); (, , uint256 floatingBorrowShares) = marketUSDC.accounts(address(this)); assertEq(marketUSDC.previewRefund(floatingBorrowShares), 0); @@ -207,11 +220,11 @@ contract DebtManagerTest is Test { debtManager.deleverage(marketUSDC, 0, 0, 0.5e18, 0); (, , floatingBorrowShares) = marketUSDC.accounts(address(this)); - uint256 deleveragedDeposit = 305076770676; + uint256 deleveragedDeposit = 305076770675; uint256 deleveragedBorrow = 205076770678; assertEq(marketUSDC.maxWithdraw(address(this)), deleveragedDeposit); assertEq(marketUSDC.previewRefund(floatingBorrowShares), deleveragedBorrow); - assertEq(leveragedDeposit - deleveragedDeposit, leveragedBorrow - deleveragedBorrow); + assertEq(leveragedDeposit - deleveragedDeposit - 1, leveragedBorrow - deleveragedBorrow); } function testFixedDeleverage() external _checkBalances { @@ -239,7 +252,7 @@ contract DebtManagerTest is Test { (uint256 principal, ) = marketUSDC.fixedBorrowPositions(maturity, address(this)); (uint256 newBalance, ) = marketUSDC.accountSnapshot(address(this)); assertEq(principal, 0); - assertEq(newBalance, balance - debt); + assertEq(newBalance, balance - debt - 1); } function testPartialFixedDeleverage() external _checkBalances { @@ -285,7 +298,12 @@ contract DebtManagerTest is Test { } function testLeverageWithInvalidBalancerVault() external { - DebtManager lev = new DebtManager(marketUSDC.auditor(), IPermit2(address(0)), IBalancerVault(address(this))); + DebtManager lev = new DebtManager( + marketUSDC.auditor(), + IPermit2(address(0)), + IBalancerVault(address(this)), + address(0) + ); vm.expectRevert(bytes("")); lev.leverage(marketUSDC, 100_000e6, 100_000e6, 1.03e18); } @@ -315,7 +333,7 @@ contract DebtManagerTest is Test { marketUSDC.borrow(1_000_000e6, address(this), address(this)); (, , uint256 floatingBorrowShares) = marketUSDC.accounts(address(this)); uint256 initialDebt = marketUSDC.previewRefund(floatingBorrowShares); - uint256 fee = 8_855_280_712; + uint256 fee = 1_796_320_611; vm.expectRevert(Disagreement.selector); debtManager.rollFloatingToFixed(marketUSDC, maturity, 1_000_000e6 + fee, 1e18); @@ -331,7 +349,7 @@ contract DebtManagerTest is Test { marketUSDC.deposit(3_000_000e6, address(this)); marketUSDC.borrow(2_000_000e6, address(this), address(this)); vm.warp(block.timestamp + 10_000); - uint256 fee = 10_504_723_368; + uint256 fee = 3_975_653_339; vm.expectRevert(Disagreement.selector); debtManager.rollFloatingToFixed(marketUSDC, maturity, 2_000_000e6 + fee, 1e18); @@ -353,7 +371,7 @@ contract DebtManagerTest is Test { function testFixedToFloatingRollHigherThanAvailableLiquidityWithSlippage() external _checkBalances { marketUSDC.deposit(2_000_000e6, address(this)); marketUSDC.borrowAtMaturity(maturity, 1_000_000e6, type(uint256).max, address(this), address(this)); - uint256 fee = 893_502_174; + uint256 fee = 261_243_388; vm.expectRevert(Disagreement.selector); debtManager.rollFixedToFloating(marketUSDC, maturity, 1_000_000e6 + fee, 1e18); @@ -369,7 +387,7 @@ contract DebtManagerTest is Test { marketUSDC.deposit(3_000_000e6, address(this)); vm.warp(block.timestamp + 10_000); marketUSDC.borrowAtMaturity(maturity, 2_000_000e6, type(uint256).max, address(this), address(this)); - uint256 fee = 1_052_304_919; + uint256 fee = 650_802_165; vm.expectRevert(Disagreement.selector); debtManager.rollFixedToFloating(marketUSDC, maturity, 2_000_000e6 + fee, 1e18); @@ -403,7 +421,7 @@ contract DebtManagerTest is Test { function testFloatingToFixedRollWithAccurateSlippage() external _checkBalances { marketUSDC.deposit(100_000e6, address(this)); marketUSDC.borrow(50_000e6, address(this), address(this)); - uint256 maxFee = 41_656_859; + uint256 maxFee = 76_622_877; vm.expectRevert(Disagreement.selector); debtManager.rollFloatingToFixed(marketUSDC, maturity, 50_000e6 + maxFee, 1e18); @@ -418,7 +436,7 @@ contract DebtManagerTest is Test { marketUSDC.deposit(100_000e6, address(this)); marketUSDC.borrow(50_000e6, address(this), address(this)); marketUSDC.borrowAtMaturity(maturity, 10_000e6, type(uint256).max, address(this), address(this)); - uint256 maxFee = 46_473_866; + uint256 maxFee = 76_896_076; vm.expectRevert(Disagreement.selector); debtManager.rollFloatingToFixed(marketUSDC, maturity, 50_000e6 + maxFee, 1e18); @@ -454,7 +472,7 @@ contract DebtManagerTest is Test { function testFixedToFloatingRollWithAccurateSlippage() external _checkBalances { marketUSDC.deposit(100_000e6, address(this)); marketUSDC.borrowAtMaturity(maturity, 10_000e6, type(uint256).max, address(this), address(this)); - uint256 maxAssets = 10_001_221_724; + uint256 maxAssets = 10_000_460_651; vm.expectRevert(Disagreement.selector); debtManager.rollFixedToFloating(marketUSDC, maturity, maxAssets, 1e18); @@ -524,7 +542,7 @@ contract DebtManagerTest is Test { function testFixedRollWithAccurateRepaySlippage() external _checkBalances { marketUSDC.deposit(100_000e6, address(this)); marketUSDC.borrowAtMaturity(maturity, 50_000e6, type(uint256).max, address(this), address(this)); - uint256 maxRepayAssets = 50_004_915_917; + uint256 maxRepayAssets = 50_002_979_931; vm.expectRevert(Disagreement.selector); debtManager.rollFixed(marketUSDC, maturity, targetMaturity, maxRepayAssets, type(uint256).max, 1e18); @@ -535,7 +553,7 @@ contract DebtManagerTest is Test { function testFixedRollWithAccurateBorrowSlippage() external _checkBalances { marketUSDC.deposit(100_000e6, address(this)); marketUSDC.borrowAtMaturity(maturity, 50_000e6, type(uint256).max, address(this), address(this)); - uint256 maxBorrowAssets = 50_128_835_188; + uint256 maxBorrowAssets = 50_206_336_876; vm.expectRevert(Disagreement.selector); debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, maxBorrowAssets, 1e18); @@ -577,7 +595,7 @@ contract DebtManagerTest is Test { marketUSDC.deposit(3_000_000e6, address(this)); vm.warp(block.timestamp + 10_000); marketUSDC.borrowAtMaturity(maturity, 2_000_000e6, type(uint256).max, address(this), address(this)); - uint256 fees = 32_472_619_932; + uint256 fees = 10_991_896_276; vm.expectRevert(Disagreement.selector); debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, 2_000_000e6 + fees, 1e18); @@ -602,7 +620,7 @@ contract DebtManagerTest is Test { marketUSDC.deposit(3_000_000e6, address(this)); vm.warp(block.timestamp + 10_000); marketUSDC.borrowAtMaturity(maturity, 2_000_000e6, type(uint256).max, address(this), address(this)); - uint256 maxRepayAssets = 2_001_052_304_918; + uint256 maxRepayAssets = 2_000_650_802_163; vm.expectRevert(Disagreement.selector); debtManager.rollFixed(marketUSDC, maturity, targetMaturity, maxRepayAssets, type(uint256).max, 1e18); @@ -657,7 +675,12 @@ contract DebtManagerTest is Test { address( new ERC1967Proxy( address( - new DebtManager(debtManager.auditor(), IPermit2(address(0)), IBalancerVault(address(mockBalancerVault))) + new DebtManager( + debtManager.auditor(), + IPermit2(address(0)), + IBalancerVault(address(mockBalancerVault)), + address(0) + ) ), abi.encodeCall(DebtManager.initialize, ()) ) @@ -676,6 +699,59 @@ contract DebtManagerTest is Test { assertEq(principal, 50_000e6 + 1); } + function testCrossLeverageFromwstETHtoWETH() external _checkBalances { + uint256 wstETHBalanceBefore = wstETH.balanceOf(address(this)); + debtManager.auditor().enterMarket(marketwstETH); + debtManager.crossLeverage(marketwstETH, marketWETH, 500, 10e18, 1.02e18, true); + + (uint256 coll, uint256 debt) = marketwstETH.auditor().accountLiquidity(address(this), Market(address(0)), 0); + uint256 healthFactor = coll.divWadDown(debt); + (, , uint256 floatingBorrowShares) = marketWETH.accounts(address(this)); + + assertEq(wstETH.balanceOf(address(this)), wstETHBalanceBefore - 10e18); + assertEq(marketwstETH.maxWithdraw(address(this)), 29310344827586206820); + assertEq(marketWETH.previewRefund(floatingBorrowShares), 21798024958067023341); + assertGt(healthFactor, 1.0201e18); + assertLt(healthFactor, 1.0202e18); + } + + function testCrossLeverageFromUSDCToWETH() external _checkBalances { + uint256 usdcBalanceBefore = usdc.balanceOf(address(this)); + debtManager.auditor().enterMarket(marketUSDC); + + debtManager.crossLeverage(marketUSDC, marketWETH, 500, 10_000e6, 1.02e18, true); + + (uint256 coll, uint256 debt) = marketUSDC.auditor().accountLiquidity(address(this), Market(address(0)), 0); + uint256 healthFactor = coll.divWadDown(debt); + (, , uint256 floatingBorrowShares) = marketWETH.accounts(address(this)); + assertEq(usdc.balanceOf(address(this)), usdcBalanceBefore - 10_000e6); + + assertEq(marketUSDC.maxWithdraw(address(this)), 39906103285); + assertEq(marketWETH.previewRefund(floatingBorrowShares), 16536655325232070233); + assertLt(healthFactor, 1.0196e18); + assertGt(healthFactor, 1.0195e18); + } + + function testCrossLeverageWithDeposit() external _checkBalances { + debtManager.auditor().enterMarket(marketwstETH); + marketwstETH.deposit(10e18, address(this)); + + uint256 wstETHBalanceBefore = wstETH.balanceOf(address(this)); + debtManager.crossLeverage(marketwstETH, marketWETH, 500, 10e18, 1.02e18, false); + (, , uint256 floatingBorrowShares) = marketWETH.accounts(address(this)); + + assertEq(marketwstETH.maxWithdraw(address(this)), 29310344827586206820); + assertEq(marketWETH.previewRefund(floatingBorrowShares), 21798024958067023341); + assertEq(wstETHBalanceBefore, wstETH.balanceOf(address(this))); + } + + function testCrossLeverageWithInvalidFee() external _checkBalances { + debtManager.auditor().enterMarket(marketUSDC); + + vm.expectRevert(bytes("")); + debtManager.crossLeverage(marketUSDC, marketWETH, 200, 100_000e6, 1.1e18, true); + } + function testPermitAndRollFloatingToFixed() external { marketUSDC.deposit(100_000e6, bob); vm.prank(bob);