diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 7482fd1de..4f94dfb32 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -16,7 +16,7 @@ import "forge-std/console2.sol"; contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOperations { string constant public NAME = "BorrowerOperations"; - + // --- Connected contract declarations --- ITroveManager public troveManager; @@ -49,8 +49,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint newICR; uint newTCR; uint BoldFee; - uint newDebt; - uint newColl; + uint newEntireDebt; + uint newEntireColl; uint stake; } @@ -166,9 +166,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe vars.BoldFee; - if (!isRecoveryMode) { - // TODO: implement interest rate charges - } _requireAtLeastMinNetDebt(_boldAmount); // ICR is based on the composite debt, i.e. the requested Bold amount + Bold gas comp. @@ -251,33 +248,16 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // TODO: Delegation functionality ContractsCacheTMAP memory contractsCache = ContractsCacheTMAP(troveManager, activePool); - // --- Checks --- + _requireValidAnnualInterestRate(_newAnnualInterestRate); _requireTroveisActive(contractsCache.troveManager, msg.sender); - uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(msg.sender); - - // --- Effects --- - - (, uint256 redistDebtGain) = contractsCache.troveManager.getAndApplyRedistributionGains(msg.sender); - - // No debt is issued/repaid, so the net Trove debt change is purely the redistribution gain - contractsCache.activePool.mintAggInterest(redistDebtGain, 0); - - uint256 accruedTroveInterest = contractsCache.troveManager.calcTroveAccruedInterest(msg.sender); - uint256 recordedTroveDebt = contractsCache.troveManager.getTroveDebt(msg.sender); - uint256 entireTroveDebt = recordedTroveDebt + accruedTroveInterest; + uint256 entireTroveDebt = _updateActivePoolTrackersNoDebtChange(contractsCache.troveManager, contractsCache.activePool, msg.sender, _newAnnualInterestRate); sortedTroves.reInsert(msg.sender, _newAnnualInterestRate, _upperHint, _lowerHint); // Update Trove recorded debt and interest-weighted debt sum contractsCache.troveManager.updateTroveDebtAndInterest(msg.sender, entireTroveDebt, _newAnnualInterestRate); - - // Add only the Trove's accrued interest to the recorded debt tracker since we have already applied redist. gains. - // TODO: include redist. gains here if we gas-optimize them - contractsCache.activePool.increaseRecordedDebtSum(accruedTroveInterest); - // Remove the old weighted recorded debt and and add the new one to the relevant tracker - contractsCache.activePool.changeAggWeightedDebtSum(initialWeightedRecordedTroveDebt, entireTroveDebt * _newAnnualInterestRate); } /* @@ -351,13 +331,12 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } // Update the Trove's recorded debt and coll - (vars.newColl) = _updateTroveCollFromAdjustment(contractsCache.troveManager, _borrower, vars.collChange, vars.isCollIncrease); - uint256 newEntireDebt = vars.entireDebt + _boldChange; - contractsCache.troveManager.updateTroveDebt(_borrower, newEntireDebt); - + vars.newEntireColl = _updateTroveCollFromAdjustment(contractsCache.troveManager, _borrower, vars.collChange, vars.isCollIncrease); + vars.newEntireDebt = _updateTroveDebtFromAdjustment(contractsCache.troveManager, _borrower, vars.entireDebt, _boldChange, _isDebtIncrease); + vars.stake = contractsCache.troveManager.updateStakeAndTotalStakes(_borrower); - emit TroveUpdated(_borrower, vars.newDebt, vars.newColl, vars.stake, BorrowerOperation.adjustTrove); + emit TroveUpdated(_borrower, vars.newEntireDebt, vars.newEntireColl, vars.stake, BorrowerOperation.adjustTrove); emit BoldBorrowingFeePaid(msg.sender, vars.BoldFee); _moveTokensAndETHfromAdjustment( @@ -371,7 +350,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe vars.accruedTroveInterest ); - contractsCache.activePool.changeAggWeightedDebtSum(initialWeightedRecordedTroveDebt, newEntireDebt * annualInterestRate); + contractsCache.activePool.changeAggWeightedDebtSum(initialWeightedRecordedTroveDebt, vars.newEntireDebt * annualInterestRate); } function closeTrove() external override { @@ -427,6 +406,20 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe contractsCache.activePool.sendETH(msg.sender, entireTroveColl); } + function applyTroveInterestPermissionless(address _borrower) external { + ContractsCacheTMAP memory contractsCache = ContractsCacheTMAP(troveManager, activePool); + + _requireTroveIsStale(contractsCache.troveManager, _borrower); + _requireTroveisActive(contractsCache.troveManager, _borrower); + + uint256 annualInterestRate = contractsCache.troveManager.getTroveAnnualInterestRate(_borrower); + + uint256 entireTroveDebt = _updateActivePoolTrackersNoDebtChange(contractsCache.troveManager, contractsCache.activePool, _borrower, annualInterestRate); + + // Update Trove recorded debt and interest-weighted debt sum + contractsCache.troveManager.updateTroveDebt(_borrower, entireTroveDebt); + } + /** * Claim remaining collateral from a redemption or from a liquidation with ICR > MCR in Recovery Mode */ @@ -459,7 +452,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } } - // Update trove's coll whether they added or removed collateral + // Update Trove's coll whether they added or removed collateral. Assumes any ETH redistribution gain was already applied + // to the Trove's coll. function _updateTroveCollFromAdjustment ( ITroveManager _troveManager, @@ -470,10 +464,28 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe internal returns (uint256) { - uint newColl = (_isCollIncrease) ? _troveManager.increaseTroveColl(_borrower, _collChange) + uint newEntireColl = _isCollIncrease ? _troveManager.increaseTroveColl(_borrower, _collChange) : _troveManager.decreaseTroveColl(_borrower, _collChange); - return (newColl); + return newEntireColl; + } + + // Update Trove's coll whether they increased or decreased debt. Assumes any debt redistribution gain was already applied + // to the Trove's debt. + function _updateTroveDebtFromAdjustment( + ITroveManager _troveManager, + address _borrower, + uint256 _oldEntireDebt, + uint256 _debtChange, + bool _isDebtIncrease + ) + internal + returns (uint256) + { + uint newEntireDebt = _isDebtIncrease ? _oldEntireDebt + _debtChange : _oldEntireDebt - _debtChange; + _troveManager.updateTroveDebt(_borrower, newEntireDebt); + + return newEntireDebt; } // This function incorporates both the Trove's net debt change (repaid/drawn) and its accrued interest. @@ -516,6 +528,37 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe require(success, "BorrowerOps: Sending ETH to ActivePool failed"); } + function _updateActivePoolTrackersNoDebtChange + ( + ITroveManager _troveManager, + IActivePool _activePool, + address _borrower, + uint256 _annualInterestRate + ) + internal + returns (uint256) + { + uint256 initialWeightedRecordedTroveDebt = _troveManager.getTroveWeightedRecordedDebt(_borrower); + // --- Effects --- + + (, uint256 redistDebtGain) = _troveManager.getAndApplyRedistributionGains(_borrower); + + // No debt is issued/repaid, so the net Trove debt change is purely the redistribution gain + _activePool.mintAggInterest(redistDebtGain, 0); + + uint256 accruedTroveInterest = _troveManager.calcTroveAccruedInterest(_borrower); + uint256 recordedTroveDebt = _troveManager.getTroveDebt(_borrower); + uint256 entireTroveDebt = recordedTroveDebt + accruedTroveInterest; + + // Add only the Trove's accrued interest to the recorded debt tracker since we have already applied redist. gains. + // TODO: include redist. gains here if we gas-optimize them + _activePool.increaseRecordedDebtSum(accruedTroveInterest); + // Remove the old weighted recorded debt and and add the new one to the relevant tracker + _activePool.changeAggWeightedDebtSum(initialWeightedRecordedTroveDebt, entireTroveDebt * _annualInterestRate); + + return entireTroveDebt; + } + // --- 'Require' wrapper functions --- function _requireSingularCollChange(uint _collWithdrawal) internal view { @@ -635,6 +678,10 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe require(_annualInterestRate <= MAX_ANNUAL_INTEREST_RATE, "Interest rate must not be greater than max"); } + function _requireTroveIsStale(ITroveManager _troveManager, address _borrower) internal view { + require(_troveManager.troveIsStale(_borrower), "BO: Trove must be stale"); + } + // --- ICR and TCR getters --- // Compute the new collateral ratio, considering the change in coll and debt. Assumes 0 pending rewards. @@ -693,14 +740,11 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe { uint totalColl = getEntireSystemColl(); uint totalDebt = getEntireSystemDebt(); - console2.log(totalDebt, "BO:totalSystemDebt"); - console2.log(totalColl, "BO:totalSystemColl"); totalColl = _isCollIncrease ? totalColl + _collChange : totalColl - _collChange; totalDebt = _isDebtIncrease ? totalDebt + _debtChange : totalDebt - _debtChange; uint newTCR = LiquityMath._computeCR(totalColl, totalDebt, _price); - console2.log(newTCR, "BO:newTCR"); return newTCR; } diff --git a/contracts/src/Interfaces/IBorrowerOperations.sol b/contracts/src/Interfaces/IBorrowerOperations.sol index 588cacd93..66d3e4cd4 100644 --- a/contracts/src/Interfaces/IBorrowerOperations.sol +++ b/contracts/src/Interfaces/IBorrowerOperations.sol @@ -45,4 +45,6 @@ interface IBorrowerOperations is ILiquityBase { function getCompositeDebt(uint _debt) external pure returns (uint); function adjustTroveInterestRate(uint _newAnnualInterestRate, address _upperHint, address _lowerHint) external; + + function applyTroveInterestPermissionless(address _borrower) external; } diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index a022a2e4e..83dad8241 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -96,6 +96,8 @@ interface ITroveManager is ILiquityBase { function getTroveLastDebtUpdateTime(address _borrower) external view returns (uint); + function troveIsStale(address _borrower) external view returns (bool); + function setTrovePropertiesOnOpen(address _borrower, uint256 _coll, uint256 _debt, uint256 _annualInterestRate) external returns (uint256); function increaseTroveColl(address _borrower, uint _collIncrease) external returns (uint); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 5cab4cd32..e79422f39 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -35,6 +35,7 @@ contract TroveManager is LiquityBase, ITroveManager, Ownable, CheckContract { uint constant public SECONDS_IN_ONE_MINUTE = 60; uint256 constant public SECONDS_IN_ONE_YEAR = 31536000; // 60 * 60 * 24 * 365, + uint256 constant public STALE_TROVE_DURATION = 7776000; // 90 days: 60*60*24*90 = 7776000 /* * Half-life of 12h. 12h = 720 min @@ -1461,6 +1462,10 @@ contract TroveManager is LiquityBase, ITroveManager, Ownable, CheckContract { return Troves[_borrower].lastDebtUpdateTime; } + function troveIsStale(address _borrower) external view returns (bool) { + return block.timestamp - Troves[_borrower].lastDebtUpdateTime > STALE_TROVE_DURATION; + } + // --- Trove property setters, called by BorrowerOperations --- function setTrovePropertiesOnOpen(address _borrower, uint256 _coll, uint256 _debt, uint256 _annualInterestRate) external returns (uint256) { diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index 9a11237e8..c54bb8098 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -137,6 +137,12 @@ contract BaseTest is Test { vm.stopPrank(); } + function applyTroveInterestPermissionless(address _from, address _borrower) public { + vm.startPrank(_from); + borrowerOperations.applyTroveInterestPermissionless(_borrower); + vm.stopPrank(); + } + function transferBold(address _from, address _to, uint256 _amount) public { vm.startPrank(_from); boldToken.transfer(_to, _amount); diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index c6b4dd4a8..78c6bba80 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -1215,7 +1215,7 @@ contract InterestRateAggregate is DevTestSetup { // A adds coll addColl(A, collIncrease); - // Check recorded debt sum increases by the accrued interest plus debt change + // Check recorded debt sum increases by the accrued interest assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest); } @@ -1334,7 +1334,7 @@ contract InterestRateAggregate is DevTestSetup { // A withdraw coll withdrawColl(A, collDecrease); - // Check recorded debt sum increases by the accrued interest plus debt change + // Check recorded debt sum increases by the accrued interest assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest); } @@ -1367,7 +1367,136 @@ contract InterestRateAggregate is DevTestSetup { // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); } + + // --- applyTroveInterestPermissionless --- + + function testApplyTroveInterestPermissionlessWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + uint256 troveDebtRequest = 2000e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward past such that trove is Stale + vm.warp(block.timestamp + 90 days + 1); + // Confirm Trove is stale + assertTrue(troveManager.troveIsStale(A)); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, A); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); + } + + function testApplyTroveInterestPermissionlessReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + uint256 collDecrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time such that trove is Stale + vm.warp(block.timestamp + 90 days + 1); + // Confirm Trove is stale + assertTrue(troveManager.troveIsStale(A)); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, A); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testApplyTroveInterestPermissionlessUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time such that trove is Stale + vm.warp(block.timestamp + 90 days + 1); + // Confirm Trove is stale + assertTrue(troveManager.troveIsStale(A)); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, A); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + function testApplyTroveInterestPermissionlessWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { + uint256 troveDebtRequest = 2000e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time such that trove is Stale + vm.warp(block.timestamp + 90 days + 1); + // Confirm Trove is stale + assertTrue(troveManager.troveIsStale(A)); + + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(A); + assertEq(pendingRedistDebtGain, 0); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + assertGt(accruedTroveInterest, 0); + + // Get current recorded active debt + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, A); + + // Check recorded debt sum increases by the accrued interest + assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest); + } + + function testApplyTroveInterestPermissionlessAdjustsWeightedDebtSumCorrectly() public { + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + + // fast-forward time such that trove is Stale + vm.warp(block.timestamp + 90 days + 1); + // Confirm Trove is stale + assertTrue(troveManager.troveIsStale(A)); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, A); + + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); + uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * interestRate; + + // Weighted debt should have increased due to interest being applied + assertGt(expectedNewRecordedWeightedDebt, oldRecordedWeightedDebt); + + // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); + } // TODO: mixed collateral & debt adjustment opps // TODO: tests with pending debt redist. gain >0 + // TODO: tests that show total debt and TCR doesnt change under user ops + // TODO: Basic TCR getter tests + // TODO: Test total debt invariant holds i.e. (D + S * delta_T) == sum_of_all_entire_trove_debts. } diff --git a/contracts/src/test/interestRateBasic.t.sol b/contracts/src/test/interestRateBasic.t.sol index ac3ecdfee..414c23bd8 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -139,6 +139,54 @@ contract InterestRateBasic is DevTestSetup { assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); } + function testAdjustTroveInterestRateSetsReducesPendingInterestTo0() public { + priceFeed.setPrice(2000e18); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(A), 0); + + changeInterestRateNoHints(A, 75e16); + + assertEq(troveManager.calcTroveAccruedInterest(A), 0); + } + + function testAdjustTroveInterestRateDoesNotChangeEntireTroveDebt() public { + priceFeed.setPrice(2000e18); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + + vm.warp(block.timestamp + 1 days); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + assertGt(entireTroveDebt_1, 0); + + changeInterestRateNoHints(A, 75e16); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + assertEq(entireTroveDebt_1, entireTroveDebt_2); + } + + function testAdjustTroveInterestRateNoRedistGainsIncreasesRecordedDebtByAccruedInterest() public { + priceFeed.setPrice(2000e18); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); + assertGt(recordedTroveDebt_1, 0); + + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + + changeInterestRateNoHints(A, 75e16); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); + } + function testAdjustTroveInterestRateInsertsToCorrectPositionInSortedList() public { priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); @@ -228,15 +276,409 @@ contract InterestRateBasic is DevTestSetup { function testWithdrawBoldSetsTroveLastDebtUpdateTimeToNow() public { priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, 2000e18, 0); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 boldWithdrawal = 500e18; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + + // A draws more debt + withdrawBold100pctMaxFee(A, boldWithdrawal); + assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + } + + function testWithdrawBoldReducesTroveAccruedInterestTo0() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 boldWithdrawal = 500e18; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(A), 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, boldWithdrawal); + + assertEq(troveManager.calcTroveAccruedInterest(A), 0); + } + + function testWithdrawBoldIncreasesEntireTroveDebtByWithdrawnAmount() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 boldWithdrawal = 500e18; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + assertGt(entireTroveDebt_1, 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, boldWithdrawal); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + + assertEq(entireTroveDebt_2, entireTroveDebt_1 + boldWithdrawal); + } + + function testWithdrawBoldIncreasesRecordedTroveDebtByAccruedInterestPlusWithdrawnAmount() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 boldWithdrawal = 500e18; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + + // A draws more debt + withdrawBold100pctMaxFee(A, boldWithdrawal); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest + boldWithdrawal); + } + + // --- repayBold --- + + function testRepayBoldSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 3000e18; + uint256 interestRate = 25e16; + uint256 boldRepayment = 500e18; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + + // A repays bold + repayBold(A, boldRepayment); + + assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + } + + function testRepayBoldReducesTroveAccruedInterestTo0() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 3000e18; + uint256 interestRate = 25e16; + uint256 boldRepayment = 500e18; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(A), 0); + + // A repays bold + repayBold(A, boldRepayment); + + assertEq(troveManager.calcTroveAccruedInterest(A), 0); + } + + function testRepayBoldReducesEntireTroveDebtByRepaidAmount() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 3000e18; + uint256 interestRate = 25e16; + uint256 boldRepayment = 500e18; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + assertGt(entireTroveDebt_1, 0); + + // A repays bold + repayBold(A, boldRepayment); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + + + assertEq(entireTroveDebt_2, entireTroveDebt_1 - boldRepayment); + } + + function testRepayBoldChangesRecordedTroveDebtByAccruedInterestMinusRepaidAmount() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 3000e18; + uint256 interestRate = 25e16; + uint256 boldRepayment = 500e18; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + + // A repays bold + repayBold(A, boldRepayment); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest - boldRepayment); + } + + // --- addColl --- + + function testAddCollSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collIncrease = 1 ether; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); - // // A draws more debt - withdrawBold100pctMaxFee(A, 500e18); + // A adds coll + addColl(A, collIncrease); + assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); } + + function testAddCollReducesTroveAccruedInterestTo0() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collIncrease = 1 ether; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(A), 0); + + // A adds coll + addColl(A, collIncrease); + + assertEq(troveManager.calcTroveAccruedInterest(A), 0); + } + + function testAddCollDoesntChangeEntireTroveDebt() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collIncrease = 1 ether; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + assertGt(entireTroveDebt_1, 0); + + // A adds coll + addColl(A, collIncrease); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + + assertEq(entireTroveDebt_2, entireTroveDebt_1); + } + + function testAddCollIncreasesRecordedTroveDebtByAccruedInterest() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collIncrease = 1 ether; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + + // A adds coll + addColl(A, collIncrease); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); + } + + // --- withdrawColl --- + + function testWithdrawCollSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collDecrease = 1 ether; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + + // A withdraws coll + withdrawColl(A, collDecrease); + + assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + } + + function testWithdrawCollReducesTroveAccruedInterestTo0() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collDecrease = 1 ether; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(A), 0); + + // A withdraws coll + withdrawColl(A, collDecrease); + + assertEq(troveManager.calcTroveAccruedInterest(A), 0); + } + + function testWithdrawCollDoesntChangeEntireTroveDebt() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collDecrease = 1 ether; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + assertGt(entireTroveDebt_1, 0); + + // A withdraws coll + withdrawColl(A, collDecrease); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + + assertEq(entireTroveDebt_2, entireTroveDebt_1); + } + + function testWithdrawCollIncreasesRecordedTroveDebtByAccruedInterest() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collDecrease = 1 ether; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + + // A withdraws coll + withdrawColl(A, collDecrease); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); + } + + // --- applyTroveInterestPermissionless --- + + function testApplyTroveInterestPermissionlessSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + // Fast-forward time such that trove is Stale + vm.warp(block.timestamp + 90 days + 1); + // Confirm Trove is stale + assertTrue(troveManager.troveIsStale(A)); + + assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, A); + + assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + } + + function testApplyTroveInterestPermissionlessReducesTroveAccruedInterestTo0() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + // Fast-forward time such that trove is Stale + vm.warp(block.timestamp + 90 days + 1); + // Confirm Trove is stale + assertTrue(troveManager.troveIsStale(A)); + + assertGt(troveManager.calcTroveAccruedInterest(A), 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, A); + + assertEq(troveManager.calcTroveAccruedInterest(A), 0); + } + + function testApplyTroveInterestPermissionlessDoesntChangeEntireTroveDebt() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + // Fast-forward time such that trove is Stale + vm.warp(block.timestamp + 90 days + 1); + // Confirm Trove is stale + assertTrue(troveManager.troveIsStale(A)); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + assertGt(entireTroveDebt_1, 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, A); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + + assertEq(entireTroveDebt_2, entireTroveDebt_1); + } + + function testApplyTroveInterestPermissionlessIncreasesRecordedTroveDebtByAccruedInterest() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + // Fast-forward time such that trove is Stale + vm.warp(block.timestamp + 90 days + 1); + // Confirm Trove is stale + assertTrue(troveManager.troveIsStale(A)); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, A); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); + } } - \ No newline at end of file + +// commit + next: liqs apply interest \ No newline at end of file