diff --git a/snapshots/BatchRepayBadDebtSteward.json b/snapshots/BatchRepayBadDebtSteward.json index 4bf2f91..906b741 100644 --- a/snapshots/BatchRepayBadDebtSteward.json +++ b/snapshots/BatchRepayBadDebtSteward.json @@ -1,35 +1,28 @@ { - "function batchLiquidate: with 0 users": "250226", - "function batchLiquidate: with 1 user": "580464", - "function batchLiquidate: with 2 users": "907296", - "function batchLiquidate: with 3 users": "1125027", - "function batchLiquidate: with 4 users": "1342771", - "function batchLiquidate: with 5 users": "1666388", - "function batchLiquidate: with 6 users": "1884131", - "function batchLiquidateWithMaxCap: with 0 users": "271158", - "function batchLiquidateWithMaxCap: with 1 user": "572541", - "function batchLiquidateWithMaxCap: with 2 users": "893497", - "function batchLiquidateWithMaxCap: with 3 users": "1105352", - "function batchLiquidateWithMaxCap: with 4 users": "1317219", - "function batchLiquidateWithMaxCap: with 5 users": "1634961", - "function batchLiquidateWithMaxCap: with 6 users": "1846828", - "function batchRepayBadDebt: with 0 users": "70072", - "function batchRepayBadDebt: with 1 user": "222244", - "function batchRepayBadDebt: with 2 users": "284640", - "function batchRepayBadDebt: with 3 users": "347036", - "function batchRepayBadDebt: with 4 users": "409431", - "function batchRepayBadDebt: with 5 users": "471827", - "function batchRepayBadDebt: with 6 users": "534223", - "function getBadDebtAmount: with 0 users": "12852", - "function getBadDebtAmount: with 1 user": "38658", - "function getBadDebtAmount: with 2 users": "51412", - "function getBadDebtAmount: with 4 users": "76922", - "function getBadDebtAmount: with 5 users": "89677", - "function getBadDebtAmount: with 6 users": "102432", - "function getDebtAmount: with 0 users": "12875", - "function getDebtAmount: with 1 user": "34341", - "function getDebtAmount: with 2 users": "51412", - "function getDebtAmount: with 4 users": "59584", - "function getDebtAmount: with 5 users": "67998", - "function getDebtAmount: with 6 users": "76413" + "function batchLiquidate: with 0 users": "98545", + "function batchLiquidate: with 1 user": "585000", + "function batchLiquidate: with 2 users": "911832", + "function batchLiquidate: with 3 users": "1129564", + "function batchLiquidate: with 4 users": "1347307", + "function batchLiquidate: with 5 users": "1670924", + "function batchLiquidate: with 6 users": "1888668", + "function batchRepayBadDebt: with 0 users": "70526", + "function batchRepayBadDebt: with 1 user": "222698", + "function batchRepayBadDebt: with 2 users": "285094", + "function batchRepayBadDebt: with 3 users": "347490", + "function batchRepayBadDebt: with 4 users": "409885", + "function batchRepayBadDebt: with 5 users": "472281", + "function batchRepayBadDebt: with 6 users": "534677", + "function getBadDebtAmount: with 0 users": "12830", + "function getBadDebtAmount: with 1 user": "38636", + "function getBadDebtAmount: with 2 users": "51390", + "function getBadDebtAmount: with 4 users": "76900", + "function getBadDebtAmount: with 5 users": "89655", + "function getBadDebtAmount: with 6 users": "102410", + "function getDebtAmount: with 0 users": "12853", + "function getDebtAmount: with 1 user": "34319", + "function getDebtAmount: with 2 users": "51390", + "function getDebtAmount: with 4 users": "59562", + "function getDebtAmount: with 5 users": "67976", + "function getDebtAmount: with 6 users": "76391" } \ No newline at end of file diff --git a/src/maintenance/BatchRepayBadDebtSteward.sol b/src/maintenance/BatchRepayBadDebtSteward.sol index 6f26989..0ebddb5 100644 --- a/src/maintenance/BatchRepayBadDebtSteward.sol +++ b/src/maintenance/BatchRepayBadDebtSteward.sol @@ -77,50 +77,33 @@ contract BatchRepayBadDebtSteward is IBatchRepayBadDebtSteward, RescuableBase, M } /* EXTERNAL FUNCTIONS */ - - /// @inheritdoc IBatchRepayBadDebtSteward - function batchLiquidate(address debtAsset, address collateralAsset, address[] memory users) external override { - (uint256 maxDebtAmount,) = getDebtAmount(debtAsset, users); - - batchLiquidateWithMaxCap(debtAsset, collateralAsset, users, maxDebtAmount); - } - /// @inheritdoc IBatchRepayBadDebtSteward - function batchRepayBadDebt(address asset, address[] memory users) external override onlyRole(CLEANUP) { + function batchRepayBadDebt(address asset, address[] memory users, bool useATokens) + external + override + onlyRole(CLEANUP) + { (uint256 totalDebtAmount, uint256[] memory debtAmounts) = getBadDebtAmount(asset, users); - - ICollector(COLLECTOR).transfer(IERC20Col(asset), address(this), totalDebtAmount); - IERC20(asset).forceApprove(address(POOL), totalDebtAmount); + _pullFundsAndApprove(asset, totalDebtAmount, useATokens); for (uint256 i = 0; i < users.length; i++) { POOL.repay({asset: asset, amount: debtAmounts[i], interestRateMode: 2, onBehalfOf: users[i]}); } - uint256 balanceLeft = IERC20(asset).balanceOf(address(this)); - if (balanceLeft != 0) IERC20(asset).transfer(COLLECTOR, balanceLeft); + _transferExcessToCollector(asset); } /// @inheritdoc IBatchRepayBadDebtSteward - function rescueToken(address token) external override { - _emergencyTokenTransfer(token, COLLECTOR, type(uint256).max); - } - - /// @inheritdoc IBatchRepayBadDebtSteward - function rescueEth() external override { - _emergencyEtherTransfer(COLLECTOR, address(this).balance); - } - - /* PUBLIC FUNCTIONS */ - - /// @inheritdoc IBatchRepayBadDebtSteward - function batchLiquidateWithMaxCap( - address debtAsset, - address collateralAsset, - address[] memory users, - uint256 maxDebtTokenAmount - ) public override onlyRole(CLEANUP) { - ICollector(COLLECTOR).transfer(IERC20Col(debtAsset), address(this), maxDebtTokenAmount); - IERC20(debtAsset).forceApprove(address(POOL), maxDebtTokenAmount); + function batchLiquidate(address debtAsset, address collateralAsset, address[] memory users, bool useAToken) + external + override + onlyRole(CLEANUP) + { + // this is an over approximation as not necessarily all bad debt can be liquidated + // the excess is transfered back to the collector + (uint256 maxDebtAmount,) = getDebtAmount(debtAsset, users); + _pullFundsAndApprove(debtAsset, maxDebtAmount, useAToken); + IERC20(debtAsset).forceApprove(address(POOL), maxDebtAmount); for (uint256 i = 0; i < users.length; i++) { POOL.liquidationCall({ @@ -132,16 +115,22 @@ contract BatchRepayBadDebtSteward is IBatchRepayBadDebtSteward, RescuableBase, M }); } - // transfer back surplus - uint256 balanceAfter = IERC20(debtAsset).balanceOf(address(this)); - if (balanceAfter != 0) { - IERC20(debtAsset).safeTransfer(COLLECTOR, balanceAfter); - } + // the excess is always in the underlying + _transferExcessToCollector(debtAsset); // transfer back liquidated assets address collateralAToken = POOL.getReserveAToken(collateralAsset); - uint256 collateralATokenBalance = IERC20(collateralAToken).balanceOf(address(this)); - IERC20(collateralAToken).safeTransfer(COLLECTOR, collateralATokenBalance); + _transferExcessToCollector(collateralAToken); + } + + /// @inheritdoc IBatchRepayBadDebtSteward + function rescueToken(address token) external override { + _emergencyTokenTransfer(token, COLLECTOR, type(uint256).max); + } + + /// @inheritdoc IBatchRepayBadDebtSteward + function rescueEth() external override { + _emergencyEtherTransfer(COLLECTOR, address(this).balance); } /* PUBLIC VIEW FUNCTIONS */ @@ -202,4 +191,23 @@ contract BatchRepayBadDebtSteward is IBatchRepayBadDebtSteward, RescuableBase, M return (totalDebtAmount, debtAmounts); } + + function _pullFundsAndApprove(address asset, uint256 amount, bool unwrapAToken) internal { + if (unwrapAToken) { + address aToken = POOL.getReserveAToken(asset); + // 1 wei surplus to account for rounding on multiple operations + ICollector(COLLECTOR).transfer(IERC20Col(aToken), address(this), amount + 1); + POOL.withdraw(asset, type(uint256).max, address(this)); + } else { + ICollector(COLLECTOR).transfer(IERC20Col(asset), address(this), amount); + } + IERC20(asset).forceApprove(address(POOL), amount); + } + + function _transferExcessToCollector(address asset) internal { + uint256 balanceAfter = IERC20(asset).balanceOf(address(this)); + if (balanceAfter != 0) { + IERC20(asset).safeTransfer(COLLECTOR, balanceAfter); + } + } } diff --git a/src/maintenance/interfaces/IBatchRepayBadDebtSteward.sol b/src/maintenance/interfaces/IBatchRepayBadDebtSteward.sol index 720a539..0b19613 100644 --- a/src/maintenance/interfaces/IBatchRepayBadDebtSteward.sol +++ b/src/maintenance/interfaces/IBatchRepayBadDebtSteward.sol @@ -31,27 +31,13 @@ interface IBatchRepayBadDebtSteward is IRescuableBase, IAccessControl { /* EXTERNAL FUNCTIONS */ - /// @notice Liquidates all the users - /// @param debtAsset The address of the debt asset - /// @param collateralAsset The address of the collateral asset that will be liquidated - /// @param users The addresses of the users to liquidate - /// @dev This amount is pulled from the Aave collector. The contract sends - /// any surplus back to the collector. - function batchLiquidate(address debtAsset, address collateralAsset, address[] memory users) external; - /// @notice Liquidates all the users with a max debt amount to be liquidated /// @param debtAsset The address of the debt asset /// @param collateralAsset The address of the collateral asset that will be liquidated /// @param users The addresses of the users to liquidate - /// @param maxDebtTokenAmount The maximum amount of debt tokens to be liquidated - /// @dev This amount is pulled from the Aave collector. The contract sends - /// any surplus back to the collector. - function batchLiquidateWithMaxCap( - address debtAsset, - address collateralAsset, - address[] memory users, - uint256 maxDebtTokenAmount - ) external; + /// @param useATokens If true the token will pull aTokens from the collector. + /// If false it will pull the underlying. + function batchLiquidate(address debtAsset, address collateralAsset, address[] memory users, bool useATokens) external; /// @notice Repays all the bad debt of users /// @dev Will revert if the user has a collateral or no debt. @@ -59,7 +45,9 @@ interface IBatchRepayBadDebtSteward is IRescuableBase, IAccessControl { /// any surplus back to the collector. /// @param asset The address of an asset to repay /// @param users The addresses of users to repay - function batchRepayBadDebt(address asset, address[] calldata users) external; + /// @param useATokens If true the token will pull aTokens from the collector. + /// If false it will pull the underlying. + function batchRepayBadDebt(address asset, address[] calldata users, bool useATokens) external; /// @notice Rescues the tokens /// @param token The address of the token to rescue diff --git a/tests/gas/maintenance/BatchRepayBadDebtSteward.gas.t.sol b/tests/gas/maintenance/BatchRepayBadDebtSteward.gas.t.sol index 9045545..c6d9fc6 100644 --- a/tests/gas/maintenance/BatchRepayBadDebtSteward.gas.t.sol +++ b/tests/gas/maintenance/BatchRepayBadDebtSteward.gas.t.sol @@ -181,48 +181,6 @@ contract BatchRepayBadDebtStewardGasTest is BatchRepayBadDebtStewardBaseTest { vm.snapshotGasLastCall("BatchRepayBadDebtSteward", "function batchLiquidate: with 6 users"); } - function test_batchLiquidateWithMaxCap_zero_users() public { - _callBatchLiquidateWithMaxCapWithNumberOfUsers(0); - - vm.snapshotGasLastCall("BatchRepayBadDebtSteward", "function batchLiquidateWithMaxCap: with 0 users"); - } - - function test_batchLiquidateWithMaxCap_one_user() public { - _callBatchLiquidateWithMaxCapWithNumberOfUsers(1); - - vm.snapshotGasLastCall("BatchRepayBadDebtSteward", "function batchLiquidateWithMaxCap: with 1 user"); - } - - function test_batchLiquidateWithMaxCap_two_users() public { - _callBatchLiquidateWithMaxCapWithNumberOfUsers(2); - - vm.snapshotGasLastCall("BatchRepayBadDebtSteward", "function batchLiquidateWithMaxCap: with 2 users"); - } - - function test_batchLiquidateWithMaxCap_three_users() public { - _callBatchLiquidateWithMaxCapWithNumberOfUsers(3); - - vm.snapshotGasLastCall("BatchRepayBadDebtSteward", "function batchLiquidateWithMaxCap: with 3 users"); - } - - function test_batchLiquidateWithMaxCap_four_users() public { - _callBatchLiquidateWithMaxCapWithNumberOfUsers(4); - - vm.snapshotGasLastCall("BatchRepayBadDebtSteward", "function batchLiquidateWithMaxCap: with 4 users"); - } - - function test_batchLiquidateWithMaxCap_five_users() public { - _callBatchLiquidateWithMaxCapWithNumberOfUsers(5); - - vm.snapshotGasLastCall("BatchRepayBadDebtSteward", "function batchLiquidateWithMaxCap: with 5 users"); - } - - function test_batchLiquidateWithMaxCap_six_users() public { - _callBatchLiquidateWithMaxCapWithNumberOfUsers(6); - - vm.snapshotGasLastCall("BatchRepayBadDebtSteward", "function batchLiquidateWithMaxCap: with 6 users"); - } - function _callGetBadDebtAmountWithNumberOfUsers(uint256 userAmount) private view { address[] memory users = new address[](userAmount); for (uint256 i = 0; i < userAmount; ++i) { @@ -251,7 +209,7 @@ contract BatchRepayBadDebtStewardGasTest is BatchRepayBadDebtStewardBaseTest { deal(assetUnderlying, collector, mintAmount); vm.prank(guardian); - steward.batchRepayBadDebt(assetUnderlying, users); + steward.batchRepayBadDebt(assetUnderlying, users, false); } function _callBatchLiquidateWithNumberOfUsers(uint256 userAmount) private { @@ -264,19 +222,6 @@ contract BatchRepayBadDebtStewardGasTest is BatchRepayBadDebtStewardBaseTest { deal(assetUnderlying, collector, mintAmount); vm.prank(guardian); - steward.batchLiquidate(assetUnderlying, collateralEligibleForLiquidations, users); - } - - function _callBatchLiquidateWithMaxCapWithNumberOfUsers(uint256 userAmount) private { - address[] memory users = new address[](userAmount); - for (uint256 i = 0; i < userAmount; ++i) { - users[i] = usersEligibleForLiquidations[i]; - } - - uint256 mintAmount = 1_000_000e18; - deal(assetUnderlying, collector, mintAmount); - - vm.prank(guardian); - steward.batchLiquidateWithMaxCap(assetUnderlying, collateralEligibleForLiquidations, users, totalDebtToLiquidate); + steward.batchLiquidate(assetUnderlying, collateralEligibleForLiquidations, users, false); } } diff --git a/tests/maintenance/BatchRepayBadDebtSteward.t.sol b/tests/maintenance/BatchRepayBadDebtSteward.t.sol index 8d01aab..44dca9c 100644 --- a/tests/maintenance/BatchRepayBadDebtSteward.t.sol +++ b/tests/maintenance/BatchRepayBadDebtSteward.t.sol @@ -60,11 +60,9 @@ contract BatchRepayBadDebtStewardBaseTest is Test { address public collector = address(AaveV3Avalanche.COLLECTOR); function setUp() public { - vm.createSelectFork(vm.rpcUrl("avalanche"), 56921378); // https://snowscan.xyz/block/56768474 + vm.createSelectFork(vm.rpcUrl("avalanche"), 57114758); // https://snowscan.xyz/block/56768474 steward = new BatchRepayBadDebtSteward(address(AaveV3Avalanche.POOL), collector, admin, guardian); - // collector upgrade - GovV3Helpers.executePayload(vm, 65); // v3.3 pool upgrade GovV3Helpers.executePayload(vm, 67); vm.prank(AaveV3Avalanche.ACL_ADMIN); @@ -114,7 +112,7 @@ contract BatchRepayBadDebtStewardTest is BatchRepayBadDebtStewardBaseTest { uint256 collectorBalanceBefore = IERC20(assetUnderlying).balanceOf(collector); vm.prank(guardian); - steward.batchRepayBadDebt(assetUnderlying, usersWithBadDebt); + steward.batchRepayBadDebt(assetUnderlying, usersWithBadDebt, false); uint256 collectorBalanceAfter = IERC20(assetUnderlying).balanceOf(collector); @@ -125,6 +123,29 @@ contract BatchRepayBadDebtStewardTest is BatchRepayBadDebtStewardBaseTest { } } + function test_batchRepayBadDebtUseAToken() public { + address aToken = AaveV3Avalanche.POOL.getReserveAToken(assetUnderlying); + uint256 collectorUnderlyingBalanceBefore = IERC20(assetUnderlying).balanceOf(collector); + uint256 collectorBalanceBefore = IERC20(aToken).balanceOf(collector); + + vm.prank(guardian); + steward.batchRepayBadDebt(assetUnderlying, usersWithBadDebt, false); + + uint256 collectorUnderlyingBalanceAfter = IERC20(assetUnderlying).balanceOf(collector); + uint256 collectorBalanceAfter = IERC20(aToken).balanceOf(collector); + + assertApproxEqAbs( + (collectorBalanceBefore + collectorUnderlyingBalanceBefore) + - (collectorBalanceAfter + collectorUnderlyingBalanceAfter), + totalBadDebt, + 100 + ); + + for (uint256 i = 0; i < usersWithBadDebt.length; i++) { + assertEq(IERC20(assetDebtToken).balanceOf(usersWithBadDebt[i]), 0); + } + } + function test_reverts_batchRepayBadDebt_caller_not_cleaner(address caller) public { vm.assume(caller != guardian); @@ -135,7 +156,7 @@ contract BatchRepayBadDebtStewardTest is BatchRepayBadDebtStewardBaseTest { ); vm.prank(caller); - steward.batchRepayBadDebt(assetUnderlying, usersWithBadDebt); + steward.batchRepayBadDebt(assetUnderlying, usersWithBadDebt, false); } function test_batchLiquidate() public { @@ -148,7 +169,7 @@ contract BatchRepayBadDebtStewardTest is BatchRepayBadDebtStewardBaseTest { uint256 stewardCollateralBalanceBefore = IERC20(collateralReserveData.aTokenAddress).balanceOf(address(steward)); vm.prank(guardian); - steward.batchLiquidate(assetUnderlying, collateralEligibleForLiquidations, usersEligibleForLiquidations); + steward.batchLiquidate(assetUnderlying, collateralEligibleForLiquidations, usersEligibleForLiquidations, false); uint256 collectorBalanceAfter = IERC20(assetUnderlying).balanceOf(collector); uint256 stewardBalanceAfter = IERC20(assetUnderlying).balanceOf(address(steward)); @@ -173,28 +194,36 @@ contract BatchRepayBadDebtStewardTest is BatchRepayBadDebtStewardBaseTest { } } - function test_batchLiquidateWithMaxCap() public { - uint256 passedAmount = totalDebtToLiquidate - 100; + function test_batchLiquidateUseAToken() public { + address debtAToken = AaveV3Avalanche.POOL.getReserveAToken(assetUnderlying); + uint256 collectorBalanceBefore = IERC20(debtAToken).balanceOf(collector); + uint256 stewardBalanceBefore = IERC20(assetUnderlying).balanceOf(address(steward)); - uint256 collectorBalanceBefore = IERC20(assetUnderlying).balanceOf(collector); + address collateralAToken = AaveV3Avalanche.POOL.getReserveAToken(collateralEligibleForLiquidations); + uint256 collectorCollateralBalanceBefore = IERC20(collateralAToken).balanceOf(collector); + uint256 stewardCollateralBalanceBefore = IERC20(collateralAToken).balanceOf(address(steward)); vm.prank(guardian); - steward.batchLiquidateWithMaxCap( - assetUnderlying, collateralEligibleForLiquidations, usersEligibleForLiquidations, passedAmount - ); + steward.batchLiquidate(assetUnderlying, collateralEligibleForLiquidations, usersEligibleForLiquidations, true); - uint256 collectorBalanceAfter = IERC20(assetUnderlying).balanceOf(collector); + uint256 collectorBalanceAfter = IERC20(debtAToken).balanceOf(collector); + uint256 stewardBalanceAfter = IERC20(assetUnderlying).balanceOf(address(steward)); - assertTrue(collectorBalanceBefore >= collectorBalanceAfter, "EXPECTED_BALANCE_DECREASE"); - assertTrue(collectorBalanceBefore - collectorBalanceAfter <= passedAmount, "OVERSPENT"); + uint256 collectorCollateralBalanceAfter = IERC20(collateralAToken).balanceOf(collector); + uint256 stewardCollateralBalanceAfter = IERC20(collateralAToken).balanceOf(address(steward)); - DataTypes.ReserveDataLegacy memory collateralReserveData = - AaveV3Avalanche.POOL.getReserveData(collateralEligibleForLiquidations); + assertGe(collectorBalanceBefore, collectorBalanceAfter); + assertLe(collectorBalanceBefore - collectorBalanceAfter, totalDebtToLiquidate + 1); // account for 1 wei rounding surplus + + assertTrue(collectorCollateralBalanceAfter >= collectorCollateralBalanceBefore); + + assertEq(stewardBalanceBefore, stewardBalanceAfter); + assertEq(stewardCollateralBalanceBefore, stewardCollateralBalanceAfter); for (uint256 i = 0; i < usersEligibleForLiquidations.length; i++) { uint256 currentDebtAmount = IERC20(assetDebtToken).balanceOf(usersEligibleForLiquidations[i]); - uint256 collateralBalance = IERC20(collateralReserveData.aTokenAddress).balanceOf(usersEligibleForLiquidations[i]); + uint256 collateralBalance = IERC20(collateralAToken).balanceOf(usersEligibleForLiquidations[i]); assertTrue(currentDebtAmount == 0 || collateralBalance == 0); } @@ -210,22 +239,7 @@ contract BatchRepayBadDebtStewardTest is BatchRepayBadDebtStewardBaseTest { ); vm.prank(caller); - steward.batchLiquidate(assetUnderlying, collateralEligibleForLiquidations, usersEligibleForLiquidations); - } - - function test_reverts_batchLiquidateWithMaxCap_caller_not_cleaner(address caller) public { - vm.assume(caller != guardian); - - vm.expectRevert( - abi.encodePacked( - IAccessControl.AccessControlUnauthorizedAccount.selector, uint256(uint160(caller)), steward.CLEANUP() - ) - ); - - vm.prank(caller); - steward.batchLiquidateWithMaxCap( - assetUnderlying, collateralEligibleForLiquidations, usersEligibleForLiquidations, 1 - ); + steward.batchLiquidate(assetUnderlying, collateralEligibleForLiquidations, usersEligibleForLiquidations, false); } function test_rescueToken() public {