diff --git a/.gas-snapshot b/.gas-snapshot index 545204105..fba125368 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -10,48 +10,51 @@ AuditorTest:testEnableMarketAuditorMismatch() (gas: 24805) AuditorTest:testEnableMarketShouldRevertWithInvalidPriceFeed() (gas: 149281) AuditorTest:testEnterExitMarket() (gas: 178761) AuditorTest:testExitMarketOwning() (gas: 177445) -DebtManagerTest:testApproveMaliciousMarket() (gas: 29117) -DebtManagerTest:testApproveMarket() (gas: 61608) -DebtManagerTest:testAvailableLiquidity() (gas: 100500) -DebtManagerTest:testBalancerFlashloanCallFromDifferentOrigin() (gas: 65633) -DebtManagerTest:testCallReceiveFlashLoanFromAnyAddress() (gas: 33487) -DebtManagerTest:testDeleverage() (gas: 461764) -DebtManagerTest:testDeleverageHalfBorrowPosition() (gas: 500887) -DebtManagerTest:testFixedDeleverage() (gas: 451787) -DebtManagerTest:testFixedRoll() (gas: 525758) -DebtManagerTest:testFixedRollSameMaturityWithThreeLoops() (gas: 400516) -DebtManagerTest:testFixedRollWithAccurateBorrowSlippage() (gas: 796127) -DebtManagerTest:testFixedRollWithAccurateBorrowSlippageWithThreeLoops() (gas: 1130239) -DebtManagerTest:testFixedRollWithAccurateRepaySlippage() (gas: 796087) -DebtManagerTest:testFixedRollWithAccurateRepaySlippageWithThreeLoops() (gas: 1124999) -DebtManagerTest:testFixedToFloatingRoll() (gas: 480272) -DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidity() (gas: 542894) -DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidityWithSlippage() (gas: 833202) -DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidityWithSlippageWithThreeLoops() (gas: 1004802) -DebtManagerTest:testFixedToFloatingRollWithAccurateSlippage() (gas: 680799) -DebtManagerTest:testFlashloanFeeGreaterThanZero() (gas: 429690) -DebtManagerTest:testFloatingToFixedRoll() (gas: 516123) -DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidity() (gas: 601753) -DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippage() (gas: 999625) -DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippageWithThreePools() (gas: 1208041) -DebtManagerTest:testFloatingToFixedRollWithAccurateSlippage() (gas: 806522) -DebtManagerTest:testFloatingToFixedRollWithAccurateSlippageWithPreviousPosition() (gas: 757420) -DebtManagerTest:testLateFixedDeleverage() (gas: 484933) -DebtManagerTest:testLateFixedRoll() (gas: 536460) -DebtManagerTest:testLateFixedRollWithThreeLoops() (gas: 720031) -DebtManagerTest:testLateFixedToFloatingRoll() (gas: 487661) -DebtManagerTest:testLateFixedToFloatingRollWithThreeLoops() (gas: 649169) -DebtManagerTest:testLeverage() (gas: 375539) -DebtManagerTest:testLeverageShouldFailWhenHealthFactorNearOne() (gas: 751905) -DebtManagerTest:testLeverageWithAlreadyDepositedAmount() (gas: 403912) -DebtManagerTest:testLeverageWithInvalidBalancerVault() (gas: 2936191) -DebtManagerTest:testMockBalancerVault() (gas: 4289437) -DebtManagerTest:testPartialFixedDeleverage() (gas: 525791) -DebtManagerTest:testPartialFixedRoll() (gas: 589035) -DebtManagerTest:testPartialFixedToFloatingRoll() (gas: 553142) -DebtManagerTest:testPartialLateFixedRoll() (gas: 581414) -DebtManagerTest:testPartialLateFixedToFloatingRoll() (gas: 551840) -DebtManagerTest:testPermitAndRollFloatingToFixed() (gas: 597947) +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) 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 6ee63c55d..5a3233892 100644 --- a/contracts/periphery/DebtManager.sol +++ b/contracts/periphery/DebtManager.sol @@ -5,7 +5,7 @@ import { SafeTransferLib } from "solmate/src/utils/SafeTransferLib.sol"; import { FixedPointMathLib } from "solmate/src/utils/FixedPointMathLib.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { AddressUpgradeable as Address } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; -import { Market, ERC20, ERC4626, FixedLib, Disagreement } from "../Market.sol"; +import { Market, ERC20, FixedLib, Disagreement } from "../Market.sol"; import { Auditor, MarketNotListed } from "../Auditor.sol"; /// @title DebtManager @@ -20,13 +20,17 @@ contract DebtManager is Initializable { /// @notice Auditor contract that lists the markets that can be leveraged. /// @custom:oz-upgrades-unsafe-allow state-variable-immutable Auditor public immutable auditor; + /// @notice Permit2 contract to be used to transfer assets from accounts. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IPermit2 public immutable permit2; /// @notice Balancer's vault contract that is used to take flash loans. /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IBalancerVault public immutable balancerVault; /// @custom:oz-upgrades-unsafe-allow constructor - constructor(Auditor auditor_, IBalancerVault balancerVault_) { + constructor(Auditor auditor_, IPermit2 permit2_, IBalancerVault balancerVault_) { auditor = auditor_; + permit2 = permit2_; balancerVault = balancerVault_; _disableInitializers(); @@ -44,23 +48,32 @@ contract DebtManager is Initializable { /// @notice Leverages the floating position of `msg.sender` to match `targetHealthFactor` by taking a flash loan /// from Balancer's vault. /// @param market The Market to leverage the position in. - /// @param principal The amount of assets to deposit or deposited. + /// @param principal The amount of assets to leverage. + /// @param deposit The amount of assets to deposit. + /// @param targetHealthFactor The desired target health factor that the account will be leveraged to. + function leverage(Market market, uint256 principal, uint256 deposit, uint256 targetHealthFactor) external { + if (deposit != 0) market.asset().safeTransferFrom(msg.sender, address(this), deposit); + + noTransferLeverage(market, principal, deposit, targetHealthFactor); + } + + /// @notice Leverages the floating position of `msg.sender` to match `targetHealthFactor` by taking a flash loan + /// from Balancer's vault. + /// @param market The Market to leverage the position in. + /// @param principal The amount of assets to leverage. + /// @param deposit The amount of assets to deposit. /// @param targetHealthFactor The desired target health factor that the account will be leveraged to. - /// @param deposit True if the principal is being deposited, false if the principal is already deposited. - function leverage(Market market, uint256 principal, uint256 targetHealthFactor, bool deposit) external { + function noTransferLeverage(Market market, uint256 principal, uint256 deposit, uint256 targetHealthFactor) internal { uint256[] memory amounts = new uint256[](1); ERC20[] memory tokens = new ERC20[](1); bytes[] memory calls = new bytes[](2); - ERC20 asset = market.asset(); - - if (deposit) asset.safeTransferFrom(msg.sender, address(this), principal); (uint256 adjustFactor, , , , ) = auditor.markets(market); uint256 factor = adjustFactor.mulWadDown(adjustFactor).divWadDown(targetHealthFactor); - tokens[0] = asset; + tokens[0] = market.asset(); amounts[0] = principal.mulWadDown(factor).divWadDown(1e18 - factor); - calls[0] = abi.encodeCall(ERC4626.deposit, (amounts[0] + (deposit ? principal : 0), msg.sender)); - calls[1] = abi.encodeCall(Market.borrow, (amounts[0], address(balancerVault), msg.sender)); + calls[0] = abi.encodeCall(market.deposit, (amounts[0] + deposit, msg.sender)); + calls[1] = abi.encodeCall(market.borrow, (amounts[0], address(balancerVault), msg.sender)); balancerVault.flashLoan(address(this), tokens, amounts, call(abi.encode(market, calls))); } @@ -69,24 +82,32 @@ contract DebtManager is Initializable { /// Balancer's vault to repay the borrow. /// @param market The Market to deleverage the position out. /// @param maturity The maturity of the fixed pool that the position is being deleveraged out of, `0` if floating. - /// @param maxAssets Max amount of fixed debt that the sender is willing to accept. + /// @param maxRepayAssets Max amount of fixed debt that the sender is willing to accept. /// @param percentage The percentage of the borrow that will be repaid, represented with 18 decimals. - function deleverage(Market market, uint256 maturity, uint256 maxAssets, uint256 percentage) external { + /// @param withdraw The amount of assets that will be withdrawn to `msg.sender`. + function deleverage( + Market market, + uint256 maturity, + uint256 maxRepayAssets, + uint256 percentage, + uint256 withdraw + ) public { uint256[] memory amounts = new uint256[](1); ERC20[] memory tokens = new ERC20[](1); - bytes[] memory calls = new bytes[](2); + bytes[] memory calls = new bytes[](withdraw == 0 ? 2 : 3); tokens[0] = market.asset(); if (maturity == 0) { (, , uint256 floatingBorrowShares) = market.accounts(msg.sender); amounts[0] = market.previewRefund(floatingBorrowShares.mulWadDown(percentage)); - calls[0] = abi.encodeCall(Market.repay, (amounts[0], msg.sender)); + calls[0] = abi.encodeCall(market.repay, (amounts[0], msg.sender)); } else { uint256 positionAssets; (amounts[0], positionAssets) = repayAtMaturityAssets(market, maturity, percentage); - calls[0] = abi.encodeCall(Market.repayAtMaturity, (maturity, positionAssets, maxAssets, msg.sender)); + calls[0] = abi.encodeCall(market.repayAtMaturity, (maturity, positionAssets, maxRepayAssets, msg.sender)); } - calls[1] = abi.encodeCall(Market.withdraw, (amounts[0], address(balancerVault), msg.sender)); + calls[1] = abi.encodeCall(market.withdraw, (amounts[0], address(balancerVault), msg.sender)); + if (withdraw > 0) calls[2] = abi.encodeCall(market.withdraw, (withdraw, msg.sender, msg.sender)); balancerVault.flashLoan(address(this), tokens, amounts, call(abi.encode(market, calls))); } @@ -119,11 +140,11 @@ contract DebtManager is Initializable { calls = new bytes[](2 * r.loopCount); for (r.i = 0; r.i < r.loopCount; ) { calls[r.callIndex++] = abi.encodeCall( - Market.repay, + market.repay, (r.i == 0 ? amounts[0] : r.repayAssets / r.loopCount, msg.sender) ); calls[r.callIndex++] = abi.encodeCall( - Market.borrowAtMaturity, + market.borrowAtMaturity, ( borrowMaturity, r.i + 1 == r.loopCount ? amounts[0] : r.repayAssets / r.loopCount, @@ -171,11 +192,11 @@ contract DebtManager is Initializable { calls = new bytes[](2 * r.loopCount); for (r.i = 0; r.i < r.loopCount; ) { calls[r.callIndex++] = abi.encodeCall( - Market.repayAtMaturity, + market.repayAtMaturity, (repayMaturity, positionAssets, type(uint256).max, msg.sender) ); calls[r.callIndex++] = abi.encodeCall( - Market.borrow, + market.borrow, (amounts[0], r.i + 1 == r.loopCount ? address(balancerVault) : address(this), msg.sender) ); unchecked { @@ -221,11 +242,11 @@ contract DebtManager is Initializable { calls = new bytes[](2 * r.loopCount); for (r.i = 0; r.i < r.loopCount; ) { calls[r.callIndex++] = abi.encodeCall( - Market.repayAtMaturity, + market.repayAtMaturity, (repayMaturity, r.positionAssets, type(uint256).max, msg.sender) ); calls[r.callIndex++] = abi.encodeCall( - Market.borrowAtMaturity, + market.borrowAtMaturity, ( borrowMaturity, amounts[0], @@ -328,6 +349,70 @@ contract DebtManager is Initializable { _; } + /// @notice Calls `permit2.permitTransferFrom` to transfer `msg.sender` assets. + /// @param token The `ERC20` to transfer from `msg.sender` to this contract. + /// @param assets The amount of assets to transfer from `msg.sender`. + /// @param deadline The deadline for the permit call. + /// @param signature The signature for the permit call. + modifier permitTransfer( + ERC20 token, + uint256 assets, + uint256 deadline, + bytes calldata signature + ) { + permit2.permitTransferFrom( + IPermit2.PermitTransferFrom( + IPermit2.TokenPermissions(address(token), assets), + uint256(keccak256(abi.encode(msg.sender, token, assets, deadline))), + deadline + ), + IPermit2.SignatureTransferDetails(address(this), assets), + msg.sender, + signature + ); + _; + } + + /// @notice Leverages the floating position of `msg.sender` to match `targetHealthFactor` by taking a flash loan + /// from Balancer's vault. + /// @param market The Market to leverage the position in. + /// @param principal The amount of assets to leverage. + /// @param deposit The amount of assets to deposit. + /// @param targetHealthFactor The desired target health factor that the account will be leveraged to. + /// @param deadline The deadline for the permit call. + /// @param signature The signature for the permit call. + function leverage( + Market market, + uint256 principal, + uint256 deposit, + uint256 targetHealthFactor, + uint256 deadline, + bytes calldata signature + ) external permitTransfer(market.asset(), deposit, deadline, signature) { + noTransferLeverage(market, principal, deposit, targetHealthFactor); + } + + /// @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. + /// @param maturity The maturity of the fixed pool that the position is being deleveraged out of, `0` if floating. + /// @param maxRepayAssets Max amount of fixed debt that the sender is willing to accept. + /// @param percentage The percentage of the borrow that will be repaid, represented with 18 decimals. + /// @param withdraw The amount of assets that will be withdrawn to `msg.sender`. + /// @param permitAssets The amount of assets to allow this contract to withdraw on behalf of `msg.sender`. + /// @param p Arguments for the permit call to `market` on behalf of `permit.account`. + function deleverage( + Market market, + uint256 maturity, + uint256 maxRepayAssets, + uint256 percentage, + uint256 withdraw, + uint256 permitAssets, + Permit calldata p + ) external permit(market, permitAssets, p) { + deleverage(market, maturity, maxRepayAssets, percentage, withdraw); + } + /// @notice Rolls a percentage of the floating position of `msg.sender` to a fixed position /// after calling `market.permit`. /// @param market The Market to roll the position in. @@ -438,3 +523,31 @@ interface IBalancerVault { bytes memory userData ) external; } + +interface IPermit2 { + struct TokenPermissions { + address token; + uint256 amount; + } + + struct PermitTransferFrom { + TokenPermissions permitted; + uint256 nonce; + uint256 deadline; + } + + struct SignatureTransferDetails { + address to; + uint256 requestedAmount; + } + + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/deploy/DebtManager.ts b/deploy/DebtManager.ts index 6c6a7dd25..f4c2dc54f 100644 --- a/deploy/DebtManager.ts +++ b/deploy/DebtManager.ts @@ -2,31 +2,36 @@ import type { DeployFunction } from "hardhat-deploy/types"; import validateUpgrade from "./.utils/validateUpgrade"; const func: DeployFunction = async ({ deployments: { deploy, get }, getNamedAccounts }) => { - const [{ address: timelock }, { address: balancerVault }, { address: auditor }, { deployer }] = await Promise.all([ - get("TimelockController"), - get("BalancerVault"), - get("Auditor"), - getNamedAccounts(), - ]); + const [{ address: auditor }, { address: permit2 }, { address: balancerVault }, { address: timelock }, { deployer }] = + await Promise.all([ + get("Auditor"), + get("Permit2"), + get("BalancerVault"), + get("TimelockController"), + getNamedAccounts(), + ]); - await validateUpgrade("DebtManager", { args: [auditor, balancerVault], envKey: "DEBT_MANAGER" }, async (name, opts) => - deploy(name, { - ...opts, - proxy: { - owner: timelock, - viaAdminContract: "ProxyAdmin", - proxyContract: "TransparentUpgradeableProxy", - execute: { - init: { methodName: "initialize", args: [] }, + await validateUpgrade( + "DebtManager", + { args: [auditor, permit2, balancerVault], envKey: "DEBT_MANAGER" }, + async (name, opts) => + deploy(name, { + ...opts, + proxy: { + owner: timelock, + viaAdminContract: "ProxyAdmin", + proxyContract: "TransparentUpgradeableProxy", + execute: { + init: { methodName: "initialize", args: [] }, + }, }, - }, - from: deployer, - log: true, - }), + from: deployer, + log: true, + }), ); }; func.tags = ["DebtManager"]; -func.dependencies = ["Auditor", "Markets", "BalancerVault"]; +func.dependencies = ["TimelockController", "Auditor", "Markets", "BalancerVault", "Permit2"]; export default func; diff --git a/deploy/mocks/Permit2.ts b/deploy/mocks/Permit2.ts new file mode 100644 index 000000000..0830d3eee --- /dev/null +++ b/deploy/mocks/Permit2.ts @@ -0,0 +1,15 @@ +import type { DeployFunction } from "hardhat-deploy/types"; + +const func: DeployFunction = async ({ + ethers: { + constants: { AddressZero }, + }, + deployments: { getOrNull, save }, + network: { live }, +}) => { + if (!(await getOrNull("Permit2")) && !live) await save("Permit2", { address: AddressZero, abi: [] }); +}; + +func.tags = ["Permit2"]; + +export default func; diff --git a/test/solidity/DebtManager.t.sol b/test/solidity/DebtManager.t.sol index 39403ca7d..cedbcef20 100644 --- a/test/solidity/DebtManager.t.sol +++ b/test/solidity/DebtManager.t.sol @@ -7,6 +7,7 @@ import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy import { ERC20, Permit, + IPermit2, DebtManager, Disagreement, IBalancerVault, @@ -22,6 +23,8 @@ contract DebtManagerTest is Test { using FixedLib for FixedLib.Pool; using stdJson for string; + uint256 internal constant BOB_KEY = 0xb0b; + address internal bob; DebtManager internal debtManager; Market internal marketUSDC; ERC20 internal usdc; @@ -36,7 +39,13 @@ contract DebtManagerTest is Test { debtManager = DebtManager( address( new ERC1967Proxy( - address(new DebtManager(Auditor(deployment("Auditor")), IBalancerVault(deployment("BalancerVault")))), + address( + new DebtManager( + Auditor(deployment("Auditor")), + IPermit2(deployment("Permit2")), + IBalancerVault(deployment("BalancerVault")) + ) + ), abi.encodeCall(DebtManager.initialize, ()) ) ) @@ -51,6 +60,9 @@ contract DebtManagerTest is Test { maturity = block.timestamp - (block.timestamp % FixedLib.INTERVAL) + FixedLib.INTERVAL; targetMaturity = maturity + FixedLib.INTERVAL; + + bob = vm.addr(BOB_KEY); + vm.label(bob, "bob"); } function testFuzzRolls( @@ -139,7 +151,7 @@ contract DebtManagerTest is Test { } function testLeverage() external _checkBalances { - debtManager.leverage(marketUSDC, 100_000e6, 1.03e18, true); + debtManager.leverage(marketUSDC, 100_000e6, 100_000e6, 1.03e18); (, , uint256 floatingBorrowShares) = marketUSDC.accounts(address(this)); assertEq(marketUSDC.maxWithdraw(address(this)), 510153541353); @@ -149,7 +161,7 @@ contract DebtManagerTest is Test { function testLeverageWithAlreadyDepositedAmount() external _checkBalances { usdc.approve(address(marketUSDC), type(uint256).max); marketUSDC.deposit(100_000e6, address(this)); - debtManager.leverage(marketUSDC, 100_000e6, 1.03e18, false); + debtManager.leverage(marketUSDC, 100_000e6, 0, 1.03e18); (, , uint256 floatingBorrowShares) = marketUSDC.accounts(address(this)); assertEq(marketUSDC.maxWithdraw(address(this)), 510153541352); @@ -158,17 +170,17 @@ contract DebtManagerTest is Test { function testLeverageShouldFailWhenHealthFactorNearOne() external _checkBalances { vm.expectRevert(abi.encodeWithSelector(InsufficientAccountLiquidity.selector)); - debtManager.leverage(marketUSDC, 100_000e6, 1.000000000001e18, true); + debtManager.leverage(marketUSDC, 100_000e6, 100_000e6, 1.000000000001e18); - debtManager.leverage(marketUSDC, 100_000e6, 1.00000000001e18, true); + debtManager.leverage(marketUSDC, 100_000e6, 100_000e6, 1.00000000001e18); (, , uint256 floatingBorrowShares) = marketUSDC.accounts(address(this)); assertEq(marketUSDC.maxWithdraw(address(this)), 581733565996); assertEq(marketUSDC.previewRefund(floatingBorrowShares), 481733565998); } function testDeleverage() external _checkBalances { - debtManager.leverage(marketUSDC, 100_000e6, 1.03e18, true); - debtManager.deleverage(marketUSDC, 0, 0, 1e18); + debtManager.leverage(marketUSDC, 100_000e6, 100_000e6, 1.03e18); + debtManager.deleverage(marketUSDC, 0, 0, 1e18, 0); (, , uint256 floatingBorrowShares) = marketUSDC.accounts(address(this)); // precision loss (2) @@ -176,15 +188,24 @@ contract DebtManagerTest is Test { 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); + + (, , uint256 floatingBorrowShares) = marketUSDC.accounts(address(this)); + assertEq(marketUSDC.previewRefund(floatingBorrowShares), 0); + assertEq(marketUSDC.maxWithdraw(address(this)), 0); + } + function testDeleverageHalfBorrowPosition() external _checkBalances { - debtManager.leverage(marketUSDC, 100_000e6, 1.03e18, true); + debtManager.leverage(marketUSDC, 100_000e6, 100_000e6, 1.03e18); (, , uint256 floatingBorrowShares) = marketUSDC.accounts(address(this)); uint256 leveragedDeposit = 510153541353; uint256 leveragedBorrow = 410153541355; assertEq(marketUSDC.maxWithdraw(address(this)), leveragedDeposit); assertEq(marketUSDC.previewRefund(floatingBorrowShares), leveragedBorrow); - debtManager.deleverage(marketUSDC, 0, 0, 0.5e18); + debtManager.deleverage(marketUSDC, 0, 0, 0.5e18, 0); (, , floatingBorrowShares) = marketUSDC.accounts(address(this)); uint256 deleveragedDeposit = 305076770676; uint256 deleveragedBorrow = 205076770678; @@ -197,7 +218,7 @@ contract DebtManagerTest is Test { marketUSDC.deposit(100_000e6, address(this)); marketUSDC.borrowAtMaturity(maturity, 30_000e6, type(uint256).max, address(this), address(this)); - debtManager.deleverage(marketUSDC, maturity, type(uint256).max, 1e18); + debtManager.deleverage(marketUSDC, maturity, type(uint256).max, 1e18, 0); (uint256 principal, ) = marketUSDC.fixedBorrowPositions(maturity, address(this)); (uint256 balance, uint256 debt) = marketUSDC.accountSnapshot(address(this)); assertEq(principal, 0); @@ -214,7 +235,7 @@ contract DebtManagerTest is Test { // we update accumulated earnings, etc... marketUSDC.deposit(2, address(this)); (uint256 balance, uint256 debt) = marketUSDC.accountSnapshot(address(this)); - debtManager.deleverage(marketUSDC, maturity, type(uint256).max, 1e18); + debtManager.deleverage(marketUSDC, maturity, type(uint256).max, 1e18, 0); (uint256 principal, ) = marketUSDC.fixedBorrowPositions(maturity, address(this)); (uint256 newBalance, ) = marketUSDC.accountSnapshot(address(this)); assertEq(principal, 0); @@ -225,7 +246,7 @@ contract DebtManagerTest is Test { marketUSDC.deposit(100_000e6, address(this)); marketUSDC.borrowAtMaturity(maturity, 30_000e6, type(uint256).max, address(this), address(this)); - debtManager.deleverage(marketUSDC, maturity, type(uint256).max, 0.1e18); + debtManager.deleverage(marketUSDC, maturity, type(uint256).max, 0.1e18, 0); (uint256 principal, ) = marketUSDC.fixedBorrowPositions(maturity, address(this)); (uint256 balance, uint256 debt) = marketUSDC.accountSnapshot(address(this)); assertGt(debt, 0); @@ -239,7 +260,7 @@ contract DebtManagerTest is Test { ProtocolFeesCollector(0xce88686553686DA562CE7Cea497CE749DA109f9F).setFlashLoanFeePercentage(1e15); vm.expectRevert("BAL#602"); - debtManager.leverage(marketUSDC, 100_000e6, 1.03e18, true); + debtManager.leverage(marketUSDC, 100_000e6, 100_000e6, 1.03e18); } function testApproveMarket() external { @@ -264,9 +285,9 @@ contract DebtManagerTest is Test { } function testLeverageWithInvalidBalancerVault() external { - DebtManager lev = new DebtManager(marketUSDC.auditor(), IBalancerVault(address(this))); + DebtManager lev = new DebtManager(marketUSDC.auditor(), IPermit2(address(0)), IBalancerVault(address(this))); vm.expectRevert(bytes("")); - lev.leverage(marketUSDC, 100_000e6, 1.03e18, true); + lev.leverage(marketUSDC, 100_000e6, 100_000e6, 1.03e18); } function testFloatingToFixedRoll() external _checkBalances { @@ -635,7 +656,9 @@ contract DebtManagerTest is Test { debtManager = DebtManager( address( new ERC1967Proxy( - address(new DebtManager(debtManager.auditor(), IBalancerVault(address(mockBalancerVault)))), + address( + new DebtManager(debtManager.auditor(), IPermit2(address(0)), IBalancerVault(address(mockBalancerVault))) + ), abi.encodeCall(DebtManager.initialize, ()) ) ) @@ -654,16 +677,12 @@ contract DebtManagerTest is Test { } function testPermitAndRollFloatingToFixed() external { - uint256 bobKey = 0xb0b; - address bob = vm.addr(bobKey); - vm.label(bob, "bob"); - marketUSDC.deposit(100_000e6, bob); vm.prank(bob); marketUSDC.borrow(50_000e6, bob, bob); (uint8 v, bytes32 r, bytes32 s) = vm.sign( - bobKey, + BOB_KEY, keccak256( abi.encodePacked( "\x19\x01", @@ -687,6 +706,82 @@ contract DebtManagerTest is Test { assertEq(principal, 50_000e6 + 1); } + function testPermitAndDeleverage() external { + marketUSDC.deposit(100_000e6, bob); + vm.prank(bob); + marketUSDC.borrow(50_000e6, bob, bob); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + BOB_KEY, + keccak256( + abi.encodePacked( + "\x19\x01", + marketUSDC.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + bob, + debtManager, + 60_000e6, + marketUSDC.nonces(bob), + block.timestamp + ) + ) + ) + ) + ); + vm.prank(bob); + debtManager.deleverage(marketUSDC, 0, 0, 1e18, 10_000e6, 60_000e6, Permit(bob, block.timestamp, v, r, s)); + } + + function testPermit2AndLeverage() external { + IPermit2 permit2 = debtManager.permit2(); + uint256 amount = 100_000e6; + + deal(address(usdc), bob, amount); + vm.startPrank(bob); + usdc.approve(address(permit2), type(uint256).max); + marketUSDC.approve(address(debtManager), type(uint256).max); + vm.stopPrank(); + + IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({ token: address(usdc), amount: amount }), + nonce: uint256(keccak256(abi.encode(bob, usdc, amount, block.timestamp))), + deadline: block.timestamp + }); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + BOB_KEY, + keccak256( + abi.encodePacked( + "\x19\x01", + permit2.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + // solhint-disable-next-line max-line-length + "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ), + keccak256( + abi.encode( + keccak256("TokenPermissions(address token,uint256 amount)"), + permit.permitted.token, + permit.permitted.amount + ) + ), + debtManager, + permit.nonce, + permit.deadline + ) + ) + ) + ) + ); + bytes memory sig = abi.encodePacked(r, s, v); + + vm.prank(bob); + debtManager.leverage(marketUSDC, amount, amount, 1.03e18, permit.deadline, sig); + } + modifier _checkBalances() { uint256 vault = usdc.balanceOf(address(debtManager.balancerVault())); _;