diff --git a/src/ERC20Pool.sol b/src/ERC20Pool.sol index fc1e6a5bd..ca149424e 100644 --- a/src/ERC20Pool.sol +++ b/src/ERC20Pool.sol @@ -345,7 +345,7 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { function settle( address borrowerAddress_, uint256 maxDepth_ - ) external override nonReentrant { + ) external override nonReentrant returns (uint256 collateralSettled_, bool isBorrowerSettled_) { PoolState memory poolState = _accruePoolInterest(); SettleResult memory result = SettlerActions.settlePoolDebt( @@ -363,6 +363,9 @@ contract ERC20Pool is FlashloanablePool, IERC20Pool { ); _updatePostSettleState(result, poolState); + + collateralSettled_ = result.collateralSettled; + isBorrowerSettled_ = (result.debtPostAction == 0); } /** diff --git a/src/ERC721Pool.sol b/src/ERC721Pool.sol index 75cab09a9..3792eaed2 100644 --- a/src/ERC721Pool.sol +++ b/src/ERC721Pool.sol @@ -394,7 +394,7 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { function settle( address borrowerAddress_, uint256 maxDepth_ - ) external nonReentrant override { + ) external nonReentrant override returns (uint256 collateralSettled_, bool isBorrowerSettled_) { PoolState memory poolState = _accruePoolInterest(); SettleParams memory params = SettleParams({ @@ -417,6 +417,9 @@ contract ERC721Pool is FlashloanablePool, IERC721Pool { // move token ids from borrower array to pool claimable array if any collateral used to settle bad debt _rebalanceTokens(params.borrower, result.collateralRemaining); + + collateralSettled_ = result.collateralSettled; + isBorrowerSettled_ = (result.debtPostAction == 0); } /** diff --git a/src/base/Pool.sol b/src/base/Pool.sol index 914c29987..d4c41d70a 100644 --- a/src/base/Pool.sol +++ b/src/base/Pool.sol @@ -562,19 +562,23 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { // if new interest may have accrued, call accrueInterest function and update inflator and debt fields of poolState_ struct if (poolState_.isNewInterestAccrued) { - (uint256 newInflator, uint256 newInterest) = PoolCommons.accrueInterest( + try PoolCommons.accrueInterest( emaState, deposits, poolState_, Loans.getMax(loans).thresholdPrice, elapsed - ); - poolState_.inflator = newInflator; - // After debt owed to lenders has accrued, calculate current debt owed by borrowers - poolState_.debt = Maths.wmul(poolState_.t0Debt, poolState_.inflator); - - // update total interest earned accumulator with the newly accrued interest - reserveAuction.totalInterestEarned += newInterest; + ) returns (uint256 newInflator, uint256 newInterest) { + poolState_.inflator = newInflator; + // After debt owed to lenders has accrued, calculate current debt owed by borrowers + poolState_.debt = Maths.wmul(poolState_.t0Debt, poolState_.inflator); + + // update total interest earned accumulator with the newly accrued interest + reserveAuction.totalInterestEarned += newInterest; + } catch { + poolState_.isNewInterestAccrued = false; + emit InterestUpdateFailure(); + } } } } @@ -680,7 +684,9 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { PoolState memory poolState_, uint256 lup_ ) internal { - PoolCommons.updateInterestState(interestState, emaState, deposits, poolState_, lup_); + try PoolCommons.updateInterestState(interestState, emaState, deposits, poolState_, lup_) {} catch { + emit InterestUpdateFailure(); + } // update pool inflator if (poolState_.isNewInterestAccrued) { diff --git a/src/interfaces/pool/commons/IPoolEvents.sol b/src/interfaces/pool/commons/IPoolEvents.sol index ededcc548..e26f7c589 100644 --- a/src/interfaces/pool/commons/IPoolEvents.sol +++ b/src/interfaces/pool/commons/IPoolEvents.sol @@ -367,4 +367,9 @@ interface IPoolEvents { uint256 newRate ); + /** + * @notice Emitted when interest accural or update interest overflows. + */ + event InterestUpdateFailure(); + } \ No newline at end of file diff --git a/src/interfaces/pool/commons/IPoolSettlerActions.sol b/src/interfaces/pool/commons/IPoolSettlerActions.sol index 0639e4574..c5fcbb359 100644 --- a/src/interfaces/pool/commons/IPoolSettlerActions.sol +++ b/src/interfaces/pool/commons/IPoolSettlerActions.sol @@ -9,13 +9,15 @@ interface IPoolSettlerActions { /** * @notice Called by actors to settle an amount of debt in a completed liquidation. - * @param borrowerAddress_ Address of the auctioned borrower. - * @param maxDepth_ Measured from `HPB`, maximum number of buckets deep to settle debt. + * @param borrowerAddress_ Address of the auctioned borrower. + * @param maxDepth_ Measured from `HPB`, maximum number of buckets deep to settle debt. + * @return collateralSettled_ Amount of collateral settled. + * @return isBorrowerSettled_ Is all borrower's debt is settled. * @dev `maxDepth_` is used to prevent unbounded iteration clearing large liquidations. */ function settle( address borrowerAddress_, uint256 maxDepth_ - ) external; + ) external returns (uint256 collateralSettled_, bool isBorrowerSettled_); } \ No newline at end of file diff --git a/src/libraries/external/KickerActions.sol b/src/libraries/external/KickerActions.sol index 39f9e47e0..2762dcbcd 100644 --- a/src/libraries/external/KickerActions.sol +++ b/src/libraries/external/KickerActions.sol @@ -204,11 +204,10 @@ library KickerActions { KickReserveAuctionParams calldata params_ ) external { // retrieve timestamp of latest burn event and last burn timestamp - uint256 latestBurnEpoch = reserveAuction_.latestBurnEventEpoch; - uint256 lastBurnTimestamp = reserveAuction_.burnEvents[latestBurnEpoch].timestamp; + uint256 latestBurnEpoch = reserveAuction_.latestBurnEventEpoch; - // check that at least two weeks have passed since the last reserve auction completed, and that the auction was not kicked within the past 72 hours - if (block.timestamp < lastBurnTimestamp + 2 weeks || block.timestamp - reserveAuction_.kicked <= 72 hours) { + // check that at least two weeks have passed since the last reserve auction completed + if (block.timestamp < reserveAuction_.kicked + 2 weeks + 72 hours) { revert ReserveAuctionTooSoon(); } diff --git a/tests/forge/unit/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol b/tests/forge/unit/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol index ffd369a2a..b4504aab3 100644 --- a/tests/forge/unit/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol +++ b/tests/forge/unit/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol @@ -1109,7 +1109,7 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { }); } - function testAccruePoolInterestRevertDueToExpLimit() external { + function testAccruePoolInterestInterestUpdateFailureDueToExpLimit() external tearDown { _mintQuoteAndApproveTokens(_lender, 1_000_000_000 * 1e18); _mintCollateralAndApproveTokens(_borrower, 1_000_000_000 * 1e18); @@ -1180,12 +1180,27 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { // wait 32 years skip(365 days * 32); - // Reverts with PRBMathUD60x18__ExpInputTooBig - vm.expectRevert(); + // Interest update should fail + vm.expectEmit(true, true, false, true); + emit InterestUpdateFailure(); _updateInterest(); + + // repay all borrower debt based on last inflator + (uint256 inflator, ) = _pool.inflatorInfo(); + (uint256 debt, , ) = _pool.borrowerInfo(_borrower); + + _mintQuoteAndApproveTokens(_borrower, Maths.ceilWmul(inflator, debt)); + _repayDebt({ + from: _borrower, + borrower: _borrower, + amountToRepay: type(uint256).max, + amountRepaid: Maths.ceilWmul(inflator, debt), + collateralToPull: 0, + newLup: MAX_PRICE + }); } - function testAccrueInterestNewInterestLimit() external { + function testAccrueInterestInterestUpdateFailure() external tearDown { _mintQuoteAndApproveTokens(_lender, 1_000_000_000 * 1e18); _mintCollateralAndApproveTokens(_borrower, 1_000_000_000 * 1e18); @@ -1252,8 +1267,9 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { skip(13 hours); - // Revert with Arithmetic overflow in `Maths.wmul(pendingFactor - Maths.WAD, poolState_.debt)` in accrue interest - vm.expectRevert(); + // Interest update should fail + vm.expectEmit(true, true, false, true); + emit InterestUpdateFailure(); _updateInterest(); } @@ -1324,7 +1340,7 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { } } - function testUpdateInterestTuLimit() external { + function testTuLimitInterestUpdateFailure() external tearDown { _mintQuoteAndApproveTokens(_lender, 1_000_000_000 * 1e18); _mintCollateralAndApproveTokens(_borrower, 1_000_000_000 * 1e18); @@ -1393,8 +1409,9 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { skip(1 days); - // Revert with Arithmetic overflow in `(((tu + mau102 - 1e18) / 1e9) ** 2)` in update interest - vm.expectRevert(); + // Interest update should fail + vm.expectEmit(true, true, false, true); + emit InterestUpdateFailure(); _updateInterest(); } } diff --git a/tests/forge/unit/ERC721Pool/ERC721PoolReserveAuction.t.sol b/tests/forge/unit/ERC721Pool/ERC721PoolReserveAuction.t.sol index 9e579ef35..58b72f936 100644 --- a/tests/forge/unit/ERC721Pool/ERC721PoolReserveAuction.t.sol +++ b/tests/forge/unit/ERC721Pool/ERC721PoolReserveAuction.t.sol @@ -218,8 +218,25 @@ contract ERC721PoolReserveAuctionTest is ERC721HelperContract { // pass time to allow auction to complete skip(48 hours); - // check that you can't start a new auction unless two weeks have passed + // check that you can't start a new auction immediately after the last one finished _assertReserveAuctionTooSoon(); + + // check that you can't start a new auction two weeks after the last start... + skip(2 weeks - 72 hours); + _assertReserveAuctionTooSoon(); + + // ...or a day later... + skip(1 days); + _assertReserveAuctionTooSoon(); + + // ...but you can start another auction 2 weeks after the last one completed + skip(72 hours - 1 days); + _kickReserveAuction({ + from: _bidder, + remainingReserves: 415.792367191826572589 * 1e18, + price: 1_000_000_000 * 1e18, + epoch: 2 + }); } function testClaimableReserveAuction() external {