diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index 5a228b2a..94f54612 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -46,6 +46,7 @@ jobs: id: test deploy: + if: false name: Deploy contracts to Liquity v2 Testnet runs-on: ubuntu-latest steps: @@ -128,6 +129,7 @@ jobs: run: pnpm test coverage: + if: false name: Coverage runs-on: ubuntu-latest continue-on-error: true diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index 6a02d920..5be629ec 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -173,24 +173,26 @@ contract ActivePool is Ownable, CheckContract, IActivePool { emit ActivePoolETHBalanceUpdated(newETHBalance); } - function increaseRecordedDebtSum(uint _amount) external override { - _requireCallerIsBOorTroveM(); - uint256 newRecordedDebtSum = recordedDebtSum + _amount; - recordedDebtSum = newRecordedDebtSum; - emit ActivePoolBoldDebtUpdated(newRecordedDebtSum); + function increaseRecordedDebtSum(uint256 _amount) external { + _requireCallerIsTroveManager(); + _changeRecordedDebtSum(_amount, 0); } - function decreaseRecordedDebtSum(uint _amount) external override { - _requireCallerIsBOorTroveMorSP(); - uint256 newRecordedDebtSum = recordedDebtSum - _amount; - - recordedDebtSum = newRecordedDebtSum; + // TODO: remove this once we implement interest minting in redemptions + function decreaseRecordedDebtSum(uint256 _amount) external { + _requireCallerIsTroveManager(); + _changeRecordedDebtSum(0, _amount); + } + function _changeRecordedDebtSum(uint256 _recordedDebtIncrease, uint256 _recordedDebtDecrease) internal { + // Do the arithmetic in 2 steps here to avoid overflow from the decrease + uint256 newRecordedDebtSum = recordedDebtSum + _recordedDebtIncrease; // 1 SLOAD + newRecordedDebtSum -= _recordedDebtDecrease; + recordedDebtSum = newRecordedDebtSum; // 1 SSTORE emit ActivePoolBoldDebtUpdated(newRecordedDebtSum); } - function changeAggWeightedDebtSum(uint256 _oldWeightedRecordedTroveDebt, uint256 _newTroveWeightedRecordedTroveDebt) external { - _requireCallerIsBOorTroveM(); + function _changeAggWeightedDebtSum( uint256 _newTroveWeightedRecordedTroveDebt, uint256 _oldWeightedRecordedTroveDebt) internal { // Do the arithmetic in 2 steps here to avoid overflow from the decrease uint256 newAggWeightedDebtSum = aggWeightedDebtSum + _newTroveWeightedRecordedTroveDebt; // 1 SLOAD newAggWeightedDebtSum -= _oldWeightedRecordedTroveDebt; @@ -206,21 +208,42 @@ contract ActivePool is Ownable, CheckContract, IActivePool { // It does *not* include the Trove's individual accrued interest - this gets accounted for in the aggregate accrued interest. // The net Trove debt change could be positive or negative in a repayment (depending on whether its redistribution gain or repayment amount is larger), // so this function accepts both the increase and the decrease to avoid using (and converting to/from) signed ints. - function mintAggInterest(uint256 _troveDebtIncrease, uint256 _troveDebtDecrease) public { - _requireCallerIsBOorTroveMorSP(); - uint256 aggInterest = calcPendingAggInterest(); - // Mint the new BOLD interest to a mock interest router that would split it and send it onward to SP, LP staking, etc. - // TODO: implement interest routing and SP Bold reward tracking - if (aggInterest > 0) {boldToken.mint(address(interestRouter), aggInterest);} - + function mintAggInterest( + uint256 _troveDebtIncrease, + uint256 _troveDebtDecrease, + uint256 recordedSumIncrease, + uint256 recordedSumDecrease, + uint256 newWeightedRecordedTroveDebt, + uint256 oldWeightedRecordedTroveDebt + ) + external + { + _requireCallerIsBOorTroveM(); + // Do the arithmetic in 2 steps here to avoid overflow from the decrease - uint256 newAggRecordedDebt = aggRecordedDebt + aggInterest + _troveDebtIncrease; // 1 SLOAD + uint256 newAggRecordedDebt = _mintAggInterestNoTroveChange() + _troveDebtIncrease; // 1 SLOAD newAggRecordedDebt -=_troveDebtDecrease; aggRecordedDebt = newAggRecordedDebt; // 1 SSTORE // assert(aggRecordedDebt >= 0) // This should never be negative. If all redistribution gians and all aggregate interest was applied // and all Trove debts were repaid, it should become 0. + _changeRecordedDebtSum(recordedSumIncrease, recordedSumDecrease); + _changeAggWeightedDebtSum(newWeightedRecordedTroveDebt, oldWeightedRecordedTroveDebt); + } + + function mintAggInterestNoTroveChange() external returns (uint256) { + _requireCallerIsSP(); + aggRecordedDebt = _mintAggInterestNoTroveChange(); + } + + function _mintAggInterestNoTroveChange() internal returns (uint256) { + uint256 aggInterest = calcPendingAggInterest(); + // Mint the new BOLD interest to a mock interest router that would split it and send it onward to SP, LP staking, etc. + // TODO: implement interest routing and SP Bold reward tracking + if (aggInterest > 0) {boldToken.mint(address(interestRouter), aggInterest);} + lastAggUpdateTime = block.timestamp; + return aggRecordedDebt + aggInterest; } // --- 'require' functions --- @@ -240,6 +263,11 @@ contract ActivePool is Ownable, CheckContract, IActivePool { "ActivePool: Caller is neither BorrowerOperations nor TroveManager nor StabilityPool"); } + function _requireCallerIsSP() internal view { + require( + msg.sender == stabilityPoolAddress, "ActivePool: Caller is not StabilityPool"); + } + function _requireCallerIsBOorTroveM() internal view { require( msg.sender == borrowerOperationsAddress || diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 7ac44d59..3f78e126 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -49,6 +49,13 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint newEntireDebt; uint newEntireColl; uint stake; + uint256 initialWeightedRecordedTroveDebt; + uint256 newWeightedTroveDebt; + uint256 annualInterestRate; + uint256 troveDebtIncrease; + uint256 troveDebtDecrease; + uint256 recordedDebtIncrease; + uint256 recordedDebtDecrease; } struct LocalVariables_openTrove { @@ -202,7 +209,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // --- Effects & interactions --- - contractsCache.activePool.mintAggInterest(vars.compositeDebt, 0); + uint256 weightedRecordedTroveDebt = vars.compositeDebt * _annualInterestRate; + contractsCache.activePool.mintAggInterest(vars.compositeDebt, 0, vars.compositeDebt, 0, weightedRecordedTroveDebt, 0); // Set the stored Trove properties and mint the NFT vars.stake = contractsCache.troveManager.setTrovePropertiesOnOpen( @@ -224,11 +232,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe contractsCache.boldToken.mint(msg.sender, _boldAmount); contractsCache.boldToken.mint(gasPoolAddress, BOLD_GAS_COMPENSATION); - // Add the whole debt to the recorded debt tracker - contractsCache.activePool.increaseRecordedDebtSum(vars.compositeDebt); - // Add the whole weighted debt to the weighted recorded debt tracker - contractsCache.activePool.changeAggWeightedDebtSum(0, vars.compositeDebt * _annualInterestRate); - emit TroveUpdated(troveId, vars.compositeDebt, _ETHAmount, vars.stake, BorrowerOperation.openTrove); emit BoldBorrowingFeePaid(troveId, vars.BoldFee); // TODO @@ -315,8 +318,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe vars.price = priceFeed.fetchPrice(); - uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(_troveId); - uint256 annualInterestRate = contractsCache.troveManager.getTroveAnnualInterestRate(_troveId); + vars.initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(_troveId); + vars.annualInterestRate = contractsCache.troveManager.getTroveAnnualInterestRate(_troveId); // --- Checks --- @@ -364,14 +367,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe contractsCache.troveManager.getAndApplyRedistributionGains(_troveId); - if (_isDebtIncrease) { - // Increase Trove debt by the drawn debt + redist. gain - activePool.mintAggInterest(_boldChange + vars.redistDebtGain, 0); - } else { - // Increase Trove debt by redist. gain and decrease by the repaid debt - activePool.mintAggInterest(vars.redistDebtGain, _boldChange); - } - // Update the Trove's recorded coll and debt vars.newEntireColl = _updateTroveCollFromAdjustment( contractsCache.troveManager, @@ -393,6 +388,30 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe vars.stake = contractsCache.troveManager.updateStakeAndTotalStakes(_troveId); + vars.newWeightedTroveDebt = vars.newEntireDebt * vars.annualInterestRate; + + if (_isDebtIncrease) { + // Increase Trove debt by the drawn debt + redist. gain + vars.troveDebtIncrease = _boldChange + vars.redistDebtGain; + vars.recordedDebtIncrease = _boldChange + vars.accruedTroveInterest; + } else { + // Increase Trove debt by redist. gain and decrease by the repaid debt + vars.troveDebtIncrease = vars.redistDebtGain; + vars.troveDebtDecrease = _boldChange; + + vars.recordedDebtIncrease = vars.accruedTroveInterest; + vars.recordedDebtDecrease = _boldChange; + } + + activePool.mintAggInterest( + vars.troveDebtIncrease, + vars.troveDebtDecrease, + vars.recordedDebtIncrease, + vars.recordedDebtDecrease, + vars.newWeightedTroveDebt, + vars.initialWeightedRecordedTroveDebt + ); + emit TroveUpdated(_troveId, vars.newEntireDebt, vars.newEntireColl, vars.stake, BorrowerOperation.adjustTrove); emit BoldBorrowingFeePaid(_troveId, vars.BoldFee); // TODO @@ -404,11 +423,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _collChange, _isCollIncrease, _boldChange, - _isDebtIncrease, - vars.accruedTroveInterest + _isDebtIncrease ); - - contractsCache.activePool.changeAggWeightedDebtSum(initialWeightedRecordedTroveDebt, vars.newEntireDebt * annualInterestRate); } function closeTrove(uint256 _troveId) external override { @@ -445,19 +461,17 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // Remove the Trove's initial recorded debt plus its accrued interest from ActivePool.aggRecordedDebt, // but *don't* remove the redistribution gains, since these were not yet incorporated into the sum. - contractsCache.activePool.mintAggInterest(0, initialRecordedTroveDebt + accruedTroveInterest); - - contractsCache.troveManager.removeStake(_troveId); - contractsCache.troveManager.closeTrove(_troveId); - emit TroveUpdated(_troveId, 0, 0, 0, BorrowerOperation.closeTrove); - + uint256 troveDebtDecrease = initialRecordedTroveDebt + accruedTroveInterest; // Remove only the Trove's latest recorded debt (inc. redist. gains) from the recorded debt tracker, // i.e. exclude the accrued interest since it has not been added. // TODO: If/when redist. gains are gas-optimized, exclude them from here too. - contractsCache.activePool.decreaseRecordedDebtSum(initialRecordedTroveDebt + debtRedistGain); + uint256 recordedDebtSumDecrease = initialRecordedTroveDebt + debtRedistGain; + + contractsCache.activePool.mintAggInterest(0, troveDebtDecrease, 0, recordedDebtSumDecrease, 0, initialWeightedRecordedTroveDebt); - // Remove Trove's weighted debt from the weighted sum - activePool.changeAggWeightedDebtSum(initialWeightedRecordedTroveDebt, 0); + contractsCache.troveManager.removeStake(_troveId); + contractsCache.troveManager.closeTrove(_troveId); + emit TroveUpdated(_troveId, 0, 0, 0, BorrowerOperation.closeTrove); // Burn the 200 BOLD gas compensation contractsCache.boldToken.burn(gasPoolAddress, BOLD_GAS_COMPENSATION); @@ -478,7 +492,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint256 entireTroveDebt = _updateActivePoolTrackersNoDebtChange(contractsCache.troveManager, contractsCache.activePool, _troveId, annualInterestRate); - // Update Trove recorded debt and interest-weighted debt sum + // Update Trove recorded debt contractsCache.troveManager.updateTroveDebtFromInterestApplication(_troveId, entireTroveDebt); } @@ -578,9 +592,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe return newEntireDebt; } - // This function incorporates both the Trove's net debt change (repaid/drawn) and its accrued interest. - // Redist. gains have already been applied before this is called. - // TODO: explicitly pass redist. gains too if we gas-optimize them. + // This function mints the BOLD corresponding to the borrower's chosen debt increase + // (it does not mint the accrued interest). function _moveTokensAndETHfromAdjustment ( IActivePool _activePool, @@ -590,20 +603,14 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint _collChange, bool _isCollIncrease, uint _boldChange, - bool _isDebtIncrease, - uint256 _accruedTroveInterest + bool _isDebtIncrease ) internal { if (_isDebtIncrease) { - _activePool.increaseRecordedDebtSum(_boldChange + _accruedTroveInterest); address borrower = _troveManager.ownerOf(_troveId); _boldToken.mint(borrower, _boldChange); } else { - // TODO: Gas optimize this - _activePool.increaseRecordedDebtSum(_accruedTroveInterest); - _activePool.decreaseRecordedDebtSum(_boldChange); - _boldToken.burn(msg.sender, _boldChange); } @@ -639,19 +646,15 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe (, uint256 redistDebtGain) = _troveManager.getAndApplyRedistributionGains(_troveId); - // No debt is issued/repaid, so the net Trove debt change is purely the redistribution gain - _activePool.mintAggInterest(redistDebtGain, 0); - uint256 accruedTroveInterest = _troveManager.calcTroveAccruedInterest(_troveId); uint256 recordedTroveDebt = _troveManager.getTroveDebt(_troveId); uint256 entireTroveDebt = recordedTroveDebt + accruedTroveInterest; - + uint256 newWeightedTroveDebt = entireTroveDebt * _annualInterestRate; // 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); - + // No debt is issued/repaid, so the net Trove debt change is purely the redistribution gain + // TODO: also include redist. gains here in the recordedSumIncrease arg if we gas-optimize them + _activePool.mintAggInterest(redistDebtGain, 0, accruedTroveInterest, 0, newWeightedTroveDebt, initialWeightedRecordedTroveDebt); + return entireTroveDebt; } diff --git a/contracts/src/Interfaces/IActivePool.sol b/contracts/src/Interfaces/IActivePool.sol index 8fdc929a..f4682b40 100644 --- a/contracts/src/Interfaces/IActivePool.sol +++ b/contracts/src/Interfaces/IActivePool.sol @@ -26,11 +26,16 @@ interface IActivePool { function aggWeightedDebtSum() external view returns (uint256); function calcPendingAggInterest() external view returns (uint256); - function mintAggInterest(uint256 _troveDebtIncrease, uint256 _troveDebtDecrease) external; - function changeAggWeightedDebtSum( - uint256 _oldWeightedRecordedTroveDebt, - uint256 _newTroveWeightedRecordedTroveDebt + function mintAggInterest( + uint256 _troveDebtIncrease, + uint256 _troveDebtDecrease, + uint256 recordedSumIncrease, + uint256 recordedSumDecrease, + uint256 newWeightedRecordedTroveDebt, + uint256 oldWeightedRecordedTroveDebt ) external; + + function mintAggInterestNoTroveChange() external returns (uint256); function increaseRecordedDebtSum(uint256 _amount) external; function decreaseRecordedDebtSum(uint256 _amount) external; function sendETH(address _account, uint _amount) external; diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index a5f7a198..94cc530f 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -287,7 +287,7 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { function provideToSP(uint _amount) external override { _requireNonZeroAmount(_amount); - activePool.mintAggInterest(0, 0); + activePool.mintAggInterestNoTroveChange(); uint initialDeposit = deposits[msg.sender].initialValue; @@ -318,7 +318,7 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { uint initialDeposit = deposits[msg.sender].initialValue; _requireUserHasDeposit(initialDeposit); - activePool.mintAggInterest(0, 0); + activePool.mintAggInterestNoTroveChange(); uint depositorETHGain = getDepositorETHGain(msg.sender); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index dd7cdea3..d8fe8707 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -146,6 +146,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana bool recoveryModeAtStart; uint liquidatedDebt; uint liquidatedColl; + uint256 totalRecordedDebtPlusInterestInSequence; } struct LocalVariables_InnerSingleLiquidateFunction { @@ -215,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 --- @@ -651,14 +662,16 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana require(totals.totalDebtInSequence > 0, "TroveManager: nothing to liquidate"); - // Mint aggregate interest - activePool.mintAggInterest(0, totals.totalRecordedDebtInSequence + totals.totalAccruedInterestInSequence); + vars.totalRecordedDebtPlusInterestInSequence = totals.totalRecordedDebtInSequence + totals.totalAccruedInterestInSequence; - // TODO - Gas: combine these into one call to activePool? - // Remove the liquidated recorded debt from the sum - activePoolCached.decreaseRecordedDebtSum(totals.totalRecordedDebtInSequence + totals.totalRedistDebtGainsInSequence); - // Remove the liqudiated weighted recorded debt from the sum - activePool.changeAggWeightedDebtSum(totals.totalWeightedRecordedDebtInSequence, 0); + activePool.mintAggInterest( + 0, + vars.totalRecordedDebtPlusInterestInSequence, + 0, + totals.totalRecordedDebtInSequence + totals.totalRedistDebtGainsInSequence, + 0, + totals.totalWeightedRecordedDebtInSequence + ); // Move liquidated ETH and Bold to the appropriate pools stabilityPoolCached.offset(totals.totalDebtToOffset, totals.totalCollToSendToSP); @@ -824,55 +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); + singleRedemption.newRecordedTroveDebt = entireTroveDebt - singleRedemption.BoldLot; + uint newColl = Troves[_troveId].coll - singleRedemption.ETHLot; - } else { - Troves[_troveId].debt = newDebt; - Troves[_troveId].coll = newColl; - _updateStakeAndTotalStakes(_troveId); - - 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); - // Update Active Pool Bold, and send ETH to account - _contractsCache.activePool.decreaseRecordedDebtSum(_bold); + // TODO: Gas optimize? We update totalStakes N times for a sequence of N Trovres(!). + _updateStakeAndTotalStakes(_troveId); - // send ETH from Active Pool to CollSurplus Pool - _contractsCache.collSurplusPool.accountSurplus(_troveId, _ETH); - _contractsCache.activePool.sendETH(address(_contractsCache.collSurplusPool), _ETH); + emit TroveUpdated( + _troveId, + singleRedemption.newRecordedTroveDebt, newColl, + Troves[_troveId].stake, + TroveManagerOperation.redeemCollateral + ); + + 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 @@ -934,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) { @@ -942,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. @@ -975,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); } @@ -1107,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 25add719..65ff8679 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -153,4 +153,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 e650958e..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); @@ -157,48 +156,48 @@ contract InterestRateAggregate is DevTestSetup { // --- mintAggInterest --- - function testMintAggInterestRevertsWhenNotCalledByBOorSP() public { + function testMintAggInterestRevertsWhenNotCalledByBOorTM() public { // pass positive debt change uint256 debtChange = 37e18; vm.startPrank(A); vm.expectRevert(); - activePool.mintAggInterest(debtChange, 0); + activePool.mintAggInterest(debtChange, 0, 0, 0, 0, 0); vm.stopPrank(); vm.startPrank(address(borrowerOperations)); - activePool.mintAggInterest(debtChange, 0); + activePool.mintAggInterest(debtChange, 0, 0, 0, 0, 0); vm.stopPrank(); - vm.startPrank(address(stabilityPool)); - activePool.mintAggInterest(debtChange, 0); + vm.startPrank(address(troveManager)); + activePool.mintAggInterest(debtChange, 0, 0, 0, 0, 0); vm.stopPrank(); // pass negative debt change vm.startPrank(A); vm.expectRevert(); - activePool.mintAggInterest(0, debtChange); + activePool.mintAggInterest(0, debtChange, 0, 0, 0, 0); vm.stopPrank(); vm.startPrank(address(borrowerOperations)); - activePool.mintAggInterest(0, debtChange); + activePool.mintAggInterest(0, debtChange, 0, 0, 0, 0); vm.stopPrank(); - vm.startPrank(address(stabilityPool)); - activePool.mintAggInterest(0, debtChange); + vm.startPrank(address(troveManager)); + activePool.mintAggInterest(0, debtChange, 0, 0, 0, 0); vm.stopPrank(); // pass 0 debt change vm.startPrank(A); vm.expectRevert(); - activePool.mintAggInterest(0, 0); + activePool.mintAggInterest(0, 0, 0, 0, 0, 0); vm.stopPrank(); vm.startPrank(address(borrowerOperations)); - activePool.mintAggInterest(0, 0); + activePool.mintAggInterest(0, 0, 0, 0, 0, 0); vm.stopPrank(); - vm.startPrank(address(stabilityPool)); - activePool.mintAggInterest(0, 0); + vm.startPrank(address(troveManager)); + activePool.mintAggInterest(0, 0, 0, 0, 0, 0); vm.stopPrank(); } @@ -332,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); @@ -930,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 @@ -957,7 +954,7 @@ contract InterestRateAggregate is DevTestSetup { // --- withdrawBold tests --- - function testWithdrawBoldWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterestPlusBorrowerDebtChange() public { + function testWithdrawBoldWithNoRedistGainsIncreasesAggRecordedDebtByPendingAggInterestPlusBorrowerDebtChange() public { uint256 troveDebtRequest = 2000e18; uint256 debtIncrease = 500e18; @@ -1043,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; @@ -1098,7 +1095,7 @@ contract InterestRateAggregate is DevTestSetup { // --- repayBold tests --- - function testRepayBoldWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterestMinusBorrowerDebtChange() public { + function testRepayBoldWithNoRedistGainsIncreasesAggRecordedDebtByPendingAggInterestMinusBorrowerDebtChange() public { uint256 troveDebtRequest = 3000e18; uint256 debtDecrease = 500e18; @@ -1181,7 +1178,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.lastAggUpdateTime(), block.timestamp); } - function testRepayBoldWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterestMinusDebtChange() public { + function testRepayBoldWithNoRedistGainsIncreasesRecordedDebtSumByTrovesAccruedInterestMinusDebtChange() public { uint256 troveDebtRequest = 3000e18; uint256 debtDecrease = 500e18; @@ -1236,7 +1233,7 @@ contract InterestRateAggregate is DevTestSetup { // --- addColl tests --- - function testAddCollWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + function testAddCollWithNoRedistGainsIncreasesAggRecordedDebtByPendingAggInterest() public { uint256 troveDebtRequest = 3000e18; uint256 collIncrease = 1 ether; @@ -1319,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; @@ -1377,7 +1374,7 @@ contract InterestRateAggregate is DevTestSetup { // --- withdrawColl --- - function testWithdrawCollWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + function testWithdrawCollWithNoRedistGainsIncreasesAggRecordedDebtByPendingAggInterest() public { uint256 troveDebtRequest = 2000e18; uint256 collDecrease = 1 ether; @@ -1460,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; @@ -1518,7 +1515,7 @@ contract InterestRateAggregate is DevTestSetup { // --- applyTroveInterestPermissionless --- - function testApplyTroveInterestPermissionlessWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + function testApplyTroveInterestPermissionlessWithNoRedistGainsIncreasesAggRecordedDebtByPendingAggInterest() public { uint256 troveDebtRequest = 2000e18; // A opens Trove @@ -1608,7 +1605,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.lastAggUpdateTime(), block.timestamp); } - function testApplyTroveInterestPermissionlessWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { + function testApplyTroveInterestPermissionlessWithNoRedistGainsIncreasesRecordedDebtSumByTrovesAccruedInterest() public { uint256 troveDebtRequest = 2000e18; // A opens Trove @@ -1697,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); } @@ -1739,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); } @@ -2229,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); @@ -2252,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; @@ -2296,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; @@ -2391,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 + + + // - +} diff --git a/contracts/test/AccessControlTest.js b/contracts/test/AccessControlTest.js index f9d2cbb7..e8b23ddd 100644 --- a/contracts/test/AccessControlTest.js +++ b/contracts/test/AccessControlTest.js @@ -212,7 +212,7 @@ contract( assert.include(err.message, "revert"); assert.include( err.message, - "Caller is neither BorrowerOperations nor TroveManager" + "ActivePool: Caller is not TroveManager" ); } }); @@ -228,7 +228,7 @@ contract( assert.include(err.message, "revert"); assert.include( err.message, - "Caller is neither BorrowerOperations nor TroveManager nor StabilityPool" + "ActivePool: Caller is not TroveManager" ); } }); diff --git a/contracts/test/CollSurplusPool.js b/contracts/test/CollSurplusPool.js index d6b3861a..57e1d968 100644 --- a/contracts/test/CollSurplusPool.js +++ b/contracts/test/CollSurplusPool.js @@ -48,39 +48,6 @@ contract("CollSurplusPool", async (accounts) => { ], contracts.WETH); }); - it("CollSurplusPool::getETHBalance(): Returns the ETH balance of the CollSurplusPool after redemption", async () => { - const ETH_1 = await collSurplusPool.getETHBalance(); - assert.equal(ETH_1, "0"); - - const price = toBN(dec(100, 18)); - await priceFeed.setPrice(price); - - const { collateral: B_coll, netDebt: B_netDebt } = await openTrove({ - ICR: toBN(dec(200, 16)), - extraParams: { from: B }, - }); - await openTrove({ - extraBoldAmount: B_netDebt, - extraParams: { - from: A, - value: dec(3000, "ether"), - annualInterestRate: toBN(1) // We want A to be further from redemption than B - }, - }); - - // skip bootstrapping phase - await time.increase(timeValues.SECONDS_IN_ONE_WEEK * 2); - - // At ETH:USD = 100, this redemption should leave 1 ether of coll surplus - await th.redeemCollateralAndGetTxObject(A, contracts, B_netDebt); - - const ETH_2 = await collSurplusPool.getETHBalance(); - th.assertIsApproximatelyEqual( - ETH_2, - B_coll.sub(B_netDebt.mul(mv._1e18BN).div(price)) - ); - }); - it("CollSurplusPool: claimColl(): Reverts if caller is not Borrower Operations", async () => { await th.assertRevert( collSurplusPool.claimColl(A, th.addressToTroveId(A), { from: A }), diff --git a/contracts/test/PoolsTest.js b/contracts/test/PoolsTest.js index 414bbf33..e4d35e33 100644 --- a/contracts/test/PoolsTest.js +++ b/contracts/test/PoolsTest.js @@ -41,15 +41,16 @@ contract('StabilityPool', async accounts => { contract('ActivePool', async accounts => { - let activePool, mockBorrowerOperations, WETH + let activePool, mockBorrowerOperations, mockTroveManager, WETH const [owner, alice] = accounts; beforeEach(async () => { WETH = await ERC20.new("WETH", "WETH"); activePool = await ActivePool.new(WETH.address) mockBorrowerOperations = await NonPayableSwitch.new() + mockTroveManager = await NonPayableSwitch.new() const dumbContractAddress = (await NonPayableSwitch.new()).address - await activePool.setAddresses(mockBorrowerOperations.address, dumbContractAddress, dumbContractAddress, dumbContractAddress, dumbContractAddress, dumbContractAddress) + await activePool.setAddresses(mockBorrowerOperations.address, mockTroveManager.address, dumbContractAddress, dumbContractAddress, dumbContractAddress, dumbContractAddress) }) it('getETHBalance(): gets the recorded ETH balance', async () => { @@ -68,17 +69,17 @@ contract('ActivePool', async accounts => { // await activePool.increaseBoldDebt(100, { from: mockBorrowerOperationsAddress }) const increaseBoldDebtData = th.getTransactionData('increaseRecordedDebtSum(uint256)', ['0x64']) - const tx = await mockBorrowerOperations.forward(activePool.address, increaseBoldDebtData) + const tx = await mockTroveManager.forward(activePool.address, increaseBoldDebtData) assert.isTrue(tx.receipt.status) const recordedBold_balanceAfter = await activePool.getRecordedDebtSum() assert.equal(recordedBold_balanceAfter, 100) }) // Decrease - it('decreaseBoldDebt(): decreases the recorded BOLD balance by the correct amount', async () => { + it('decreaseRecordedDebtSum(): decreases the recorded BOLD balance by the correct amount', async () => { // start the pool on 100 wei //await activePool.increaseBoldDebt(100, { from: mockBorrowerOperationsAddress }) const increaseBoldDebtData = th.getTransactionData('increaseRecordedDebtSum(uint256)', ['0x64']) - const tx1 = await mockBorrowerOperations.forward(activePool.address, increaseBoldDebtData) + const tx1 = await mockTroveManager.forward(activePool.address, increaseBoldDebtData) assert.isTrue(tx1.receipt.status) const recordedBold_balanceBefore = await activePool.getRecordedDebtSum() @@ -86,7 +87,7 @@ contract('ActivePool', async accounts => { //await activePool.decreaseBoldDebt(100, { from: mockBorrowerOperationsAddress }) const decreaseBoldDebtData = th.getTransactionData('decreaseRecordedDebtSum(uint256)', ['0x64']) - const tx2 = await mockBorrowerOperations.forward(activePool.address, decreaseBoldDebtData) + const tx2 = await mockTroveManager.forward(activePool.address, decreaseBoldDebtData) assert.isTrue(tx2.receipt.status) const recordedBold_balanceAfter = await activePool.getRecordedDebtSum() assert.equal(recordedBold_balanceAfter, 0) diff --git a/frontend/next.config.js b/frontend/next.config.js index 5206c370..92de6dd9 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,7 +1,18 @@ +const pkg = require("./package.json"); + +const commitHash = require("child_process") + .execSync("git log --pretty=format:\"%h\" -n1") + .toString() + .trim(); + /** @type {import('next').NextConfig} */ module.exports = { output: "export", reactStrictMode: false, + env: { + APP_VERSION: pkg.version, + COMMIT_HASH: commitHash, + }, webpack: (config) => { // RainbowKit related dependencies config.externals.push("pino-pretty", "lokijs", "encoding"); diff --git a/frontend/src/comps/Footer/Footer.tsx b/frontend/src/comps/Footer/Footer.tsx index bbef8596..98160e12 100644 --- a/frontend/src/comps/Footer/Footer.tsx +++ b/frontend/src/comps/Footer/Footer.tsx @@ -3,6 +3,7 @@ import { palette } from "@/src/colors"; import { TextButton } from "@/src/comps/Button/TextButton"; import { useConfigModal } from "@/src/comps/ConfigModal/ConfigModal"; +import { APP_VERSION, COMMIT_HASH } from "@/src/env"; import { css } from "@/styled-system/css"; import Image from "next/image"; import Link from "next/link"; @@ -12,11 +13,21 @@ import logo from "./footer-logo.svg"; export function Footer() { const { open: openConfigModal } = useConfigModal(); - const links: Array<[string, string | (() => void)]> = [ - ["Liquity", "https://liquity.org"], - ["Disclaimer", "https://example.org"], + const links: Array<[ + string | [string, string], + string | (() => void), + ]> = [ + // ["Liquity", "https://liquity.org"], + // ["Disclaimer", "https://example.org"], // ["Privacy Policy", "https://example.org"], ["Settings", openConfigModal], + [ + [ + `${APP_VERSION}-${COMMIT_HASH}`, + `Version ${APP_VERSION} (${COMMIT_HASH})`, + ], + "https://github.com/liquity/bold/tree/" + COMMIT_HASH, + ], ]; return ( @@ -61,40 +72,45 @@ export function Footer() { gap: 16, })} > - {links.map(([label, href], index) => ( -
  • - {typeof href === "string" - ? ( - - {label} - - ) - : ( - - )} -
  • - ))} + {links.map(([labelTitle, href], index) => { + const [label, title] = Array.isArray(labelTitle) ? labelTitle : [labelTitle, undefined]; + return ( +
  • + {typeof href === "string" + ? ( + + {label} + + ) + : ( + + )} +
  • + ); + })} diff --git a/frontend/src/env.ts b/frontend/src/env.ts index 3344125a..12c325f3 100644 --- a/frontend/src/env.ts +++ b/frontend/src/env.ts @@ -2,7 +2,9 @@ import z from "zod"; import { zAddress } from "./zod-utils"; export const EnvSchema = z.object({ + APP_VERSION: z.string(), CHAIN_ID: z.string(), + COMMIT_HASH: z.string(), CONTRACT_ACTIVE_POOL: zAddress(), CONTRACT_BOLD_TOKEN: zAddress(), CONTRACT_BORROWER_OPERATIONS: zAddress(), @@ -24,7 +26,9 @@ export const EnvSchema = z.object({ export type Env = z.infer; export const { + APP_VERSION, CHAIN_ID, + COMMIT_HASH, CONTRACT_ACTIVE_POOL, CONTRACT_BOLD_TOKEN, CONTRACT_BORROWER_OPERATIONS, @@ -39,7 +43,9 @@ export const { CONTRACT_TROVE_MANAGER, WALLET_CONNECT_PROJECT_ID, } = EnvSchema.parse({ + APP_VERSION: process.env.APP_VERSION, CHAIN_ID: process.env.NEXT_PUBLIC_CHAIN_ID, + COMMIT_HASH: process.env.COMMIT_HASH, CONTRACT_ACTIVE_POOL: process.env.NEXT_PUBLIC_CONTRACT_ACTIVE_POOL, CONTRACT_BOLD_TOKEN: process.env.NEXT_PUBLIC_CONTRACT_BOLD_TOKEN, CONTRACT_BORROWER_OPERATIONS: process.env.NEXT_PUBLIC_CONTRACT_BORROWER_OPERATIONS,