From 25f8d02f30e75eba9e54b37d5e916ffe827add77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Mon, 16 Sep 2024 21:30:46 +0100 Subject: [PATCH] fix: Use last zombie trove first in a redemption sequence Closes #425 --- contracts/src/BorrowerOperations.sol | 28 ++-- .../src/Interfaces/IBorrowerOperations.sol | 2 +- contracts/src/Interfaces/ITroveManager.sol | 6 +- contracts/src/NFTMetadata/MetadataNFT.sol | 2 +- contracts/src/TroveManager.sol | 49 ++++-- contracts/src/Zappers/GasCompZapper.sol | 4 +- contracts/src/Zappers/WETHZapper.sol | 4 +- contracts/src/test/HintHelpers.t.sol | 2 +- contracts/src/test/Invariants.t.sol | 6 +- contracts/src/test/TestContracts/BaseTest.sol | 4 +- .../src/test/TestContracts/DevTestSetup.sol | 24 ++- .../Interfaces/ITroveManagerTester.sol | 2 +- .../TestContracts/InvariantsTestHandler.t.sol | 152 ++++++++++-------- .../TestContracts/TroveManagerTester.t.sol | 6 +- .../src/test/interestBatchManagement.t.sol | 10 +- .../test/interestIndividualDelegation.t.sol | 8 +- contracts/src/test/redemptions.t.sol | 92 +++++++---- contracts/src/test/shutdown.t.sol | 10 +- contracts/src/test/zapperGasComp.t.sol | 6 +- contracts/src/test/zapperWETH.t.sol | 6 +- 20 files changed, 261 insertions(+), 162 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 3e8e6574..322b6f2a 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -128,7 +128,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio error BatchInterestRateChangePeriodNotPassed(); error TroveNotOpen(); error TroveNotActive(); - error TroveNotUnredeemable(); + error TroveNotZombie(); error TroveOpen(); error UpfrontFeeTooHigh(); error BelowCriticalThreshold(); @@ -470,7 +470,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _adjustTrove(troveManagerCached, _troveId, troveChange, _maxUpfrontFee); } - function adjustUnredeemableTrove( + function adjustZombieTrove( uint256 _troveId, uint256 _collChange, bool _isCollIncrease, @@ -481,7 +481,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 _maxUpfrontFee ) external override { ITroveManager troveManagerCached = troveManager; - _requireTroveIsUnredeemable(troveManagerCached, _troveId); + _requireTroveIsZombie(troveManagerCached, _troveId); TroveChange memory troveChange; _initTroveChange(troveChange, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease); @@ -650,8 +650,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - // Make sure the Trove doesn't end up unredeemable - // Now the max repayment is capped to stay above MIN_DEBT, so this only applies to adjustUnredeemableTrove + // Make sure the Trove doesn't end up zombie + // Now the max repayment is capped to stay above MIN_DEBT, so this only applies to adjustZombieTrove _requireAtLeastMinDebt(vars.newDebt); vars.newICR = LiquityMath._computeCR(vars.newColl, vars.newDebt, vars.price); @@ -787,8 +787,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ); activePool.mintAggInterestAndAccountForTroveChange(change, batchManager); - // If the trove was unredeemable, and now it’s not anymore, put it back in the list - if (_checkTroveIsUnredeemable(troveManagerCached, _troveId) && trove.entireDebt >= MIN_DEBT) { + // If the trove was zombie, and now it’s not anymore, put it back in the list + if (_checkTroveIsZombie(troveManagerCached, _troveId) && trove.entireDebt >= MIN_DEBT) { troveManagerCached.setTroveStatusToActive(_troveId); _reInsertIntoSortedTroves( _troveId, trove.annualInterestRate, _upperHint, _lowerHint, batchManager, batch.annualInterestRate @@ -1304,7 +1304,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio function _requireTroveIsOpen(ITroveManager _troveManager, uint256 _troveId) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); - if (status != ITroveManager.Status.active && status != ITroveManager.Status.unredeemable) { + if (status != ITroveManager.Status.active && status != ITroveManager.Status.zombie) { revert TroveNotOpen(); } } @@ -1316,20 +1316,20 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireTroveIsUnredeemable(ITroveManager _troveManager, uint256 _troveId) internal view { - if (!_checkTroveIsUnredeemable(_troveManager, _troveId)) { - revert TroveNotUnredeemable(); + function _requireTroveIsZombie(ITroveManager _troveManager, uint256 _troveId) internal view { + if (!_checkTroveIsZombie(_troveManager, _troveId)) { + revert TroveNotZombie(); } } - function _checkTroveIsUnredeemable(ITroveManager _troveManager, uint256 _troveId) internal view returns (bool) { + function _checkTroveIsZombie(ITroveManager _troveManager, uint256 _troveId) internal view returns (bool) { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); - return status == ITroveManager.Status.unredeemable; + return status == ITroveManager.Status.zombie; } function _requireTroveIsNotOpen(ITroveManager _troveManager, uint256 _troveId) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); - if (status == ITroveManager.Status.active || status == ITroveManager.Status.unredeemable) { + if (status == ITroveManager.Status.active || status == ITroveManager.Status.zombie) { revert TroveOpen(); } } diff --git a/contracts/src/Interfaces/IBorrowerOperations.sol b/contracts/src/Interfaces/IBorrowerOperations.sol index d4eb9c61..e23a0820 100644 --- a/contracts/src/Interfaces/IBorrowerOperations.sol +++ b/contracts/src/Interfaces/IBorrowerOperations.sol @@ -67,7 +67,7 @@ interface IBorrowerOperations is ILiquityBase, IAddRemoveManagers { uint256 _maxUpfrontFee ) external; - function adjustUnredeemableTrove( + function adjustZombieTrove( uint256 _troveId, uint256 _collChange, bool _isCollIncrease, diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 6dc5ff6f..9db8cc66 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -18,7 +18,7 @@ interface ITroveManager is ILiquityBase { active, closedByOwner, closedByLiquidation, - unredeemable + zombie } function shutdownTime() external view returns (uint256); @@ -53,6 +53,8 @@ interface ITroveManager is ILiquityBase { function getCurrentICR(uint256 _troveId, uint256 _price) external view returns (uint256); + function lastZombieTroveId() external view returns (uint256); + function batchLiquidateTroves(uint256[] calldata _troveArray) external; function redeemCollateral( @@ -88,7 +90,7 @@ interface ITroveManager is ILiquityBase { uint256 _batchDebt ) external; - // Called from `adjustUnredeemableTrove()` + // Called from `adjustZombieTrove()` function setTroveStatusToActive(uint256 _troveId) external; function onAdjustTroveInterestRate( diff --git a/contracts/src/NFTMetadata/MetadataNFT.sol b/contracts/src/NFTMetadata/MetadataNFT.sol index 80714a4a..8a05146a 100644 --- a/contracts/src/NFTMetadata/MetadataNFT.sol +++ b/contracts/src/NFTMetadata/MetadataNFT.sol @@ -62,7 +62,7 @@ contract MetadataNFT is IMetadataNFT { if (status == ITroveManager.Status.active) return "Active"; if (status == ITroveManager.Status.closedByOwner) return "Closed"; if (status == ITroveManager.Status.closedByLiquidation) return "Liquidated"; - if (status == ITroveManager.Status.unredeemable) return "Unredeemable"; + if (status == ITroveManager.Status.zombie) return "Zombie"; return ""; } } diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 668116e3..44e58f97 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -111,6 +111,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // Array of all batch managers - used to fetch them off-chain address[] public batchIds; + uint256 public lastZombieTroveId; + // Error trackers for the trove redistribution calculation uint256 internal lastCollError_Redistribution; uint256 internal lastBoldDebtError_Redistribution; @@ -149,6 +151,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 oldWeightedRecordedDebt; uint256 newWeightedRecordedDebt; uint256 newStake; + bool isZombieTrove; LatestTroveData trove; LatestBatchData batch; } @@ -452,7 +455,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } function _isLiquidatableStatus(Status _status) internal pure returns (bool) { - return _status == Status.active || _status == Status.unredeemable; + return _status == Status.active || _status == Status.zombie; } function _batchLiquidateTroves( @@ -663,14 +666,23 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { bool isTroveInBatch = _singleRedemption.batchAddress != address(0); uint256 newDebt = _applySingleRedemption(_defaultPool, _singleRedemption, isTroveInBatch); - // Make Trove unredeemable if it's tiny, in order to prevent griefing future (normal, sequential) redemptions + // Make Trove zombie if it's tiny (and it wasn’t already), in order to prevent griefing future (normal, sequential) redemptions if (newDebt < MIN_DEBT) { - Troves[_singleRedemption.troveId].status = Status.unredeemable; - if (isTroveInBatch) { - sortedTroves.removeFromBatch(_singleRedemption.troveId); - } else { - sortedTroves.remove(_singleRedemption.troveId); + if (!_singleRedemption.isZombieTrove) { + Troves[_singleRedemption.troveId].status = Status.zombie; + if (isTroveInBatch) { + sortedTroves.removeFromBatch(_singleRedemption.troveId); + } else { + sortedTroves.remove(_singleRedemption.troveId); + } + // If it’s a partial redemption, let’s store a pointer to it so it’s used first in the next one + if (newDebt > 0) { + lastZombieTroveId = _singleRedemption.troveId; + } } + } else { + // Reset last zombie trove pointer if the previous one was fully redeemed now + lastZombieTroveId = 0; } } @@ -730,7 +742,13 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 remainingBold = _boldamount; SingleRedemptionValues memory singleRedemption; - singleRedemption.troveId = sortedTrovesCached.getLast(); + // Let’s check if there’s a pending zombie trove from previous redemption + if (lastZombieTroveId != 0) { + singleRedemption.troveId = lastZombieTroveId; + singleRedemption.isZombieTrove = true; + } else { + singleRedemption.troveId = sortedTrovesCached.getLast(); + } address lastBatchUpdatedInterest = address(0); // Loop through the Troves starting from the one with lowest collateral ratio until _amount of Bold is exchanged for collateral @@ -738,7 +756,13 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { while (singleRedemption.troveId != 0 && remainingBold > 0 && _maxIterations > 0) { _maxIterations--; // Save the uint256 of the Trove preceding the current one - uint256 nextUserToCheck = sortedTrovesCached.getPrev(singleRedemption.troveId); + uint256 nextUserToCheck; + if (singleRedemption.isZombieTrove) { + nextUserToCheck = sortedTrovesCached.getLast(); + } else { + nextUserToCheck = sortedTrovesCached.getPrev(singleRedemption.troveId); + } + // Skip if ICR < 100%, to make sure that redemptions always improve the CR of hit Troves if (getCurrentICR(singleRedemption.troveId, _price) < _100pct) { singleRedemption.troveId = nextUserToCheck; @@ -769,6 +793,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { remainingBold -= singleRedemption.boldLot; singleRedemption.troveId = nextUserToCheck; + singleRedemption.isZombieTrove = false; } // We are removing this condition to prevent blocking redemptions @@ -811,7 +836,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { bool isTroveInBatch = _singleRedemption.batchAddress != address(0); _applySingleRedemption(_defaultPool, _singleRedemption, isTroveInBatch); - // No need to make this Trove unredeemable if it has tiny debt, since: + // No need to make this Trove zombie if it has tiny debt, since: // - This collateral branch has shut down and urgent redemptions are enabled // - Urgent redemptions aren't sequential, so they can't be griefed by tiny Troves. } @@ -1436,6 +1461,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { if (_batchAddress != address(0)) { if (trove.status == Status.active) { sortedTroves.removeFromBatch(_troveId); + } else if (trove.status == Status.zombie && lastZombieTroveId == _troveId) { + lastZombieTroveId = 0; } _removeTroveSharesFromBatch( @@ -1450,6 +1477,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } else { if (trove.status == Status.active) { sortedTroves.remove(_troveId); + } else if (trove.status == Status.zombie && lastZombieTroveId == _troveId) { + lastZombieTroveId = 0; } } diff --git a/contracts/src/Zappers/GasCompZapper.sol b/contracts/src/Zappers/GasCompZapper.sol index 19d11794..801892d4 100644 --- a/contracts/src/Zappers/GasCompZapper.sol +++ b/contracts/src/Zappers/GasCompZapper.sol @@ -155,7 +155,7 @@ contract GasCompZapper is AddRemoveManagers { _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver); } - function adjustUnredeemableTroveWithRawETH( + function adjustZombieTroveWithRawETH( uint256 _troveId, uint256 _collChange, bool _isCollIncrease, @@ -166,7 +166,7 @@ contract GasCompZapper is AddRemoveManagers { uint256 _maxUpfrontFee ) external { address receiver = _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease); - borrowerOperations.adjustUnredeemableTrove( + borrowerOperations.adjustZombieTrove( _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _upperHint, _lowerHint, _maxUpfrontFee ); _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver); diff --git a/contracts/src/Zappers/WETHZapper.sol b/contracts/src/Zappers/WETHZapper.sol index eca59bc5..a11e58df 100644 --- a/contracts/src/Zappers/WETHZapper.sol +++ b/contracts/src/Zappers/WETHZapper.sol @@ -143,7 +143,7 @@ contract WETHZapper is AddRemoveManagers { _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver); } - function adjustUnredeemableTroveWithRawETH( + function adjustZombieTroveWithRawETH( uint256 _troveId, uint256 _collChange, bool _isCollIncrease, @@ -154,7 +154,7 @@ contract WETHZapper is AddRemoveManagers { uint256 _maxUpfrontFee ) external { address payable receiver = _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease); - borrowerOperations.adjustUnredeemableTrove( + borrowerOperations.adjustZombieTrove( _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _upperHint, _lowerHint, _maxUpfrontFee ); _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver); diff --git a/contracts/src/test/HintHelpers.t.sol b/contracts/src/test/HintHelpers.t.sol index 2d0465b9..1eb02cba 100644 --- a/contracts/src/test/HintHelpers.t.sol +++ b/contracts/src/test/HintHelpers.t.sol @@ -16,7 +16,7 @@ contract HintHelpersTest is DevTestSetup { assertEq( uint8(troveManager.getTroveStatus(redeemedTroveId)), - uint8(ITroveManager.Status.unredeemable), + uint8(ITroveManager.Status.zombie), "Redeemed Trove should have become a zombie" ); diff --git a/contracts/src/test/Invariants.t.sol b/contracts/src/test/Invariants.t.sol index d861a11c..8b5f2ede 100644 --- a/contracts/src/test/Invariants.t.sol +++ b/contracts/src/test/Invariants.t.sol @@ -38,7 +38,7 @@ library ToStringFunctions { if (status == ITroveManager.Status.active) return "ITroveManager.Status.active"; if (status == ITroveManager.Status.closedByOwner) return "ITroveManager.Status.closedByOwner"; if (status == ITroveManager.Status.closedByLiquidation) return "ITroveManager.Status.closedByLiquidation"; - if (status == ITroveManager.Status.unredeemable) return "ITroveManager.Status.unredeemable"; + if (status == ITroveManager.Status.zombie) return "ITroveManager.Status.zombie"; revert("Invalid status"); } } @@ -186,14 +186,14 @@ contract InvariantsTest is Logging, BaseInvariantTest, BaseMultiCollateralTest { ITroveManager.Status status = c.troveManager.getTroveStatus(troveId); assertTrue( - status == ITroveManager.Status.active || status == ITroveManager.Status.unredeemable, + status == ITroveManager.Status.active || status == ITroveManager.Status.zombie, "Unexpected status" ); if (status == ITroveManager.Status.active) { assertTrue(c.sortedTroves.contains(troveId), "SortedTroves should contain active Troves"); } else { - assertFalse(c.sortedTroves.contains(troveId), "SortedTroves shouldn't contain unredeemable Troves"); + assertFalse(c.sortedTroves.contains(troveId), "SortedTroves shouldn't contain zombie Troves"); } } } diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index d5271a7c..2cbded88 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -302,7 +302,7 @@ contract BaseTest is TestAccounts, Logging { vm.stopPrank(); } - function adjustUnredeemableTrove( + function adjustZombieTrove( address _account, uint256 _troveId, uint256 _collChange, @@ -312,7 +312,7 @@ contract BaseTest is TestAccounts, Logging { ) public { vm.startPrank(_account); - borrowerOperations.adjustUnredeemableTrove( + borrowerOperations.adjustZombieTrove( _troveId, _collChange, _isCollIncrease, diff --git a/contracts/src/test/TestContracts/DevTestSetup.sol b/contracts/src/test/TestContracts/DevTestSetup.sol index 9ce9d087..4ed74043 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -258,8 +258,26 @@ contract DevTestSetup is BaseTest { assertLt(troveManager.getTroveEntireDebt(_troveIDs.B), MIN_DEBT); // Check A and B tagged as Zombie troves - assertEq(uint8(troveManager.getTroveStatus(_troveIDs.A)), uint8(ITroveManager.Status.unredeemable)); - assertEq(uint8(troveManager.getTroveStatus(_troveIDs.A)), uint8(ITroveManager.Status.unredeemable)); + assertEq(uint8(troveManager.getTroveStatus(_troveIDs.A)), uint8(ITroveManager.Status.zombie)); + assertEq(uint8(troveManager.getTroveStatus(_troveIDs.A)), uint8(ITroveManager.Status.zombie)); + } + + function _redeemAndCreateEmptyZombieTrovesAAndB(ABCDEF memory _troveIDs) internal { + // Redeem enough to leave A with 0 debt and B with debt < MIN_DEBT + uint256 redeemFromA = troveManager.getTroveEntireDebt(_troveIDs.A); + uint256 redeemFromB = troveManager.getTroveEntireDebt(_troveIDs.B); + uint256 redeemAmount = redeemFromA + redeemFromB; + + // Fully redeem A and B + redeem(E, redeemAmount); + + // Check A, B has debt == 0 + assertEq(troveManager.getTroveEntireDebt(_troveIDs.A), 0); + assertEq(troveManager.getTroveEntireDebt(_troveIDs.B), 0); + + // Check A and B tagged as Zombie troves + assertEq(uint8(troveManager.getTroveStatus(_troveIDs.A)), uint8(ITroveManager.Status.zombie)); + assertEq(uint8(troveManager.getTroveStatus(_troveIDs.A)), uint8(ITroveManager.Status.zombie)); } function _redeemAndCreateZombieTroveAAndHitB(ABCDEF memory _troveIDs) internal { @@ -276,7 +294,7 @@ contract DevTestSetup is BaseTest { assertGt(troveManager.getTroveEntireDebt(_troveIDs.B), MIN_DEBT); // // Check A is zombie Trove but B is not - assertEq(uint8(troveManager.getTroveStatus(_troveIDs.A)), uint8(ITroveManager.Status.unredeemable)); + assertEq(uint8(troveManager.getTroveStatus(_troveIDs.A)), uint8(ITroveManager.Status.zombie)); assertEq(uint8(troveManager.getTroveStatus(_troveIDs.B)), uint8(ITroveManager.Status.active)); } diff --git a/contracts/src/test/TestContracts/Interfaces/ITroveManagerTester.sol b/contracts/src/test/TestContracts/Interfaces/ITroveManagerTester.sol index 6ee22a4a..4d234d4c 100644 --- a/contracts/src/test/TestContracts/Interfaces/ITroveManagerTester.sol +++ b/contracts/src/test/TestContracts/Interfaces/ITroveManagerTester.sol @@ -40,7 +40,7 @@ interface ITroveManagerTester is ITroveManager { // Trove and batch getters function checkTroveIsActive(uint256 _troveId) external view returns (bool); function checkTroveIsOpen(uint256 _troveId) external view returns (bool); - function checkTroveIsUnredeemable(uint256 _troveId) external view returns (bool); + function checkTroveIsZombie(uint256 _troveId) external view returns (bool); function hasRedistributionGains(uint256 _troveId) external view returns (bool); diff --git a/contracts/src/test/TestContracts/InvariantsTestHandler.t.sol b/contracts/src/test/TestContracts/InvariantsTestHandler.t.sol index 617a0bc1..14597f22 100644 --- a/contracts/src/test/TestContracts/InvariantsTestHandler.t.sol +++ b/contracts/src/test/TestContracts/InvariantsTestHandler.t.sol @@ -143,8 +143,8 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { uint256 batchManagementFee; Trove trove; bool wasActive; - bool wasUnredeemable; - bool useUnredeemable; + bool wasZombie; + bool useZombie; uint256 maxDebtDec; int256 collDelta; int256 debtDelta; @@ -272,6 +272,20 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { string errorString; } + struct RedemptionContext { + uint256 totalProportions; + uint256[] proportions; + uint256 remainingAmount; + uint256 troveId; + uint256 lastZombieTroveId; + uint256 j; + uint256 i; + uint256 debtRedeemed; + uint256 collRedeemedPlusFee; + uint256 fee; + uint256 collRedeemed; + } + struct LiquidationTotals { uint256 collGasComp; uint256 spCollGain; @@ -328,7 +342,7 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { ITroveManager.Status constant ACTIVE = ITroveManager.Status.active; ITroveManager.Status constant CLOSED_BY_OWNER = ITroveManager.Status.closedByOwner; ITroveManager.Status constant CLOSED_BY_LIQ = ITroveManager.Status.closedByLiquidation; - ITroveManager.Status constant UNREDEEMABLE = ITroveManager.Status.unredeemable; + ITroveManager.Status constant UNREDEEMABLE = ITroveManager.Status.zombie; FunctionCaller immutable _functionCaller; bool immutable _assumeNoExpectedFailures; // vm.assume() away calls that fail extectedly @@ -421,7 +435,7 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { coll = trove.coll; debt = trove.debt; - status = _isUnredeemable(i, troveId) ? UNREDEEMABLE : ACTIVE; + status = _isZombie(i, troveId) ? UNREDEEMABLE : ACTIVE; batchManager = _batchManagerOf[i][troveId]; } @@ -694,7 +708,7 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { bool isCollInc, uint256 debtChange, bool isDebtInc, - uint32 useUnredeemableSeed, + uint32 useZombieSeed, uint32 upperHintSeed, uint32 lowerHintSeed ) external { @@ -702,7 +716,7 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { i = _bound(i, 0, branches.length - 1); v.prop = AdjustedTroveProperties(_bound(prop, 0, uint8(AdjustedTroveProperties._COUNT) - 1)); - useUnredeemableSeed %= 100; + useZombieSeed %= 100; v.upperHint = _pickHint(i, upperHintSeed); v.lowerHint = _pickHint(i, lowerHintSeed); @@ -715,18 +729,18 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { v.batchManagementFee = v.c.troveManager.getLatestBatchData(v.batchManager).accruedManagementFee; v.trove = _troves[i][v.troveId]; v.wasActive = _isActive(i, v.troveId); - v.wasUnredeemable = _isUnredeemable(i, v.troveId); + v.wasZombie = _isZombie(i, v.troveId); - if (v.wasActive || v.wasUnredeemable) { + if (v.wasActive || v.wasZombie) { // Choose the wrong type of adjustment 1% of the time - if (v.wasUnredeemable) { - v.useUnredeemable = useUnredeemableSeed != 0; + if (v.wasZombie) { + v.useZombie = useZombieSeed != 0; } else { - v.useUnredeemable = useUnredeemableSeed == 0; + v.useZombie = useZombieSeed == 0; } } else { - // Choose with equal probability between normal vs. unredeemable adjustment - v.useUnredeemable = useUnredeemableSeed < 50; + // Choose with equal probability between normal vs. zombie adjustment + v.useZombie = useZombieSeed < 50; } collChange = v.prop != AdjustedTroveProperties.onlyDebt ? _bound(collChange, 0, v.t.entireColl + 1) : 0; @@ -738,7 +752,7 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { v.$collDelta = v.collDelta * int256(_price[i]) / int256(DECIMAL_PRECISION); v.upfrontFee = hintHelpers.predictAdjustTroveUpfrontFee(i, v.troveId, isDebtInc ? debtChange : 0); if (v.upfrontFee > 0) assertGtDecimal(v.debtDelta, 0, 18, "Only debt increase should incur upfront fee"); - v.functionName = _getAdjustmentFunctionName(v.prop, isCollInc, isDebtInc, v.useUnredeemable); + v.functionName = _getAdjustmentFunctionName(v.prop, isCollInc, isDebtInc, v.useZombie); info("upper hint: ", _hintToString(i, v.upperHint)); info("lower hint: ", _hintToString(i, v.lowerHint)); @@ -753,7 +767,7 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { isCollInc.toString(), debtChange.decimal(), isDebtInc.toString(), - useUnredeemableSeed.toString(), + useZombieSeed.toString(), upperHintSeed.toString(), lowerHintSeed.toString() ); @@ -765,8 +779,8 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { vm.prank(msg.sender); try _functionCaller.call( address(v.c.borrowerOperations), - v.useUnredeemable - ? _encodeUnredeemableTroveAdjustment( + v.useZombie + ? _encodeZombieTroveAdjustment( v.troveId, collChange, isCollInc, debtChange, isDebtInc, v.upperHint, v.lowerHint, v.upfrontFee ) : _encodeActiveTroveAdjustment( @@ -779,8 +793,8 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { // Preconditions assertFalse(isShutdown[i], "Should have failed as branch had been shut down"); assertFalse(v.collDelta == 0 && v.debtDelta == 0, "Should have failed as there was no change"); - if (v.useUnredeemable) assertTrue(v.wasUnredeemable, "Should have failed as Trove wasn't unredeemable"); - if (!v.useUnredeemable) assertTrue(v.wasActive, "Should have failed as Trove wasn't active"); + if (v.useZombie) assertTrue(v.wasZombie, "Should have failed as Trove wasn't zombie"); + if (!v.useZombie) assertTrue(v.wasActive, "Should have failed as Trove wasn't active"); assertLeDecimal(-v.collDelta, int256(v.t.entireColl), 18, "Should have failed as withdrawal > coll"); assertLeDecimal(-v.debtDelta, int256(v.t.entireDebt), 18, "Should have failed as repayment > debt"); v.newDebt = v.t.entireDebt.add(v.debtDelta) + v.upfrontFee; @@ -817,11 +831,11 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { assertEqDecimal(v.collDelta, 0, 18, "Shouldn't have failed as there was a coll change"); assertEqDecimal(v.debtDelta, 0, 18, "Shouldn't have failed as there was a debt change"); } else if (selector == BorrowerOperations.TroveNotActive.selector) { - assertFalse(v.useUnredeemable, string.concat("Shouldn't have been thrown by ", v.functionName)); + assertFalse(v.useZombie, string.concat("Shouldn't have been thrown by ", v.functionName)); assertFalse(v.wasActive, "Shouldn't have failed as Trove was active"); - } else if (selector == BorrowerOperations.TroveNotUnredeemable.selector) { - assertTrue(v.useUnredeemable, string.concat("Shouldn't have been thrown by ", v.functionName)); - assertFalse(v.wasUnredeemable, "Shouldn't have failed as Trove was unredeemable"); + } else if (selector == BorrowerOperations.TroveNotZombie.selector) { + assertTrue(v.useZombie, string.concat("Shouldn't have been thrown by ", v.functionName)); + assertFalse(v.wasZombie, "Shouldn't have failed as Trove was zombie"); } else if (selector == BorrowerOperations.CollWithdrawalTooHigh.selector) { assertGtDecimal(-v.collDelta, int256(v.t.entireColl), 18, "Shouldn't have failed as withdrawal <= coll"); } else if (selector == BorrowerOperations.DebtBelowMin.selector) { @@ -1275,7 +1289,8 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { _troves[j][redeemed.troveId] = trove; - if (branches[j].troveManager.getTroveEntireDebt(redeemed.troveId) < MIN_DEBT) { + uint256 troveDebt = branches[j].troveManager.getTroveEntireDebt(redeemed.troveId); + if (troveDebt < MIN_DEBT) { _zombieTroveIds[j].add(redeemed.troveId); } } @@ -2423,12 +2438,12 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { return _troveIds[i].has(troveId); } - function _isUnredeemable(uint256 i, uint256 troveId) internal view returns (bool) { + function _isZombie(uint256 i, uint256 troveId) internal view returns (bool) { return _zombieTroveIds[i].has(troveId); } function _isActive(uint256 i, uint256 troveId) internal view returns (bool) { - return _isOpen(i, troveId) && !_isUnredeemable(i, troveId); + return _isOpen(i, troveId) && !_isZombie(i, troveId); } function _pickHint(uint256 i, uint256 seed) internal view returns (uint256) { @@ -2636,56 +2651,63 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { internal returns (uint256 totalDebtRedeemed, mapping(uint256 branchIdx => RedemptionTransientState) storage r) { - uint256 totalProportions = 0; - uint256[] memory proportions = new uint256[](branches.length); + RedemptionContext memory vars; + vars.totalProportions = 0; + vars.proportions = new uint256[](branches.length); r = _redemption; // Try in proportion to unbacked - for (uint256 j = 0; j < branches.length; ++j) { - if (isShutdown[j] || _TCR(j) < SCR[j]) continue; - totalProportions += proportions[j] = _getUnbacked(j); + for (vars.j = 0; vars.j < branches.length; ++vars.j) { + if (isShutdown[vars.j] || _TCR(vars.j) < SCR[vars.j]) continue; + vars.totalProportions += vars.proportions[vars.j] = _getUnbacked(vars.j); } // Fallback: in proportion to branch debt - if (totalProportions == 0) { - for (uint256 j = 0; j < branches.length; ++j) { - if (isShutdown[j] || _TCR(j) < SCR[j]) continue; - totalProportions += proportions[j] = _getTotalDebt(j); + if (vars.totalProportions == 0) { + for (vars.j = 0; vars.j < branches.length; ++vars.j) { + if (isShutdown[vars.j] || _TCR(vars.j) < SCR[vars.j]) continue; + vars.totalProportions += vars.proportions[vars.j] = _getTotalDebt(vars.j); } } - if (totalProportions == 0) return (0, r); + if (vars.totalProportions == 0) return (0, r); - for (uint256 j = 0; j < branches.length; ++j) { - r[j].attemptedAmount = amount * proportions[j] / totalProportions; - if (r[j].attemptedAmount == 0) continue; + for (vars.j = 0; vars.j < branches.length; ++vars.j) { + r[vars.j].attemptedAmount = amount * vars.proportions[vars.j] / vars.totalProportions; + if (r[vars.j].attemptedAmount == 0) continue; - TestDeployer.LiquityContractsDev memory c = branches[j]; - uint256 remainingAmount = r[j].attemptedAmount; - uint256 troveId = 0; // "root node" ID + TestDeployer.LiquityContractsDev memory c = branches[vars.j]; + vars.remainingAmount = r[vars.j].attemptedAmount; + vars.troveId = 0; // "root node" ID + vars.lastZombieTroveId = c.troveManager.lastZombieTroveId(); - for (uint256 i = 0; i < maxIterationsPerCollateral || maxIterationsPerCollateral == 0; ++i) { - if (remainingAmount == 0) break; + for (vars.i = 0; vars.i < maxIterationsPerCollateral || maxIterationsPerCollateral == 0; ++vars.i) { + if (vars.remainingAmount == 0) break; - troveId = c.sortedTroves.getPrev(troveId); - if (troveId == 0) break; + vars.troveId = vars.lastZombieTroveId != 0 ? vars.lastZombieTroveId : c.sortedTroves.getPrev(vars.troveId); + if (vars.troveId == 0) break; - LatestTroveData memory trove = c.troveManager.getLatestTroveData(troveId); - if (_ICR(j, trove) < _100pct) continue; + LatestTroveData memory trove = c.troveManager.getLatestTroveData(vars.troveId); + if (_ICR(vars.j, trove) >= _100pct) { + vars.debtRedeemed = Math.min(vars.remainingAmount, trove.entireDebt); + vars.collRedeemedPlusFee = vars.debtRedeemed * DECIMAL_PRECISION / _price[vars.j]; + vars.fee = vars.collRedeemedPlusFee * feePct / _100pct; + vars.collRedeemed = vars.collRedeemedPlusFee - vars.fee; - uint256 debtRedeemed = Math.min(remainingAmount, trove.entireDebt); - uint256 collRedeemedPlusFee = debtRedeemed * DECIMAL_PRECISION / _price[j]; - uint256 fee = collRedeemedPlusFee * feePct / _100pct; - uint256 collRedeemed = collRedeemedPlusFee - fee; + r[vars.j].redeemed.push(Redeemed({troveId: vars.troveId, coll: vars.collRedeemed, debt: vars.debtRedeemed})); - r[j].redeemed.push(Redeemed({troveId: troveId, coll: collRedeemed, debt: debtRedeemed})); + address batchManager = _batchManagerOf[vars.j][vars.troveId]; + if (batchManager != address(0)) r[vars.j].batchManagers.add(batchManager); - address batchManager = _batchManagerOf[j][troveId]; - if (batchManager != address(0)) r[j].batchManagers.add(batchManager); + r[vars.j].totalCollRedeemed += vars.collRedeemed; + totalDebtRedeemed += vars.debtRedeemed; + vars.remainingAmount -= vars.debtRedeemed; + } - r[j].totalCollRedeemed += collRedeemed; - totalDebtRedeemed += debtRedeemed; - remainingAmount -= debtRedeemed; + if (vars.lastZombieTroveId != 0) { + vars.lastZombieTroveId = 0; + vars.troveId = 0; + } } } } @@ -2783,10 +2805,10 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { AdjustedTroveProperties prop, bool isCollIncrease, bool isDebtIncrease, - bool unredeemable + bool zombie ) internal pure returns (string memory) { - if (unredeemable) { - return "adjustUnredeemableTrove()"; + if (zombie) { + return "adjustZombieTrove()"; } if (prop == AdjustedTroveProperties.onlyColl) { @@ -2847,7 +2869,7 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { revert("Invalid prop"); } - function _encodeUnredeemableTroveAdjustment( + function _encodeZombieTroveAdjustment( uint256 troveId, uint256 collChange, bool isCollIncrease, @@ -2858,7 +2880,7 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { uint256 maxUpfrontFee ) internal pure returns (bytes memory) { return abi.encodeCall( - IBorrowerOperations.adjustUnredeemableTrove, + IBorrowerOperations.adjustZombieTrove, (troveId, collChange, isCollIncrease, debtChange, isDebtIncrease, upperHint, lowerHint, maxUpfrontFee) ); } @@ -2947,8 +2969,8 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { return (selector, "BorrowerOperations.TroveNotActive()"); } - if (selector == BorrowerOperations.TroveNotUnredeemable.selector) { - return (selector, "BorrowerOperations.TroveNotUnredeemable()"); + if (selector == BorrowerOperations.TroveNotZombie.selector) { + return (selector, "BorrowerOperations.TroveNotZombie()"); } if (selector == BorrowerOperations.TroveOpen.selector) { diff --git a/contracts/src/test/TestContracts/TroveManagerTester.t.sol b/contracts/src/test/TestContracts/TroveManagerTester.t.sol index 863a6345..eaa73030 100644 --- a/contracts/src/test/TestContracts/TroveManagerTester.t.sol +++ b/contracts/src/test/TestContracts/TroveManagerTester.t.sol @@ -152,7 +152,7 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { function checkTroveIsOpen(uint256 _troveId) public view returns (bool) { Status status = Troves[_troveId].status; - return status == Status.active || status == Status.unredeemable; + return status == Status.active || status == Status.zombie; } function checkTroveIsActive(uint256 _troveId) external view returns (bool) { @@ -160,9 +160,9 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { return status == Status.active; } - function checkTroveIsUnredeemable(uint256 _troveId) external view returns (bool) { + function checkTroveIsZombie(uint256 _troveId) external view returns (bool) { Status status = Troves[_troveId].status; - return status == Status.unredeemable; + return status == Status.zombie; } function hasRedistributionGains(uint256 _troveId) external view override returns (bool) { diff --git a/contracts/src/test/interestBatchManagement.t.sol b/contracts/src/test/interestBatchManagement.t.sol index 0abc63f1..fe1b11e5 100644 --- a/contracts/src/test/interestBatchManagement.t.sol +++ b/contracts/src/test/interestBatchManagement.t.sol @@ -114,7 +114,7 @@ contract InterestBatchManagementTest is DevTestSetup { vm.stopPrank(); } - function testCannotSetBatchManagerIfTroveIsUnredeemable() public { + function testCannotSetBatchManagerIfTroveIsZombie() public { registerBatchManager(B); // Open trove @@ -754,7 +754,7 @@ contract InterestBatchManagementTest is DevTestSetup { redeem(A, 500e18); // Check A is zombie - assertEq(uint8(troveManager.getTroveStatus(ATroveId)), uint8(ITroveManager.Status.unredeemable)); + assertEq(uint8(troveManager.getTroveStatus(ATroveId)), uint8(ITroveManager.Status.zombie)); // Fast-forward time vm.warp(block.timestamp + 3650 days); @@ -1135,7 +1135,7 @@ contract InterestBatchManagementTest is DevTestSetup { assertEq(troveData.lastInterestRateAdjTime, block.timestamp, "Wrong interest rate adj time for A"); } - function testAnUnredeemableTroveGoesBackToTheBatch() public { + function testAnZombieTroveGoesBackToTheBatch() public { // A opens trove and joins batch manager B uint256 troveId = openTroveAndJoinBatchManager(A, 100 ether, 2000e18, B, 5e16); @@ -1144,11 +1144,11 @@ contract InterestBatchManagementTest is DevTestSetup { vm.warp(block.timestamp + 10 days); - // C redeems and makes A unredeemable + // C redeems and makes A zombie redeem(C, 1000e18); // A adjusts back to normal - adjustUnredeemableTrove(A, troveId, 0, false, 1000e18, true); + adjustZombieTrove(A, troveId, 0, false, 1000e18, true); assertEq(borrowerOperations.interestBatchManagerOf(troveId), B, "A should be in batch (BO)"); (,,,,,,,, address tmBatchManagerAddress,) = troveManager.Troves(troveId); diff --git a/contracts/src/test/interestIndividualDelegation.t.sol b/contracts/src/test/interestIndividualDelegation.t.sol index e29a1f91..dad517b0 100644 --- a/contracts/src/test/interestIndividualDelegation.t.sol +++ b/contracts/src/test/interestIndividualDelegation.t.sol @@ -121,17 +121,17 @@ contract InterestIndividualDelegationTest is DevTestSetup { vm.stopPrank(); } - function testSetDelegateRevertsIfTroveIsUnredeemable() public { + function testSetDelegateRevertsIfTroveIsZombie() public { vm.startPrank(B); borrowerOperations.registerBatchManager(1e16, 20e16, 5e16, 25e14, MIN_INTEREST_RATE_CHANGE_PERIOD); vm.stopPrank(); // Open trove uint256 troveId = openTroveNoHints100pct(A, 100e18, 5000e18, 5e16); - // Make trove unredeemable + // Make trove zombie redeem(A, 4000e18); - // Check A’s trove is unredeemable - assertEq(troveManager.checkTroveIsUnredeemable(troveId), true, "A trove should be unredeemable"); + // Check A’s trove is zombie + assertEq(troveManager.checkTroveIsZombie(troveId), true, "A trove should be zombie"); // Set batch manager (B) vm.startPrank(A); diff --git a/contracts/src/test/redemptions.t.sol b/contracts/src/test/redemptions.t.sol index ebf93e7e..0e6ac638 100644 --- a/contracts/src/test/redemptions.t.sol +++ b/contracts/src/test/redemptions.t.sol @@ -73,8 +73,8 @@ contract Redemptions is DevTestSetup { redeem(E, redeemAmount_1); // Check A and B still open - assertEq(uint8(troveManager.getTroveStatus(troveIDs.A)), uint8(ITroveManager.Status.unredeemable)); - assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.unredeemable)); + assertEq(uint8(troveManager.getTroveStatus(troveIDs.A)), uint8(ITroveManager.Status.zombie)); + assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.zombie)); } function testFullRedemptionLeavesTrovesWithDebtEqualToZero() public { @@ -112,8 +112,8 @@ contract Redemptions is DevTestSetup { redeem(E, redeemAmount_2); // Check A and B still open with debt == zero - assertEq(uint8(troveManager.getTroveStatus(troveIDs.A)), uint8(ITroveManager.Status.unredeemable)); - assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.unredeemable)); + assertEq(uint8(troveManager.getTroveStatus(troveIDs.A)), uint8(ITroveManager.Status.zombie)); + assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.zombie)); assertEq(troveManager.getTroveEntireDebt(troveIDs.A), 0); assertEq(troveManager.getTroveEntireDebt(troveIDs.B), 0); @@ -279,7 +279,8 @@ contract Redemptions is DevTestSetup { _redeemAndCreateZombieTrovesAAndB(troveIDs); - assertEq(uint8(troveManager.getTroveStatus(troveIDs.A)), uint8(ITroveManager.Status.unredeemable)); + assertEq(uint8(troveManager.getTroveStatus(troveIDs.A)), uint8(ITroveManager.Status.zombie)); + assertEq(troveManager.lastZombieTroveId(), troveIDs.B, "Wrong last zombie trove pointer"); } function testTroveRedeemedToBelowMIN_DEBTBecomesZombieTrove() public { @@ -287,7 +288,8 @@ contract Redemptions is DevTestSetup { _redeemAndCreateZombieTrovesAAndB(troveIDs); - assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.unredeemable)); + assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.zombie)); + assertEq(troveManager.lastZombieTroveId(), troveIDs.B, "Wrong last zombie trove pointer"); } function testTroveRedeemedToAboveMIN_DEBTDoesNotBecomesZombieTrove() public { @@ -296,6 +298,7 @@ contract Redemptions is DevTestSetup { _redeemAndCreateZombieTroveAAndHitB(troveIDs); assertEq(uint8(troveManager.getTroveStatus(troveIDs.C)), uint8(ITroveManager.Status.active)); + assertEq(troveManager.lastZombieTroveId(), 0, "Wrong last zombie trove pointer"); } function testZombieTrovesRemovedFromSortedList() public { @@ -317,9 +320,12 @@ contract Redemptions is DevTestSetup { // Check Trove with lowest interest rate is C assertEq(sortedTroves.getLast(), troveIDs.C); + + // Check last Zombie trove pointer + assertEq(troveManager.lastZombieTroveId(), troveIDs.B, "Wrong last zombie trove pointer"); } - function testZombieTroveCantBeRedeemedFrom() public { + function testZombieTroveCanStillBeRedeemedFrom() public { (,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); _redeemAndCreateZombieTrovesAAndB(troveIDs); @@ -331,14 +337,36 @@ contract Redemptions is DevTestSetup { uint256 redeemAmount = debt_B / 2; redeem(E, redeemAmount); - // Check B's debt unchanged from redeemAmount < debt_B; - assertEq(debt_B, troveManager.getTroveEntireDebt(troveIDs.B)); + // Check B's debt changed from redeemAmount < debt_B; + assertEq(troveManager.getTroveEntireDebt(troveIDs.B), debt_B - redeemAmount); + debt_B = troveManager.getTroveEntireDebt(troveIDs.B); redeemAmount = debt_B + 1; redeem(E, redeemAmount); - // Check B's debt unchanged from redeemAmount > debt_B; - assertEq(debt_B, troveManager.getTroveEntireDebt(troveIDs.B)); + // Check B's debt changed from redeemAmount > debt_B; + assertEq(troveManager.getTroveEntireDebt(troveIDs.B), 0); + } + + function testRedemptionsWithNoPartialLeaveNoPointerToZombieTroves() public { + (,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); + + _redeemAndCreateEmptyZombieTrovesAAndB(troveIDs); + + // Check A, B removed from sorted list + assertFalse(sortedTroves.contains(troveIDs.A)); + assertFalse(sortedTroves.contains(troveIDs.B)); + + // Check A, B zombie (already checked in helper above) + //assertEq(uint8(troveManager.getTroveStatus(troveIDs.A)), uint8(ITroveManager.Status.zombie)); + //assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.zombie)); + + // Check A, B empty (already checked in helper above) + //assertEq(troveManager.getTroveEntireDebt(troveIDs.A), 0); + //assertEq(troveManager.getTroveEntireDebt(troveIDs.B), 0); + + // Check last Zombie trove pointer + assertEq(troveManager.lastZombieTroveId(), 0, "Wrong last zombie trove pointer"); } function testZombieTrovesCanReceiveRedistGains() public { @@ -469,8 +497,8 @@ contract Redemptions is DevTestSetup { transferBold(E, A, boldToken.balanceOf(E) / 2); transferBold(E, B, boldToken.balanceOf(E)); - assertEq(uint8(troveManager.getTroveStatus(troveIDs.A)), uint8(ITroveManager.Status.unredeemable)); - assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.unredeemable)); + assertEq(uint8(troveManager.getTroveStatus(troveIDs.A)), uint8(ITroveManager.Status.zombie)); + assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.zombie)); closeTrove(A, troveIDs.A); closeTrove(B, troveIDs.B); @@ -496,8 +524,8 @@ contract Redemptions is DevTestSetup { uint256 surplusDebt = 37; // A and B withdraw Bold from their zombie Trove - adjustUnredeemableTrove(A, troveIDs.A, 0, false, debtDelta_A + surplusDebt, true); - adjustUnredeemableTrove(B, troveIDs.B, 0, false, debtDelta_A + surplusDebt, true); + adjustZombieTrove(A, troveIDs.A, 0, false, debtDelta_A + surplusDebt, true); + adjustZombieTrove(B, troveIDs.B, 0, false, debtDelta_A + surplusDebt, true); // Check they are above the min debt assertGt(troveManager.getTroveEntireDebt(troveIDs.A), MIN_DEBT); @@ -521,8 +549,8 @@ contract Redemptions is DevTestSetup { uint256 surplusDebt = 37; // A and B withdraw Bold from their zombie Trove - adjustUnredeemableTrove(A, troveIDs.A, 0, false, debtDelta_A + surplusDebt, true); - adjustUnredeemableTrove(B, troveIDs.B, 0, false, debtDelta_A + surplusDebt, true); + adjustZombieTrove(A, troveIDs.A, 0, false, debtDelta_A + surplusDebt, true); + adjustZombieTrove(B, troveIDs.B, 0, false, debtDelta_A + surplusDebt, true); // Check they are above the min debt assertGt(troveManager.getTroveEntireDebt(troveIDs.A), MIN_DEBT); @@ -554,8 +582,8 @@ contract Redemptions is DevTestSetup { assertFalse(sortedTroves.contains(troveIDs.B)); // A and B withdraw Bold from their zombie Trove - adjustUnredeemableTrove(A, troveIDs.A, 0, false, debtDelta_A + surplusDebt, true); - adjustUnredeemableTrove(B, troveIDs.B, 0, false, debtDelta_A + surplusDebt, true); + adjustZombieTrove(A, troveIDs.A, 0, false, debtDelta_A + surplusDebt, true); + adjustZombieTrove(B, troveIDs.B, 0, false, debtDelta_A + surplusDebt, true); // Check they are above the min debt assertGt(troveManager.getTroveEntireDebt(troveIDs.A), MIN_DEBT); @@ -586,8 +614,8 @@ contract Redemptions is DevTestSetup { uint256 surplusDebt = 37; // A and B withdraw Bold from their zombie Trove - adjustUnredeemableTrove(A, troveIDs.A, 0, false, debtDelta_A + surplusDebt, true); - adjustUnredeemableTrove(B, troveIDs.B, 0, false, debtDelta_A + surplusDebt, true); + adjustZombieTrove(A, troveIDs.A, 0, false, debtDelta_A + surplusDebt, true); + adjustZombieTrove(B, troveIDs.B, 0, false, debtDelta_A + surplusDebt, true); // Check they are above the min debt assertGt(troveManager.getTroveEntireDebt(troveIDs.A), MIN_DEBT, "A debt should be above min"); @@ -611,10 +639,10 @@ contract Redemptions is DevTestSetup { // A and B attempt to withdraw Bold, but not enough vm.expectRevert(BorrowerOperations.DebtBelowMin.selector); - this.adjustUnredeemableTrove(A, troveIDs.A, 0, false, borrow_A, true); + this.adjustZombieTrove(A, troveIDs.A, 0, false, borrow_A, true); vm.expectRevert(BorrowerOperations.DebtBelowMin.selector); - this.adjustUnredeemableTrove(B, troveIDs.B, 0, false, borrow_B, true); + this.adjustZombieTrove(B, troveIDs.B, 0, false, borrow_B, true); } function testZombieTroveBorrowerCanNotRepayDebt() public { @@ -723,9 +751,9 @@ contract Redemptions is DevTestSetup { assertEq(troveManager.calcTroveAccruedInterest(troveIDs.A), 0); assertGt(troveManager.calcTroveAccruedInterest(troveIDs.B), 0); // Troves are zombie - assertTrue(troveManager.checkTroveIsUnredeemable(troveIDs.A)); + assertTrue(troveManager.checkTroveIsZombie(troveIDs.A)); assertFalse(sortedTroves.contains(troveIDs.A)); - assertTrue(troveManager.checkTroveIsUnredeemable(troveIDs.B)); + assertTrue(troveManager.checkTroveIsZombie(troveIDs.B)); assertFalse(sortedTroves.contains(troveIDs.B)); // E applies interest on A and B's Troves @@ -736,9 +764,9 @@ contract Redemptions is DevTestSetup { assertEq(troveManager.calcTroveAccruedInterest(troveIDs.B), 0); // Troves are still zombie - assertTrue(troveManager.checkTroveIsUnredeemable(troveIDs.A)); + assertTrue(troveManager.checkTroveIsZombie(troveIDs.A)); assertFalse(sortedTroves.contains(troveIDs.A)); - assertTrue(troveManager.checkTroveIsUnredeemable(troveIDs.B)); + assertTrue(troveManager.checkTroveIsZombie(troveIDs.B)); assertFalse(sortedTroves.contains(troveIDs.B)); } @@ -753,9 +781,9 @@ contract Redemptions is DevTestSetup { assertEq(troveManager.calcTroveAccruedInterest(troveIDs.A), 0); assertGt(troveManager.calcTroveAccruedInterest(troveIDs.B), 0); // Troves are zombie - assertTrue(troveManager.checkTroveIsUnredeemable(troveIDs.A)); + assertTrue(troveManager.checkTroveIsZombie(troveIDs.A)); assertFalse(sortedTroves.contains(troveIDs.A)); - assertTrue(troveManager.checkTroveIsUnredeemable(troveIDs.B)); + assertTrue(troveManager.checkTroveIsZombie(troveIDs.B)); assertFalse(sortedTroves.contains(troveIDs.B)); // E applies interest on A and B's Troves @@ -765,9 +793,9 @@ contract Redemptions is DevTestSetup { assertEq(troveManager.calcTroveAccruedInterest(troveIDs.A), 0); assertEq(troveManager.calcTroveAccruedInterest(troveIDs.B), 0); // Troves B is not zombie anymore (A still is) - assertTrue(troveManager.checkTroveIsUnredeemable(troveIDs.A)); + assertTrue(troveManager.checkTroveIsZombie(troveIDs.A)); assertFalse(sortedTroves.contains(troveIDs.A)); - assertFalse(troveManager.checkTroveIsUnredeemable(troveIDs.B)); + assertFalse(troveManager.checkTroveIsZombie(troveIDs.B)); assertTrue(sortedTroves.contains(troveIDs.B)); } @@ -791,7 +819,7 @@ contract Redemptions is DevTestSetup { // assertFalse(troveManager.checkBelowCriticalThreshold(price)); assertLt(troveManager.getCurrentICR(troveIDs.B, price), MCR); - assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.unredeemable)); + assertEq(uint8(troveManager.getTroveStatus(troveIDs.B)), uint8(ITroveManager.Status.zombie)); // E liquidates B liquidate(E, troveIDs.B); diff --git a/contracts/src/test/shutdown.t.sol b/contracts/src/test/shutdown.t.sol index 540297b9..1f7e677b 100644 --- a/contracts/src/test/shutdown.t.sol +++ b/contracts/src/test/shutdown.t.sol @@ -219,11 +219,11 @@ contract ShutdownTest is DevTestSetup { vm.stopPrank(); } - function testCannotAdjustUnredeemableTroveAfterShutdown() public { + function testCannotAdjustZombieTroveAfterShutdown() public { uint256 troveId = openMulticollateralTroveNoHints100pctWithIndex(0, A, 0, 11e18, 10000e18, 5e16); openMulticollateralTroveNoHints100pctWithIndex(0, B, 0, 22e18, 20000e18, 6e16); - // B redeems from A’s trove, to make it unredeemable + // B redeems from A’s trove, to make it zombie //deal(address(boldToken), B, 20000e18); vm.startPrank(B); collateralRegistry.redeemCollateral(10000e18, 0, 1e18); @@ -234,12 +234,12 @@ contract ShutdownTest is DevTestSetup { contractsArray[0].priceFeed.setPrice(500e18); contractsArray[0].borrowerOperations.shutdown(); - // Check A’s trove is unredeemable - assertEq(troveManager.checkTroveIsUnredeemable(troveId), true, "A trove should be unredeemable"); + // Check A’s trove is zombie + assertEq(troveManager.checkTroveIsZombie(troveId), true, "A trove should be zombie"); vm.startPrank(A); vm.expectRevert(BorrowerOperations.IsShutDown.selector); - borrowerOperations.adjustUnredeemableTrove(troveId, 1e18, true, 0, false, 0, 0, 1000e18); + borrowerOperations.adjustZombieTrove(troveId, 1e18, true, 0, false, 0, 0, 1000e18); vm.stopPrank(); } diff --git a/contracts/src/test/zapperGasComp.t.sol b/contracts/src/test/zapperGasComp.t.sol index 1a21afb7..4c5e41e6 100644 --- a/contracts/src/test/zapperGasComp.t.sol +++ b/contracts/src/test/zapperGasComp.t.sol @@ -307,7 +307,7 @@ contract ZapperGasCompTest is DevTestSetup { } // TODO: more adjustment combinations - function testCanAdjustUnredeemableTroveWithdrawCollAndBold() external { + function testCanAdjustZombieTroveWithdrawCollAndBold() external { uint256 collAmount1 = 10 ether; uint256 collAmount2 = 1 ether; uint256 boldAmount1 = 10000e18; @@ -335,7 +335,7 @@ contract ZapperGasCompTest is DevTestSetup { gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); vm.stopPrank(); - // Redeem to make trove unredeemable + // Redeem to make trove zombie vm.startPrank(A); collateralRegistry.redeemCollateral(boldAmount1 - boldAmount2, 10, 1e18); vm.stopPrank(); @@ -347,7 +347,7 @@ contract ZapperGasCompTest is DevTestSetup { // Adjust (withdraw coll and Bold) vm.startPrank(B); - gasCompZapper.adjustUnredeemableTroveWithRawETH( + gasCompZapper.adjustZombieTroveWithRawETH( troveId, collAmount2, false, boldAmount2, true, 0, 0, boldAmount2 ); vm.stopPrank(); diff --git a/contracts/src/test/zapperWETH.t.sol b/contracts/src/test/zapperWETH.t.sol index 78df9d3d..2d5bb28c 100644 --- a/contracts/src/test/zapperWETH.t.sol +++ b/contracts/src/test/zapperWETH.t.sol @@ -342,7 +342,7 @@ contract ZapperWETHTest is DevTestSetup { } // TODO: more adjustment combinations - function testCanAdjustUnredeemableTroveWithdrawCollAndBold() external { + function testCanAdjustZombieTroveWithdrawCollAndBold() external { uint256 ethAmount1 = 10 ether; uint256 ethAmount2 = 1 ether; uint256 boldAmount1 = 10000e18; @@ -369,7 +369,7 @@ contract ZapperWETHTest is DevTestSetup { wethZapper.setRemoveManagerWithReceiver(troveId, B, A); vm.stopPrank(); - // Redeem to make trove unredeemable + // Redeem to make trove zombie vm.startPrank(A); collateralRegistry.redeemCollateral(boldAmount1 - boldAmount2, 10, 1e18); vm.stopPrank(); @@ -381,7 +381,7 @@ contract ZapperWETHTest is DevTestSetup { // Adjust (withdraw coll and Bold) vm.startPrank(B); - wethZapper.adjustUnredeemableTroveWithRawETH(troveId, ethAmount2, false, boldAmount2, true, 0, 0, boldAmount2); + wethZapper.adjustZombieTroveWithRawETH(troveId, ethAmount2, false, boldAmount2, true, 0, 0, boldAmount2); vm.stopPrank(); assertEq(troveManager.getTroveEntireColl(troveId), troveCollBefore - ethAmount2, "Trove coll mismatch");