From c276e77f328e6eff77b3f63cff3c295f17dafe9d Mon Sep 17 00:00:00 2001 From: grandizzy Date: Fri, 24 Feb 2023 10:55:09 +0200 Subject: [PATCH 1/8] Take 1 --- src/base/Pool.sol | 6 +++ src/interfaces/pool/commons/IPoolErrors.sol | 10 +++++ .../pool/commons/IPoolLenderActions.sol | 7 +++- src/libraries/external/Auctions.sol | 12 +++++- src/libraries/external/BorrowerActions.sol | 33 +++++++-------- src/libraries/external/LenderActions.sol | 15 +++++++ tests/forge/ERC20Pool/ERC20DSTestPlus.sol | 22 ++-------- tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol | 16 +++---- .../ERC20PoolInterestRateAndEMAs.t.sol | 42 ++++--------------- .../ERC20Pool/ERC20PoolLiquidationsMisc.t.sol | 4 +- .../ERC20Pool/ERC20PoolLiquidationsTake.t.sol | 8 +--- .../forge/ERC20Pool/ERC20PoolPrecision.t.sol | 31 ++++---------- tests/forge/ERC721Pool/ERC721DSTestPlus.sol | 4 +- .../ERC721Pool/ERC721PoolCollateral.t.sol | 10 +---- tests/forge/utils/DSTestPlus.sol | 4 ++ 15 files changed, 100 insertions(+), 124 deletions(-) diff --git a/src/base/Pool.sol b/src/base/Pool.sol index 8b05c56b0..9061f8e37 100644 --- a/src/base/Pool.sol +++ b/src/base/Pool.sol @@ -318,6 +318,12 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { ); } + /// @inheritdoc IPoolLenderActions + function updateInterest() external override nonReentrant { + PoolState memory poolState = _accruePoolInterest(); + _updateInterestState(poolState, _lup(poolState.debt)); + } + /*****************************/ /*** Liquidation Functions ***/ /*****************************/ diff --git a/src/interfaces/pool/commons/IPoolErrors.sol b/src/interfaces/pool/commons/IPoolErrors.sol index 8af81693a..4e02a7df4 100644 --- a/src/interfaces/pool/commons/IPoolErrors.sol +++ b/src/interfaces/pool/commons/IPoolErrors.sol @@ -102,11 +102,21 @@ interface IPoolErrors { */ error InsufficientLiquidity(); + /** + * @notice When settling pool debt the number of buckets to use should be greater than 0. + */ + error InvalidBucketDepth(); + /** * @notice When transferring LP tokens between indices, the new index must be a valid index. */ error InvalidIndex(); + /** + * @notice The amount used for performed action should be greater than 0. + */ + error InvalidAmount(); + /** * @notice Borrower is attempting to borrow more quote token than is available before the supplied limitIndex. */ diff --git a/src/interfaces/pool/commons/IPoolLenderActions.sol b/src/interfaces/pool/commons/IPoolLenderActions.sol index ae437b9cd..2452d57c2 100644 --- a/src/interfaces/pool/commons/IPoolLenderActions.sol +++ b/src/interfaces/pool/commons/IPoolLenderActions.sol @@ -92,7 +92,7 @@ interface IPoolLenderActions { ) external returns (uint256 quoteTokenAmount, uint256 lpAmount); /** - * @notice Called by lenders to transfers their LP tokens to a different address. approveLpOwnership needs to be run first + * @notice Called by lenders to transfers their LP tokens to a different address. approveLpOwnership needs to be run first. * @dev Used by PositionManager.memorializePositions(). * @param owner The original owner address of the position. * @param newOwner The new owner address of the position. @@ -103,4 +103,9 @@ interface IPoolLenderActions { address newOwner, uint256[] calldata indexes ) external; + + /** + * @notice Called by lenders to update pool interest rate (can be updated only once in a 12 hours period of time). + */ + function updateInterest() external; } diff --git a/src/libraries/external/Auctions.sol b/src/libraries/external/Auctions.sol index fd6380ee8..4e565dffa 100644 --- a/src/libraries/external/Auctions.sol +++ b/src/libraries/external/Auctions.sol @@ -161,6 +161,8 @@ library Auctions { error CollateralRoundingNeededButNotPossible(); error InsufficientLiquidity(); error InsufficientCollateral(); + error InvalidBucketDepth(); + error InvalidAmount(); error NoAuction(); error NoReserves(); error NoReservesAuction(); @@ -203,6 +205,9 @@ library Auctions { uint256 collateralSettled_, uint256 t0DebtSettled_ ) { + // revert if no bucket to settle + if (params_.bucketDepth == 0 ) revert InvalidBucketDepth(); + uint256 kickTime = auctions_.liquidations[params_.borrower].kickTime; if (kickTime == 0) revert NoAuction(); @@ -557,10 +562,12 @@ library Auctions { uint256 collateral_, uint256 collateralScale_ ) external returns (TakeResult memory result_) { + // revert if no amount to take + if (collateral_ == 0) revert InvalidAmount(); + Borrower memory borrower = loans_.borrowers[borrowerAddress_]; if ( - (collateral_ == 0) || // revert if amount to take is 0 (poolState_.poolType == uint8(PoolType.ERC721) && borrower.collateral < 1e18) || // revert in case of NFT take when there isn't a full token to be taken (poolState_.poolType == uint8(PoolType.ERC20) && borrower.collateral == 0) // revert in case of ERC20 take when no collateral to be taken ) { @@ -672,6 +679,9 @@ library Auctions { ReserveAuctionState storage reserveAuction_, uint256 maxAmount_ ) external returns (uint256 amount_, uint256 ajnaRequired_) { + // revert if no amount to be taken + if (maxAmount_ == 0) revert InvalidAmount(); + uint256 kicked = reserveAuction_.kicked; if (kicked != 0 && block.timestamp - kicked <= 72 hours) { diff --git a/src/libraries/external/BorrowerActions.sol b/src/libraries/external/BorrowerActions.sol index bbbd9f189..5231e8a5b 100644 --- a/src/libraries/external/BorrowerActions.sol +++ b/src/libraries/external/BorrowerActions.sol @@ -73,6 +73,7 @@ library BorrowerActions { error BorrowerNotSender(); error BorrowerUnderCollateralized(); error InsufficientCollateral(); + error InvalidAmount(); error LimitIndexExceeded(); error NoDebt(); @@ -117,12 +118,15 @@ library BorrowerActions { ) external returns ( DrawDebtResult memory result_ ) { - Borrower memory borrower = loans_.borrowers[borrowerAddress_]; - DrawDebtLocalVars memory vars; + vars.pledge = collateralToPledge_ != 0; + vars.borrow = amountToBorrow_ != 0; + + // revert if no amount to pledge or borrow + if (!vars.pledge && !vars.borrow) revert InvalidAmount(); + + Borrower memory borrower = loans_.borrowers[borrowerAddress_]; - vars.pledge = collateralToPledge_ != 0; - vars.borrow = amountToBorrow_ != 0 || limitIndex_ != 0; // enable an intentional 0 borrow loan call to update borrower's loan state vars.borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator); vars.inAuction = _inAuction(auctions_, borrowerAddress_); @@ -219,11 +223,6 @@ library BorrowerActions { vars.stampT0Np = true; } - // calculate LUP if it wasn't calculated previously - if (!vars.pledge && !vars.borrow) { - result_.newLup = _lup(deposits_, result_.poolDebt); - } - // update loan state Loans.update( loans_, @@ -277,12 +276,15 @@ library BorrowerActions { ) external returns ( RepayDebtResult memory result_ ) { - Borrower memory borrower = loans_.borrowers[borrowerAddress_]; - RepayDebtLocalVars memory vars; + vars.repay = maxQuoteTokenAmountToRepay_ != 0; + vars.pull = collateralAmountToPull_ != 0; + + // revert if no amount to pledge or borrow + if (!vars.repay && !vars.pull) revert InvalidAmount(); + + Borrower memory borrower = loans_.borrowers[borrowerAddress_]; - vars.repay = maxQuoteTokenAmountToRepay_ != 0; - vars.pull = collateralAmountToPull_ != 0; vars.borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator); vars.inAuction = _inAuction(auctions_, borrowerAddress_); @@ -379,11 +381,6 @@ library BorrowerActions { result_.poolCollateral -= collateralAmountToPull_; } - // calculate LUP if it wasn't calculated previously - if (!vars.repay && !vars.pull) { - result_.newLup = _lup(deposits_, result_.poolDebt); - } - // update loan state Loans.update( loans_, diff --git a/src/libraries/external/LenderActions.sol b/src/libraries/external/LenderActions.sol index ac49ce334..207d0574b 100644 --- a/src/libraries/external/LenderActions.sol +++ b/src/libraries/external/LenderActions.sol @@ -81,6 +81,7 @@ library LenderActions { error DustAmountNotExceeded(); error NoAllowance(); error InvalidIndex(); + error InvalidAmount(); error LUPBelowHTP(); error NoClaim(); error InsufficientLPs(); @@ -110,6 +111,9 @@ library LenderActions { uint256 collateralAmountToAdd_, uint256 index_ ) external returns (uint256 bucketLPs_) { + // revert if no amount to be added + if (collateralAmountToAdd_ == 0) revert InvalidAmount(); + // revert if adding at invalid index if (index_ == 0 || index_ > MAX_FENWICK_INDEX) revert InvalidIndex(); uint256 bucketDeposit = Deposits.valueAt(deposits_, index_); @@ -143,6 +147,9 @@ library LenderActions { PoolState calldata poolState_, AddQuoteParams calldata params_ ) external returns (uint256 bucketLPs_, uint256 lup_) { + // revert if no amount to be added + if (params_.amount == 0) revert InvalidAmount(); + // revert if adding to an invalid index if (params_.index == 0 || params_.index > MAX_FENWICK_INDEX) revert InvalidIndex(); Bucket storage bucket = buckets_[params_.index]; @@ -209,6 +216,8 @@ library LenderActions { PoolState calldata poolState_, MoveQuoteParams calldata params_ ) external returns (uint256 fromBucketRedeemedLPs_, uint256 toBucketLPs_, uint256 movedAmount_, uint256 lup_) { + if (params_.maxAmountToMove == 0) + revert InvalidAmount(); if (params_.fromIndex == params_.toIndex) revert MoveToSamePrice(); if (params_.maxAmountToMove != 0 && params_.maxAmountToMove < poolState_.quoteDustLimit) @@ -344,6 +353,9 @@ library LenderActions { PoolState calldata poolState_, RemoveQuoteParams calldata params_ ) external returns (uint256 removedAmount_, uint256 redeemedLPs_, uint256 lup_) { + // revert if no amount to be removed + if (params_.maxAmount == 0) revert InvalidAmount(); + Bucket storage bucket = buckets_[params_.index]; Lender storage lender = bucket.lenders[msg.sender]; @@ -415,6 +427,9 @@ library LenderActions { uint256 amount_, uint256 index_ ) external returns (uint256 lpAmount_) { + // revert if no amount to be removed + if (amount_ == 0) revert InvalidAmount(); + Bucket storage bucket = buckets_[index_]; uint256 bucketCollateral = bucket.collateral; diff --git a/tests/forge/ERC20Pool/ERC20DSTestPlus.sol b/tests/forge/ERC20Pool/ERC20DSTestPlus.sol index f911d3735..d3d19879b 100644 --- a/tests/forge/ERC20Pool/ERC20DSTestPlus.sol +++ b/tests/forge/ERC20Pool/ERC20DSTestPlus.sol @@ -54,8 +54,10 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { // mint quote tokens to borrower address equivalent to the current debt deal(_pool.quoteTokenAddress(), borrower, currentDebt); - // repay current debt and pull all collateral - _repayDebtNoLupCheck(borrower, borrower, tokenDebt, currentDebt, borrowerCollateral); + // repay current debt and pull all collateral if any + if (tokenDebt != 0 || borrowerCollateral != 0) { + _repayDebtNoLupCheck(borrower, borrower, tokenDebt, currentDebt, borrowerCollateral); + } // check borrower state after repay of loan and pull collateral (borrowerT0debt, borrowerCollateral, ) = _pool.borrowerInfo(borrower); @@ -237,22 +239,6 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { borrowers.add(from); } - function _borrowZeroAmount( - address from, - uint256 amount, - uint256 indexLimit, - uint256 newLup - ) internal { - changePrank(from); - vm.expectEmit(true, true, false, true); - emit DrawDebt(from, amount, 0, newLup); - - ERC20Pool(address(_pool)).drawDebt(from, amount, indexLimit, 0); - - // Add for tearDown - borrowers.add(from); - } - function _drawDebt( address from, address borrower, diff --git a/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol b/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol index d9bd972ce..fc46a46da 100644 --- a/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol @@ -432,12 +432,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { skip(10 days); - _borrowZeroAmount({ - from: _borrower, - amount: 0, - indexLimit: 3_000, - newLup: 2_981.007422784467321543 * 1e18 - }); + _updateInterest(); expectedDebt = 21_157.152643010853304038 * 1e18; @@ -462,15 +457,14 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { borrower: _borrower, borrowerDebt: expectedDebt, borrowerCollateral: 50 * 1e18, - borrowert0Np: 448.381722115384615591 * 1e18, + borrowert0Np: 445.838278846153846359 * 1e18, borrowerCollateralization: 7.044916376706357984 * 1e18 }); _assertLenderInterest(liquidityAdded, 119.836959946754650000 * 1e18); skip(10 days); - // call drawDebt to restamp the loan's neutral price - IERC20Pool(address(_pool)).drawDebt(_borrower, 0, 0, 0); + _updateInterest(); expectedDebt = 21_199.628356897284442294 * 1e18; @@ -495,7 +489,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { borrower: _borrower, borrowerDebt: expectedDebt, borrowerCollateral: 50 * 1e18, - borrowert0Np: 448.381722115384615591 * 1e18, + borrowert0Np: 445.838278846153846359 * 1e18, borrowerCollateralization: 7.030801136225104190 * 1e18 }); _assertLenderInterest(liquidityAdded, 157.005764521268350000 * 1e18); @@ -526,7 +520,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { borrower: _borrower, borrowerDebt: expectedDebt, borrowerCollateral: 50 * 1e18, - borrowert0Np: 448.381722115384615591 * 1e18, + borrowert0Np: 445.838278846153846359 * 1e18, borrowerCollateralization: 7.015307034516347067 * 1e18 }); } diff --git a/tests/forge/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol b/tests/forge/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol index 0e4e3e4ff..9507a9f8f 100644 --- a/tests/forge/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolInterestRateAndEMAs.t.sol @@ -120,14 +120,8 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { skip(14 hours); uint256 snapshot = vm.snapshot(); - // force interest rate update by calling repay debt with 0 amounts - _repayDebtNoLupCheck({ - from: _borrower, - borrower: _borrower, - amountToRepay: 0, - amountRepaid: 0, - collateralToPull: 0 - }); + // update interest rate + _updateInterest(); _assertPool( PoolParams({ @@ -226,16 +220,10 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { }) ); - // force an interest rate update skip(13 hours); - _addLiquidity({ - from: _lender, - amount: 0, - index: 3232, - lpAward: 0, - newLup: 100.332368143282009890 * 1e18 - }); + // update interest rate + _updateInterest(); _assertPool( PoolParams({ @@ -369,12 +357,7 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { // trigger an interest accumulation skip(12 hours); - _borrowZeroAmount({ - from: _borrower, - amount: 0, - indexLimit: _i1505_26, - newLup: _p1505_26 - }); + _updateInterest(); unchecked { ++i; } } @@ -440,12 +423,7 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { // trigger an interest accumulation skip(12 hours); - _borrowZeroAmount({ - from: _borrower, - amount: 0, - indexLimit: _i1505_26, - newLup: _p1505_26 - }); + _updateInterest(); unchecked { ++i; } } @@ -723,12 +701,8 @@ contract ERC20PoolInterestRateTestAndEMAs is ERC20HelperContract { (uint256 poolDebt,,) = _pool.debtInfo(); assertEq(poolDebt, expectedPoolDebt); - // force accrue interest - _addLiquidityNoEventCheck({ - from: _lender2, - amount: 0, - index: 1 - }); + // accrue interest + _updateInterest(); // check that no interest earned if HTP is over the highest price bucket (poolDebt,,) = _pool.debtInfo(); diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsMisc.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsMisc.t.sol index 049aeeadd..4ca4afe15 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsMisc.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsMisc.t.sol @@ -411,8 +411,8 @@ contract ERC20PoolLiquidationsMiscTest is ERC20HelperContract { borrowerCollateralization: 1.192575121957988603 * 1e18 }); - // draw debt is called to trigger accrual of pool interest that will push the lup back up - IERC20Pool(address(_pool)).drawDebt(_lender, 0, 0, 0); + // trigger accrual of pool interest that will push the lup back up + _updateInterest(); _assertPool( PoolParams({ diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol index 49e993b57..3678a32c9 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol @@ -1926,13 +1926,7 @@ contract ERC20PoolLiquidationsTakeAndRepayAllDebtInPoolTest is ERC20HelperContra } function testTakeAuctionRepaidAmountGreaterThanPoolDebt() external tearDown { - _repayDebtNoLupCheck({ - from: _borrower, - borrower: _borrower, - amountToRepay: 0, - amountRepaid: 0, - collateralToPull: 0 - }); + _updateInterest(); _drawDebtNoLupCheck({ from: _borrower, diff --git a/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol b/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol index dbc6e5346..b201fb8df 100644 --- a/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol @@ -442,7 +442,8 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { assertEq(_quote.balanceOf(_borrower), 5_000 * _quotePrecision); } - function testDepositTwoActorSameBucket( + // FIXME: the scaled amounts are always 0 + function _testDepositTwoActorSameBucket( uint8 collateralPrecisionDecimals_, uint8 quotePrecisionDecimals_, uint16 bucketId_, @@ -522,7 +523,8 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { assertEq(bucketLpBalance, lenderLpBalance + bidderLpBalance); } - function testDepositTwoLendersSameBucket( + // FIXME: the scaled amounts are always 0 + function _testDepositTwoLendersSameBucket( uint8 collateralPrecisionDecimals_, uint8 quotePrecisionDecimals_, uint16 bucketId_, @@ -595,10 +597,12 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { // setup fuzzy bounds and initialize the pool uint256 boundColPrecision = bound(uint256(collateralPrecisionDecimals_), 1, 18); uint256 boundQuotePrecision = bound(uint256(quotePrecisionDecimals_), 1, 18); + // init to set lender deposit normalized + init(boundColPrecision, boundQuotePrecision); + uint256 fromBucketId = bound(uint256(fromBucketId_), 1, 7388); uint256 toBucketId = bound(uint256(toBucketId_), 1, 7388); - uint256 amountToMove = bound(uint256(amountToMove_), 0, _lenderDepositNormalized); - init(boundColPrecision, boundQuotePrecision); + uint256 amountToMove = bound(uint256(amountToMove_), 1, _lenderDepositNormalized); _addInitialLiquidity({ from: _lender, @@ -885,23 +889,4 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { }); } - function testMoveQuoteDustAmountRevert() external virtual tearDown { - init(8, 6); - - _addInitialLiquidity({ - from: _lender, - amount: 50_000 * 1e6, - index: 2550 - }); - - assertEq(_quoteDust, 0.000001 * 1e18); - - _assertMoveLiquidityDustRevert({ - from: _lender, - amount: 0.00000001 * 1e18, - fromIndex: 2550, - toIndex: 2551 - }); - } - } \ No newline at end of file diff --git a/tests/forge/ERC721Pool/ERC721DSTestPlus.sol b/tests/forge/ERC721Pool/ERC721DSTestPlus.sol index 964941451..6f5898b9e 100644 --- a/tests/forge/ERC721Pool/ERC721DSTestPlus.sol +++ b/tests/forge/ERC721Pool/ERC721DSTestPlus.sol @@ -59,7 +59,9 @@ abstract contract ERC721DSTestPlus is DSTestPlus, IERC721PoolEvents { // repay current debt and pull all collateral uint256 noOfNfts = borrowerCollateral / 1e18; // round down to pull correct num of NFTs - _repayDebtNoLupCheck(borrower, borrower, currentDebt, currentDebt, noOfNfts); + if (currentDebt != 0 || noOfNfts != 0) { + _repayDebtNoLupCheck(borrower, borrower, currentDebt, currentDebt, noOfNfts); + } // check borrower state after repay of loan and pull Nfts (borrowerT0debt, borrowerCollateral, ) = _pool.borrowerInfo(borrower); diff --git a/tests/forge/ERC721Pool/ERC721PoolCollateral.t.sol b/tests/forge/ERC721Pool/ERC721PoolCollateral.t.sol index 89c99740a..126aa37d5 100644 --- a/tests/forge/ERC721Pool/ERC721PoolCollateral.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolCollateral.t.sol @@ -705,14 +705,8 @@ contract ERC721PoolCollateralTest is ERC721HelperContract { }) ); - // force an interest accumulation to assert bucket with interest - _addLiquidity({ - from: _lender, - amount: 0 * 1e18, - index: 7000, - newLup: 99836282890, - lpAward: 0 - }); + // interest accumulation to assert bucket with interest + _updateInterest(); _assertBucket({ index: 3060, lpBalance: 20 * 1e18, diff --git a/tests/forge/utils/DSTestPlus.sol b/tests/forge/utils/DSTestPlus.sol index 19e7c6f15..52ec38e78 100644 --- a/tests/forge/utils/DSTestPlus.sol +++ b/tests/forge/utils/DSTestPlus.sol @@ -392,6 +392,10 @@ abstract contract DSTestPlus is Test, IPoolEvents { _pool.takeReserves(amount); } + function _updateInterest() internal { + _pool.updateInterest(); + } + function _assertQuoteTokenTransferEvent( address from, address to, From 71568b5e5bc70bacae9875510e2be26053094b5e Mon Sep 17 00:00:00 2001 From: grandizzy Date: Fri, 24 Feb 2023 15:39:43 +0200 Subject: [PATCH 2/8] Fix fuzz tests --- .../forge/ERC20Pool/ERC20PoolPrecision.t.sol | 115 +++++++----------- 1 file changed, 42 insertions(+), 73 deletions(-) diff --git a/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol b/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol index d3e478023..865899811 100644 --- a/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolPrecision.t.sol @@ -442,8 +442,7 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { assertEq(_quote.balanceOf(_borrower), 5_000 * _quotePrecision); } - // FIXME: the scaled amounts are always 0 - function _testDepositTwoActorSameBucket( + function testDepositTwoActorSameBucket( uint8 collateralPrecisionDecimals_, uint8 quotePrecisionDecimals_, uint16 bucketId_, @@ -453,17 +452,24 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { // setup fuzzy bounds and initialize the pool uint256 boundColPrecision = bound(uint256(collateralPrecisionDecimals_), 1, 18); uint256 boundQuotePrecision = bound(uint256(quotePrecisionDecimals_), 1, 18); - uint256 bucketId = bound(uint256(bucketId_), 1, 7388); + + init(boundColPrecision, boundQuotePrecision); + + uint256 bucketId = bound(uint256(bucketId_), 1, 7388); + // ensure half of deposits are below the scale limit uint256 maxColAmountBound = collateralAmount_ % 2 == 0 ? MAX_COLLATERAL : uint256(10) ** boundColPrecision; uint256 maxQuoteAmountBound = quoteAmount_ % 2 == 0 ? MAX_DEPOSIT : uint256(10) ** boundQuotePrecision; - uint256 collateralAmount = bound(uint256(collateralAmount_), 0, maxColAmountBound); - uint256 quoteAmount = bound(uint256(quoteAmount_), 0, maxQuoteAmountBound); - init(boundColPrecision, boundQuotePrecision); + uint256 collateralAmount = bound(uint256(collateralAmount_), 1, maxColAmountBound); + uint256 quoteAmount = bound(uint256(quoteAmount_), 1, maxQuoteAmountBound); + + uint256 quoteScale = _pool.quoteTokenScale(); + uint256 quoteDust = _pool.quoteTokenDust(); + if (quoteAmount < quoteDust) quoteAmount = quoteDust; - uint256 scaledQuoteAmount = (quoteAmount / 10 ** (18 - boundQuotePrecision)) * 10 ** (18 - boundQuotePrecision); - uint256 scaledColAmount = (collateralAmount / 10 ** (18 - boundColPrecision)) * 10 ** (18 - boundColPrecision); - uint256 colDustAmount = ERC20Pool(address(_pool)).bucketCollateralDust(bucketId); + uint256 colScale = ERC20Pool(address(_pool)).collateralScale(); + uint256 colDustAmount = ERC20Pool(address(_pool)).bucketCollateralDust(bucketId); + if (collateralAmount < colDustAmount) collateralAmount = colDustAmount; assertEq(ERC20Pool(address(_pool)).collateralScale(), 10 ** (18 - boundColPrecision)); assertEq(_pool.quoteTokenScale(), 10 ** (18 - boundQuotePrecision)); @@ -485,46 +491,24 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { }); // addQuoteToken should add scaled quote token amount validate LP - _addInitialLiquidity({ - from: _lender, - amount: quoteAmount, - index: bucketId - }); - - (uint256 lenderLpBalance, ) = _pool.lenderInfo(bucketId, _lender); - assertEq(lenderLpBalance, scaledQuoteAmount); + _addLiquidityNoEventCheck(_lender, quoteAmount, bucketId); // deposit collateral and sanity check bidder LPs - uint256 bidderLpBalance; - if (collateralAmount != 0 && collateralAmount < colDustAmount) { - _assertAddCollateralDustRevert(_bidder, collateralAmount, bucketId); - } else { - _addCollateralWithoutCheckingLP(_bidder, collateralAmount, bucketId); - (bidderLpBalance, ) = _pool.lenderInfo(bucketId, _bidder); - if (collateralAmount == 0) { - assertEq(bidderLpBalance, 0); - } else { - assertGt(bidderLpBalance, 0); - } - } + _addCollateralWithoutCheckingLP(_bidder, collateralAmount, bucketId); // check bucket quantities and LPs - uint256 curDeposit; - uint256 availableCollateral; - uint256 bucketLpBalance; - (, curDeposit, availableCollateral, bucketLpBalance,,) = _poolUtils.bucketInfo(address(_pool), bucketId); - if (bucketLpBalance == 0) { - assertEq(curDeposit, 0); - assertEq(availableCollateral, 0); - } else { - assertEq(curDeposit, scaledQuoteAmount); - assertEq(availableCollateral, collateralAmount >= colDustAmount ? scaledColAmount : 0); - } + (, uint256 curDeposit, uint256 availableCollateral, uint256 bucketLpBalance,,) = _poolUtils.bucketInfo(address(_pool), bucketId); + assertEq(curDeposit, _roundToScale(quoteAmount, quoteScale)); + assertEq(availableCollateral, _roundToScale(collateralAmount, colScale)); + + (uint256 lenderLpBalance, ) = _pool.lenderInfo(bucketId, _lender); + assertEq(lenderLpBalance, _roundToScale(quoteAmount, quoteScale)); + (uint256 bidderLpBalance, ) = _pool.lenderInfo(bucketId, _bidder); + assertGt(bidderLpBalance, 0); assertEq(bucketLpBalance, lenderLpBalance + bidderLpBalance); } - // FIXME: the scaled amounts are always 0 - function _testDepositTwoLendersSameBucket( + function testDepositTwoLendersSameBucket( uint8 collateralPrecisionDecimals_, uint8 quotePrecisionDecimals_, uint16 bucketId_, @@ -534,17 +518,17 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { // setup fuzzy bounds and initialize the pool uint256 boundColPrecision = bound(uint256(collateralPrecisionDecimals_), 1, 18); uint256 boundQuotePrecision = bound(uint256(quotePrecisionDecimals_), 1, 18); - uint256 bucketId = bound(uint256(bucketId_), 1, 7388); - // ensure half of deposits are below the scale limit - uint256 maxQuoteAmount1 = quoteAmount1_ % 2 == 0 ? MAX_DEPOSIT : uint256(10) ** boundQuotePrecision; - uint256 maxQuoteAmount2 = quoteAmount2_ % 2 == 0 ? MAX_DEPOSIT : uint256(10) ** boundQuotePrecision; - uint256 quoteAmount1 = bound(uint256(quoteAmount1_), 0, maxQuoteAmount1); - uint256 quoteAmount2 = bound(uint256(quoteAmount2_), 0, maxQuoteAmount2); + init(boundColPrecision, boundQuotePrecision); - // Scaled Quote Amount - uint256 scaledQuoteAmount1 = (quoteAmount1 / 10 ** (18 - boundQuotePrecision)) * 10 ** (18 - boundQuotePrecision); - uint256 scaledQuoteAmount2 = (quoteAmount2 / 10 ** (18 - boundQuotePrecision)) * 10 ** (18 - boundQuotePrecision); + uint256 bucketId = bound(uint256(bucketId_), 1, 7388); + + // ensure half of deposits are below the scale limit + uint256 maxQuoteAmount1 = quoteAmount1_ % 2 == 0 ? MAX_DEPOSIT : 1e18; + uint256 maxQuoteAmount2 = quoteAmount2_ % 2 == 0 ? MAX_DEPOSIT : 1e18; + + uint256 quoteAmount1 = bound(uint256(quoteAmount1_), 1e18, maxQuoteAmount1); + uint256 quoteAmount2 = bound(uint256(quoteAmount2_), 1e18, maxQuoteAmount2); // mint and run approvals, ignoring amounts already init approved above deal(address(_quote), _lender, quoteAmount1 * _quotePrecision); @@ -556,35 +540,20 @@ contract ERC20PoolPrecisionTest is ERC20DSTestPlus { _quote.approve(address(_pool), quoteAmount2 * _quotePrecision); // addQuoteToken should add scaled quote token amount and LP - _addInitialLiquidity({ - from: _lender, - amount: scaledQuoteAmount1, - index: bucketId - }); - + _addLiquidityNoEventCheck(_lender, quoteAmount1, bucketId); (uint256 lpBalance1, ) = _pool.lenderInfo(bucketId, _lender); + assertGt(lpBalance1, 0); // addQuoteToken should add scaled quote token amount and LP - vm.expectEmit(true, true, false, true); - emit AddQuoteToken(lender2, bucketId, scaledQuoteAmount2, scaledQuoteAmount2, MAX_PRICE); _addLiquidityNoEventCheck(lender2, quoteAmount2, bucketId); (uint256 lpBalance2, ) = _pool.lenderInfo(bucketId, lender2); - if (scaledQuoteAmount2 != 0) { - assertGt(lpBalance2, 0); - } else { - assertEq(lpBalance2, 0); - } + assertGt(lpBalance2, 0); // check bucket - uint256 curDeposit; - uint256 bucketLPs; - (, curDeposit, , bucketLPs,,) = _poolUtils.bucketInfo(address(_pool), bucketId); - assertEq(curDeposit, scaledQuoteAmount1 + scaledQuoteAmount2); - if (curDeposit == 0) { - assertEq(bucketLPs, 0); - } else { - assertEq(bucketLPs, lpBalance1 + lpBalance2); - } + uint256 quoteScale = _pool.quoteTokenScale(); + (, uint256 curDeposit, , uint256 bucketLPs,,) = _poolUtils.bucketInfo(address(_pool), bucketId); + assertEq(curDeposit, _roundToScale(quoteAmount1, quoteScale) + _roundToScale(quoteAmount2, quoteScale)); + assertEq(bucketLPs, lpBalance1 + lpBalance2); } function testMoveQuoteToken( From e07f6bdee142e9b1bf3689bf836329288e79d240 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Tue, 28 Feb 2023 16:04:00 +0200 Subject: [PATCH 3/8] Add stampLoan for borrowers to be able to restamp the Neutral Price of the loan --- docs/Functions.md | 34 ++++++++++ src/base/Pool.sol | 20 ++++++ src/interfaces/pool/IPool.sol | 2 + .../pool/commons/IPoolBorrowerActions.sol | 20 ++++++ src/interfaces/pool/commons/IPoolEvents.sol | 8 +++ src/libraries/external/BorrowerActions.sol | 63 +++++++++++++++++++ tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol | 19 ++++-- .../ERC20Pool/ERC20PoolLiquidationsKick.t.sol | 5 ++ tests/forge/utils/DSTestPlus.sol | 16 +++++ 9 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 src/interfaces/pool/commons/IPoolBorrowerActions.sol diff --git a/docs/Functions.md b/docs/Functions.md index fe0bb6642..f5fc48ad6 100644 --- a/docs/Functions.md +++ b/docs/Functions.md @@ -264,6 +264,40 @@ - Auctions.takeReserves(): - ReserveAuction +### stampLoan + external libraries call: + - BorrowerActions.stampLoan() + + write state: + - _accruePoolInterest(): + - PoolCommons.accrueInterest(): + - Deposits.mult (scale Fenwick tree with new interest accrued): + - update scaling array state + - increment reserveAuction.totalInterestEarned accumulator + - BorrowerActions.stampLoan(): + - Loans.update(): + - _upsert(): + - insert or update loan in loans array + - remove(): + - remove loan from loans array + - update borrower in address => borrower mapping + - _updateInterestState(): + - PoolCommons.updateInterestRate(): + - interest debt and lup * collateral EMAs accumulators + - interest rate accumulator and interestRateUpdate state + - pool inflator and inflatorUpdate state + + reverts on: + - BorrowerActions.stampLoan() + - loan in auction AuctionActive() + - borrower under collateralized BorrowerUnderCollateralized() + + emit events: + - BorrowerActions.stampLoan(): + - LoanStamped + - PoolCommons.updateInterestRate(): + - UpdateInterestRate + ## ERC20Pool contract ### addCollateral diff --git a/src/base/Pool.sol b/src/base/Pool.sol index 0f9d96f5a..80a6a27e3 100644 --- a/src/base/Pool.sol +++ b/src/base/Pool.sol @@ -11,6 +11,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol" import { IPool, IPoolImmutables, + IPoolBorrowerActions, IPoolLenderActions, IPoolState, IPoolLiquidationActions, @@ -332,6 +333,25 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { _updateInterestState(poolState, _lup(poolState.debt)); } + /***********************************/ + /*** Borrower External Functions ***/ + /***********************************/ + + /// @inheritdoc IPoolBorrowerActions + function stampLoan(address borrowerAddress_) external override nonReentrant { + PoolState memory poolState = _accruePoolInterest(); + + uint256 newLup = BorrowerActions.stampLoan( + auctions, + deposits, + loans, + poolState, + borrowerAddress_ + ); + + _updateInterestState(poolState, newLup); + } + /*****************************/ /*** Liquidation Functions ***/ /*****************************/ diff --git a/src/interfaces/pool/IPool.sol b/src/interfaces/pool/IPool.sol index 8ced68587..35b91fa08 100644 --- a/src/interfaces/pool/IPool.sol +++ b/src/interfaces/pool/IPool.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.14; +import { IPoolBorrowerActions } from './commons/IPoolBorrowerActions.sol'; import { IPoolLenderActions } from './commons/IPoolLenderActions.sol'; import { IPoolLiquidationActions } from './commons/IPoolLiquidationActions.sol'; import { IPoolReserveAuctionActions } from './commons/IPoolReserveAuctionActions.sol'; @@ -16,6 +17,7 @@ import { IERC3156FlashLender } from './IERC3156FlashLender.sol'; * @title Base Pool */ interface IPool is + IPoolBorrowerActions, IPoolLenderActions, IPoolLiquidationActions, IPoolReserveAuctionActions, diff --git a/src/interfaces/pool/commons/IPoolBorrowerActions.sol b/src/interfaces/pool/commons/IPoolBorrowerActions.sol new file mode 100644 index 000000000..82e448e21 --- /dev/null +++ b/src/interfaces/pool/commons/IPoolBorrowerActions.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.14; + +/** + * @title Pool Borrower Actions + */ +interface IPoolBorrowerActions { + + /** + * @notice Called by fully colalteralized borrowers to restamp the Neutral Price of the loan (only if loan is fully collateralized and not in auction). + * @notice The reason for stamping the neutral price on the loan is to provide some certainty to the borrower as to at what price they can expect to be liquidated. + * @notice This action can be initiated by borrower itself or by a different actor on behalf of borrower. + * @param borrowerAddress The borrower address to restamp Neutral Price for. + */ + function stampLoan( + address borrowerAddress + ) external; + +} diff --git a/src/interfaces/pool/commons/IPoolEvents.sol b/src/interfaces/pool/commons/IPoolEvents.sol index de6f7fda4..40251016e 100644 --- a/src/interfaces/pool/commons/IPoolEvents.sol +++ b/src/interfaces/pool/commons/IPoolEvents.sol @@ -138,6 +138,14 @@ interface IPoolEvents { uint256 bond ); + /** + * @notice Emitted when a loan Neutral Price is restamped. + * @param borrower Identifies the loan to update the Neutral Price. + */ + event LoanStamped( + address indexed borrower + ); + /** * @notice Emitted when lender moves quote token from a bucket price to another. * @param lender Recipient that moved quote tokens. diff --git a/src/libraries/external/BorrowerActions.sol b/src/libraries/external/BorrowerActions.sol index 5231e8a5b..dd5e9f62c 100644 --- a/src/libraries/external/BorrowerActions.sol +++ b/src/libraries/external/BorrowerActions.sol @@ -64,6 +64,13 @@ library BorrowerActions { uint256 t0RepaidDebt; // [WAD] t0 debt repaid } + /**************/ + /*** Events ***/ + /**************/ + + // See `IPoolEvents` for descriptions + event LoanStamped(address indexed borrowerAddress); + /**************/ /*** Errors ***/ /**************/ @@ -396,6 +403,62 @@ library BorrowerActions { ); } + /** + * @notice See `IPoolBorrowerActions` for descriptions + * @dev write state: + * - Loans.update: + * - _upsert: + * - insert or update loan in loans array + * - remove: + * - remove loan from loans array + * - update borrower in address => borrower mapping + * @dev reverts on: + * - auction active AuctionActive() + * - loan not fully collateralized BorrowerUnderCollateralized() + * @dev emit events: + * - LoanStamped + */ + function stampLoan( + AuctionsState storage auctions_, + DepositsState storage deposits_, + LoansState storage loans_, + PoolState calldata poolState_, + address borrowerAddress_ + ) external returns ( + uint256 newLup_ + ) { + Borrower memory borrower = loans_.borrowers[borrowerAddress_]; + + bool inAuction = _inAuction(auctions_, borrowerAddress_); + + // revert if loan is in auction + if (inAuction) revert AuctionActive(); + + newLup_ = _lup(deposits_, poolState_.debt); + + uint256 borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator); + bool isCollateralized = _isCollateralized(borrowerDebt, borrower.collateral, newLup_, poolState_.poolType); + + // revert if loan is not fully collateralized at current LUP + if (!isCollateralized) revert BorrowerUnderCollateralized(); + + // update loan state to stamp Neutral Price + Loans.update( + loans_, + auctions_, + deposits_, + borrower, + borrowerAddress_, + poolState_.debt, + poolState_.rate, + newLup_, + false, // loan not in auction + true // stamp Neutral Price of the loan + ); + + emit LoanStamped(borrowerAddress_); + } + /**********************/ /*** View Functions ***/ /**********************/ diff --git a/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol b/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol index fc46a46da..b4f773cc7 100644 --- a/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol @@ -432,7 +432,10 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { skip(10 days); - _updateInterest(); + // accrue debt and restamp Neutral Price of the loan + vm.expectEmit(true, true, true, true); + emit LoanStamped(_borrower); + _pool.stampLoan(_borrower); expectedDebt = 21_157.152643010853304038 * 1e18; @@ -457,7 +460,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { borrower: _borrower, borrowerDebt: expectedDebt, borrowerCollateral: 50 * 1e18, - borrowert0Np: 445.838278846153846359 * 1e18, + borrowert0Np: 448.381722115384615591 * 1e18, borrowerCollateralization: 7.044916376706357984 * 1e18 }); _assertLenderInterest(liquidityAdded, 119.836959946754650000 * 1e18); @@ -489,7 +492,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { borrower: _borrower, borrowerDebt: expectedDebt, borrowerCollateral: 50 * 1e18, - borrowert0Np: 445.838278846153846359 * 1e18, + borrowert0Np: 448.381722115384615591 * 1e18, borrowerCollateralization: 7.030801136225104190 * 1e18 }); _assertLenderInterest(liquidityAdded, 157.005764521268350000 * 1e18); @@ -520,7 +523,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { borrower: _borrower, borrowerDebt: expectedDebt, borrowerCollateral: 50 * 1e18, - borrowert0Np: 445.838278846153846359 * 1e18, + borrowert0Np: 448.381722115384615591 * 1e18, borrowerCollateralization: 7.015307034516347067 * 1e18 }); } @@ -596,6 +599,14 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { indexLimit: 3_000, newLup: 2_995.912459898389633881 * 1e18 }); + + // skip to make loan undercolalteralized + skip(10000 days); + + // should not allow borrower to restamp the Neutral Price of the loan if under collateralized + _assertStampLoanBorrowerUnderCollateralizedRevert({ + borrower: _borrower2 + }); } function testMinBorrowAmountCheck() external tearDown { diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsKick.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsKick.t.sol index 2df38d406..f2a324313 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsKick.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsKick.t.sol @@ -627,6 +627,11 @@ contract ERC20PoolLiquidationsKickTest is ERC20HelperContract { amount: 1 * 1e18, indexLimit: 7000 }); + + // should not allow borrower to restamp the Neutral Price of the loan if auction kicked + _assertStampLoanAuctionActiveRevert({ + borrower: _borrower + }); } function testInterestsAccumulationWithAllLoansAuctioned() external tearDown { diff --git a/tests/forge/utils/DSTestPlus.sol b/tests/forge/utils/DSTestPlus.sol index 745ac531c..f4a53b946 100644 --- a/tests/forge/utils/DSTestPlus.sol +++ b/tests/forge/utils/DSTestPlus.sol @@ -881,6 +881,22 @@ abstract contract DSTestPlus is Test, IPoolEvents { _pool.bucketTake(borrower, true, index); } + function _assertStampLoanAuctionActiveRevert( + address borrower + ) internal { + changePrank(borrower); + vm.expectRevert(IPoolErrors.AuctionActive.selector); + _pool.stampLoan(borrower); + } + + function _assertStampLoanBorrowerUnderCollateralizedRevert( + address borrower + ) internal { + changePrank(borrower); + vm.expectRevert(IPoolErrors.BorrowerUnderCollateralized.selector); + _pool.stampLoan(borrower); + } + function _assertBorrowAuctionActiveRevert( address from, uint256, From 08b8943817ef449bcc48e091eb2aec28a6c4d7c4 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Tue, 28 Feb 2023 16:44:27 +0200 Subject: [PATCH 4/8] Remove ability of different actors updating Neutral Price of loans --- src/base/Pool.sol | 5 ++--- src/interfaces/pool/commons/IPoolBorrowerActions.sol | 7 ++----- src/libraries/external/BorrowerActions.sol | 12 ++++++------ tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol | 2 +- tests/forge/utils/DSTestPlus.sol | 4 ++-- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/base/Pool.sol b/src/base/Pool.sol index 80a6a27e3..69e819efc 100644 --- a/src/base/Pool.sol +++ b/src/base/Pool.sol @@ -338,15 +338,14 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { /***********************************/ /// @inheritdoc IPoolBorrowerActions - function stampLoan(address borrowerAddress_) external override nonReentrant { + function stampLoan() external override nonReentrant { PoolState memory poolState = _accruePoolInterest(); uint256 newLup = BorrowerActions.stampLoan( auctions, deposits, loans, - poolState, - borrowerAddress_ + poolState ); _updateInterestState(poolState, newLup); diff --git a/src/interfaces/pool/commons/IPoolBorrowerActions.sol b/src/interfaces/pool/commons/IPoolBorrowerActions.sol index 82e448e21..141f085a9 100644 --- a/src/interfaces/pool/commons/IPoolBorrowerActions.sol +++ b/src/interfaces/pool/commons/IPoolBorrowerActions.sol @@ -10,11 +10,8 @@ interface IPoolBorrowerActions { /** * @notice Called by fully colalteralized borrowers to restamp the Neutral Price of the loan (only if loan is fully collateralized and not in auction). * @notice The reason for stamping the neutral price on the loan is to provide some certainty to the borrower as to at what price they can expect to be liquidated. - * @notice This action can be initiated by borrower itself or by a different actor on behalf of borrower. - * @param borrowerAddress The borrower address to restamp Neutral Price for. + * @notice This action can restamp only the loan of `msg.sender`. */ - function stampLoan( - address borrowerAddress - ) external; + function stampLoan() external; } diff --git a/src/libraries/external/BorrowerActions.sol b/src/libraries/external/BorrowerActions.sol index dd5e9f62c..8007813a5 100644 --- a/src/libraries/external/BorrowerActions.sol +++ b/src/libraries/external/BorrowerActions.sol @@ -422,14 +422,14 @@ library BorrowerActions { AuctionsState storage auctions_, DepositsState storage deposits_, LoansState storage loans_, - PoolState calldata poolState_, - address borrowerAddress_ + PoolState calldata poolState_ ) external returns ( uint256 newLup_ ) { - Borrower memory borrower = loans_.borrowers[borrowerAddress_]; + address borrowerAddress = msg.sender; + Borrower memory borrower = loans_.borrowers[borrowerAddress]; - bool inAuction = _inAuction(auctions_, borrowerAddress_); + bool inAuction = _inAuction(auctions_, borrowerAddress); // revert if loan is in auction if (inAuction) revert AuctionActive(); @@ -448,7 +448,7 @@ library BorrowerActions { auctions_, deposits_, borrower, - borrowerAddress_, + borrowerAddress, poolState_.debt, poolState_.rate, newLup_, @@ -456,7 +456,7 @@ library BorrowerActions { true // stamp Neutral Price of the loan ); - emit LoanStamped(borrowerAddress_); + emit LoanStamped(borrowerAddress); } /**********************/ diff --git a/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol b/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol index b4f773cc7..45b1870a3 100644 --- a/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolBorrow.t.sol @@ -435,7 +435,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { // accrue debt and restamp Neutral Price of the loan vm.expectEmit(true, true, true, true); emit LoanStamped(_borrower); - _pool.stampLoan(_borrower); + _pool.stampLoan(); expectedDebt = 21_157.152643010853304038 * 1e18; diff --git a/tests/forge/utils/DSTestPlus.sol b/tests/forge/utils/DSTestPlus.sol index f4a53b946..37b2af1f4 100644 --- a/tests/forge/utils/DSTestPlus.sol +++ b/tests/forge/utils/DSTestPlus.sol @@ -886,7 +886,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(borrower); vm.expectRevert(IPoolErrors.AuctionActive.selector); - _pool.stampLoan(borrower); + _pool.stampLoan(); } function _assertStampLoanBorrowerUnderCollateralizedRevert( @@ -894,7 +894,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(borrower); vm.expectRevert(IPoolErrors.BorrowerUnderCollateralized.selector); - _pool.stampLoan(borrower); + _pool.stampLoan(); } function _assertBorrowAuctionActiveRevert( From 05826d6744eecd457aef36e4d42bf27c4ad0cbba Mon Sep 17 00:00:00 2001 From: grandizzy Date: Tue, 28 Feb 2023 20:04:57 +0200 Subject: [PATCH 5/8] Style and grouping --- src/libraries/external/BorrowerActions.sol | 39 ++++++++++------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/libraries/external/BorrowerActions.sol b/src/libraries/external/BorrowerActions.sol index 8007813a5..3cb782d10 100644 --- a/src/libraries/external/BorrowerActions.sol +++ b/src/libraries/external/BorrowerActions.sol @@ -137,10 +137,9 @@ library BorrowerActions { vars.borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator); vars.inAuction = _inAuction(auctions_, borrowerAddress_); - result_.t0PoolDebt = poolState_.t0Debt; - result_.poolDebt = poolState_.debt; - result_.poolCollateral = poolState_.collateral; - + result_.t0PoolDebt = poolState_.t0Debt; + result_.poolDebt = poolState_.debt; + result_.poolCollateral = poolState_.collateral; result_.remainingCollateral = borrower.collateral; if (vars.pledge) { @@ -148,8 +147,7 @@ library BorrowerActions { borrower.collateral += collateralToPledge_; result_.remainingCollateral += collateralToPledge_; - - result_.newLup = _lup(deposits_, result_.poolDebt); + result_.newLup = _lup(deposits_, result_.poolDebt); // if loan is auctioned and becomes collateralized by newly pledged collateral then settle auction if ( @@ -161,7 +159,6 @@ library BorrowerActions { vars.stampT0Np = true; // stamp borrower t0Np when exiting from auction result_.settledAuction = true; - // remove debt from pool accumulator and settle auction result_.t0DebtInAuctionChange = borrower.t0Debt; @@ -178,7 +175,8 @@ library BorrowerActions { poolState_.poolType ); - borrower.collateral = result_.remainingCollateral; + borrower.collateral = result_.remainingCollateral; + result_.poolCollateral -= vars.compensatedCollateral; } @@ -213,8 +211,7 @@ library BorrowerActions { // add debt change to pool's debt result_.t0PoolDebt += vars.t0DebtChange; result_.poolDebt = Maths.wmul(result_.t0PoolDebt, poolState_.inflator); - - result_.newLup = _lup(deposits_, result_.poolDebt); + result_.newLup = _lup(deposits_, result_.poolDebt); // revert if borrow drives LUP price under the specified price limit _revertIfPriceDroppedBelowLimit(result_.newLup, limitIndex_); @@ -295,10 +292,9 @@ library BorrowerActions { vars.borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator); vars.inAuction = _inAuction(auctions_, borrowerAddress_); - result_.t0PoolDebt = poolState_.t0Debt; - result_.poolDebt = poolState_.debt; - result_.poolCollateral = poolState_.collateral; - + result_.t0PoolDebt = poolState_.t0Debt; + result_.poolDebt = poolState_.debt; + result_.poolCollateral = poolState_.collateral; result_.remainingCollateral = borrower.collateral; if (vars.repay) { @@ -313,11 +309,11 @@ library BorrowerActions { ); } - result_.t0PoolDebt -= vars.t0RepaidDebt; + result_.t0PoolDebt -= vars.t0RepaidDebt; + result_.poolDebt = Maths.wmul(result_.t0PoolDebt, poolState_.inflator); + result_.quoteTokenToRepay = Maths.wmul(vars.t0RepaidDebt, poolState_.inflator); - result_.poolDebt = Maths.wmul(result_.t0PoolDebt, poolState_.inflator); - result_.quoteTokenToRepay = Maths.wmul(vars.t0RepaidDebt, poolState_.inflator); - vars.borrowerDebt = Maths.wmul(borrower.t0Debt - vars.t0RepaidDebt, poolState_.inflator); + vars.borrowerDebt = Maths.wmul(borrower.t0Debt - vars.t0RepaidDebt, poolState_.inflator); // check that paying the loan doesn't leave borrower debt under min debt amount _revertOnMinDebt( @@ -337,7 +333,6 @@ library BorrowerActions { vars.stampT0Np = true; // stamp borrower t0Np when exiting from auction result_.settledAuction = true; - // remove entire borrower debt from pool auctions debt accumulator result_.t0DebtInAuctionChange = borrower.t0Debt; @@ -354,7 +349,8 @@ library BorrowerActions { poolState_.poolType ); - borrower.collateral = result_.remainingCollateral; + borrower.collateral = result_.remainingCollateral; + result_.poolCollateral -= vars.compensatedCollateral; } else { // partial repay, remove only the paid debt from pool auctions debt accumulator @@ -384,7 +380,8 @@ library BorrowerActions { // stamp borrower t0Np when pull collateral action vars.stampT0Np = true; - borrower.collateral -= collateralAmountToPull_; + borrower.collateral -= collateralAmountToPull_; + result_.poolCollateral -= collateralAmountToPull_; } From e87949c989bbc113dbabb0aa847a5b04d857c549 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Wed, 1 Mar 2023 05:27:33 +0200 Subject: [PATCH 6/8] Remove bucket depth constraint, add test for settle called with 0 buckets (settle only with reserves) --- src/interfaces/pool/commons/IPoolErrors.sol | 5 ----- src/libraries/external/Auctions.sol | 4 ---- .../ERC20Pool/ERC20PoolInputValidation.t.sol | 6 ------ .../ERC20Pool/ERC20PoolLiquidationsTake.t.sol | 19 +++++++++++++------ .../ERC721PoolInputValidation.t.sol | 6 ------ 5 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/interfaces/pool/commons/IPoolErrors.sol b/src/interfaces/pool/commons/IPoolErrors.sol index f58e62419..a85af9f01 100644 --- a/src/interfaces/pool/commons/IPoolErrors.sol +++ b/src/interfaces/pool/commons/IPoolErrors.sol @@ -107,11 +107,6 @@ interface IPoolErrors { */ error InsufficientLiquidity(); - /** - * @notice When settling pool debt the number of buckets to use should be greater than 0. - */ - error InvalidBucketDepth(); - /** * @notice When transferring LPs between indices, the new index must be a valid index. */ diff --git a/src/libraries/external/Auctions.sol b/src/libraries/external/Auctions.sol index 0ad1d0e97..74768d625 100644 --- a/src/libraries/external/Auctions.sol +++ b/src/libraries/external/Auctions.sol @@ -162,7 +162,6 @@ library Auctions { error CollateralRoundingNeededButNotPossible(); error InsufficientLiquidity(); error InsufficientCollateral(); - error InvalidBucketDepth(); error InvalidAmount(); error NoAuction(); error NoReserves(); @@ -207,9 +206,6 @@ library Auctions { uint256 collateralSettled_, uint256 t0DebtSettled_ ) { - // revert if no bucket to settle - if (params_.bucketDepth == 0 ) revert InvalidBucketDepth(); - uint256 kickTime = auctions_.liquidations[params_.borrower].kickTime; if (kickTime == 0) revert NoAuction(); diff --git a/tests/forge/ERC20Pool/ERC20PoolInputValidation.t.sol b/tests/forge/ERC20Pool/ERC20PoolInputValidation.t.sol index 22eb62485..d245fcb57 100644 --- a/tests/forge/ERC20Pool/ERC20PoolInputValidation.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolInputValidation.t.sol @@ -82,12 +82,6 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { ERC20Pool(address(_pool)).removeCollateral(0, 1000); } - function testValidateSettleInput() external tearDown { - // revert on zero amount - vm.expectRevert(IPoolErrors.InvalidBucketDepth.selector); - ERC20Pool(address(_pool)).settle(address(this), 0); - } - function testValidateTakeInput() external tearDown { // revert on zero amount vm.expectRevert(IPoolErrors.InvalidAmount.selector); diff --git a/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol b/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol index 3678a32c9..90d6b7949 100644 --- a/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol +++ b/tests/forge/ERC20Pool/ERC20PoolLiquidationsTake.t.sol @@ -1576,22 +1576,29 @@ contract ERC20PoolLiquidationsTakeTest is ERC20HelperContract { auctionPrice: 0, timeRemaining: 0 }); - - // partial clears / debt settled - max buckets to use is 1, remaining will be taken from reserves + // partial clears / debt settled - max buckets to use is 0, settle only from reserves _settle({ from: _lender, borrower: _borrower2, - maxDepth: 1, - settledDebt: 2_923.975862386543877283 * 1e18 + maxDepth: 0, + settledDebt: 834 * 1e18 }); - _assertReserveAuction({ - reserves: 0.989870342666661239 * 1e18, + reserves: 0.989870342666662235 * 1e18, claimableReserves : 0, claimableReservesRemaining: 0, auctionPrice: 0, timeRemaining: 0 }); + + // partial clears / debt settled with max buckets to use is 1 + _settle({ + from: _lender, + borrower: _borrower2, + maxDepth: 1, + settledDebt: 2_089.975862386543877283 * 1e18 + }); + _assertAuction( AuctionParams({ borrower: _borrower2, diff --git a/tests/forge/ERC721Pool/ERC721PoolInputValidation.t.sol b/tests/forge/ERC721Pool/ERC721PoolInputValidation.t.sol index 9aca25999..9cf985139 100644 --- a/tests/forge/ERC721Pool/ERC721PoolInputValidation.t.sol +++ b/tests/forge/ERC721Pool/ERC721PoolInputValidation.t.sol @@ -90,12 +90,6 @@ contract ERC721PoolBorrowTest is ERC721HelperContract { ERC721Pool(address(_pool)).removeCollateral(0, 1000); } - function testValidateSettleInput() external tearDown { - // revert on zero amount - vm.expectRevert(IPoolErrors.InvalidBucketDepth.selector); - ERC721Pool(address(_pool)).settle(address(this), 0); - } - function testValidateTakeInput() external tearDown { // revert on zero amount vm.expectRevert(IPoolErrors.InvalidAmount.selector); From 55f44a1116e35a76dc534c2252a310573a342b92 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Wed, 1 Mar 2023 07:49:12 +0200 Subject: [PATCH 7/8] Use msg.sender instead local var for gas improvements --- src/libraries/external/BorrowerActions.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libraries/external/BorrowerActions.sol b/src/libraries/external/BorrowerActions.sol index 3cb782d10..fd8e06011 100644 --- a/src/libraries/external/BorrowerActions.sol +++ b/src/libraries/external/BorrowerActions.sol @@ -423,10 +423,9 @@ library BorrowerActions { ) external returns ( uint256 newLup_ ) { - address borrowerAddress = msg.sender; - Borrower memory borrower = loans_.borrowers[borrowerAddress]; + Borrower memory borrower = loans_.borrowers[msg.sender]; - bool inAuction = _inAuction(auctions_, borrowerAddress); + bool inAuction = _inAuction(auctions_, msg.sender); // revert if loan is in auction if (inAuction) revert AuctionActive(); @@ -445,7 +444,7 @@ library BorrowerActions { auctions_, deposits_, borrower, - borrowerAddress, + msg.sender, poolState_.debt, poolState_.rate, newLup_, @@ -453,7 +452,7 @@ library BorrowerActions { true // stamp Neutral Price of the loan ); - emit LoanStamped(borrowerAddress); + emit LoanStamped(msg.sender); } /**********************/ From d82eda7715cf7a97e5b44c578affe445c7e51967 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Wed, 1 Mar 2023 08:07:27 +0200 Subject: [PATCH 8/8] Remove local vars used only once for gas savings --- src/libraries/external/BorrowerActions.sol | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libraries/external/BorrowerActions.sol b/src/libraries/external/BorrowerActions.sol index fd8e06011..77f01472e 100644 --- a/src/libraries/external/BorrowerActions.sol +++ b/src/libraries/external/BorrowerActions.sol @@ -423,20 +423,22 @@ library BorrowerActions { ) external returns ( uint256 newLup_ ) { - Borrower memory borrower = loans_.borrowers[msg.sender]; - - bool inAuction = _inAuction(auctions_, msg.sender); - // revert if loan is in auction - if (inAuction) revert AuctionActive(); + if (_inAuction(auctions_, msg.sender)) revert AuctionActive(); - newLup_ = _lup(deposits_, poolState_.debt); + Borrower memory borrower = loans_.borrowers[msg.sender]; - uint256 borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator); - bool isCollateralized = _isCollateralized(borrowerDebt, borrower.collateral, newLup_, poolState_.poolType); + newLup_ = _lup(deposits_, poolState_.debt); // revert if loan is not fully collateralized at current LUP - if (!isCollateralized) revert BorrowerUnderCollateralized(); + if ( + !_isCollateralized( + Maths.wmul(borrower.t0Debt, poolState_.inflator), // current borrower debt + borrower.collateral, + newLup_, + poolState_.poolType + ) + ) revert BorrowerUnderCollateralized(); // update loan state to stamp Neutral Price Loans.update(