From 1226e1c5d459eaae7c372668bf4c4c842be81ee0 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 9 Apr 2024 15:17:02 +0700 Subject: [PATCH] Add apply/mint interest for redemptions and leave redeemed Troves open --- contracts/src/TroveManager.sol | 109 ++++++----- contracts/src/test/TestContracts/BaseTest.sol | 17 ++ .../src/test/TestContracts/DevTestSetup.sol | 29 +++ .../src/test/interestRateAggregate.t.sol | 170 +++++++++++++++--- contracts/src/test/interestRateBasic.t.sol | 65 ++++++- contracts/src/test/redemptions.t.sol | 135 ++++++++++++++ 6 files changed, 444 insertions(+), 81 deletions(-) create mode 100644 contracts/src/test/redemptions.t.sol diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 01fb83e5..d8fe8707 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -216,12 +216,22 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint decayedBaseRate; uint price; uint totalBoldSupplyAtStart; + uint256 totalRedistDebtGains; + uint256 totalNewRecordedTroveDebts; + uint256 totalOldRecordedTroveDebts; + uint256 totalNewWeightedRecordedTroveDebts; + uint256 totalOldWeightedRecordedTroveDebts; } + struct SingleRedemptionValues { uint BoldLot; uint ETHLot; - bool cancelledPartial; + uint256 redistDebtGain; + uint256 oldRecordedTroveDebt; + uint256 newRecordedTroveDebt; + uint256 oldWeightedRecordedTroveDebt; + uint256 newWeightedRecordedTroveDebt; } // --- Events --- @@ -827,53 +837,46 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana ) internal returns (SingleRedemptionValues memory singleRedemption) { + singleRedemption.oldWeightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_troveId); + singleRedemption.oldRecordedTroveDebt = Troves[_troveId].debt; + + (, singleRedemption.redistDebtGain) = _getAndApplyRedistributionGains(_contractsCache.activePool, _contractsCache.defaultPool, _troveId); + + // TODO: Gas. We apply accrued interest here, but could gas optimize this, since all-but-one Trove in the sequence will have their + // debt zero'd by redemption. However, gas optimization for redemption is not as critical as for borrower & SP ops. + uint256 entireTroveDebt = getTroveEntireDebt(_troveId); + _updateTroveDebt(_troveId, entireTroveDebt); + // Determine the remaining amount (lot) to be redeemed, capped by the entire debt of the Trove minus the liquidation reserve - singleRedemption.BoldLot = LiquityMath._min(_maxBoldamount, Troves[_troveId].debt - BOLD_GAS_COMPENSATION); + // TODO: should we leave gas compensation (and corresponding debt) untouched for zombie Troves? Currently it's not touched. + singleRedemption.BoldLot = LiquityMath._min(_maxBoldamount, entireTroveDebt - BOLD_GAS_COMPENSATION); // Get the ETHLot of equivalent value in USD singleRedemption.ETHLot = singleRedemption.BoldLot * DECIMAL_PRECISION / _price; // Decrease the debt and collateral of the current Trove according to the Bold lot and corresponding ETH to send - uint newDebt = Troves[_troveId].debt - singleRedemption.BoldLot; - uint newColl = Troves[_troveId].coll - singleRedemption.ETHLot; - - // TODO: zombi troves - if (newDebt == BOLD_GAS_COMPENSATION) { - // No debt left in the Trove (except for the liquidation reserve), therefore the trove gets closed - _removeStake(_troveId); - _closeTrove(_troveId, Status.closedByRedemption); - _redeemCloseTrove(_contractsCache, _troveId, BOLD_GAS_COMPENSATION, newColl); - emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.redeemCollateral); - - } else { - Troves[_troveId].debt = newDebt; - Troves[_troveId].coll = newColl; - _updateStakeAndTotalStakes(_troveId); + singleRedemption.newRecordedTroveDebt = entireTroveDebt - singleRedemption.BoldLot; + uint newColl = Troves[_troveId].coll - singleRedemption.ETHLot; - emit TroveUpdated( - _troveId, - newDebt, newColl, - Troves[_troveId].stake, - TroveManagerOperation.redeemCollateral - ); + if (singleRedemption.newRecordedTroveDebt <= MIN_NET_DEBT) { + // TODO: tag it as a zombie Trove and remove from Sorted List } + Troves[_troveId].debt = singleRedemption.newRecordedTroveDebt; + Troves[_troveId].coll = newColl; - return singleRedemption; - } + singleRedemption.newWeightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_troveId); - /* - * Called when a full redemption occurs, and closes the trove. - * The redeemer swaps (debt - liquidation reserve) Bold for (debt - liquidation reserve) worth of ETH, so the Bold liquidation reserve left corresponds to the remaining debt. - * In order to close the trove, the Bold liquidation reserve is burned, and the corresponding debt is removed from the active pool. - * The debt recorded on the trove's struct is zero'd elswhere, in _closeTrove. - * Any surplus ETH left in the trove, is sent to the Coll surplus pool, and can be later claimed by the borrower. - */ - function _redeemCloseTrove(ContractsCache memory _contractsCache, uint256 _troveId, uint _bold, uint _ETH) internal { - _contractsCache.boldToken.burn(gasPoolAddress, _bold); + // TODO: Gas optimize? We update totalStakes N times for a sequence of N Trovres(!). + _updateStakeAndTotalStakes(_troveId); + + emit TroveUpdated( + _troveId, + singleRedemption.newRecordedTroveDebt, newColl, + Troves[_troveId].stake, + TroveManagerOperation.redeemCollateral + ); - // send ETH from Active Pool to CollSurplus Pool - _contractsCache.collSurplusPool.accountSurplus(_troveId, _ETH); - _contractsCache.activePool.sendETH(address(_contractsCache.collSurplusPool), _ETH); + return singleRedemption; } /* Send _boldamount Bold to the system and redeem the corresponding amount of collateral from as many Troves as are needed to fill the redemption @@ -935,7 +938,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana if (_maxIterations == 0) { _maxIterations = type(uint256).max; } while (currentTroveId != 0 && totals.remainingBold > 0 && _maxIterations > 0) { _maxIterations--; - // Save the uint256 of the Trove preceding the current one, before potentially modifying the list + // Save the uint256 of the Trove preceding the current one uint256 nextUserToCheck = contractsCache.sortedTroves.getPrev(currentTroveId); // Skip if ICR < 100%, to make sure that redemptions always improve the CR of hit Troves if (getCurrentICR(currentTroveId, totals.price) < _100pct) { @@ -943,23 +946,28 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana continue; } - _getAndApplyRedistributionGains(contractsCache.activePool, contractsCache.defaultPool, currentTroveId); - SingleRedemptionValues memory singleRedemption = _redeemCollateralFromTrove( contractsCache, currentTroveId, totals.remainingBold, totals.price ); - - if (singleRedemption.cancelledPartial) break; // Partial redemption was cancelled (out-of-date hint, or new net debt < minimum), therefore we could not redeem from the last Trove - + totals.totalBoldToRedeem = totals.totalBoldToRedeem + singleRedemption.BoldLot; - totals.totalETHDrawn = totals.totalETHDrawn + singleRedemption.ETHLot; + totals.totalRedistDebtGains = totals.totalRedistDebtGains + singleRedemption.redistDebtGain; + // For recorded and weighted recorded debt totals, we need to capture the increases and decreases, + // since the net debt change for a given Trove could be positive or negative: redemptions decrease a Trove's recorded + // (and weighted recorded) debt, but the accrued interest increases it. + totals.totalNewRecordedTroveDebts = totals.totalNewRecordedTroveDebts + singleRedemption.newRecordedTroveDebt; + totals.totalOldRecordedTroveDebts = totals.totalOldRecordedTroveDebts + singleRedemption.oldRecordedTroveDebt; + totals.totalNewWeightedRecordedTroveDebts = totals.totalNewWeightedRecordedTroveDebts + singleRedemption.newWeightedRecordedTroveDebt; + totals.totalOldWeightedRecordedTroveDebts = totals.totalOldWeightedRecordedTroveDebts + singleRedemption.oldWeightedRecordedTroveDebt; + totals.totalETHDrawn = totals.totalETHDrawn + singleRedemption.ETHLot; totals.remainingBold = totals.remainingBold - singleRedemption.BoldLot; currentTroveId = nextUserToCheck; } + require(totals.totalETHDrawn > 0, "TroveManager: Unable to redeem any amount"); // Decay the baseRate due to time passed, and then increase it according to the size of this redemption. @@ -976,10 +984,17 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana emit Redemption(_boldamount, totals.totalBoldToRedeem, totals.totalETHDrawn, totals.ETHFee); + activePool.mintAggInterest( + totals.totalRedistDebtGains, + totals.totalBoldToRedeem, + totals.totalNewRecordedTroveDebts, + totals.totalOldRecordedTroveDebts, + totals.totalNewWeightedRecordedTroveDebts, + totals.totalOldWeightedRecordedTroveDebts + ); + // Burn the total Bold that is cancelled with debt, and send the redeemed ETH to msg.sender contractsCache.boldToken.burn(msg.sender, totals.totalBoldToRedeem); - // Update Active Pool Bold, and send ETH to account - contractsCache.activePool.decreaseRecordedDebtSum(totals.totalBoldToRedeem); contractsCache.activePool.sendETH(msg.sender, totals.ETHToSendToRedeemer); } @@ -1108,7 +1123,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana entireColl = recordedColl + pendingETHReward; } - function getTroveEntireDebt(uint256 _troveId) external view returns (uint256) { + function getTroveEntireDebt(uint256 _troveId) public view returns (uint256) { (uint256 entireTroveDebt, , , , ) = getEntireDebtAndColl(_troveId); return entireTroveDebt; } diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index b10d6d90..6e8d9606 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -56,24 +56,35 @@ contract BaseTest is Test { uint256 A; uint256 B; uint256 C; + uint256 D; + } + + struct TroveIDs { + uint256 A; + uint256 B; + uint256 C; + uint256 D; } struct TroveCollAmounts { uint256 A; uint256 B; uint256 C; + uint256 D; } struct TroveInterestRates { uint256 A; uint256 B; uint256 C; + uint256 D; } struct TroveAccruedInterests { uint256 A; uint256 B; uint256 C; + uint256 D; } // --- functions --- @@ -226,6 +237,12 @@ contract BaseTest is Test { vm.stopPrank(); } + + function redeem(address _from, uint256 _boldAmount) public { + vm.startPrank(_from); + troveManager.redeemCollateral(_boldAmount, MAX_UINT256, 1e18); + vm.stopPrank(); + } function logContractAddresses() public view { console.log("ActivePool addr: ", address(activePool)); console.log("BorrowerOps addr: ", address(borrowerOperations)); diff --git a/contracts/src/test/TestContracts/DevTestSetup.sol b/contracts/src/test/TestContracts/DevTestSetup.sol index 2bd6f48c..c67f7915 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -235,4 +235,33 @@ contract DevTestSetup is BaseTest { return (ATroveId, BTroveId, CTroveId, DTroveId); } + + function _setupForRedemption() public returns (uint256, uint256, TroveIDs memory) { + TroveIDs memory troveIDs; + + priceFeed.setPrice(2000e18); + + uint256 interestRate_A = 10e16; + uint256 interestRate_B = 20e16; + uint256 interestRate_C = 30e16; + uint256 interestRate_D = 40e16; + uint256 coll = 20 ether; + uint256 debtRequest = 20000e18; + // Open in increasing order of interst rate + troveIDs.A = openTroveNoHints100pctMaxFee(A, coll, debtRequest, interestRate_A); + troveIDs.B = openTroveNoHints100pctMaxFee(B, coll, debtRequest, interestRate_B); + troveIDs.C = openTroveNoHints100pctMaxFee(C, coll, debtRequest, interestRate_C); + troveIDs.D = openTroveNoHints100pctMaxFee(D, coll, debtRequest, interestRate_D); + + // fast-forward to pass bootstrap phase + vm.warp(block.timestamp + 14 days); + + // A, B, C, D transfer all their Bold to E + transferBold(A, E, boldToken.balanceOf(A)); + transferBold(B, E, boldToken.balanceOf(B)); + transferBold(C, E, boldToken.balanceOf(C)); + transferBold(D, E, boldToken.balanceOf(D)); + + return (coll, debtRequest, troveIDs); + } } diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index 03e1e1b8..02937e3a 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -63,7 +63,6 @@ contract InterestRateAggregate is DevTestSetup { uint256 troveDebtRequest = 2000e18; uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 75e16); // 75% annual interest - console.log("A debt", troveManager.getTroveDebt(ATroveId)); uint256 expectedTroveDebt = troveDebtRequest + troveManager.BOLD_GAS_COMPENSATION(); assertEq(troveManager.getTroveDebt(ATroveId), expectedTroveDebt); @@ -209,10 +208,8 @@ contract InterestRateAggregate is DevTestSetup { priceFeed.setPrice(2000e18); assertEq(activePool.aggRecordedDebt(), 0); - console.log("here 1"); uint256 troveDebtRequest = 2000e18; openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest - console.log("here 2"); // Check aggregate recorded debt increased to non-zero uint256 aggREcordedDebt_1 = activePool.aggRecordedDebt(); @@ -334,8 +331,6 @@ contract InterestRateAggregate is DevTestSetup { // // Trove's debt should be weighted by its annual interest rate uint256 expectedWeightedDebt_A = troveDebt_A * annualInterest_A; - console.log(expectedWeightedDebt_A, "expectedWeightedDebt_A"); - console.log(activePool.aggWeightedDebtSum(), "activePool.aggWeightedDebtSum()"); assertEq(activePool.aggWeightedDebtSum(), expectedWeightedDebt_A); @@ -932,7 +927,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); } - function testAdjustTroveInterestRateWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { + function testAdjustTroveInterestRateWithNoRedistGainsIncreasesRecordedDebtSumByTrovesAccruedInterest() public { uint256 troveDebtRequest = 2000e18; // A opens Trove @@ -959,7 +954,7 @@ contract InterestRateAggregate is DevTestSetup { // --- withdrawBold tests --- - function testWithdrawBoldWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterestPlusBorrowerDebtChange() public { + function testWithdrawBoldWithNoRedistGainsIncreasesAggRecordedDebtByPendingAggInterestPlusBorrowerDebtChange() public { uint256 troveDebtRequest = 2000e18; uint256 debtIncrease = 500e18; @@ -1045,7 +1040,7 @@ contract InterestRateAggregate is DevTestSetup { // With no redist gain, increases recorded debt sum by the borrower's debt change plus Trove's accrued interest - function testWithdrawBoldWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterestPlusDebtChange() public { + function testWithdrawBoldWithNoRedistGainsIncreasesRecordedDebtSumByTrovesAccruedInterestPlusDebtChange() public { uint256 troveDebtRequest = 2000e18; uint256 debtIncrease = 500e18; @@ -1100,7 +1095,7 @@ contract InterestRateAggregate is DevTestSetup { // --- repayBold tests --- - function testRepayBoldWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterestMinusBorrowerDebtChange() public { + function testRepayBoldWithNoRedistGainsIncreasesAggRecordedDebtByPendingAggInterestMinusBorrowerDebtChange() public { uint256 troveDebtRequest = 3000e18; uint256 debtDecrease = 500e18; @@ -1183,7 +1178,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.lastAggUpdateTime(), block.timestamp); } - function testRepayBoldWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterestMinusDebtChange() public { + function testRepayBoldWithNoRedistGainsIncreasesRecordedDebtSumByTrovesAccruedInterestMinusDebtChange() public { uint256 troveDebtRequest = 3000e18; uint256 debtDecrease = 500e18; @@ -1238,7 +1233,7 @@ contract InterestRateAggregate is DevTestSetup { // --- addColl tests --- - function testAddCollWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + function testAddCollWithNoRedistGainsIncreasesAggRecordedDebtByPendingAggInterest() public { uint256 troveDebtRequest = 3000e18; uint256 collIncrease = 1 ether; @@ -1321,7 +1316,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.lastAggUpdateTime(), block.timestamp); } - function testAddCollWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { + function testAddCollWithNoRedistGainsIncreasesRecordedDebtSumByTrovesAccruedInterest() public { uint256 troveDebtRequest = 3000e18; uint256 collIncrease = 1 ether; @@ -1373,16 +1368,13 @@ contract InterestRateAggregate is DevTestSetup { // Weighted debt should have increased due to interest being applied assertGt(expectedNewRecordedWeightedDebt, oldRecordedWeightedDebt); - console.log("expectedNewRecordedWeightedDebt", expectedNewRecordedWeightedDebt); - console.log("oldRecordedWeightedDebt", oldRecordedWeightedDebt); - // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); } // --- withdrawColl --- - function testWithdrawCollWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + function testWithdrawCollWithNoRedistGainsIncreasesAggRecordedDebtByPendingAggInterest() public { uint256 troveDebtRequest = 2000e18; uint256 collDecrease = 1 ether; @@ -1465,7 +1457,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.lastAggUpdateTime(), block.timestamp); } - function testWithdrawCollWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { + function testWithdrawCollWithNoRedistGainsIncreasesRecordedDebtSumByTrovesAccruedInterest() public { uint256 troveDebtRequest = 2000e18; uint256 collDecrease = 1 ether; @@ -1523,7 +1515,7 @@ contract InterestRateAggregate is DevTestSetup { // --- applyTroveInterestPermissionless --- - function testApplyTroveInterestPermissionlessWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + function testApplyTroveInterestPermissionlessWithNoRedistGainsIncreasesAggRecordedDebtByPendingAggInterest() public { uint256 troveDebtRequest = 2000e18; // A opens Trove @@ -1613,7 +1605,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.lastAggUpdateTime(), block.timestamp); } - function testApplyTroveInterestPermissionlessWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { + function testApplyTroveInterestPermissionlessWithNoRedistGainsIncreasesRecordedDebtSumByTrovesAccruedInterest() public { uint256 troveDebtRequest = 2000e18; // A opens Trove @@ -1702,8 +1694,6 @@ contract InterestRateAggregate is DevTestSetup { assertGt(recordedDebt_C, 0); uint256 entireSystemDebt = troveManager.getEntireSystemDebt(); - console.log(entireSystemDebt); - console.log(recordedDebt_A + recordedDebt_B + recordedDebt_C); assertEq(entireSystemDebt, recordedDebt_A + recordedDebt_B + recordedDebt_C); } @@ -1744,9 +1734,6 @@ contract InterestRateAggregate is DevTestSetup { recordedDebt_B + accruedInterest_B + recordedDebt_C + accruedInterest_C; - console.log(entireSystemDebt, "entireSystemDebt"); - console.log(sumIndividualTroveDebts, "sumIndividualTroveDebts"); - assertApproximatelyEqual(entireSystemDebt, sumIndividualTroveDebts, 10); } @@ -2234,7 +2221,6 @@ contract InterestRateAggregate is DevTestSetup { function testGetTCRReturnsMaxUint256ForEmptySystem() public { uint256 price = priceFeed.fetchPrice(); - console.log(price); uint256 TCR = troveManager.getTCR(price); assertEq(TCR, MAX_UINT256); @@ -2257,7 +2243,6 @@ contract InterestRateAggregate is DevTestSetup { function testGetTCRReturnsSizeWeightedRatioForSystemWithMultipleTroves() public { uint256 price = priceFeed.fetchPrice(); - console.log(price, "price"); uint256 troveDebtRequest_A = 2000e18; uint256 troveDebtRequest_B = 3000e18; uint256 troveDebtRequest_C = 5000e18; @@ -2301,9 +2286,6 @@ contract InterestRateAggregate is DevTestSetup { } function testGetTCRIncorporatesAllTroveInterestForSystemWithMultipleTroves() public { - //uint256 price = priceFeed.fetchPrice(); - //console.log(price, "price"); - // Use structs to bi-pass "stack-too-deep" error TroveDebtRequests memory troveDebtRequests; TroveCollAmounts memory troveCollAmounts; @@ -2396,6 +2378,136 @@ contract InterestRateAggregate is DevTestSetup { assertEq(expectedICR, troveManager.getCurrentICR(ATroveId, price)); } + // --- redemption tests --- + + function testRedemptionWithNoRedistGainsChangesAggRecordedDebtCorrectly() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // E redeems + redeem(E, debt_A); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - debt_A); + } + + function testRedemptionReducesPendingAggInterestTo0() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + assertGt(activePool.calcPendingAggInterest(), 0); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + // E redeems + redeem(E, debt_A); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testRedemptionMintsPendingAggInterestToRouter() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + // Check I-router balance is 0 + assertEq(boldToken.balanceOf(address(mockInterestRouter)), 0); + + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + // E redeems + redeem(E, debt_A); + + // Check I-router Bold bal has increased by the pending agg interest + assertEq(boldToken.balanceOf(address(mockInterestRouter)), pendingAggInterest); + } + + function testRedemptionUpdatesLastAggUpdateTimeToNow() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + // E redeems + redeem(E, debt_A); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + function testRedemptionWithNoRedistGainsChangesRecordedDebtSumCorrectly() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + // Get current recorded active debt + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // Get A and B's recorded debts before + uint256 oldRecordedDebt_A = troveManager.getTroveDebt(troveIDs.A); + uint256 oldRecordedDebt_B = troveManager.getTroveDebt(troveIDs.B); + assertGt(oldRecordedDebt_A, 0); + assertGt(oldRecordedDebt_B, 0); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + uint256 debt_C = troveManager.getTroveEntireDebt(troveIDs.C); + // E redeems, hitting A fully and B partially + redeem(E, debt_A + debt_B / 2); + + // Confirm C wasn't touched + assertEq(troveManager.getTroveEntireDebt(troveIDs.C), debt_C); + + uint256 newRecordedDebt_A = troveManager.getTroveDebt(troveIDs.A); + uint256 newRecordedDebt_B = troveManager.getTroveDebt(troveIDs.B); + assertNotEq(oldRecordedDebt_A, newRecordedDebt_A); + assertNotEq(oldRecordedDebt_B, newRecordedDebt_B); + + uint256 expectedRecordedDebtSum = recordedDebtSum_1 + newRecordedDebt_A + newRecordedDebt_B - oldRecordedDebt_A - oldRecordedDebt_B; + + // Check recorded debt sum has changed correctly + assertEq(activePool.getRecordedDebtSum(), expectedRecordedDebtSum); + } + + function testRedemptionWithNoRedistGainsChangesWeightedDebtSumCorrectly() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + // Get weighted recorded active debt + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + + // Get A and B's weighted debts before + uint256 oldWeightedRecordedDebt_A = troveManager.getTroveWeightedRecordedDebt(troveIDs.A); + uint256 oldWeightedRecordedDebt_B = troveManager.getTroveWeightedRecordedDebt(troveIDs.B); + assertGt(oldWeightedRecordedDebt_A, 0); + assertGt(oldWeightedRecordedDebt_B, 0); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + uint256 debt_C = troveManager.getTroveEntireDebt(troveIDs.C); + // E redeems, hitting A fully and B partially + redeem(E, debt_A + debt_B / 2); + + // Confirm C wasn't touched + assertEq(troveManager.getTroveEntireDebt(troveIDs.C), debt_C); + + uint256 newWeightedRecordedDebt_A = troveManager.getTroveWeightedRecordedDebt(troveIDs.A); + uint256 newWeightedRecordedDebt_B = troveManager.getTroveWeightedRecordedDebt(troveIDs.B); + assertNotEq(oldWeightedRecordedDebt_A, newWeightedRecordedDebt_A); + assertNotEq(oldWeightedRecordedDebt_B, newWeightedRecordedDebt_B); + + uint256 expectedAggWeightedRecordedDebt = + aggWeightedDebtSum_1 + + newWeightedRecordedDebt_A + + newWeightedRecordedDebt_B - + oldWeightedRecordedDebt_A - + oldWeightedRecordedDebt_B; + + // Check recorded debt sum has changed correctly + assertEq(activePool.aggWeightedDebtSum(), expectedAggWeightedRecordedDebt); + } + // TODO: mixed collateral & debt adjustment opps // TODO: tests with pending debt redist. gain >0 // TODO: tests that show total debt change under user ops diff --git a/contracts/src/test/interestRateBasic.t.sol b/contracts/src/test/interestRateBasic.t.sol index f1cf0831..2f2c2fca 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -755,15 +755,10 @@ contract InterestRateBasic is DevTestSetup { (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertGt(entireTroveDebt_1, 0); - console.log(troveManager.getCurrentICR(ATroveId, 1000e18), "A ICR before"); // A withdraws ETH gain to Trove withdrawETHGainToTrove(A, ATroveId); - console.log(troveManager.getCurrentICR(ATroveId, 1000e18), "A ICR after"); - (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); - console.log(entireTroveDebt_2, "entireTroveDebt_2"); - console.log(entireTroveDebt_1, "entireTroveDebt_1"); assertEq(entireTroveDebt_2, entireTroveDebt_1); } @@ -784,4 +779,64 @@ contract InterestRateBasic is DevTestSetup { assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); } + + // --- redemptions --- + + function testRedemptionSetsTroveLastDebtUpdateTimeToNow() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + assertLt(troveManager.getTroveLastDebtUpdateTime(troveIDs.A), block.timestamp); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + // E redeems, hitting A partially + uint256 redeemAmount = debt_A / 2; + redeem(E, redeemAmount); + + assertEq(troveManager.getTroveLastDebtUpdateTime(troveIDs.A), block.timestamp); + } + + function testRedemptionReducesTroveAccruedInterestTo0() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + assertGt(troveManager.calcTroveAccruedInterest(troveIDs.A), 0); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + // E redeems, hitting A partially + uint256 redeemAmount = debt_A / 2; + redeem(E, redeemAmount); + + assertEq(troveManager.calcTroveAccruedInterest(troveIDs.A), 0); + } + + function testRedemptionReducesEntireTroveDebtByRedeemedAmount() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + uint256 entireTroveDebt_1 = troveManager.getTroveEntireDebt(troveIDs.A); + assertGt(entireTroveDebt_1, 0); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + // E redeems, hitting A partially + uint256 redeemAmount = debt_A / 2; + redeem(E, redeemAmount); + + uint256 entireTroveDebt_2 = troveManager.getTroveEntireDebt(troveIDs.A); + + assertEq(entireTroveDebt_2, entireTroveDebt_1 - redeemAmount); + } + + function testRedemptionChangesRecordedTroveDebtByAccruedInterestMinusRedeemedAmount() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(troveIDs.A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(troveIDs.A); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + // E redeems, hitting A partially + uint256 redeemAmount = debt_A / 2; + redeem(E, redeemAmount); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(troveIDs.A); + + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest - redeemAmount); + } } diff --git a/contracts/src/test/redemptions.t.sol b/contracts/src/test/redemptions.t.sol new file mode 100644 index 00000000..8acf5f4d --- /dev/null +++ b/contracts/src/test/redemptions.t.sol @@ -0,0 +1,135 @@ +pragma solidity 0.8.18; + +import "./TestContracts/DevTestSetup.sol"; + + +contract Redemptions is DevTestSetup { + + function testRedemptionIsInOrderOfInterestRate() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + uint256 debt_C = troveManager.getTroveEntireDebt(troveIDs.C); + uint256 debt_D = troveManager.getTroveEntireDebt(troveIDs.D); + + // E redeems enough to fully redeem A and partially from B + uint256 redeemAmount_1 = debt_A + debt_B / 2; + redeem(E, redeemAmount_1); + + // Check A's Trove debt equals gas comp + assertEq(troveManager.getTroveEntireDebt(troveIDs.A), troveManager.BOLD_GAS_COMPENSATION()); + // Check B coll and debt reduced + assertLt(troveManager.getTroveEntireDebt(troveIDs.B), debt_B); + assertLt(troveManager.getTroveEntireColl(troveIDs.B), coll); + // Check C coll and debt unchanged + assertEq(troveManager.getTroveEntireDebt(troveIDs.C), debt_C); + assertEq(troveManager.getTroveEntireColl(troveIDs.C), coll); + // Check D coll and debt unchanged + assertEq(troveManager.getTroveEntireDebt(troveIDs.D), debt_D); + assertEq(troveManager.getTroveEntireColl(troveIDs.D), coll); + + // E redeems enough to fully redeem B and partially redeem C + uint256 redeemAmount_2 = debt_B / 2 + debt_C / 2; + redeem(E, redeemAmount_2); + + // Check A's Trove debt equals gas comp + assertEq(troveManager.getTroveEntireDebt(troveIDs.B), troveManager.BOLD_GAS_COMPENSATION()); + // Check C coll and debt reduced + assertLt(troveManager.getTroveEntireDebt(troveIDs.C), debt_C); + assertLt(troveManager.getTroveEntireColl(troveIDs.C), coll); + // Check D coll and debt unchanged + assertEq(troveManager.getTroveEntireDebt(troveIDs.D), debt_D); + assertEq(troveManager.getTroveEntireColl(troveIDs.D), coll); + } + + // - Troves can be redeemed down to gas comp + function testFullRedemptionDoesntCloseTroves() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + + // E redeems enough to fully redeem A and B + uint256 redeemAmount_1 = debt_A + debt_B; + redeem(E, redeemAmount_1); + + // Check A and B still open + assertEq(troveManager.getTroveStatus(troveIDs.A), 1); // Status active + assertEq(troveManager.getTroveStatus(troveIDs.B), 1); // Status active + } + + function testFullRedemptionLeavesTrovesWithDebtEqualToGasComp() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + + // E redeems enough to fully redeem A and B + uint256 redeemAmount_1 = debt_A + debt_B; + redeem(E, redeemAmount_1); + + // Check A and B's Trove debt equals gas comp + assertEq(troveManager.getTroveEntireDebt(troveIDs.A), troveManager.BOLD_GAS_COMPENSATION()); + assertEq(troveManager.getTroveEntireDebt(troveIDs.B), troveManager.BOLD_GAS_COMPENSATION()); + } + + function testFullRedemptionSkipsTrovesAtGasCompDebt() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + uint256 debt_C = troveManager.getTroveEntireDebt(troveIDs.C); + + // E redeems enough to fully redeem A and B + uint256 redeemAmount_1 = debt_A + debt_B; + redeem(E, redeemAmount_1); + + // Check A and B's Trove debt equals gas comp + assertEq(troveManager.getTroveEntireDebt(troveIDs.A), troveManager.BOLD_GAS_COMPENSATION()); + assertEq(troveManager.getTroveEntireDebt(troveIDs.B), troveManager.BOLD_GAS_COMPENSATION()); + + // E redeems again, enough to partially redeem C + uint256 redeemAmount_2 = debt_C / 2; + redeem(E, redeemAmount_2); + + // Check A and B still open with debt == gas comp + assertEq(troveManager.getTroveStatus(troveIDs.A), 1); // Status active + assertEq(troveManager.getTroveStatus(troveIDs.B), 1); // Status active + assertEq(troveManager.getTroveEntireDebt(troveIDs.A), troveManager.BOLD_GAS_COMPENSATION()); + assertEq(troveManager.getTroveEntireDebt(troveIDs.B), troveManager.BOLD_GAS_COMPENSATION()); + + // Check C's debt and coll reduced + assertLt(troveManager.getTroveEntireDebt(troveIDs.C), debt_C); + assertLt(troveManager.getTroveEntireColl(troveIDs.C), coll); + } + + // - Accrued Trove interest contributes to redee into debt of a redeemed trove + + function testRedemptionIncludesAccruedTroveInterest() public { + (uint256 coll, uint256 debtRequest, TroveIDs memory troveIDs) = _setupForRedemption(); + + (uint256 entireDebt_A, , uint256 redistDebtGain_A, , uint accruedInterest_A) = troveManager.getEntireDebtAndColl(troveIDs.A); + assertGt(accruedInterest_A, 0); + assertEq(redistDebtGain_A, 0); + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + + // E redeems again, enough to fully redeem A (recorded debt + interest - gas comp), without touching the next trove B + uint256 redeemAmount = troveManager.getTroveDebt(troveIDs.A) + accruedInterest_A - troveManager.BOLD_GAS_COMPENSATION(); + redeem(E, redeemAmount); + + // Check A reduced down to gas comp + assertEq(troveManager.getTroveEntireDebt(troveIDs.A), troveManager.BOLD_GAS_COMPENSATION()); + + // Check B's debt unchanged + assertEq(troveManager.getTroveEntireDebt(troveIDs.B), debt_B); + } + + // TODO: + // individual Trove interest updates for redeemed Troves + + + // - +}