diff --git a/.changeset/great-jokes-do.md b/.changeset/great-jokes-do.md new file mode 100644 index 000000000..90afbd083 --- /dev/null +++ b/.changeset/great-jokes-do.md @@ -0,0 +1,5 @@ +--- +"@exactly/protocol": patch +--- + +✨ debt-manager: support `EIP-2612` permit diff --git a/.gas-snapshot b/.gas-snapshot index 238840a5a..545204105 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -10,47 +10,48 @@ AuditorTest:testEnableMarketAuditorMismatch() (gas: 24805) AuditorTest:testEnableMarketShouldRevertWithInvalidPriceFeed() (gas: 149281) AuditorTest:testEnterExitMarket() (gas: 178761) AuditorTest:testExitMarketOwning() (gas: 177445) -DebtManagerTest:testApproveMaliciousMarket() (gas: 29119) -DebtManagerTest:testApproveMarket() (gas: 61610) -DebtManagerTest:testAvailableLiquidity() (gas: 100410) -DebtManagerTest:testBalancerFlashloanCallFromDifferentOrigin() (gas: 65654) -DebtManagerTest:testCallReceiveFlashLoanFromAnyAddress() (gas: 33485) -DebtManagerTest:testDeleverage() (gas: 461852) -DebtManagerTest:testDeleverageHalfBorrowPosition() (gas: 500975) -DebtManagerTest:testFixedDeleverage() (gas: 451803) -DebtManagerTest:testFixedRoll() (gas: 525733) -DebtManagerTest:testFixedRollSameMaturityWithThreeLoops() (gas: 400447) -DebtManagerTest:testFixedRollWithAccurateBorrowSlippage() (gas: 796123) -DebtManagerTest:testFixedRollWithAccurateBorrowSlippageWithThreeLoops() (gas: 1130235) -DebtManagerTest:testFixedRollWithAccurateRepaySlippage() (gas: 796083) -DebtManagerTest:testFixedRollWithAccurateRepaySlippageWithThreeLoops() (gas: 1124995) -DebtManagerTest:testFixedToFloatingRoll() (gas: 480252) -DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidity() (gas: 542869) -DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidityWithSlippage() (gas: 833198) -DebtManagerTest:testFixedToFloatingRollHigherThanAvailableLiquidityWithSlippageWithThreeLoops() (gas: 1004798) -DebtManagerTest:testFixedToFloatingRollWithAccurateSlippage() (gas: 680795) -DebtManagerTest:testFlashloanFeeGreaterThanZero() (gas: 429735) -DebtManagerTest:testFloatingToFixedRoll() (gas: 516165) -DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidity() (gas: 601795) -DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippage() (gas: 999755) -DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippageWithThreePools() (gas: 1208171) -DebtManagerTest:testFloatingToFixedRollWithAccurateSlippage() (gas: 806652) -DebtManagerTest:testFloatingToFixedRollWithAccurateSlippageWithPreviousPosition() (gas: 757550) -DebtManagerTest:testLateFixedDeleverage() (gas: 484949) -DebtManagerTest:testLateFixedRoll() (gas: 536412) -DebtManagerTest:testLateFixedRollWithThreeLoops() (gas: 720006) -DebtManagerTest:testLateFixedToFloatingRoll() (gas: 487641) -DebtManagerTest:testLateFixedToFloatingRollWithThreeLoops() (gas: 649144) -DebtManagerTest:testLeverage() (gas: 375538) -DebtManagerTest:testLeverageShouldFailWhenHealthFactorNearOne() (gas: 751995) -DebtManagerTest:testLeverageWithAlreadyDepositedAmount() (gas: 403934) -DebtManagerTest:testLeverageWithInvalidBalancerVault() (gas: 2751564) -DebtManagerTest:testMockBalancerVault() (gas: 4104898) -DebtManagerTest:testPartialFixedDeleverage() (gas: 525811) -DebtManagerTest:testPartialFixedRoll() (gas: 589054) -DebtManagerTest:testPartialFixedToFloatingRoll() (gas: 553117) -DebtManagerTest:testPartialLateFixedRoll() (gas: 581433) -DebtManagerTest:testPartialLateFixedToFloatingRoll() (gas: 551859) +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) 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 0087e7dd8..6ee63c55d 100644 --- a/contracts/periphery/DebtManager.sol +++ b/contracts/periphery/DebtManager.sol @@ -101,7 +101,7 @@ contract DebtManager is Initializable { uint256 borrowMaturity, uint256 maxBorrowAssets, uint256 percentage - ) external { + ) public { uint256[] memory amounts = new uint256[](1); ERC20[] memory tokens = new ERC20[](1); bytes[] memory calls; @@ -152,7 +152,7 @@ contract DebtManager is Initializable { uint256 repayMaturity, uint256 maxRepayAssets, uint256 percentage - ) external { + ) public { uint256[] memory amounts = new uint256[](1); ERC20[] memory tokens = new ERC20[](1); bytes[] memory calls; @@ -196,14 +196,14 @@ contract DebtManager is Initializable { /// @param maxRepayAssets Max amount of debt that the account is willing to accept to be repaid. /// @param maxBorrowAssets Max amount of debt that the sender is willing to accept to be borrowed. /// @param percentage The percentage of the position that will be rolled, represented with 18 decimals. - function fixedRoll( + function rollFixed( Market market, uint256 repayMaturity, uint256 borrowMaturity, uint256 maxRepayAssets, uint256 maxBorrowAssets, uint256 percentage - ) external { + ) public { uint256[] memory amounts = new uint256[](1); ERC20[] memory tokens = new ERC20[](1); bytes[] memory calls; @@ -316,6 +316,73 @@ contract DebtManager is Initializable { } } + /// @notice Calls `token.permit` on behalf of `permit.account`. + /// @param token The `ERC20` to call `permit`. + /// @param p Arguments for the permit call. + modifier permit( + ERC20 token, + uint256 assets, + Permit calldata p + ) { + token.permit(p.account, address(this), assets, p.deadline, p.v, p.r, p.s); + _; + } + + /// @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. + /// @param borrowMaturity The maturity of the fixed pool that the position is being rolled to. + /// @param maxBorrowAssets Max amount of debt that the sender is willing to accept to be borrowed. + /// @param percentage The percentage of the position that will be rolled, represented with 18 decimals. + /// @param p Arguments for the permit call to `market` on behalf of `permit.account`. + function rollFloatingToFixed( + Market market, + uint256 borrowMaturity, + uint256 maxBorrowAssets, + uint256 percentage, + Permit calldata p + ) external permit(market, maxBorrowAssets, p) { + rollFloatingToFixed(market, borrowMaturity, maxBorrowAssets, percentage); + } + + /// @notice Rolls a percentage of the fixed position of `msg.sender` to a floating position + /// after calling `market.permit`. + /// @param market The Market to roll the position in. + /// @param repayMaturity The maturity of the fixed pool that the position is being rolled from. + /// @param maxRepayAssets Max amount of debt that the account is willing to accept to be repaid. + /// @param percentage The percentage of the position that will be rolled, represented with 18 decimals. + /// @param p Arguments for the permit call to `market` on behalf of `permit.account`. + function rollFixedToFloating( + Market market, + uint256 repayMaturity, + uint256 maxRepayAssets, + uint256 percentage, + Permit calldata p + ) external permit(market, maxRepayAssets, p) { + rollFixedToFloating(market, repayMaturity, maxRepayAssets, percentage); + } + + /// @notice Rolls a percentage of the fixed position of `msg.sender` to another fixed pool + /// after calling `market.permit`. + /// @param market The Market to roll the position in. + /// @param repayMaturity The maturity of the fixed pool that the position is being rolled from. + /// @param borrowMaturity The maturity of the fixed pool that the position is being rolled to. + /// @param maxRepayAssets Max amount of debt that the account is willing to accept to be repaid. + /// @param maxBorrowAssets Max amount of debt that the sender is willing to accept to be borrowed. + /// @param percentage The percentage of the position that will be rolled, represented with 18 decimals. + /// @param p Arguments for the permit call to `market` on behalf of `permit.account`. + function rollFixed( + Market market, + uint256 repayMaturity, + uint256 borrowMaturity, + uint256 maxRepayAssets, + uint256 maxBorrowAssets, + uint256 percentage, + Permit calldata p + ) external permit(market, maxBorrowAssets, p) { + rollFixed(market, repayMaturity, borrowMaturity, maxRepayAssets, maxBorrowAssets, percentage); + } + /// @notice Returns Balancer Vault's available liquidity of each enabled underlying asset. function availableLiquidity() external view returns (AvailableAsset[] memory availableAssets) { uint256 marketsCount = auditor.allMarkets().length; @@ -345,6 +412,14 @@ contract DebtManager is Initializable { error InvalidOperation(); +struct Permit { + address account; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; +} + struct RollVars { uint256 positionAssets; uint256 repayAssets; diff --git a/test/solidity/DebtManager.t.sol b/test/solidity/DebtManager.t.sol index 8c3f956b7..39403ca7d 100644 --- a/test/solidity/DebtManager.t.sol +++ b/test/solidity/DebtManager.t.sol @@ -6,6 +6,7 @@ import { FixedPointMathLib } from "solmate/src/utils/FixedPointMathLib.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { ERC20, + Permit, DebtManager, Disagreement, IBalancerVault, @@ -40,6 +41,7 @@ contract DebtManagerTest is Test { ) ) ); + vm.label(address(debtManager), "DebtManager"); assertLt(usdc.balanceOf(address(debtManager.balancerVault())), 1_000_000e6); deal(address(usdc), address(this), 22_000_000e6); @@ -82,7 +84,7 @@ contract DebtManagerTest is Test { debtManager.rollFixedToFloating(marketUSDC, maturity, type(uint256).max, percentage); checkRevert(percentage, Operation.FixedToFixed); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, percentage); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, percentage); vm.warp(block.timestamp + uint256(times[k]) * 1 hours); } @@ -479,7 +481,7 @@ contract DebtManagerTest is Test { marketUSDC.deposit(100_000e6, address(this)); marketUSDC.borrowAtMaturity(maturity, 50_000e6, type(uint256).max, address(this), address(this)); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, 1e18); (uint256 principal, ) = marketUSDC.fixedBorrowPositions(maturity, address(this)); assertEq(principal, 0); (principal, ) = marketUSDC.fixedBorrowPositions(targetMaturity, address(this)); @@ -490,7 +492,7 @@ contract DebtManagerTest is Test { marketUSDC.deposit(100_000e6, address(this)); marketUSDC.borrowAtMaturity(maturity, 50_000e6, type(uint256).max, address(this), address(this)); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, 0.1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, 0.1e18); (uint256 principal, ) = marketUSDC.fixedBorrowPositions(maturity, address(this)); assertEq(principal, 45_000e6); (principal, ) = marketUSDC.fixedBorrowPositions(targetMaturity, address(this)); @@ -504,9 +506,9 @@ contract DebtManagerTest is Test { uint256 maxRepayAssets = 50_004_915_917; vm.expectRevert(Disagreement.selector); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, maxRepayAssets, type(uint256).max, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, maxRepayAssets, type(uint256).max, 1e18); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, ++maxRepayAssets, type(uint256).max, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, ++maxRepayAssets, type(uint256).max, 1e18); } function testFixedRollWithAccurateBorrowSlippage() external _checkBalances { @@ -515,9 +517,9 @@ contract DebtManagerTest is Test { uint256 maxBorrowAssets = 50_128_835_188; vm.expectRevert(Disagreement.selector); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, type(uint256).max, maxBorrowAssets, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, maxBorrowAssets, 1e18); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, type(uint256).max, ++maxBorrowAssets, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, ++maxBorrowAssets, 1e18); } function testLateFixedRoll() external _checkBalances { @@ -527,7 +529,7 @@ contract DebtManagerTest is Test { vm.warp(maturity + 1 days); uint256 debt = marketUSDC.previewDebt(address(this)); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, 1e18); assertGt(marketUSDC.previewDebt(address(this)), debt); (uint256 newRepayPrincipal, ) = marketUSDC.fixedBorrowPositions(maturity, address(this)); assertEq(newRepayPrincipal, 0); @@ -542,7 +544,7 @@ contract DebtManagerTest is Test { vm.warp(maturity + 1 days); uint256 debt = marketUSDC.previewDebt(address(this)); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, 0.5e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, 0.5e18); assertGt(marketUSDC.previewDebt(address(this)), debt); (uint256 newRepayPrincipal, ) = marketUSDC.fixedBorrowPositions(maturity, address(this)); assertEq(newRepayPrincipal, 25_000e6); @@ -557,9 +559,9 @@ contract DebtManagerTest is Test { uint256 fees = 32_472_619_932; vm.expectRevert(Disagreement.selector); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, type(uint256).max, 2_000_000e6 + fees, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, 2_000_000e6 + fees, 1e18); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, type(uint256).max, 2_000_000e6 + ++fees, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, 2_000_000e6 + ++fees, 1e18); (uint256 principal, uint256 fee) = marketUSDC.fixedBorrowPositions(maturity, address(this)); assertEq(principal + fee, 0); (principal, fee) = marketUSDC.fixedBorrowPositions(targetMaturity, address(this)); @@ -572,7 +574,7 @@ contract DebtManagerTest is Test { marketUSDC.borrowAtMaturity(maturity, 2_000_000e6, type(uint256).max, address(this), address(this)); vm.expectRevert(InvalidOperation.selector); - debtManager.fixedRoll(marketUSDC, maturity, maturity, type(uint256).max, type(uint256).max, 1e18); + debtManager.rollFixed(marketUSDC, maturity, maturity, type(uint256).max, type(uint256).max, 1e18); } function testFixedRollWithAccurateRepaySlippageWithThreeLoops() external _checkBalances { @@ -582,9 +584,9 @@ contract DebtManagerTest is Test { uint256 maxRepayAssets = 2_001_052_304_918; vm.expectRevert(Disagreement.selector); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, maxRepayAssets, type(uint256).max, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, maxRepayAssets, type(uint256).max, 1e18); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, ++maxRepayAssets, type(uint256).max, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, ++maxRepayAssets, type(uint256).max, 1e18); } function testLateFixedRollWithThreeLoops() external _checkBalances { @@ -595,7 +597,7 @@ contract DebtManagerTest is Test { vm.warp(maturity + 1 days); uint256 debt = marketUSDC.previewDebt(address(this)); - debtManager.fixedRoll(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, 1e18); + debtManager.rollFixed(marketUSDC, maturity, targetMaturity, type(uint256).max, type(uint256).max, 1e18); assertGt(marketUSDC.previewDebt(address(this)), debt); (uint256 newRepayPrincipal, ) = marketUSDC.fixedBorrowPositions(maturity, address(this)); assertEq(newRepayPrincipal, 0); @@ -616,11 +618,9 @@ contract DebtManagerTest is Test { tokens[0] = usdc; uint256[] memory amounts = new uint256[](1); amounts[0] = 0; - bytes memory maliciousCall = abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - address(this), - address(1), - usdc.balanceOf(address(this)) + bytes memory maliciousCall = abi.encodeCall( + ERC20.transferFrom, + (address(this), address(1), usdc.balanceOf(address(this))) ); bytes[] memory calls = new bytes[](1); calls[0] = abi.encodePacked(maliciousCall); @@ -653,6 +653,40 @@ contract DebtManagerTest is Test { assertEq(principal, 50_000e6 + 1); } + 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, + 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, + 66_666e6, + marketUSDC.nonces(bob), + block.timestamp + ) + ) + ) + ) + ); + vm.prank(bob); + debtManager.rollFloatingToFixed(marketUSDC, maturity, 66_666e6, 1e18, Permit(bob, block.timestamp, v, r, s)); + (uint256 principal, ) = marketUSDC.fixedBorrowPositions(maturity, bob); + assertEq(principal, 50_000e6 + 1); + } + modifier _checkBalances() { uint256 vault = usdc.balanceOf(address(debtManager.balancerVault())); _;