diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 9a63f10c4..2bc3ee1e9 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -290,7 +290,8 @@ contract PositionManager is PermitERC721, IPositionManager, Multicall, Reentranc uint256 tokenId_, uint256 fromIndex_, uint256 toIndex_, - uint256 expiry_ + uint256 expiry_, + bool revertIfBelowLup_ ) external override nonReentrant mayInteract(pool_, tokenId_) { TokenInfo storage tokenInfo = positionTokens[tokenId_]; Position storage fromPosition = tokenInfo.positions[fromIndex_]; @@ -334,7 +335,8 @@ contract PositionManager is PermitERC721, IPositionManager, Multicall, Reentranc vars.maxQuote, fromIndex_, toIndex_, - expiry_ + expiry_, + revertIfBelowLup_ ); EnumerableSet.UintSet storage positionIndexes = tokenInfo.positionIndexes; diff --git a/src/base/Pool.sol b/src/base/Pool.sol index a71b63e42..5f26cb0fb 100644 --- a/src/base/Pool.sol +++ b/src/base/Pool.sol @@ -25,6 +25,7 @@ import { PoolState, AuctionsState, DepositsState, + Loan, LoansState, InflatorState, EmaState, @@ -32,6 +33,7 @@ import { PoolBalancesState, ReserveAuctionState, Bucket, + Lender, BurnEvent, Liquidation } from '../interfaces/pool/commons/IPoolState.sol'; @@ -182,13 +184,21 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { uint256 maxAmount_, uint256 fromIndex_, uint256 toIndex_, - uint256 expiry_ + uint256 expiry_, + bool revertIfBelowLup_ ) external override nonReentrant returns (uint256 fromBucketLP_, uint256 toBucketLP_, uint256 movedAmount_) { _revertAfterExpiry(expiry_); PoolState memory poolState = _accruePoolInterest(); _revertIfAuctionDebtLocked(deposits, poolState.t0DebtInAuction, fromIndex_, poolState.inflator); + MoveQuoteParams memory moveParams; + moveParams.maxAmountToMove = maxAmount_; + moveParams.fromIndex = fromIndex_; + moveParams.toIndex = toIndex_; + moveParams.thresholdPrice = Loans.getMax(loans).thresholdPrice; + moveParams.revertIfBelowLup = revertIfBelowLup_; + uint256 newLup; ( fromBucketLP_, @@ -199,12 +209,7 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { buckets, deposits, poolState, - MoveQuoteParams({ - maxAmountToMove: maxAmount_, - fromIndex: fromIndex_, - toIndex: toIndex_, - thresholdPrice: Loans.getMax(loans).thresholdPrice - }) + moveParams ); // update pool interest rate state @@ -785,10 +790,11 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { uint256 index_ ) external view override returns (uint256, uint256, uint256, uint256, uint256) { uint256 scale = Deposits.scale(deposits, index_); + Bucket storage bucket = buckets[index_]; return ( - buckets[index_].lps, - buckets[index_].collateral, - buckets[index_].bankruptcyTime, + bucket.lps, + bucket.collateral, + bucket.bankruptcyTime, Maths.wmul(scale, Deposits.unscaledValueAt(deposits, index_)), scale ); @@ -826,15 +832,20 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { /// @inheritdoc IPoolState function debtInfo() external view returns (uint256, uint256, uint256, uint256) { - uint256 pendingInflator = PoolCommons.pendingInflator( - inflatorState.inflator, - inflatorState.inflatorUpdate, - interestState.interestRate - ); + uint256 t0Debt = poolBalances.t0Debt; + uint256 inflator = inflatorState.inflator; + return ( - Maths.ceilWmul(poolBalances.t0Debt, pendingInflator), - Maths.ceilWmul(poolBalances.t0Debt, inflatorState.inflator), - Maths.ceilWmul(poolBalances.t0DebtInAuction, inflatorState.inflator), + Maths.ceilWmul( + t0Debt, + PoolCommons.pendingInflator( + inflator, + inflatorState.inflatorUpdate, + interestState.interestRate + ) + ), + Maths.ceilWmul(t0Debt, inflator), + Maths.ceilWmul(poolBalances.t0DebtInAuction, inflator), interestState.t0Debt2ToCollateral ); } @@ -906,8 +917,11 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { uint256 index_, address lender_ ) external view override returns (uint256 lpBalance_, uint256 depositTime_) { - depositTime_ = buckets[index_].lenders[lender_].depositTime; - if (buckets[index_].bankruptcyTime < depositTime_) lpBalance_ = buckets[index_].lenders[lender_].lps; + Bucket storage bucket = buckets[index_]; + Lender storage lender = bucket.lenders[lender_]; + + depositTime_ = lender.depositTime; + if (bucket.bankruptcyTime < depositTime_) lpBalance_ = lender.lps; } /// @inheritdoc IPoolState @@ -923,17 +937,19 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool { function loanInfo( uint256 loanId_ ) external view override returns (address, uint256) { + Loan memory loan = Loans.getByIndex(loans, loanId_); return ( - Loans.getByIndex(loans, loanId_).borrower, - Loans.getByIndex(loans, loanId_).thresholdPrice + loan.borrower, + loan.thresholdPrice ); } /// @inheritdoc IPoolState function loansInfo() external view override returns (address, uint256, uint256) { + Loan memory maxLoan = Loans.getMax(loans); return ( - Loans.getMax(loans).borrower, - Maths.wmul(Loans.getMax(loans).thresholdPrice, inflatorState.inflator), + maxLoan.borrower, + Maths.wmul(maxLoan.thresholdPrice, inflatorState.inflator), Loans.noOfLoans(loans) ); } diff --git a/src/interfaces/pool/commons/IPoolErrors.sol b/src/interfaces/pool/commons/IPoolErrors.sol index ad6a3eaa1..b5fddaff9 100644 --- a/src/interfaces/pool/commons/IPoolErrors.sol +++ b/src/interfaces/pool/commons/IPoolErrors.sol @@ -180,7 +180,7 @@ interface IPoolErrors { error PoolUnderCollateralized(); /** - * @notice Actor is attempting to add quote tokens at a price below the `LUP`. + * @notice Actor is attempting to add or move quote tokens at a price below the `LUP`. * @notice Actor is attempting to kick with bucket price below the `LUP`. */ error PriceBelowLUP(); diff --git a/src/interfaces/pool/commons/IPoolInternals.sol b/src/interfaces/pool/commons/IPoolInternals.sol index 8c86c902a..f1b62ca89 100644 --- a/src/interfaces/pool/commons/IPoolInternals.sol +++ b/src/interfaces/pool/commons/IPoolInternals.sol @@ -78,10 +78,11 @@ struct AddQuoteParams { /// @dev Struct used to hold parameters for `LenderAction.moveQuoteToken` action. struct MoveQuoteParams { - uint256 fromIndex; // the deposit index from where amount is moved - uint256 maxAmountToMove; // [WAD] max amount to move between deposits - uint256 toIndex; // the deposit index where amount is moved to - uint256 thresholdPrice; // [WAD] max threshold price in pool + uint256 fromIndex; // the deposit index from where amount is moved + uint256 maxAmountToMove; // [WAD] max amount to move between deposits + uint256 toIndex; // the deposit index where amount is moved to + uint256 thresholdPrice; // [WAD] max threshold price in pool + bool revertIfBelowLup; // revert tx if quote token is moved from above the LUP to below the LUP } /// @dev Struct used to hold parameters for `LenderAction.removeQuoteToken` action. diff --git a/src/interfaces/pool/commons/IPoolLenderActions.sol b/src/interfaces/pool/commons/IPoolLenderActions.sol index 6fc1e3b9e..25ac4c048 100644 --- a/src/interfaces/pool/commons/IPoolLenderActions.sol +++ b/src/interfaces/pool/commons/IPoolLenderActions.sol @@ -28,19 +28,21 @@ interface IPoolLenderActions { /** * @notice Called by lenders to move an amount of credit from a specified price bucket to another specified price bucket. - * @param maxAmount_ The maximum amount of quote token to be moved by a lender (`WAD` precision). - * @param fromIndex_ The bucket index from which the quote tokens will be removed. - * @param toIndex_ The bucket index to which the quote tokens will be added. - * @param expiry_ Timestamp after which this transaction will revert, preventing inclusion in a block with unfavorable price. - * @return fromBucketLP_ The amount of `LP` moved out from bucket (`WAD` precision). - * @return toBucketLP_ The amount of `LP` moved to destination bucket (`WAD` precision). - * @return movedAmount_ The amount of quote token moved (`WAD` precision). + * @param maxAmount_ The maximum amount of quote token to be moved by a lender (`WAD` precision). + * @param fromIndex_ The bucket index from which the quote tokens will be removed. + * @param toIndex_ The bucket index to which the quote tokens will be added. + * @param expiry_ Timestamp after which this transaction will revert, preventing inclusion in a block with unfavorable price. + * @param revertIfBelowLup_ The tx will revert if quote token is moved from above the `LUP` to below the `LUP` (and avoid paying fee for move below `LUP`). + * @return fromBucketLP_ The amount of `LP` moved out from bucket (`WAD` precision). + * @return toBucketLP_ The amount of `LP` moved to destination bucket (`WAD` precision). + * @return movedAmount_ The amount of quote token moved (`WAD` precision). */ function moveQuoteToken( uint256 maxAmount_, uint256 fromIndex_, uint256 toIndex_, - uint256 expiry_ + uint256 expiry_, + bool revertIfBelowLup_ ) external returns (uint256 fromBucketLP_, uint256 toBucketLP_, uint256 movedAmount_); /** diff --git a/src/interfaces/position/IPositionManagerOwnerActions.sol b/src/interfaces/position/IPositionManagerOwnerActions.sol index 6e468061a..3af266a9d 100644 --- a/src/interfaces/position/IPositionManagerOwnerActions.sol +++ b/src/interfaces/position/IPositionManagerOwnerActions.sol @@ -50,18 +50,20 @@ interface IPositionManagerOwnerActions { /** * @notice Called by owners to move liquidity between two buckets. - * @param pool_ The pool address associated with positions NFT. - * @param tokenId_ The tokenId of the positions NFT. - * @param fromIndex_ The bucket index from which liquidity should be moved. - * @param toIndex_ The bucket index to which liquidity should be moved. - * @param expiry_ Timestamp after which this TX will revert, preventing inclusion in a block with unfavorable price. + * @param pool_ The pool address associated with positions NFT. + * @param tokenId_ The tokenId of the positions NFT. + * @param fromIndex_ The bucket index from which liquidity should be moved. + * @param toIndex_ The bucket index to which liquidity should be moved. + * @param expiry_ Timestamp after which this TX will revert, preventing inclusion in a block with unfavorable price. + * @param revertIfBelowLup_ The tx will revert if quote token is moved from above the `LUP` to below the `LUP` (and avoid paying fee for move below `LUP`). */ function moveLiquidity( address pool_, uint256 tokenId_, uint256 fromIndex_, uint256 toIndex_, - uint256 expiry_ + uint256 expiry_, + bool revertIfBelowLup_ ) external; /** diff --git a/src/libraries/external/LenderActions.sol b/src/libraries/external/LenderActions.sol index 72cd30953..97501bf83 100644 --- a/src/libraries/external/LenderActions.sol +++ b/src/libraries/external/LenderActions.sol @@ -142,6 +142,7 @@ library LenderActions { * @dev same block when bucket becomes insolvent `BucketBankruptcyBlock()` * @dev no LP awarded in bucket `InsufficientLP()` * @dev calculated unscaled amount to add is 0 `InvalidAmount()` + * @dev deposit below `LUP` `PriceBelowLUP()` * @dev === Emit events === * @dev - `AddQuoteToken` */ @@ -233,6 +234,7 @@ library LenderActions { * @dev dust amount `DustAmountNotExceeded()` * @dev invalid index `InvalidIndex()` * @dev no LP awarded in to bucket `InsufficientLP()` + * @dev move below `LUP` `PriceBelowLUP()` * @dev === Emit events === * @dev - `BucketBankruptcy` * @dev - `MoveQuoteToken` @@ -288,6 +290,8 @@ library LenderActions { lup_ = Deposits.getLup(deposits_, poolState_.debt); // apply unutilized deposit fee if quote token is moved from above the LUP to below the LUP if (vars.fromBucketPrice >= lup_ && vars.toBucketPrice < lup_) { + if (params_.revertIfBelowLup) revert PriceBelowLUP(); + movedAmount_ = Maths.wmul(movedAmount_, Maths.WAD - _depositFeeRate(poolState_.rate)); } diff --git a/tests/brownie/test_scaled_pool.py b/tests/brownie/test_scaled_pool.py index 344dde473..24a1a17d3 100644 --- a/tests/brownie/test_scaled_pool.py +++ b/tests/brownie/test_scaled_pool.py @@ -25,7 +25,7 @@ def test_quote_deposit_move_remove_scaled( move_txes = [] for i in range(2530, 2550): - tx = scaled_pool.moveQuoteToken(100 * 10**18, i, i + 30, chain.time() + 30, {"from": lenders[0]}) + tx = scaled_pool.moveQuoteToken(100 * 10**18, i, i + 30, chain.time() + 30, False, {"from": lenders[0]}) move_txes.append(tx) with capsys.disabled(): print("\n==================================") diff --git a/tests/forge/invariants/PositionsAndRewards/handlers/unbounded/UnboundedERC20PoolPositionsHandler.sol b/tests/forge/invariants/PositionsAndRewards/handlers/unbounded/UnboundedERC20PoolPositionsHandler.sol index e18c751c1..e9204dcbf 100644 --- a/tests/forge/invariants/PositionsAndRewards/handlers/unbounded/UnboundedERC20PoolPositionsHandler.sol +++ b/tests/forge/invariants/PositionsAndRewards/handlers/unbounded/UnboundedERC20PoolPositionsHandler.sol @@ -242,7 +242,7 @@ abstract contract UnboundedERC20PoolPositionsHandler is UnboundedBasePositionHan * @notice Struct holding parameters for moving the liquidity of a position. */ - try _positionManager.moveLiquidity(address(_pool), tokenId_, fromIndex_, toIndex_, block.timestamp + 30) { + try _positionManager.moveLiquidity(address(_pool), tokenId_, fromIndex_, toIndex_, block.timestamp + 30, false) { bucketIndexesByTokenId[tokenId_].add(toIndex_); bucketIndexesByTokenId[tokenId_].remove(fromIndex_); diff --git a/tests/forge/invariants/PositionsAndRewards/handlers/unbounded/UnboundedERC721PoolPositionsHandler.sol b/tests/forge/invariants/PositionsAndRewards/handlers/unbounded/UnboundedERC721PoolPositionsHandler.sol index 9523c437b..e1e6acbf7 100644 --- a/tests/forge/invariants/PositionsAndRewards/handlers/unbounded/UnboundedERC721PoolPositionsHandler.sol +++ b/tests/forge/invariants/PositionsAndRewards/handlers/unbounded/UnboundedERC721PoolPositionsHandler.sol @@ -242,7 +242,7 @@ abstract contract UnboundedERC721PoolPositionsHandler is UnboundedBasePositionHa * @notice Struct holding parameters for moving the liquidity of a position. */ - try _positionManager.moveLiquidity(address(_pool), tokenId_, fromIndex_, toIndex_, block.timestamp + 30) { + try _positionManager.moveLiquidity(address(_pool), tokenId_, fromIndex_, toIndex_, block.timestamp + 30, false) { bucketIndexesByTokenId[tokenId_].add(toIndex_); bucketIndexesByTokenId[tokenId_].remove(fromIndex_); diff --git a/tests/forge/invariants/base/handlers/unbounded/UnboundedBasicPoolHandler.sol b/tests/forge/invariants/base/handlers/unbounded/UnboundedBasicPoolHandler.sol index cacf4c04f..4bff808a3 100644 --- a/tests/forge/invariants/base/handlers/unbounded/UnboundedBasicPoolHandler.sol +++ b/tests/forge/invariants/base/handlers/unbounded/UnboundedBasicPoolHandler.sol @@ -112,7 +112,8 @@ abstract contract UnboundedBasicPoolHandler is BaseHandler { amount_, fromIndex_, toIndex_, - block.timestamp + 1 minutes + block.timestamp + 1 minutes, + false ) returns (uint256, uint256, uint256 movedAmount_) { (, uint256 fromBucketDepositTime) = _pool.lenderInfo(fromIndex_, _actor); diff --git a/tests/forge/unit/ERC20Pool/ERC20DSTestPlus.sol b/tests/forge/unit/ERC20Pool/ERC20DSTestPlus.sol index 2e94d7b82..076816f82 100644 --- a/tests/forge/unit/ERC20Pool/ERC20DSTestPlus.sol +++ b/tests/forge/unit/ERC20Pool/ERC20DSTestPlus.sol @@ -740,7 +740,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.LUPBelowHTP.selector); - ERC20Pool(address(_pool)).moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); + ERC20Pool(address(_pool)).moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max, false); } } diff --git a/tests/forge/unit/ERC20Pool/ERC20PoolGasLoadTest.t.sol b/tests/forge/unit/ERC20Pool/ERC20PoolGasLoadTest.t.sol index 3e3dd6091..8be2b0cc4 100644 --- a/tests/forge/unit/ERC20Pool/ERC20PoolGasLoadTest.t.sol +++ b/tests/forge/unit/ERC20Pool/ERC20PoolGasLoadTest.t.sol @@ -278,7 +278,7 @@ contract ERC20PoolCommonActionsGasLoadTest is ERC20PoolGasLoadTest { _pool.removeQuoteToken(5_000 * 1e18, index_); skip(15 hours); - _pool.moveQuoteToken(1_000 * 1e18, index_, index_ + 1, block.timestamp + 2 minutes); + _pool.moveQuoteToken(1_000 * 1e18, index_, index_ + 1, block.timestamp + 2 minutes, false); skip(15 hours); _pool.removeQuoteToken(type(uint256).max, index_); diff --git a/tests/forge/unit/ERC20Pool/ERC20PoolInputValidation.t.sol b/tests/forge/unit/ERC20Pool/ERC20PoolInputValidation.t.sol index c82b82725..fcb3e5632 100644 --- a/tests/forge/unit/ERC20Pool/ERC20PoolInputValidation.t.sol +++ b/tests/forge/unit/ERC20Pool/ERC20PoolInputValidation.t.sol @@ -25,16 +25,16 @@ contract ERC20PoolBorrowTest is ERC20HelperContract { function testValidateMoveQuoteTokenInput() external tearDown { // revert on zero amount vm.expectRevert(IPoolErrors.InvalidAmount.selector); - _pool.moveQuoteToken(0, 1, 2, block.timestamp + 1); + _pool.moveQuoteToken(0, 1, 2, block.timestamp + 1, false); // revert on move to same index vm.expectRevert(IPoolErrors.MoveToSameIndex.selector); - _pool.moveQuoteToken(1000, 1, 1, block.timestamp + 1); + _pool.moveQuoteToken(1000, 1, 1, block.timestamp + 1, false); // revert on to zero index vm.expectRevert(IPoolErrors.InvalidIndex.selector); - _pool.moveQuoteToken(1000, 1, 0, block.timestamp + 1); + _pool.moveQuoteToken(1000, 1, 0, block.timestamp + 1, false); // revert on to index greater than max index vm.expectRevert(IPoolErrors.InvalidIndex.selector); - _pool.moveQuoteToken(1000, 1, MAX_FENWICK_INDEX + 1, block.timestamp + 1); + _pool.moveQuoteToken(1000, 1, MAX_FENWICK_INDEX + 1, block.timestamp + 1, false); } function testValidateRemoveQuoteTokenInput() external tearDown { diff --git a/tests/forge/unit/ERC20Pool/ERC20PoolLiquidationsSettle.t.sol b/tests/forge/unit/ERC20Pool/ERC20PoolLiquidationsSettle.t.sol index 815bb2b5d..f607bb907 100644 --- a/tests/forge/unit/ERC20Pool/ERC20PoolLiquidationsSettle.t.sol +++ b/tests/forge/unit/ERC20Pool/ERC20PoolLiquidationsSettle.t.sol @@ -812,7 +812,7 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { skip(1 hours); // move quote token in a bankrupt bucket should set deposit time to time of bankruptcy + 1 to prevent losing deposit - _pool.moveQuoteToken(10 * 1e18, _i9_52, _i9_91, block.timestamp + 1 minutes); + _pool.moveQuoteToken(10 * 1e18, _i9_52, _i9_91, block.timestamp + 1 minutes, false); (, , uint256 bankruptcyTime, , ) = _pool.bucketInfo(_i9_91); _assertLenderLpBalance({ lender: _lender1, @@ -848,7 +848,7 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { depositTime: _startTime }); - _pool.moveQuoteToken(1_000 * 1e18, _i9_52, _i9_91, block.timestamp + 1 minutes); + _pool.moveQuoteToken(1_000 * 1e18, _i9_52, _i9_91, block.timestamp + 1 minutes, false); _assertLenderLpBalance({ lender: _lender, @@ -858,7 +858,7 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { }); _pool.addQuoteToken(1_000 * 1e18, _i9_52, block.timestamp + 1 minutes, false); - _pool.moveQuoteToken(1_000 * 1e18, _i9_52, _i9_91, block.timestamp + 1 minutes); + _pool.moveQuoteToken(1_000 * 1e18, _i9_52, _i9_91, block.timestamp + 1 minutes, false); _assertLenderLpBalance({ lender: _lender, @@ -876,7 +876,7 @@ contract ERC20PoolLiquidationsSettleTest is ERC20HelperContract { exchangeRate: 0.821534360361016385 * 1e18 }); - _pool.moveQuoteToken(10000000000 * 1e18, _i9_72, _i9_91, type(uint256).max); + _pool.moveQuoteToken(10000000000 * 1e18, _i9_72, _i9_91, type(uint256).max, false); _assertBucket({ index: _i9_72, diff --git a/tests/forge/unit/ERC20Pool/ERC20PoolQuoteToken.t.sol b/tests/forge/unit/ERC20Pool/ERC20PoolQuoteToken.t.sol index ff071c79a..6ecfb0a3b 100644 --- a/tests/forge/unit/ERC20Pool/ERC20PoolQuoteToken.t.sol +++ b/tests/forge/unit/ERC20Pool/ERC20PoolQuoteToken.t.sol @@ -1223,6 +1223,14 @@ contract ERC20PoolQuoteTokenTest is ERC20HelperContract { expiry: block.timestamp - 20 }); + // should revert if move below LUP with revertIfBelowLup set to true + _assertMoveDepositBelowLUPRevert({ + from: _lender, + amount: 10_000 * 1e18, + fromIndex: 4549, + toIndex: 5000 + }); + // should be charged unutilized deposit fee if moving below LUP _moveLiquidityWithPenalty({ from: _lender, diff --git a/tests/forge/unit/ERC721Pool/ERC721PoolInputValidation.t.sol b/tests/forge/unit/ERC721Pool/ERC721PoolInputValidation.t.sol index cfb268c40..c19b18895 100644 --- a/tests/forge/unit/ERC721Pool/ERC721PoolInputValidation.t.sol +++ b/tests/forge/unit/ERC721Pool/ERC721PoolInputValidation.t.sol @@ -30,16 +30,16 @@ contract ERC721PoolBorrowTest is ERC721HelperContract { function testValidateMoveQuoteTokenInput() external tearDown { // revert on zero amount vm.expectRevert(IPoolErrors.InvalidAmount.selector); - _pool.moveQuoteToken(0, 1, 2, block.timestamp + 1); + _pool.moveQuoteToken(0, 1, 2, block.timestamp + 1, false); // revert on move to same index vm.expectRevert(IPoolErrors.MoveToSameIndex.selector); - _pool.moveQuoteToken(1000, 1, 1, block.timestamp + 1); + _pool.moveQuoteToken(1000, 1, 1, block.timestamp + 1, false); // revert on to zero index vm.expectRevert(IPoolErrors.InvalidIndex.selector); - _pool.moveQuoteToken(1000, 1, 0, block.timestamp + 1); + _pool.moveQuoteToken(1000, 1, 0, block.timestamp + 1, false); // revert on to index greater than max index vm.expectRevert(IPoolErrors.InvalidIndex.selector); - _pool.moveQuoteToken(1000, 1, MAX_FENWICK_INDEX + 1, block.timestamp + 1); + _pool.moveQuoteToken(1000, 1, MAX_FENWICK_INDEX + 1, block.timestamp + 1, false); } function testValidateRemoveQuoteTokenInput() external tearDown { diff --git a/tests/forge/unit/Positions/CodearenaReports.t.sol b/tests/forge/unit/Positions/CodearenaReports.t.sol index df64e5948..6bfe61eea 100644 --- a/tests/forge/unit/Positions/CodearenaReports.t.sol +++ b/tests/forge/unit/Positions/CodearenaReports.t.sol @@ -188,11 +188,11 @@ contract PositionManagerCodeArenaTest is PositionManagerERC20PoolHelperContract changePrank(testMinter2); vm.expectRevert(IPoolErrors.BucketBankruptcyBlock.selector); - _positionManager.moveLiquidity(address(_pool), tokenId2, _i9_52, _i9_91, block.timestamp + 5 hours); + _positionManager.moveLiquidity(address(_pool), tokenId2, _i9_52, _i9_91, block.timestamp + 5 hours, false); // skip time to avoid move in same block as bucket bankruptcy skip(1 hours); - _positionManager.moveLiquidity(address(_pool), tokenId2, _i9_52, _i9_91, block.timestamp + 5 hours); + _positionManager.moveLiquidity(address(_pool), tokenId2, _i9_52, _i9_91, block.timestamp + 5 hours, false); // report 494: testMinter2 position at _i9_91 should not be bankrupt assertFalse(_positionManager.isPositionBucketBankrupt(tokenId2, _i9_91)); @@ -214,11 +214,11 @@ contract PositionManagerCodeArenaTest is PositionManagerERC20PoolHelperContract // testMinter1 moves liquidity from bankrupt _i9_91 deposit to healthy deposit _i9_52 // call reverts as cannot move from bankrupt bucket vm.expectRevert(IPositionManagerErrors.BucketBankrupt.selector); - _positionManager.moveLiquidity(address(_pool), tokenId, _i9_91, _i9_52, block.timestamp + 5 hours); + _positionManager.moveLiquidity(address(_pool), tokenId, _i9_91, _i9_52, block.timestamp + 5 hours, false); // testMinter1 moves liquidity from healthy deposit _i9_52 to bankrupt _i9_91 // _i9_52 should remain with 0 LP, _i9_91 should have 30_000 - _positionManager.moveLiquidity(address(_pool), tokenId, _i9_52, _i9_91, block.timestamp + 5 hours); + _positionManager.moveLiquidity(address(_pool), tokenId, _i9_52, _i9_91, block.timestamp + 5 hours, false); assertFalse(_positionManager.isPositionBucketBankrupt(tokenId, _i9_91)); assertFalse(_positionManager.isPositionBucketBankrupt(tokenId, _i9_52)); @@ -266,7 +266,7 @@ contract PositionManagerCodeArenaTest is PositionManagerERC20PoolHelperContract vm.expectEmit(true, true, true, true); emit MoveLiquidity(testAddress1, tokenId1, mintIndex, moveIndex, 2_500 * 1e18, 2_500 * 1e18); changePrank(address(testAddress1)); - _positionManager.moveLiquidity(address(_pool), tokenId1, mintIndex, moveIndex, block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId1, mintIndex, moveIndex, block.timestamp + 30, false); // check from and to positions after move // from position should have 0 LP and 0 deposit time (FROM Position struct is deleted) @@ -339,7 +339,7 @@ contract PositionManagerCodeArenaTest is PositionManagerERC20PoolHelperContract // but the amount of LP that can be moved (constrained by available max quote token) is only 200002500 changePrank(address(testAddress1)); vm.expectRevert(IPositionManagerErrors.RemovePositionFailed.selector); - _positionManager.moveLiquidity(address(_pool), tokenId1, mintIndex, moveIndex, block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId1, mintIndex, moveIndex, block.timestamp + 30, false); } /** @@ -735,14 +735,14 @@ contract PositionManagerCodeArenaTest is PositionManagerERC20PoolHelperContract // Move positiion upwards from _i9_81 to _i9_91 changePrank(testMinter); - _positionManager.moveLiquidity(address(_pool), tokenId, _i9_81, _i9_91, block.timestamp + 5 hours); + _positionManager.moveLiquidity(address(_pool), tokenId, _i9_81, _i9_91, block.timestamp + 5 hours, false); vm.revertTo(preMoveUpState); uint256 preMoveDownState = vm.snapshot(); // Move positiion downwards from _i9_91 to _i9_81 - _positionManager.moveLiquidity(address(_pool), tokenId, _i9_91, _i9_81, block.timestamp + 5 hours); + _positionManager.moveLiquidity(address(_pool), tokenId, _i9_91, _i9_81, block.timestamp + 5 hours, false); vm.revertTo(preMoveDownState); @@ -764,7 +764,7 @@ contract PositionManagerCodeArenaTest is PositionManagerERC20PoolHelperContract exchangeRate: 1e18 }); - _positionManager.moveLiquidity(address(_pool), tokenId, _i9_81, _i9_52, block.timestamp + 5 hours); + _positionManager.moveLiquidity(address(_pool), tokenId, _i9_81, _i9_52, block.timestamp + 5 hours, false); _assertBucketAssets({ index: _i9_81, diff --git a/tests/forge/unit/Positions/PositionManager.t.sol b/tests/forge/unit/Positions/PositionManager.t.sol index 652888669..d351c68b8 100644 --- a/tests/forge/unit/Positions/PositionManager.t.sol +++ b/tests/forge/unit/Positions/PositionManager.t.sol @@ -962,7 +962,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // move liquidity should fail as the bucket has bankrupted vm.expectRevert(IPositionManagerErrors.BucketBankrupt.selector); - _positionManager.moveLiquidity(address(_pool), tokenId, _i9_91, _i9_72, block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId, _i9_91, _i9_72, block.timestamp + 30, false); // check lender state after bankruptcy before rememorializing _assertLenderLpBalance({ @@ -1565,7 +1565,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // move liquidity should fail because is not performed by owner changePrank(notOwner); vm.expectRevert(IPositionManagerErrors.NoAuth.selector); - _positionManager.moveLiquidity(address(_pool), tokenId, 2550, 2551, block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId, 2550, 2551, block.timestamp + 30, false); } function testMoveLiquidity() external tearDown { @@ -1709,7 +1709,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract vm.expectEmit(true, true, true, true); emit MoveLiquidity(testAddress1, tokenId1, mintIndex, moveIndex, lpRedeemed, lpAwarded); changePrank(address(testAddress1)); - _positionManager.moveLiquidity(address(_pool), tokenId1, mintIndex, moveIndex, block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId1, mintIndex, moveIndex, block.timestamp + 30, false); // check pool state _assertLenderLpBalance({ @@ -1829,7 +1829,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract vm.expectEmit(true, true, true, true); emit MoveLiquidity(testAddress2, tokenId2, mintIndex, moveIndex, lpRedeemed, lpAwarded); changePrank(address(testAddress2)); - _positionManager.moveLiquidity(address(_pool), tokenId2, mintIndex, moveIndex, block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId2, mintIndex, moveIndex, block.timestamp + 30, false); // check pool state _assertLenderLpBalance({ @@ -1881,7 +1881,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract changePrank(address(testAddress2)); vm.expectRevert(IPositionManagerErrors.RemovePositionFailed.selector); - _positionManager.moveLiquidity(address(_pool), tokenId2, 1000, 2000, block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId2, 1000, 2000, block.timestamp + 30, false); } function testMoveLiquidityWithInterest() external tearDown { @@ -1960,7 +1960,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // lender 1 moves liquidity changePrank(lender1); - _positionManager.moveLiquidity(address(_pool), tokenId1, mintIndex, moveIndex, block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId1, mintIndex, moveIndex, block.timestamp + 30, false); // check pool state _assertLenderLpBalance({ @@ -2411,7 +2411,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // minter cannot move liquidity on behalf of lender (is not approved) vm.expectRevert(IPositionManagerErrors.NoAuth.selector); - _positionManager.moveLiquidity(address(_pool), tokenId, 2550, 2551, block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId, 2550, 2551, block.timestamp + 30, false); // minter cannot redeem positions on behalf of lender (is not approved) vm.expectRevert(IPositionManagerErrors.NoAuth.selector); @@ -2426,7 +2426,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract changePrank(minter); // minter can move liquidity on behalf of lender - _positionManager.moveLiquidity(address(_pool), tokenId, 2550, 2551, block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId, 2550, 2551, block.timestamp + 30, false); _assertLenderLpBalance({ lender: lender, @@ -2918,7 +2918,7 @@ contract PositionManagerERC20PoolTest is PositionManagerERC20PoolHelperContract // testMinter moves 8_936 QT _i9_72 to bankrupt _i9_91 deposit, should not have any pre bankruptcy LP changePrank(testMinter); - _positionManager.moveLiquidity(address(_pool), tokenId, _i9_72, testIndex, block.timestamp + 5 hours); + _positionManager.moveLiquidity(address(_pool), tokenId, _i9_72, testIndex, block.timestamp + 5 hours, false); _assertBucketAssets({ index: _i9_91, @@ -3273,7 +3273,7 @@ contract PositionManagerERC721PoolTest is PositionManagerERC721PoolHelperContrac vm.expectEmit(true, true, true, true); emit MoveLiquidity(testAddress1, tokenId, indexes[0], indexes[1], lpRedeemed, lpAwarded); changePrank(testAddress1); - _positionManager.moveLiquidity(address(_pool), tokenId, indexes[0], indexes[1], block.timestamp + 30); + _positionManager.moveLiquidity(address(_pool), tokenId, indexes[0], indexes[1], block.timestamp + 30, false); // check LP balance _assertLenderLpBalance({ diff --git a/tests/forge/utils/DSTestPlus.sol b/tests/forge/utils/DSTestPlus.sol index ed9edd177..653f0ddd7 100644 --- a/tests/forge/utils/DSTestPlus.sol +++ b/tests/forge/utils/DSTestPlus.sol @@ -297,7 +297,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { changePrank(from); vm.expectEmit(true, true, true, true); emit MoveQuoteToken(from, fromIndex, toIndex, amountMoved, lpRedeemFrom, lpAwardTo, newLup); - (uint256 lpbFrom, uint256 lpbTo, ) = _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); + (uint256 lpbFrom, uint256 lpbTo, ) = _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max, false); assertEq(lpbFrom, lpRedeemFrom); assertEq(lpbTo, lpAwardTo); @@ -1235,7 +1235,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(abi.encodeWithSignature('BucketBankruptcyBlock()')); - _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max, false); } function _assertMoveLiquidityLupBelowHtpRevert( @@ -1246,7 +1246,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.LUPBelowHTP.selector); - _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max, false); } function _assertMoveLiquidityExpiredRevert( @@ -1258,7 +1258,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.TransactionExpired.selector); - _pool.moveQuoteToken(amount, fromIndex, toIndex, expiry); + _pool.moveQuoteToken(amount, fromIndex, toIndex, expiry, false); } function _assertMoveLiquidityDustRevert( @@ -1269,7 +1269,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.DustAmountNotExceeded.selector); - _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max, false); } function _assertMoveLiquidityToSameIndexRevert( @@ -1280,7 +1280,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.MoveToSameIndex.selector); - _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max, false); } function _assertMoveLiquidityToIndex0Revert( @@ -1290,7 +1290,7 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.InvalidIndex.selector); - _pool.moveQuoteToken(amount, fromIndex, 0, type(uint256).max); + _pool.moveQuoteToken(amount, fromIndex, 0, type(uint256).max, false); } function _assertMoveDepositLockedByAuctionDebtRevert( @@ -1301,7 +1301,18 @@ abstract contract DSTestPlus is Test, IPoolEvents { ) internal { changePrank(from); vm.expectRevert(IPoolErrors.RemoveDepositLockedByAuctionDebt.selector); - _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max, false); + } + + function _assertMoveDepositBelowLUPRevert( + address from, + uint256 amount, + uint256 fromIndex, + uint256 toIndex + ) internal { + changePrank(from); + vm.expectRevert(IPoolErrors.PriceBelowLUP.selector); + _pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max, true); } function _assertTakeAuctionInCooldownRevert(