From 3602904f3178365350934ad76abda5300a9a0c9d Mon Sep 17 00:00:00 2001 From: RickGriff Date: Thu, 8 Feb 2024 16:34:34 +0700 Subject: [PATCH 01/18] Include agg. interest state updates in to openTrove --- contracts/src/BoldToken.sol | 8 +- contracts/src/BorrowerOperations.sol | 63 +++-- contracts/src/Interfaces/IInterestRouter.sol | 7 + contracts/src/Interfaces/ILiquityBase.sol | 1 + contracts/src/Interfaces/ITroveManager.sol | 36 ++- contracts/src/MockInterestRouter.sol | 7 + contracts/src/MultiTroveGetter.sol | 6 +- contracts/src/TroveManager.sol | 181 ++++++++++--- contracts/src/test/TestContracts/BaseTest.sol | 30 ++- .../src/test/TestContracts/DevTestSetup.sol | 5 +- contracts/src/test/deployment.t.sol | 7 + .../src/test/interestRateAggregate.t.sol | 244 ++++++++++++++++++ contracts/src/test/interestRateBasic.t.sol | 23 +- contracts/test/TroveManagerTest.js | 10 +- contracts/utils/deploymentHelpers.js | 11 +- 15 files changed, 543 insertions(+), 96 deletions(-) create mode 100644 contracts/src/Interfaces/IInterestRouter.sol create mode 100644 contracts/src/MockInterestRouter.sol create mode 100644 contracts/src/test/interestRateAggregate.t.sol diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol index 96d0206f..8bf560b2 100644 --- a/contracts/src/BoldToken.sol +++ b/contracts/src/BoldToken.sol @@ -95,7 +95,7 @@ contract BoldToken is CheckContract, IBoldToken { // --- Functions for intra-Liquity calls --- function mint(address _account, uint256 _amount) external override { - _requireCallerIsBorrowerOperations(); + _requireCallerIsBOorTM(); _mint(_account, _amount); } @@ -258,8 +258,10 @@ contract BoldToken is CheckContract, IBoldToken { ); } - function _requireCallerIsBorrowerOperations() internal view { - require(msg.sender == borrowerOperationsAddress, "BoldToken: Caller is not BorrowerOperations"); + function _requireCallerIsBOorTM() internal view { + require(msg.sender == borrowerOperationsAddress || + msg.sender == troveManagerAddress, + "BoldToken: Caller is not BO or TM"); } function _requireCallerIsBOorTroveMorSP() internal view { diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 96257f24..8d882277 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -131,7 +131,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe priceFeed = IPriceFeed(_priceFeedAddress); sortedTroves = ISortedTroves(_sortedTrovesAddress); boldToken = IBoldToken(_boldTokenAddress); - + emit TroveManagerAddressChanged(_troveManagerAddress); emit ActivePoolAddressChanged(_activePoolAddress); emit DefaultPoolAddressChanged(_defaultPoolAddress); @@ -176,8 +176,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint256 troveId = uint256(keccak256(abi.encode(_owner, _ownerIndex))); _requireTroveisNotActive(contractsCache.troveManager, troveId); - // TODO: apply aggregate pending interest, and take snapshot of current timestamp. - vars.BoldFee; vars.netDebt = _boldAmount; @@ -189,7 +187,9 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // ICR is based on the composite debt, i.e. the requested Bold amount + Bold borrowing fee + Bold gas comp. vars.compositeDebt = _getCompositeDebt(vars.netDebt); assert(vars.compositeDebt > 0); - + + troveManager.mintAggInterest(int256(vars.compositeDebt)); + vars.ICR = LiquityMath._computeCR(_ETHAmount, vars.compositeDebt, vars.price); if (isRecoveryMode) { @@ -197,7 +197,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } else { _requireICRisAboveMCR(vars.ICR); uint newTCR = _getNewTCRFromTroveChange(_ETHAmount, true, vars.compositeDebt, true, vars.price); // bools: coll increase, debt increase - _requireNewTCRisAboveCCR(newTCR); + _requireNewTCRisAboveCCR(newTCR); } // Set the stored Trove properties and mint the NFT @@ -205,7 +205,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _owner, troveId, _ETHAmount, - vars.compositeDebt, + vars.compositeDebt, _annualInterestRate ); @@ -274,9 +274,9 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _requireTroveisActive(troveManagerCached, _troveId); // TODO: apply individual and aggregate pending interest, and take snapshots of current timestamp. - // TODO: determine how applying pending interest should interact / be sequenced with applying pending rewards from redistributions. + // TODO: determine how applying pending interest should interact / be sequenced with applying pending rewards from redistributions. - troveManagerCached.applyPendingRewards(_troveId); + troveManagerCached.getAndApplyRedistributionGains(msg.sender); sortedTroves.reInsert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); @@ -284,7 +284,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } /* - * _adjustTrove(): Alongside a debt change, this function can perform either a collateral top-up or a collateral withdrawal. + * _adjustTrove(): Alongside a debt change, this function can perform either a collateral top-up or a collateral withdrawal. */ function _adjustTrove( address _sender, @@ -318,26 +318,36 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // TODO: apply individual and aggregate pending interest, and take snapshots of current timestamp. - contractsCache.troveManager.applyPendingRewards(_troveId); + contractsCache.troveManager.getAndApplyRedistributionGains(_borrower); + + // Get the collChange based on whether or not ETH was sent in the transaction + (vars.collChange, vars.isCollIncrease) = _getCollChange(msg.value, _collWithdrawal); vars.netDebtChange = _boldChange; // If the adjustment incorporates a debt increase and system is in Normal Mode, then trigger a borrowing fee - if (_isDebtIncrease && !isRecoveryMode) { + if (_isDebtIncrease && !isRecoveryMode) { // TODO: implement interest rate charges } - vars.debt = contractsCache.troveManager.getTroveDebt(_troveId); - vars.coll = contractsCache.troveManager.getTroveColl(_troveId); - + (vars.debt, vars.coll, , , ) = contractsCache.troveManager.getEntireDebtAndColl(_troveId); + // Get the trove's old ICR before the adjustment, and what its new ICR will be after the adjustment vars.oldICR = LiquityMath._computeCR(vars.coll, vars.debt, vars.price); - vars.newICR = _getNewICRFromTroveChange(vars.coll, vars.debt, _collChange, _isCollIncrease, vars.netDebtChange, _isDebtIncrease, vars.price); + vars.newICR = _getNewICRFromTroveChange( + vars.coll, + vars.debt, + _collChange, + _isCollIncrease, + vars.netDebtChange, + _isDebtIncrease, + vars.price + ); assert(_isCollIncrease || _collChange <= vars.coll); // TODO: do we still need this? // Check the adjustment satisfies all conditions for the current system mode _requireValidAdjustmentInCurrentMode(isRecoveryMode, _collChange, _isCollIncrease, _isDebtIncrease, vars); - + // When the adjustment is a debt repayment, check it's a valid amount and that the caller has enough Bold if (!_isDebtIncrease && _boldChange > 0) { _requireAtLeastMinNetDebt(_getNetDebt(vars.debt) - vars.netDebtChange); @@ -345,6 +355,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _requireSufficientBoldBalance(contractsCache.boldToken, msg.sender, vars.netDebtChange); } + // Finally actually update the Trove's recorded debt and coll + // TODO: use the composite update function (vars.newColl, vars.newDebt) = _updateTroveFromAdjustment( contractsCache.troveManager, _sender, @@ -362,6 +374,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe emit BoldBorrowingFeePaid(_troveId, vars.BoldFee); // Use the unmodified _boldChange here, as we don't send the fee to the user + //TODO: any macro changes due to interest rates here? _moveTokensAndETHfromAdjustment( contractsCache.activePool, contractsCache.boldToken, @@ -387,7 +400,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // TODO: apply individual and aggregate pending interest, and take snapshots of current timestamp. - troveManagerCached.applyPendingRewards(_troveId); + troveManagerCached.getAndApplyRedistributionGains(msg.sender); uint coll = troveManagerCached.getTroveColl(_troveId); uint debt = troveManagerCached.getTroveDebt(_troveId); @@ -560,18 +573,18 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe require(_collWithdrawal == 0 || _isCollIncrease, "BorrowerOps: Collateral withdrawal not permitted Recovery Mode"); } - function _requireValidAdjustmentInCurrentMode + function _requireValidAdjustmentInCurrentMode ( bool _isRecoveryMode, uint _collChange, bool _isCollIncrease, bool _isDebtIncrease, LocalVariables_adjustTrove memory _vars - ) - internal - view + ) + internal + view { - /* + /* *In Recovery Mode, only allow: * * - Pure collateral top-up @@ -589,11 +602,11 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe if (_isDebtIncrease) { _requireICRisAboveCCR(_vars.newICR); _requireNewICRisAboveOldICR(_vars.newICR, _vars.oldICR); - } + } } else { // if Normal Mode _requireICRisAboveMCR(_vars.newICR); _vars.newTCR = _getNewTCRFromTroveChange(_collChange, _isCollIncrease, _vars.netDebtChange, _isDebtIncrease, _vars.price); - _requireNewTCRisAboveCCR(_vars.newTCR); + _requireNewTCRisAboveCCR(_vars.newTCR); } } @@ -640,7 +653,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal pure { - require(_annualInterestRate <= MAX_ANNUAL_INTEREST_RATE, "Interest rate must not be greater than max"); + require(_annualInterestRate <= MAX_ANNUAL_INTEREST_RATE, "Interest rate must not be greater than max"); } // --- ICR and TCR getters --- diff --git a/contracts/src/Interfaces/IInterestRouter.sol b/contracts/src/Interfaces/IInterestRouter.sol new file mode 100644 index 00000000..60fd652d --- /dev/null +++ b/contracts/src/Interfaces/IInterestRouter.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +interface IInterestRouter { + // TODO: functions that the interest router will have +} \ No newline at end of file diff --git a/contracts/src/Interfaces/ILiquityBase.sol b/contracts/src/Interfaces/ILiquityBase.sol index 46dc1005..3d3f0f24 100644 --- a/contracts/src/Interfaces/ILiquityBase.sol +++ b/contracts/src/Interfaces/ILiquityBase.sol @@ -10,4 +10,5 @@ interface ILiquityBase { function activePool() external view returns (IActivePool); function defaultPool() external view returns (IDefaultPool); function priceFeed() external view returns (IPriceFeed); + function BOLD_GAS_COMPENSATION() external view returns (uint256); } diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 871406e2..58dea80e 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -8,6 +8,7 @@ import "./ILiquityBase.sol"; import "./IStabilityPool.sol"; import "./IBoldToken.sol"; import "./ISortedTroves.sol"; +import "./IInterestRouter.sol"; // Common interface for the Trove Manager. interface ITroveManager is IERC721, ILiquityBase { @@ -20,16 +21,24 @@ interface ITroveManager is IERC721, ILiquityBase { address _collSurplusPoolAddress, address _priceFeedAddress, address _boldTokenAddress, - address _sortedTrovesAddress + address _sortedTrovesAddress, + address _mockInterestRouterAddress ) external; function stabilityPool() external view returns (IStabilityPool); function boldToken() external view returns (IBoldToken); function sortedTroves() external view returns(ISortedTroves); function borrowerOperationsAddress() external view returns (address); + function interestRouter() external view returns (IInterestRouter); function BOOTSTRAP_PERIOD() external view returns (uint256); - + + // function BOLD_GAS_COMPENSATION() external view returns (uint256); + + function lastAggUpdateTime() external view returns (uint256); + function aggRecordedDebt() external view returns (uint256); + function aggWeightedDebtSum() external view returns (uint256); + function getTroveIdsCount() external view returns (uint); function getTroveFromTroveIdsArray(uint _index) external view returns (uint256); @@ -50,22 +59,25 @@ interface ITroveManager is IERC721, ILiquityBase { function addTroveIdToArray(uint256 _troveId) external returns (uint index); - function applyPendingRewards(uint256 _troveId) external; + function mintAggInterest(int256 _debtChange) external; function getPendingETHReward(uint256 _troveId) external view returns (uint); function getPendingBoldDebtReward(uint256 _troveId) external view returns (uint); - function hasPendingRewards(uint256 _troveId) external view returns (bool); + function hasRedistributionGains(address _borrower) external view returns (bool); function getEntireDebtAndColl(uint256 _troveId) external view returns ( - uint debt, - uint coll, - uint pendingBoldDebtReward, - uint pendingETHReward + uint debt, + uint coll, + uint pendingBoldDebtReward, + uint pendingETHReward, + uint pendingBoldInterest ); - function closeTrove(uint256 _troveId) external; + function getAndApplyRedistributionGains(address _borrower) external; + + function closeTrove(address _troveId) external; function removeStake(uint256 _troveId) external; @@ -75,7 +87,7 @@ interface ITroveManager is IERC721, ILiquityBase { function getRedemptionFeeWithDecay(uint _ETHDrawn) external view returns (uint); function getTroveStatus(uint256 _troveId) external view returns (uint); - + function getTroveStake(uint256 _troveId) external view returns (uint); function getTroveDebt(uint256 _troveId) external view returns (uint); @@ -87,6 +99,8 @@ interface ITroveManager is IERC721, ILiquityBase { function TroveAddManagers(uint256 _troveId) external view returns (address); function TroveRemoveManagers(uint256 _troveId) external view returns (address); + function getTroveLastDebtUpdateTime(address _borrower) external view returns (uint); + function setTrovePropertiesOnOpen(address _owner, uint256 _troveId, uint256 _coll, uint256 _debt, uint256 _annualInterestRate) external returns (uint256); function increaseTroveColl(address _sender, uint256 _troveId, uint _collIncrease) external returns (uint); @@ -107,4 +121,6 @@ interface ITroveManager is IERC721, ILiquityBase { function checkRecoveryMode(uint _price) external view returns (bool); function checkTroveIsActive(uint256 _troveId) external view returns (bool); + + function calcPendingAggInterest() external view returns (uint256); } diff --git a/contracts/src/MockInterestRouter.sol b/contracts/src/MockInterestRouter.sol new file mode 100644 index 00000000..625ad9b1 --- /dev/null +++ b/contracts/src/MockInterestRouter.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import "./Interfaces/IInterestRouter.sol"; + +contract MockInterestRouter is IInterestRouter {} \ No newline at end of file diff --git a/contracts/src/MultiTroveGetter.sol b/contracts/src/MultiTroveGetter.sol index efa88b0c..ed3d35b9 100644 --- a/contracts/src/MultiTroveGetter.sol +++ b/contracts/src/MultiTroveGetter.sol @@ -79,7 +79,8 @@ contract MultiTroveGetter { _troves[idx].stake, /* status */, /* arrayIndex */, - /* annualInterestRate */ + /* annualInterestRate */, + /* lastDebtUpdateTime */ ) = troveManager.Troves(currentTroveId); ( _troves[idx].snapshotETH, @@ -109,7 +110,8 @@ contract MultiTroveGetter { _troves[idx].stake, /* status */, /* arrayIndex */, - /* annualInterestRate */ + /* annualInterestRate */, + /* lastDebtUpdateTime */ ) = troveManager.Troves(currentTroveId); ( _troves[idx].snapshotETH, diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 1850c86d..b133fc2b 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -9,11 +9,12 @@ import "./Interfaces/IStabilityPool.sol"; import "./Interfaces/ICollSurplusPool.sol"; import "./Interfaces/IBoldToken.sol"; import "./Interfaces/ISortedTroves.sol"; +import "./Interfaces/IInterestRouter.sol"; import "./Dependencies/LiquityBase.sol"; import "./Dependencies/Ownable.sol"; import "./Dependencies/CheckContract.sol"; -// import "forge-std/console2.sol"; +import "forge-std/console2.sol"; contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveManager { string constant public NAME = "TroveManager"; // TODO @@ -34,9 +35,14 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // A doubly linked list of Troves, sorted by their sorted by their collateral ratios ISortedTroves public sortedTroves; + IInterestRouter public interestRouter; + // --- Data structures --- uint constant public SECONDS_IN_ONE_MINUTE = 60; + + uint256 constant public SECONDS_IN_ONE_YEAR = 31536000; // 60 * 60 * 24 * 365, + /* * Half-life of 12h. 12h = 720 min * (1/2) = d^720 => d = (1/2)^(1/720) @@ -74,7 +80,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint stake; Status status; uint128 arrayIndex; - uint256 annualInterestRate; + uint256 annualInterestRate; + uint64 lastDebtUpdateTime; // TODO: optimize this struct packing for gas reduction, which may break v1 tests that assume a certain order of properties } @@ -129,6 +136,14 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint public lastETHError_Redistribution; uint public lastBoldDebtError_Redistribution; + uint256 public aggRecordedDebt; + /* Sum of individual recorded Trove debts, weighted by their respective chosen interest rates. + * Updated at all Trove operations. + */ + uint256 public aggWeightedDebtSum; + + // Last time at which the aggregate recorded debt and weighted sum were updated + uint256 public lastAggUpdateTime; /* * --- Variable container structs for liquidations --- * @@ -235,8 +250,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana event TroveSnapshotsUpdated(uint _L_ETH, uint _L_boldDebt); event TroveIndexUpdated(uint256 _troveId, uint _newIndex); - enum TroveManagerOperation { - applyPendingRewards, + enum TroveManagerOperation { + getAndApplyRedistributionGains, liquidateInNormalMode, liquidateInRecoveryMode, redeemCollateral @@ -255,7 +270,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana address _collSurplusPoolAddress, address _priceFeedAddress, address _boldTokenAddress, - address _sortedTrovesAddress + address _sortedTrovesAddress, + address _interestRouterAddress ) external override @@ -270,6 +286,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana checkContract(_priceFeedAddress); checkContract(_boldTokenAddress); checkContract(_sortedTrovesAddress); + checkContract(_interestRouterAddress); borrowerOperationsAddress = _borrowerOperationsAddress; activePool = IActivePool(_activePoolAddress); @@ -280,6 +297,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana priceFeed = IPriceFeed(_priceFeedAddress); boldToken = IBoldToken(_boldTokenAddress); sortedTroves = ISortedTroves(_sortedTrovesAddress); + interestRouter = IInterestRouter(_interestRouterAddress); emit BorrowerOperationsAddressChanged(_borrowerOperationsAddress); emit ActivePoolAddressChanged(_activePoolAddress); @@ -332,7 +350,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana (singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, vars.pendingDebtReward, - vars.pendingCollReward) = getEntireDebtAndColl(_troveId); + vars.pendingCollReward, ) = getEntireDebtAndColl(_troveId); _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward); _removeStake(_troveId); @@ -370,7 +388,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana (singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, vars.pendingDebtReward, - vars.pendingCollReward) = getEntireDebtAndColl(_troveId); + vars.pendingCollReward, ) = getEntireDebtAndColl(_troveId); singleLiquidation.collGasCompensation = _getCollGasCompensation(singleLiquidation.entireTroveColl); singleLiquidation.BoldGasCompensation = BOLD_GAS_COMPENSATION; @@ -380,7 +398,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana if (_ICR <= _100pct) { _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward); _removeStake(_troveId); - + singleLiquidation.debtToOffset = 0; singleLiquidation.collToSendToSP = 0; singleLiquidation.debtToRedistribute = singleLiquidation.entireTroveDebt; @@ -389,7 +407,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana _closeTrove(_troveId, Status.closedByLiquidation); emit TroveLiquidated(_troveId, singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, TroveManagerOperation.liquidateInRecoveryMode); emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.liquidateInRecoveryMode); - + // If 100% < ICR < MCR, offset as much as possible, and redistribute the remainder } else if ((_ICR > _100pct) && (_ICR < MCR)) { _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward); @@ -494,7 +512,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana singleLiquidation.debtToRedistribute = 0; singleLiquidation.collToRedistribute = 0; } - + /* * This function is used when the liquidateTroves sequence starts during Recovery Mode. However, it * handle the case where the system *leaves* Recovery Mode, part way through the liquidation sequence @@ -829,7 +847,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana } /* Send _boldamount Bold to the system and redeem the corresponding amount of collateral from as many Troves as are needed to fill the redemption - * request. Applies pending rewards to a Trove before reducing its debt and coll. + * request. Applies redistribution gains to a Trove before reducing its debt and coll. * * Note that if _amount is very large, this function can run out of gas, specially if traversed troves are small. This can be easily avoided by * splitting the total _amount in appropriate chunks and calling the function multiple times. @@ -892,7 +910,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // TODO: check ICR? //getCurrentICR(currentTroveId, _price) < MCR - _applyPendingRewards(contractsCache.activePool, contractsCache.defaultPool, currentTroveId); + _getAndApplyRedistributionGains(contractsCache.activePool, contractsCache.defaultPool, currentTroveId); SingleRedemptionValues memory singleRedemption = _redeemCollateralFromTrove( contractsCache, @@ -952,27 +970,27 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return (currentETH, currentBoldDebt); } - function applyPendingRewards(uint256 _troveId) external override { + function getAndApplyRedistributionGains(address _troveId) external override { _requireCallerIsBorrowerOperations(); - return _applyPendingRewards(activePool, defaultPool, _troveId); + return _getAndApplyRedistributionGains(activePool, defaultPool, _troveId); } // Add the borrowers's coll and debt rewards earned from redistributions, to their Trove - function _applyPendingRewards(IActivePool _activePool, IDefaultPool _defaultPool, uint256 _troveId) internal { - if (hasPendingRewards(_troveId)) { + function _getAndApplyRedistributionGains(IActivePool _activePool, IDefaultPool _defaultPool, address _troveId) internal { + if (hasRedistributionGains(_troveId)) { _requireTroveIsActive(_troveId); - // Compute pending rewards + // Compute redistribution gains uint pendingETHReward = getPendingETHReward(_troveId); uint pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); - // Apply pending rewards to trove's state + // Apply redistribution gains to trove's state Troves[_troveId].coll = Troves[_troveId].coll + pendingETHReward; Troves[_troveId].debt = Troves[_troveId].debt + pendingBoldDebtReward; _updateTroveRewardSnapshots(_troveId); - // Transfer from DefaultPool to ActivePool + // Transfer redistribution gains from DefaultPool to ActivePool _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, pendingBoldDebtReward, pendingETHReward); emit TroveUpdated( @@ -980,7 +998,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana Troves[_troveId].debt, Troves[_troveId].coll, Troves[_troveId].stake, - TroveManagerOperation.applyPendingRewards + TroveManagerOperation.getAndApplyRedistributionGains ); } } @@ -1004,7 +1022,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return pendingETHReward; } - + // Get the borrower's pending accumulated Bold reward, earned by their stake function getPendingBoldDebtReward(uint256 _troveId) public view override returns (uint) { uint snapshotBoldDebt = rewardSnapshots[_troveId].boldDebt; @@ -1019,33 +1037,34 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return pendingBoldDebtReward; } - function hasPendingRewards(uint256 _troveId) public view override returns (bool) { + function hasRedistributionGains(address _troveId) public view override returns (bool) { /* - * A Trove has pending rewards if its snapshot is less than the current rewards per-unit-staked sum: + * A Trove has redistribution gains if its snapshot is less than the current rewards per-unit-staked sum: * this indicates that rewards have occured since the snapshot was made, and the user therefore has - * pending rewards + * redistribution gains */ if (Troves[_troveId].status != Status.active) {return false;} - + return (rewardSnapshots[_troveId].ETH < L_ETH); } - // Return the Troves entire debt and coll, including pending rewards from redistributions. + // Return the Troves entire debt and coll, including redistribution gains from redistributions. function getEntireDebtAndColl( uint256 _troveId ) public view override - returns (uint debt, uint coll, uint pendingBoldDebtReward, uint pendingETHReward) + returns (uint debt, uint coll, uint pendingBoldDebtReward, uint pendingETHReward, uint pendingBoldInterest) { debt = Troves[_troveId].debt; coll = Troves[_troveId].coll; pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); + pendingBoldInterest = _calcPendingTroveInterest(_troveId); pendingETHReward = getPendingETHReward(_troveId); - debt = debt + pendingBoldDebtReward; + debt = debt + pendingBoldDebtReward + pendingBoldInterest; coll = coll + pendingETHReward; } @@ -1087,7 +1106,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana /* * The following assert() holds true because: * - The system always contains >= 1 trove - * - When we close or liquidate a trove, we redistribute the pending rewards, so if all troves were closed/liquidated, + * - When we close or liquidate a trove, we redistribute the redistribution gains, so if all troves were closed/liquidated, * rewards would’ve been emptied and totalCollateralSnapshot would be zero too. */ assert(totalStakesSnapshot > 0); @@ -1268,7 +1287,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // Update the baseRate state variable baseRate = newBaseRate; emit BaseRateUpdated(newBaseRate); - + _updateLastFeeOpTime(); return newBaseRate; @@ -1333,6 +1352,97 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return Troves[_troveId].status == Status.active; } + // --- Interest rate calculations --- + + // TODO: analyze precision loss in interest functions and decide upon the minimum granularity + // (per-second, per-block, etc) + function _calcPendingTroveInterest(address _borrower) internal view returns (uint256) { + uint256 recordedDebt = Troves[_borrower].debt; + // convert annual interest to per-second and multiply by the principal + uint256 annualInterestRate = Troves[_borrower].annualInterestRate; + uint256 lastDebtUpdateTime = Troves[_borrower].lastDebtUpdateTime; + + return recordedDebt * annualInterestRate * (block.timestamp - lastDebtUpdateTime) / SECONDS_IN_ONE_YEAR; + } + + function calcPendingAggInterest() public view returns (uint256) { + return aggWeightedDebtSum * (block.timestamp - lastAggUpdateTime) / SECONDS_IN_ONE_YEAR / 1e18; + } + + // --- Aggregate interest operations --- + function mintAggInterest(int256 debtChange) public { + 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);} + + // TODO: cleaner way to deal with debt changes that can be positive or negative? + aggRecordedDebt = addUint256ToInt256(aggRecordedDebt + aggInterest, debtChange); + // assert(aggRecordedDebt > 0) // This should never be negative. If all principal debt were repaid, it should be 0, and if all + lastAggUpdateTime = block.timestamp; + } + + function addUint256ToInt256(uint256 _x, int256 _y) internal returns (uint256) { + // Assumption: _x + _y > 0. Will revert otherwise. + if (_y >= 0) { + return _x + uint256(_y); + } else { + return (_x - uint256(-_y)); + } + } + + // TODO: make this purely for existing Trove touches which alter coll or debt. // TODO: How about redemptions and liqs? + // function _applyInterestAndRedistributionGains( + // address _borrower, + // uint256 _debtChange, + // uint256 _mintedAggInterest, + // uint256, _pendingTroveInterest, + // uint256 _annualInterestRate, + // uint256 _pendingCollGain, + // uint256 _pendingDebtGain + // ) + // internal + // { + // uint256 oldRecordedDebt = Troves[_borrower].debt; + + // // uint256 pendingTroveInterest = _calcPendingTroveInterest(oldRecordedDebt, annualInterestRate, Troves[_borrower].lastDebtUpdateTime); + + // // Apply all changes to the Trove's state + // Troves[_borrower].coll = Troves[_borrower].coll + _pendingCollGain; + // uint256 newRecordedDebt = oldRecordedDebt + pendingTroveInterest + debtChange + _pendingDebtGain; + // Troves[borrower].debt = newRecordedDebt; + + // _updateTroveRewardSnapshots(_borrower); + + // // Record the Trove’s latest individual update time + // Troves[borrower].lastDebtUpdateTime = block.timestamp; + + // // Update aggregate recorded debt + // aggRecordedDebt += debtChange + _pendingDebtGain + _mintedAggInterest; // we don't add the Trove's fresh interest here, since this aggregate recorded debt gets updated with minted interest separately. + // assert(lastAggUpdateTime == block.timestamp); // Confirm there's no aggregate pending interest + + // // Update aggregate weighted debt sum + // uint256 annualInterestRate = Troves[borrower].annualInterestRate; + // uint256 oldWeightedRecordedDebt = oldTroveRecordedDebt * annualInterestRate; + // uint256 newWeightedRecordedDebt = newRecordedDebt * annualInterestRate; + // aggWeightedDebtSum = aggWeightedDebtSum - oldWeightedRecordedDebt + newWeightedRecordedDebt; + + // // Transfer redistribution gains from DefaultPool to ActivePool + // _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, _pendingDebtGain, _pendingCollGain); + + // emit TroveUpdated( + // _borrower, + // Troves[_borrower].debt, + // Troves[_borrower].coll, + // Troves[_borrower].stake, + // Troves[_borrower].lastDebtUpdateTime + // TroveManagerOperation.getAndApplyRedistributionGains + // ); + // } + + // function updateAggAndTroveRecordedDebt + // // TODO: bake in getAndApplyRedistributionGains in gas-efficient way + // --- 'require' wrapper functions --- function _requireCallerIsBorrowerOperations() internal view { @@ -1405,6 +1515,10 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return Troves[_troveId].annualInterestRate; } + function getTroveLastDebtUpdateTime(address _borrower) external view returns (uint) { + return Troves[_borrower].lastDebtUpdateTime; + } + // --- Trove property setters, called by BorrowerOperations --- function setTrovePropertiesOnOpen( @@ -1423,13 +1537,14 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana Troves[_troveId].coll = _coll; Troves[_troveId].debt = _debt; Troves[_troveId].annualInterestRate = _annualInterestRate; + Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp); _updateTroveRewardSnapshots(_troveId); - // mint ERC721 - // TODO: Should we use safeMint? I guess not - _mint(_owner, _troveId); + // Update weighted debt sum with the Trove's interest-weighted debt + aggWeightedDebtSum += _debt * _annualInterestRate; + // Record the Trove's stake (for redistributions) and update the total stakes return _updateStakeAndTotalStakes(_troveId); } diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index 38ed5b69..8d17e9bd 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -12,7 +12,7 @@ import "../../Interfaces/ISortedTroves.sol"; import "../../Interfaces/IStabilityPool.sol"; import "../../Interfaces/ITroveManager.sol"; import "./PriceFeedTestnet.sol"; - +import "../../Interfaces/IInterestRouter.sol"; import "../../GasPool.sol"; import "forge-std/Test.sol"; @@ -36,7 +36,6 @@ contract BaseTest is Test { uint256 CCR = 150e16; address public constant ZERO_ADDRESS = address(0); - // Core contracts IActivePool activePool; IBorrowerOperations borrowerOperations; @@ -49,6 +48,7 @@ contract BaseTest is Test { IPriceFeedTestnet priceFeed; GasPool gasPool; + IInterestRouter mockInterestRouter; function createAccounts() public { address[10] memory tempAccounts; @@ -60,11 +60,11 @@ contract BaseTest is Test { } function openTroveNoHints100pctMaxFee( - address _account, - uint256 _coll, - uint256 _boldAmount, + address _account, + uint256 _coll, + uint256 _boldAmount, uint256 _annualInterestRate - ) + ) public returns (uint256) { @@ -93,11 +93,11 @@ contract BaseTest is Test { address _account, uint256 _troveId, uint256 _collChange, - uint256 _boldChange, + uint256 _boldChange, bool _isCollIncrease, bool _isDebtIncrease - ) - public + ) + public { vm.startPrank(_account); borrowerOperations.adjustTrove(_troveId, 1e18, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease); @@ -116,6 +116,18 @@ contract BaseTest is Test { assertEq(recoveryMode, _enabled); } + function makeSPDeposit(address _account, uint256 _amount) public { + vm.startPrank(_account); + stabilityPool.provideToSP(_amount); + vm.stopPrank(); + } + + function makeSPWithdrawal(address _account, uint256 _amount) public { + vm.startPrank(_account); + stabilityPool.withdrawFromSP(_amount); + 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 4abe6a83..2c2e34cc 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -17,6 +17,7 @@ import "../../MultiTroveGetter.sol"; import "../../SortedTroves.sol"; import "../../StabilityPool.sol"; import "../../TroveManager.sol"; +import "../../MockInterestRouter.sol"; import "./BaseTest.sol"; @@ -65,6 +66,7 @@ contract DevTestSetup is BaseTest { stabilityPool = new StabilityPool(address(WETH)); troveManager = new TroveManager(); boldToken = new BoldToken(address(troveManager), address(stabilityPool), address(borrowerOperations)); + mockInterestRouter = new MockInterestRouter(); // Connect contracts sortedTroves.setParams( @@ -83,7 +85,8 @@ contract DevTestSetup is BaseTest { address(collSurplusPool), address(priceFeed), address(boldToken), - address(sortedTroves) + address(sortedTroves), + address(mockInterestRouter) ); // set contracts in BorrowerOperations diff --git a/contracts/src/test/deployment.t.sol b/contracts/src/test/deployment.t.sol index d8992215..86cdcf11 100644 --- a/contracts/src/test/deployment.t.sol +++ b/contracts/src/test/deployment.t.sol @@ -15,6 +15,7 @@ contract Deployment is DevTestSetup { assertNotEq(address(sortedTroves), address(0)); assertNotEq(address(stabilityPool), address(0)); assertNotEq(address(troveManager), address(0)); + assertNotEq(address(mockInterestRouter), address(0)); logContractAddresses(); } @@ -63,6 +64,12 @@ contract Deployment is DevTestSetup { assertEq(stabilityPoolAddress, recordedStabilityPoolAddress); } + function testTroveManagerHasCorrectInterestRouterAddress() public { + address interestRouter = address(mockInterestRouter); + address recordedInterestRouterAddress = address(troveManager.interestRouter()); + assertEq(interestRouter, recordedInterestRouterAddress); + } + // Active Pool function testActivePoolHasCorrectStabilityPoolAddress() public { diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol new file mode 100644 index 00000000..d114fa18 --- /dev/null +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -0,0 +1,244 @@ +pragma solidity 0.8.18; + +import "./TestContracts/DevTestSetup.sol"; + +contract InterestRateAggregate is DevTestSetup { + + // --- Pending aggregate interest calculator --- + + function testCalcPendingAggInterestReturns0For0TimePassedSinceLastUpdate() public { + priceFeed.setPrice(2000e18); + assertEq(troveManager.lastAggUpdateTime(), 0); + assertEq(troveManager.calcPendingAggInterest(), 0); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); + assertEq(troveManager.lastAggUpdateTime(), block.timestamp); + assertEq(troveManager.calcPendingAggInterest(), 0); + + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 5e17); + assertEq(troveManager.lastAggUpdateTime(), block.timestamp); + assertEq(troveManager.calcPendingAggInterest(), 0); + } + + // calcPendingAggInterest returns 0 with no recorded aggregate debt + + function testCalcPendingAggInterestReturns0When0AggRecordedDebt() public { + priceFeed.setPrice(2000e18); + assertEq(troveManager.aggRecordedDebt(), 0); + assertEq(troveManager.aggWeightedDebtSum(), 0); + assertEq(troveManager.calcPendingAggInterest(), 0); + + vm.warp(block.timestamp + 1000); + assertEq(troveManager.aggRecordedDebt(), 0); + assertEq(troveManager.aggWeightedDebtSum(), 0); + assertEq(troveManager.calcPendingAggInterest(), 0); + } + + // calcPendingAggInterest returns 0 when all troves have 0 interest rate + function testCalcPendingAggInterestReturns0WhenAllTrovesHave0InterestRate() public { + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 0); + + assertEq(troveManager.calcPendingAggInterest(), 0); + + vm.warp(block.timestamp + 1000); + + assertEq(troveManager.calcPendingAggInterest(), 0); + + openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 0); + + assertEq(troveManager.calcPendingAggInterest(), 0); + + vm.warp(block.timestamp + 1000); + + assertEq(troveManager.calcPendingAggInterest(), 0); + } + + // TODO: create additional fuzz test + function testCalcPendingInterestReturnsCorrectInterestForGivenPeriod() public { + priceFeed.setPrice(2000e18); + uint256 _duration = 1 days; + + uint256 troveDebtRequest = 2000e18; + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest + openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 75e16); // 75% annual interest + console.log("A debt", troveManager.getTroveDebt(A)); + + uint256 expectedTroveDebt = troveDebtRequest + troveManager.BOLD_GAS_COMPENSATION(); + assertEq(troveManager.getTroveDebt(A), expectedTroveDebt); + assertEq(troveManager.getTroveDebt(B), expectedTroveDebt); + + vm.warp(block.timestamp + _duration); + + // Expect weighted average of 2 * troveDebt debt at 50% interest + uint256 expectedPendingAggInterest = expectedTroveDebt * 2 * 5e17 * _duration / SECONDS_IN_1_YEAR / 1e18; + + assertEq(expectedPendingAggInterest, troveManager.calcPendingAggInterest()); + } + + // --- openTrove impact on aggregates --- + + // openTrove increases recorded aggregate debt by correct amount + function testOpenTroveIncreasesRecordedAggDebtByAggPendingInterestPlusNewDebt() public { + priceFeed.setPrice(2000e18); + assertEq(troveManager.aggRecordedDebt(), 0); + + uint256 troveDebtRequest = 2000e18; + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest + + // Check aggregate recorded debt increased to non-zero + uint256 aggREcordedDebt_1 = troveManager.aggRecordedDebt(); + assertGt(aggREcordedDebt_1, 0); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // check there's pending interest + uint256 pendingInterest = troveManager.calcPendingAggInterest(); + assertGt(pendingInterest, 0); + + uint256 expectedTroveDebt_B = troveDebtRequest + troveManager.BOLD_GAS_COMPENSATION(); + openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 25e16); + assertEq(troveManager.getTroveDebt(B), expectedTroveDebt_B); + + // check that opening Trove B increased the agg. recorded debt by the pending agg. interest plus Trove B's debt + assertEq(troveManager.aggRecordedDebt(), aggREcordedDebt_1 + pendingInterest + expectedTroveDebt_B); + } + + function testOpenTroveReducesPendingAggInterestTo0() public { + priceFeed.setPrice(2000e18); + assertEq(troveManager.aggRecordedDebt(), 0); + + uint256 troveDebtRequest = 2000e18; + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest + + // Check aggregate recorded debt increased to non-zero + uint256 aggREcordedDebt_1 = troveManager.aggRecordedDebt(); + assertGt(aggREcordedDebt_1, 0); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // check there's pending agg. interest + assertGt(troveManager.calcPendingAggInterest(), 0); + + openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 25e16); + + // Check pending agg. interest reduced to 0 + assertEq(troveManager.calcPendingAggInterest(), 0); + } + + function testOpenTroveUpdatesTheLastAggUpdateTime() public { + priceFeed.setPrice(2000e18); + assertEq(troveManager.lastAggUpdateTime(), 0); + + vm.warp(block.timestamp + 1 days); + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); // 25% annual interest + + assertEq(troveManager.lastAggUpdateTime(), block.timestamp); + + vm.warp(block.timestamp + 1 days); + + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 25e16); // 25% annual interest + + assertEq(troveManager.lastAggUpdateTime(), block.timestamp); + } + + function testOpenTroveMintsInterestToInterestRouter() public { + priceFeed.setPrice(2000e18); + assertEq(boldToken.balanceOf(address(mockInterestRouter)), 0); + + // Open initial Trove so that aggregate interest begins accruing + openTroveNoHints100pctMaxFee(A, 5 ether, 3000e18, 25e16); + + vm.warp(block.timestamp + 1 days); + + uint256 pendingAggInterest_1 = troveManager.calcPendingAggInterest(); + assertGt(pendingAggInterest_1, 0); + + // Open 2nd trove + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 25e16); + + // Check I-router Bold bal has increased as expected from 2nd trove opening + uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_1, pendingAggInterest_1); + + vm.warp(block.timestamp + 1 days); + + uint256 pendingAggInterest_2 = troveManager.calcPendingAggInterest(); + + // Open 3rd trove + openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 25e16); + + // Check I-router Bold bal has increased as expected from 3rd trove opening + uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_2, pendingAggInterest_1 + pendingAggInterest_2); + } + + function testOpenTroveIncreasesWeightedSumByCorrectWeightedDebt() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest_A = 2000e18; + uint256 annualInterest_A = 25e16; + uint256 troveDebtRequest_B = 2000e18; + uint256 annualInterest_B = 25e16; + + assertEq(troveManager.aggWeightedDebtSum(), 0); + + // A opens trove + openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, annualInterest_A); + uint256 troveDebt_A = troveManager.getTroveDebt(A); + assertGt(troveDebt_A, 0); + + // // 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(troveManager.aggWeightedDebtSum(), "troveManager.aggWeightedDebtSum()"); + + assertEq(troveManager.aggWeightedDebtSum(), expectedWeightedDebt_A); + + vm.warp(block.timestamp + 1000); + + // B opens Trove + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, annualInterest_B); + uint256 troveDebt_B = troveManager.getTroveDebt(A); + assertGt(troveDebt_B, 0); + + uint256 expectedWeightedDebt_B = troveDebt_B * annualInterest_B; + + assertEq(troveManager.aggWeightedDebtSum(), expectedWeightedDebt_A + expectedWeightedDebt_B); + } + + // --- SP operations --- + + // reduces pending interest agg to 0 + // function testSPDepositReducesPendingAggInterestTo0() public { + // uint256 troveDebtRequest = 2000e18; + // uint256 sPdeposit = 100e18; + // // A opens Trove to obtain BOLD + // priceFeed.setPrice(2000e18); + // openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // assertEq(troveManager.aggRecordedDebt(), 0); + + // // fast-forward time + // vm.warp(block.timestamp + 1 days); + + // // check there's pending agg. interest + // assertGt(troveManager.calcPendingAggInterest(), 0); + + // // A deposits to SP + // makeSPDeposit(A, sPdeposit); + + // // Check pending agg. interest reduced to 0 + // assertEq(troveManager.calcPendingAggInterest(), 0); + // } + + + // increases agg recorded debt by pending agg interest + // updates last agg update time to now + // mints interest to the router + // does not change the debt weighted sum + + +} diff --git a/contracts/src/test/interestRateBasic.t.sol b/contracts/src/test/interestRateBasic.t.sol index fe79b271..5b73e9ce 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -21,6 +21,18 @@ contract InterestRateBasic is DevTestSetup { assertEq(troveManager.getTroveAnnualInterestRate(D_Id), 1e18); } + function testOpenTroveSetsTroveLastDebtUpdateTime() public { + priceFeed.setPrice(2000e18); + assertEq(troveManager.getTroveLastDebtUpdateTime(A), 0); + assertEq(troveManager.getTroveLastDebtUpdateTime(B), 0); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); + assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + + vm.warp(block.timestamp + 1000); + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 1); + assertEq(troveManager.getTroveLastDebtUpdateTime(B), block.timestamp); + } function testOpenTroveInsertsToCorrectPositionInSortedList() public { priceFeed.setPrice(2000e18); @@ -31,7 +43,7 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate_D = 3e17; uint256 interestRate_E = 4e17; - // B and D open + // B and D open uint256 B_Id = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, interestRate_B); uint256 D_Id = openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, interestRate_D); @@ -62,7 +74,7 @@ contract InterestRateBasic is DevTestSetup { function testRevertWhenOpenTroveWithInterestRateGreaterThanMax() public { priceFeed.setPrice(2000e18); - + vm.startPrank(A); vm.expectRevert(); borrowerOperations.openTrove(A, 0, 1e18, 2e18, 2000e18, 0, 0, 1e18 + 1); @@ -115,7 +127,7 @@ contract InterestRateBasic is DevTestSetup { uint256 C_Id = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 3e17); uint256 D_Id = openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, 4e17); uint256 E_Id = openTroveNoHints100pctMaxFee(E, 2 ether, 2000e18, 5e17); - + // Check initial sorted list order - expect [A:10%, B:02%, C:30%, D:40%, E:50%] // A assertEq(sortedTroves.getNext(A_Id), 0); // tail @@ -132,7 +144,7 @@ contract InterestRateBasic is DevTestSetup { // E assertEq(sortedTroves.getNext(E_Id), D_Id); assertEq(sortedTroves.getPrev(E_Id), 0); // head - + // C sets rate to 0%, moves to tail - expect [C:0%, A:10%, B:20%, D:40%, E:50%] changeInterestRateNoHints(C, C_Id, 0); assertEq(sortedTroves.getNext(C_Id), 0); @@ -147,11 +159,12 @@ contract InterestRateBasic is DevTestSetup { changeInterestRateNoHints(A, A_Id, 6e17); assertEq(sortedTroves.getNext(A_Id), E_Id); assertEq(sortedTroves.getPrev(A_Id), D_Id); - } + } function testAdjustTroveDoesNotChangeListPositions() public { priceFeed.setPrice(2000e18); + // Troves opened in ascending order of interest rate uint256 A_Id = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); uint256 B_Id = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 2e17); diff --git a/contracts/test/TroveManagerTest.js b/contracts/test/TroveManagerTest.js index 4777cda1..127665e3 100644 --- a/contracts/test/TroveManagerTest.js +++ b/contracts/test/TroveManagerTest.js @@ -1840,9 +1840,9 @@ contract("TroveManager", async (accounts) => { await borrowerOperations.repayBold(ETroveId, dec(1, 18), { from: E }); // Check C is the only trove that has pending rewards - assert.isTrue(await troveManager.hasPendingRewards(CTroveId)); - assert.isFalse(await troveManager.hasPendingRewards(DTroveId)); - assert.isFalse(await troveManager.hasPendingRewards(ETroveId)); + assert.isTrue(await troveManager.hasRedistributionGains(CTroveId)); + assert.isFalse(await troveManager.hasRedistributionGains(DTroveId)); + assert.isFalse(await troveManager.hasRedistributionGains(ETroveId)); // Check C's pending coll and debt rewards are <= the coll and debt in the DefaultPool const pendingETH_C = await troveManager.getPendingETHReward(CTroveId); @@ -5068,8 +5068,8 @@ contract("TroveManager", async (accounts) => { assert.equal(C_Status, "0"); // non-existent }); - it("hasPendingRewards(): Returns false it trove is not active", async () => { - assert.isFalse(await troveManager.hasPendingRewards(th.addressToTroveId(alice))); + it("hasRedistributionGains(): Returns false it trove is not active", async () => { + assert.isFalse(await troveManager.hasRedistributionGains(th.addressToTroveId(alice))); }); }); diff --git a/contracts/utils/deploymentHelpers.js b/contracts/utils/deploymentHelpers.js index 52151764..0520c909 100644 --- a/contracts/utils/deploymentHelpers.js +++ b/contracts/utils/deploymentHelpers.js @@ -11,6 +11,7 @@ const HintHelpers = artifacts.require("./HintHelpers.sol"); const BoldToken = artifacts.require("./BoldToken.sol"); const StabilityPool = artifacts.require("./StabilityPool.sol"); const PriceFeedMock = artifacts.require("./PriceFeedMock.sol"); +const MockInterestRouter = artifacts.require("./MockInterestRouter.sol"); const ERC20 = artifacts.require("./ERC20MinterMock.sol"); // "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol" // "../node_modules/@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json" @@ -46,6 +47,7 @@ class DeploymentHelper { stabilityPool.address, borrowerOperations.address ); + const mockInterestRouter = await MockInterestRouter.new(); const hintHelpers = await HintHelpers.new(); @@ -59,7 +61,7 @@ class DeploymentHelper { // ); // TODO: setAsDeployed all above? - + BoldToken.setAsDeployed(boldToken); DefaultPool.setAsDeployed(defaultPool); PriceFeedTestnet.setAsDeployed(priceFeedTestnet); @@ -71,6 +73,7 @@ class DeploymentHelper { CollSurplusPool.setAsDeployed(collSurplusPool); BorrowerOperations.setAsDeployed(borrowerOperations); HintHelpers.setAsDeployed(hintHelpers); + MockInterestRouter.setAsDeployed(mockInterestRouter); const coreContracts = { WETH, @@ -84,7 +87,8 @@ class DeploymentHelper { defaultPool, collSurplusPool, borrowerOperations, - hintHelpers + hintHelpers, + mockInterestRouter }; return coreContracts; } @@ -110,7 +114,8 @@ class DeploymentHelper { contracts.collSurplusPool.address, contracts.priceFeedTestnet.address, contracts.boldToken.address, - contracts.sortedTroves.address + contracts.sortedTroves.address, + contracts.mockInterestRouter.address ); await contracts.stabilityPool.setAddresses( From 7b4fc234fe3488a5307c9526db6a521ecbf043b9 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Thu, 8 Feb 2024 18:20:41 +0700 Subject: [PATCH 02/18] Include agg. interest state updates in SP deposit and withdrawal --- contracts/src/StabilityPool.sol | 4 + contracts/src/TroveManager.sol | 6 + .../src/test/interestRateAggregate.t.sol | 306 ++++++++++++++++-- 3 files changed, 289 insertions(+), 27 deletions(-) diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index f3f68b0e..17f4bfdf 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -285,6 +285,8 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { function provideToSP(uint _amount) external override { _requireNonZeroAmount(_amount); + troveManager.mintAggInterest(0); + uint initialDeposit = deposits[msg.sender].initialValue; uint depositorETHGain = getDepositorETHGain(msg.sender); @@ -314,6 +316,8 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { uint initialDeposit = deposits[msg.sender].initialValue; _requireUserHasDeposit(initialDeposit); + troveManager.mintAggInterest(0); + uint depositorETHGain = getDepositorETHGain(msg.sender); uint compoundedBoldDeposit = getCompoundedBoldDeposit(msg.sender); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index b133fc2b..e717ced4 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -1371,6 +1371,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // --- Aggregate interest operations --- function mintAggInterest(int256 debtChange) public { + _requireCallerIsBOorSP(); 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 @@ -1449,6 +1450,11 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana require(msg.sender == borrowerOperationsAddress, "TroveManager: Caller is not the BorrowerOperations contract"); } + function _requireCallerIsBOorSP() internal view { + require(msg.sender == borrowerOperationsAddress || msg.sender == address(stabilityPool), + "TroveManager: Caller is not the BO or SP"); + } + function _requireIsOwnerOrAddManager(uint256 _troveId, address _sender) internal view { assert(_sender != address(0)); // TODO: remove require( diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index d114fa18..a2d11ba2 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -77,6 +77,54 @@ contract InterestRateAggregate is DevTestSetup { assertEq(expectedPendingAggInterest, troveManager.calcPendingAggInterest()); } + // --- mintAggInterest --- + + function testMintAggInterestRevertsWhenNotCalledByBOorSP() public { + // pass positive debt change + int256 debtChange = 37e18; + vm.startPrank(A); + vm.expectRevert(); + troveManager.mintAggInterest(debtChange); + vm.stopPrank(); + + vm.startPrank(address(borrowerOperations)); + troveManager.mintAggInterest(debtChange); + vm.stopPrank(); + + vm.startPrank(address(stabilityPool)); + troveManager.mintAggInterest(debtChange); + vm.stopPrank(); + + // pass negative debt change + debtChange = -37e18; + vm.startPrank(A); + vm.expectRevert(); + troveManager.mintAggInterest(debtChange); + vm.stopPrank(); + + vm.startPrank(address(borrowerOperations)); + troveManager.mintAggInterest(debtChange); + vm.stopPrank(); + + vm.startPrank(address(stabilityPool)); + troveManager.mintAggInterest(debtChange); + vm.stopPrank(); + + // pass 0 debt change + vm.startPrank(A); + vm.expectRevert(); + troveManager.mintAggInterest(0); + vm.stopPrank(); + + vm.startPrank(address(borrowerOperations)); + troveManager.mintAggInterest(0); + vm.stopPrank(); + + vm.startPrank(address(stabilityPool)); + troveManager.mintAggInterest(0); + vm.stopPrank(); + } + // --- openTrove impact on aggregates --- // openTrove increases recorded aggregate debt by correct amount @@ -108,15 +156,10 @@ contract InterestRateAggregate is DevTestSetup { function testOpenTroveReducesPendingAggInterestTo0() public { priceFeed.setPrice(2000e18); - assertEq(troveManager.aggRecordedDebt(), 0); uint256 troveDebtRequest = 2000e18; openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest - // Check aggregate recorded debt increased to non-zero - uint256 aggREcordedDebt_1 = troveManager.aggRecordedDebt(); - assertGt(aggREcordedDebt_1, 0); - // fast-forward time vm.warp(block.timestamp + 1 days); @@ -209,36 +252,245 @@ contract InterestRateAggregate is DevTestSetup { assertEq(troveManager.aggWeightedDebtSum(), expectedWeightedDebt_A + expectedWeightedDebt_B); } - // --- SP operations --- + // --- SP deposits --- + + function testSPDepositReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + // A opens Trove to obtain BOLD + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // // check there's pending agg. interest + assertGt(troveManager.calcPendingAggInterest(), 0); + + // A deposits to SP + makeSPDeposit(A, sPdeposit); + + // Check pending agg. interest reduced to 0 + assertEq(troveManager.calcPendingAggInterest(), 0); + } + + function testSPDepositIncreasesAggRecordedDebtByPendingAggInterest() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + + // A opens Trove to obtain BOLD + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 pendingInterest = troveManager.calcPendingAggInterest(); + assertGt(pendingInterest, 0); + + uint256 aggRecordedDebt_1 = troveManager.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + + // A deposits to SP + makeSPDeposit(A, sPdeposit); + + // Check pending agg. debt increased + uint256 aggRecordedDebt_2 = troveManager.aggRecordedDebt(); + assertEq(aggRecordedDebt_2, aggRecordedDebt_1 + pendingInterest); + } + + function testSPDepositUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + + // A opens Trove to obtain BOLD + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.lastAggUpdateTime(), 0); + assertLt(troveManager.lastAggUpdateTime(), block.timestamp); + + // A deposits to SP + makeSPDeposit(A, sPdeposit); + + // Check last agg update time increased to now + assertEq(troveManager.lastAggUpdateTime(), block.timestamp); + } + + function testSPDepositMintsInterestToInterestRouter() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + + // A opens Trove to obtain BOLD + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // Get I-router balance + uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_1, 0); + + uint256 pendingAggInterest = troveManager.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // Make SP deposit + makeSPDeposit(A, sPdeposit); + + // Check I-router Bold bal has increased as expected from SP deposit + uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_2, pendingAggInterest); + } + + // Does not change the debt weighted sum + function testSPDepositDoesNotChangeAggWeightedDebtSum() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + + // A opens Trove to obtain BOLD + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // Get weighted sum before + uint256 weightedDebtSum_1 = troveManager.aggWeightedDebtSum(); + assertGt(weightedDebtSum_1, 0); + + uint256 pendingAggInterest = troveManager.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // Make SP deposit + makeSPDeposit(A, sPdeposit); + + // Get weighted sum after, check no change + uint256 weightedDebtSum_2 = troveManager.aggWeightedDebtSum(); + assertEq(weightedDebtSum_2, weightedDebtSum_1); + } + + // --- SP Withdrawals --- + + function testSPWithdrawalReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + // A opens Trove to obtain BOLD and makes SP deposit + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + makeSPDeposit(A, sPdeposit); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // check there's pending agg. interest + assertGt(troveManager.calcPendingAggInterest(), 0); + + // A withdraws deposit + makeSPWithdrawal(A, sPdeposit); + + // Check pending agg. interest reduced to 0 + assertEq(troveManager.calcPendingAggInterest(), 0); + } + + function testSPWithdrawalIncreasesAggRecordedDebtByPendingAggInterest() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + // A opens Trove to obtain BOLD and makes SP deposit + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + makeSPDeposit(A, sPdeposit); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 pendingInterest = troveManager.calcPendingAggInterest(); + assertGt(pendingInterest, 0); + + uint256 aggRecordedDebt_1 = troveManager.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + + // A withdraws deposit + makeSPWithdrawal(A, sPdeposit); + + // Check pending agg. debt increased + uint256 aggRecordedDebt_2 = troveManager.aggRecordedDebt(); + assertEq(aggRecordedDebt_2, aggRecordedDebt_1 + pendingInterest); + } + + function testSPWithdrawalUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + // A opens Trove to obtain BOLD and makes SP deposit + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + makeSPDeposit(A, sPdeposit); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.lastAggUpdateTime(), 0); + assertLt(troveManager.lastAggUpdateTime(), block.timestamp); + + // A withdraws from SP + makeSPWithdrawal(A, sPdeposit); + + // Check last agg update time increased to now + assertEq(troveManager.lastAggUpdateTime(), block.timestamp); + } + + function testSPWithdrawalMintsInterestToInterestRouter() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + // A opens Trove to obtain BOLD and makes SP deposit + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + makeSPDeposit(A, sPdeposit); + + // fast-forward time + vm.warp(block.timestamp + 1 days); - // reduces pending interest agg to 0 - // function testSPDepositReducesPendingAggInterestTo0() public { - // uint256 troveDebtRequest = 2000e18; - // uint256 sPdeposit = 100e18; - // // A opens Trove to obtain BOLD - // priceFeed.setPrice(2000e18); - // openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + // Get I-router balance + uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_1, 0); - // assertEq(troveManager.aggRecordedDebt(), 0); + uint256 pendingAggInterest = troveManager.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); - // // fast-forward time - // vm.warp(block.timestamp + 1 days); + // A withdraws from SP + makeSPWithdrawal(A, sPdeposit); - // // check there's pending agg. interest - // assertGt(troveManager.calcPendingAggInterest(), 0); + // Check I-router Bold bal has increased as expected from 3rd trove opening + uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_2, pendingAggInterest); + } - // // A deposits to SP - // makeSPDeposit(A, sPdeposit); + function testSPWithdrawalDoesNotChangeAggWeightedDebtSum() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; - // // Check pending agg. interest reduced to 0 - // assertEq(troveManager.calcPendingAggInterest(), 0); - // } + // A opens Trove to obtain BOLD + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + // Get weighted sum before + uint256 weightedDebtSum_1 = troveManager.aggWeightedDebtSum(); + assertGt(weightedDebtSum_1, 0); - // increases agg recorded debt by pending agg interest - // updates last agg update time to now - // mints interest to the router - // does not change the debt weighted sum + uint256 pendingAggInterest = troveManager.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + // Make SP deposit + makeSPDeposit(A, sPdeposit); + // Get weighted sum after, check no change + uint256 weightedDebtSum_2 = troveManager.aggWeightedDebtSum(); + assertEq(weightedDebtSum_2, weightedDebtSum_1); + } } From 48085d025eab05e5e5418221d5460380cb9dc0ac Mon Sep 17 00:00:00 2001 From: RickGriff Date: Thu, 8 Feb 2024 19:57:25 +0700 Subject: [PATCH 03/18] Add comment and fix hardhat tests --- contracts/src/TroveManager.sol | 14 ++++++++++++-- contracts/test/AccessControlTest.js | 6 +++--- contracts/test/OwnershipTest.js | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index e717ced4..e18edc9b 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -1370,7 +1370,17 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana } // --- Aggregate interest operations --- - function mintAggInterest(int256 debtChange) public { + + // This function is called inside all state-changing user ops: borrower ops, liquidations, redemptions and SP deposits/withdrawals. + // Some user ops trigger debt changes to Trove(s), in which case _troveDebtChange will be non-zero. + // The _troveDebtChange is the sum of: + // - Any composite debt change to a Trove from a borrower op + // - Any debt increase due to a Trove's redistribution gaisn being applied. + // It does NOT include the Trove's pending individual interest. This is because aggregate interest and individual interest are tracked + // separately, in parallel. + //That is, the aggregate recorded debt is incremented by the aggregate pending interest. + + function mintAggInterest(int256 _troveDebtChange) public { _requireCallerIsBOorSP(); 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. @@ -1378,7 +1388,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana if (aggInterest > 0) {boldToken.mint(address(interestRouter), aggInterest);} // TODO: cleaner way to deal with debt changes that can be positive or negative? - aggRecordedDebt = addUint256ToInt256(aggRecordedDebt + aggInterest, debtChange); + aggRecordedDebt = addUint256ToInt256(aggRecordedDebt + aggInterest, _troveDebtChange); // assert(aggRecordedDebt > 0) // This should never be negative. If all principal debt were repaid, it should be 0, and if all lastAggUpdateTime = block.timestamp; } diff --git a/contracts/test/AccessControlTest.js b/contracts/test/AccessControlTest.js index d17fa409..be843e51 100644 --- a/contracts/test/AccessControlTest.js +++ b/contracts/test/AccessControlTest.js @@ -84,11 +84,11 @@ contract( }); describe("TroveManager", async (accounts) => { - // applyPendingRewards - it("applyPendingRewards(): reverts when called by an account that is not BorrowerOperations", async () => { + // getAndApplyRedistributionGains + it("getAndApplyRedistributionGains(): reverts when called by an account that is not BorrowerOperations", async () => { // Attempt call from alice try { - const txAlice = await troveManager.applyPendingRewards(th.addressToTroveId(bob), { + const txAlice = await troveManager.getAndApplyRedistributionGains(th.addressToTroveId(bob), { from: alice, }); } catch (err) { diff --git a/contracts/test/OwnershipTest.js b/contracts/test/OwnershipTest.js index 4f5cbf88..d1687409 100644 --- a/contracts/test/OwnershipTest.js +++ b/contracts/test/OwnershipTest.js @@ -68,7 +68,7 @@ contract('All Liquity functions with onlyOwner modifier', async accounts => { describe('TroveManager', async accounts => { it("setAddresses(): reverts when called by non-owner, with wrong addresses, or twice", async () => { - await testSetAddresses(troveManager, 9) + await testSetAddresses(troveManager, 10) }) }) From 8843357cddcdf373efb12f77528c964d7af3a074 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 13 Feb 2024 17:44:07 +0700 Subject: [PATCH 04/18] Include ag. interest state updates in closeTrove --- contracts/src/ActivePool.sol | 90 +++++- contracts/src/BoldToken.sol | 16 +- contracts/src/BorrowerOperations.sol | 73 +++-- contracts/src/Dependencies/LiquityBase.sol | 8 +- contracts/src/Interfaces/IActivePool.sol | 18 +- contracts/src/Interfaces/ILiquityBase.sol | 1 + contracts/src/Interfaces/ITroveManager.sol | 24 +- contracts/src/StabilityPool.sol | 4 +- contracts/src/TroveManager.sol | 111 +++---- contracts/src/test/TestContracts/BaseTest.sol | 12 + .../src/test/TestContracts/DevTestSetup.sol | 9 +- contracts/src/test/deployment.t.sol | 10 +- .../src/test/interestRateAggregate.t.sol | 283 ++++++++++++++---- 13 files changed, 457 insertions(+), 202 deletions(-) diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index e05894cb..a51cf486 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.18; import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import './Interfaces/IActivePool.sol'; +import './Interfaces/IBoldToken.sol'; +import "./Interfaces/IInterestRouter.sol"; import "./Dependencies/Ownable.sol"; import "./Dependencies/CheckContract.sol"; import './Interfaces/IDefaultPool.sol'; @@ -11,6 +14,8 @@ import './Interfaces/IActivePool.sol'; // import "forge-std/console.sol"; +import "forge-std/console2.sol"; + /* * The Active Pool holds the ETH collateral and Bold debt (but not Bold tokens) for all active troves. * @@ -28,9 +33,29 @@ contract ActivePool is Ownable, CheckContract, IActivePool { address public troveManagerAddress; address public stabilityPoolAddress; address public defaultPoolAddress; + + IBoldToken boldToken; + + IInterestRouter public interestRouter; + + uint256 constant public SECONDS_IN_ONE_YEAR = 31536000; // 60 * 60 * 24 * 365, + uint256 internal ETHBalance; // deposited ether tracker + + // Sum of individual recorded Trove debts. Updated only at individual Trove operations. uint256 internal boldDebt; + // Aggregate recorded debt tracker. Updated whenever a Trove's debt is touched AND whenever the aggregate pending interest is minted. + uint256 public aggRecordedDebt; + + /* Sum of individual recorded Trove debts weighted by their respective chosen interest rates. + * Updated at individual Trove operations. + */ + uint256 public aggWeightedDebtSum; + + // Last time at which the aggregate recorded debt and weighted sum were updated + uint256 public lastAggUpdateTime; + // --- Events --- event DefaultPoolAddressChanged(address _newDefaultPoolAddress); @@ -46,13 +71,16 @@ contract ActivePool is Ownable, CheckContract, IActivePool { ETH = IERC20(_ETHAddress); } + // --- Contract setters --- function setAddresses( address _borrowerOperationsAddress, address _troveManagerAddress, address _stabilityPoolAddress, - address _defaultPoolAddress + address _defaultPoolAddress, + address _boldTokenAddress, + address _interestRouterAddress ) external onlyOwner @@ -61,11 +89,15 @@ contract ActivePool is Ownable, CheckContract, IActivePool { checkContract(_troveManagerAddress); checkContract(_stabilityPoolAddress); checkContract(_defaultPoolAddress); + checkContract(_boldTokenAddress); + checkContract(_interestRouterAddress); borrowerOperationsAddress = _borrowerOperationsAddress; troveManagerAddress = _troveManagerAddress; stabilityPoolAddress = _stabilityPoolAddress; defaultPoolAddress = _defaultPoolAddress; + boldToken = IBoldToken(_boldTokenAddress); + interestRouter = IInterestRouter(_interestRouterAddress); emit BorrowerOperationsAddressChanged(_borrowerOperationsAddress); emit TroveManagerAddressChanged(_troveManagerAddress); @@ -93,6 +125,15 @@ contract ActivePool is Ownable, CheckContract, IActivePool { return boldDebt; } + function calcPendingAggInterest() public view returns (uint256) { + return aggWeightedDebtSum * (block.timestamp - lastAggUpdateTime) / SECONDS_IN_ONE_YEAR / 1e18; + } + + // Returns sum of agg.recorded debt plus agg. pending interest. Excludes pending redist. gains. + function getTotalActiveDebt() public view returns (uint256) { + return aggRecordedDebt + calcPendingAggInterest(); + } + // --- Pool functionality --- function sendETH(address _account, uint _amount) external override { @@ -143,6 +184,48 @@ contract ActivePool is Ownable, CheckContract, IActivePool { emit ActivePoolBoldDebtUpdated(boldDebt); } + function increaseAggWeightedDebtSum(uint256 _debt, uint256 _annualInterestRate) external { + _requireCallerIsTroveManager(); + aggWeightedDebtSum += _debt * _annualInterestRate; + } + + function decreaseAggWeightedDebtSum(uint256 _weightedRecordedTroveDebt) external { + aggWeightedDebtSum -= _weightedRecordedTroveDebt; + } + + // --- Aggregate interest operations --- + + // This function is called inside all state-changing user ops: borrower ops, liquidations, redemptions and SP deposits/withdrawals. + // Some user ops trigger debt changes to Trove(s), in which case _troveDebtChange will be non-zero. + // The _troveDebtChange is the sum of: + // - Any composite debt change to a Trove from a borrower op + // - Any debt increase due to a Trove's redistribution gaisn being applied. + // It does NOT include the Trove's pending individual interest. This is because aggregate interest and individual interest are tracked + // separately, in parallel. + //That is, the aggregate recorded debt is incremented by the aggregate pending interest. + function mintAggInterest(int256 _troveDebtChange) public { + _requireCallerIsBOorSP(); + 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);} + + // TODO: cleaner way to deal with debt changes that can be positive or negative? + aggRecordedDebt = addUint256ToInt256(aggRecordedDebt + aggInterest, _troveDebtChange); + + // assert(aggRecordedDebt > 0) // This should never be negative. If all principal debt were repaid, it should be 0, and if all + lastAggUpdateTime = block.timestamp; + } + + function addUint256ToInt256(uint256 _x, int256 _y) internal returns (uint256) { + // Assumption: _x + _y > 0. Will revert otherwise. + if (_y >= 0) { + return _x + uint256(_y); + } else { + return (_x - uint256(-_y)); + } + } + // --- 'require' functions --- function _requireCallerIsBorrowerOperationsOrDefaultPool() internal view { @@ -167,6 +250,11 @@ contract ActivePool is Ownable, CheckContract, IActivePool { "ActivePool: Caller is neither BorrowerOperations nor TroveManager"); } + function _requireCallerIsBOorSP() internal view { + require(msg.sender == borrowerOperationsAddress || msg.sender == stabilityPoolAddress, + "ActivePool: Caller is not the BO or SP"); + } + function _requireCallerIsTroveManager() internal view { require( msg.sender == troveManagerAddress, diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol index 8bf560b2..9631000f 100644 --- a/contracts/src/BoldToken.sol +++ b/contracts/src/BoldToken.sol @@ -56,7 +56,9 @@ contract BoldToken is CheckContract, IBoldToken { address public immutable troveManagerAddress; address public immutable stabilityPoolAddress; address public immutable borrowerOperationsAddress; + address public immutable activePoolAddress; + // --- Events --- event TroveManagerAddressChanged(address _troveManagerAddress); event StabilityPoolAddressChanged(address _newStabilityPoolAddress); @@ -66,11 +68,13 @@ contract BoldToken is CheckContract, IBoldToken { ( address _troveManagerAddress, address _stabilityPoolAddress, - address _borrowerOperationsAddress + address _borrowerOperationsAddress, + address _activePoolAddress ) { checkContract(_troveManagerAddress); checkContract(_stabilityPoolAddress); checkContract(_borrowerOperationsAddress); + checkContract(_activePoolAddress); troveManagerAddress = _troveManagerAddress; emit TroveManagerAddressChanged(_troveManagerAddress); @@ -80,6 +84,8 @@ contract BoldToken is CheckContract, IBoldToken { borrowerOperationsAddress = _borrowerOperationsAddress; emit BorrowerOperationsAddressChanged(_borrowerOperationsAddress); + + activePoolAddress = _activePoolAddress; bytes32 hashedName = keccak256(bytes(_NAME)); bytes32 hashedVersion = keccak256(bytes(_VERSION)); @@ -95,7 +101,7 @@ contract BoldToken is CheckContract, IBoldToken { // --- Functions for intra-Liquity calls --- function mint(address _account, uint256 _amount) external override { - _requireCallerIsBOorTM(); + _requireCallerIsBOorAP(); _mint(_account, _amount); } @@ -258,10 +264,10 @@ contract BoldToken is CheckContract, IBoldToken { ); } - function _requireCallerIsBOorTM() internal view { + function _requireCallerIsBOorAP() internal view { require(msg.sender == borrowerOperationsAddress || - msg.sender == troveManagerAddress, - "BoldToken: Caller is not BO or TM"); + msg.sender == activePoolAddress, + "BoldToken: Caller is not BO or AP"); } function _requireCallerIsBOorTroveMorSP() internal view { diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 8d882277..351dba92 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -13,7 +13,7 @@ import "./Dependencies/LiquityBase.sol"; import "./Dependencies/Ownable.sol"; import "./Dependencies/CheckContract.sol"; -// import "forge-std/console.sol"; +import "forge-std/console2.sol"; contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOperations { @@ -40,8 +40,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe struct LocalVariables_adjustTrove { uint price; uint netDebtChange; - uint debt; - uint coll; + uint entireDebt; + uint entireColl; uint oldICR; uint newICR; uint newTCR; @@ -168,6 +168,9 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe LocalVariables_openTrove memory vars; vars.price = priceFeed.fetchPrice(); + + // --- Checks --- + bool isRecoveryMode = _checkRecoveryMode(vars.price); _requireValidAnnualInterestRate(_annualInterestRate); @@ -188,8 +191,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe vars.compositeDebt = _getCompositeDebt(vars.netDebt); assert(vars.compositeDebt > 0); - troveManager.mintAggInterest(int256(vars.compositeDebt)); - vars.ICR = LiquityMath._computeCR(_ETHAmount, vars.compositeDebt, vars.price); if (isRecoveryMode) { @@ -200,6 +201,10 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _requireNewTCRisAboveCCR(newTCR); } + // --- Effects & interactions --- + + activePool.mintAggInterest(int256(vars.compositeDebt)); + // Set the stored Trove properties and mint the NFT vars.stake = contractsCache.troveManager.setTrovePropertiesOnOpen( _owner, @@ -330,28 +335,28 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // TODO: implement interest rate charges } - (vars.debt, vars.coll, , , ) = contractsCache.troveManager.getEntireDebtAndColl(_troveId); + (vars.entireDebt, vars.entireColl, , , ) = contractsCache.troveManager.getEntireDebtAndColl(_troveId); // Get the trove's old ICR before the adjustment, and what its new ICR will be after the adjustment - vars.oldICR = LiquityMath._computeCR(vars.coll, vars.debt, vars.price); + vars.oldICR = LiquityMath._computeCR(vars.entireColl, vars.entireDebt, vars.price); vars.newICR = _getNewICRFromTroveChange( - vars.coll, - vars.debt, + vars.entireColl, + vars.entireDebt, _collChange, _isCollIncrease, vars.netDebtChange, _isDebtIncrease, vars.price ); - assert(_isCollIncrease || _collChange <= vars.coll); // TODO: do we still need this? + assert(_isCollIncrease || _collChange <= vars.entireColl); // TODO: do we still need this? // Check the adjustment satisfies all conditions for the current system mode _requireValidAdjustmentInCurrentMode(isRecoveryMode, _collChange, _isCollIncrease, _isDebtIncrease, vars); // When the adjustment is a debt repayment, check it's a valid amount and that the caller has enough Bold if (!_isDebtIncrease && _boldChange > 0) { - _requireAtLeastMinNetDebt(_getNetDebt(vars.debt) - vars.netDebtChange); - _requireValidBoldRepayment(vars.debt, vars.netDebtChange); + _requireAtLeastMinNetDebt(_getNetDebt(vars.entireDebt) - vars.netDebtChange); + _requireValidBoldRepayment(vars.entireDebt, vars.netDebtChange); _requireSufficientBoldBalance(contractsCache.boldToken, msg.sender, vars.netDebtChange); } @@ -374,7 +379,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe emit BoldBorrowingFeePaid(_troveId, vars.BoldFee); // Use the unmodified _boldChange here, as we don't send the fee to the user - //TODO: any macro changes due to interest rates here? + //TODO: the _boldChange passed here should include both the borrower's debt adjustment plus interest applied. i.e. + // it should be the change in their recorded debt. _moveTokensAndETHfromAdjustment( contractsCache.activePool, contractsCache.boldToken, @@ -393,34 +399,50 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe IActivePool activePoolCached = activePool; IBoldToken boldTokenCached = boldToken; + // --- Checks --- + _requireCallerIsBorrower(troveManagerCached, _troveId); _requireTroveisActive(troveManagerCached, _troveId); uint price = priceFeed.fetchPrice(); _requireNotInRecoveryMode(price); - // TODO: apply individual and aggregate pending interest, and take snapshots of current timestamp. + uint256 initialWeightedRecordedTroveDebt = troveManager.getTroveWeightedRecordedDebt(msg.sender); + // TODO: gas optimization of pending rewards. We don't need to actually update stored Trove debt & coll properties here, since we'll + // zero them at the end. Currently, since we apply gains first, the following calls to getTroveColl and getTroveDebt + // return values with redistribution gains included. When we gas-optimize, this won't be true and we + // should change the total debt and coll calculations. troveManagerCached.getAndApplyRedistributionGains(msg.sender); - uint coll = troveManagerCached.getTroveColl(_troveId); - uint debt = troveManagerCached.getTroveDebt(_troveId); + // TODO: cav we optimize calls to TM? + uint entireTroveColl = troveManagerCached.getTroveColl(_troveId); + uint256 pendingTroveInterest = troveManager.calcPendingTroveInterest(_troveId); + uint256 recordedTroveDebt = troveManager.getTroveDebt(_troveId); + uint256 entireTroveDebt = recordedTroveDebt + pendingTroveInterest; - _requireSufficientBoldBalance(boldTokenCached, msg.sender, debt - BOLD_GAS_COMPENSATION); + // The borrower must repay their entire debt, including pending interest and redist. gains + _requireSufficientBoldBalance(boldTokenCached, msg.sender, entireTroveDebt - BOLD_GAS_COMPENSATION); - uint newTCR = _getNewTCRFromTroveChange(coll, false, debt, false, price); + // The TCR always includes this Trove's pending interest, so we must subtract the Trove's entire debt here + uint newTCR = _getNewTCRFromTroveChange(entireTroveColl, false, entireTroveDebt, false, price); _requireNewTCRisAboveCCR(newTCR); - troveManagerCached.removeStake(_troveId); - troveManagerCached.closeTrove(_troveId); + // --- Effects and interactions --- + // The aggregate recorded debt increases by the pending aggregate interest, and decrease by the Trove's entire debt (including its pending interest), + // since the pending aggregate interest also includes the pending interest for this individual Trove. + activePool.mintAggInterest(-int256(entireTroveDebt)); + + troveManagerCached.removeStake(_troveId); + troveManagerCached.closeTrove(_troveId, initialWeightedRecordedTroveDebt); emit TroveUpdated(_troveId, 0, 0, 0, BorrowerOperation.closeTrove); - // Burn the repaid Bold from the user's balance and the gas compensation from the Gas Pool - _repayBold(activePoolCached, boldTokenCached, msg.sender, debt - BOLD_GAS_COMPENSATION); + // Remove only the Trove's *recorded* debt here, excluding the pending interest, since the state variable updated by this function (ActivePool.boldDebt) + // tracks only the sum of recorded debts. + _repayBold(activePoolCached, boldTokenCached, msg.sender, recordedTroveDebt - BOLD_GAS_COMPENSATION); _repayBold(activePoolCached, boldTokenCached, gasPoolAddress, BOLD_GAS_COMPENSATION); - // Send the collateral back to the user - activePoolCached.sendETH(msg.sender, coll); + activePoolCached.sendETH(msg.sender, entireTroveColl); } function setAddManager(uint256 _troveId, address _manager) external { @@ -714,11 +736,14 @@ 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/Dependencies/LiquityBase.sol b/contracts/src/Dependencies/LiquityBase.sol index ebee5b1c..b7d8921a 100644 --- a/contracts/src/Dependencies/LiquityBase.sol +++ b/contracts/src/Dependencies/LiquityBase.sol @@ -9,11 +9,15 @@ import "../Interfaces/IDefaultPool.sol"; import "../Interfaces/IPriceFeed.sol"; import "../Interfaces/ILiquityBase.sol"; +import "forge-std/console2.sol"; + /* * Base contract for TroveManager, BorrowerOperations and StabilityPool. Contains global system constants and * common functions. */ contract LiquityBase is BaseMath, ILiquityBase { + // TODO: Pull all constants out into a separate base contract + uint constant public _100pct = 1000000000000000000; // 1e18 == 100% // Minimum collateral ratio for individual troves @@ -65,9 +69,9 @@ contract LiquityBase is BaseMath, ILiquityBase { } function getEntireSystemDebt() public view returns (uint entireSystemDebt) { - uint activeDebt = activePool.getBoldDebt(); + uint activeDebt = activePool.getTotalActiveDebt(); uint closedDebt = defaultPool.getBoldDebt(); - + return activeDebt + closedDebt; } diff --git a/contracts/src/Interfaces/IActivePool.sol b/contracts/src/Interfaces/IActivePool.sol index a16e42b1..43ee304e 100644 --- a/contracts/src/Interfaces/IActivePool.sol +++ b/contracts/src/Interfaces/IActivePool.sol @@ -3,19 +3,33 @@ pragma solidity 0.8.18; import "./IPool.sol"; - +import "./IInterestRouter.sol"; interface IActivePool is IPool { function stabilityPoolAddress() external view returns (address); function defaultPoolAddress() external view returns (address); function borrowerOperationsAddress() external view returns (address); function troveManagerAddress() external view returns (address); + function interestRouter() external view returns (IInterestRouter); + + function getTotalActiveDebt() external view returns (uint256); + function lastAggUpdateTime() external view returns (uint256); + function aggRecordedDebt() external view returns (uint256); + function aggWeightedDebtSum() external view returns (uint256); + function calcPendingAggInterest() external view returns (uint256); + + + function mintAggInterest(int256 _troveDebtChange) external; + function increaseAggWeightedDebtSum(uint256 _debt, uint256 _annualInterestRate) external; + function decreaseAggWeightedDebtSum(uint256 _weightedRecordedTroveDebt) external; function sendETH(address _account, uint _amount) external; function sendETHToDefaultPool(uint _amount) external; function setAddresses( address _borrowerOperationsAddress, address _troveManagerAddress, address _stabilityPoolAddress, - address _defaultPoolAddress + address _defaultPoolAddress, + address _boldTokenAddress, + address _interestRouterAddress ) external; } diff --git a/contracts/src/Interfaces/ILiquityBase.sol b/contracts/src/Interfaces/ILiquityBase.sol index 3d3f0f24..709e2ec8 100644 --- a/contracts/src/Interfaces/ILiquityBase.sol +++ b/contracts/src/Interfaces/ILiquityBase.sol @@ -11,4 +11,5 @@ interface ILiquityBase { function defaultPool() external view returns (IDefaultPool); function priceFeed() external view returns (IPriceFeed); function BOLD_GAS_COMPENSATION() external view returns (uint256); + function getEntireSystemDebt() external view returns (uint256); } diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 58dea80e..3dd0b9cb 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -8,7 +8,6 @@ import "./ILiquityBase.sol"; import "./IStabilityPool.sol"; import "./IBoldToken.sol"; import "./ISortedTroves.sol"; -import "./IInterestRouter.sol"; // Common interface for the Trove Manager. interface ITroveManager is IERC721, ILiquityBase { @@ -21,24 +20,18 @@ interface ITroveManager is IERC721, ILiquityBase { address _collSurplusPoolAddress, address _priceFeedAddress, address _boldTokenAddress, - address _sortedTrovesAddress, - address _mockInterestRouterAddress + address _sortedTrovesAddress ) external; function stabilityPool() external view returns (IStabilityPool); function boldToken() external view returns (IBoldToken); function sortedTroves() external view returns(ISortedTroves); function borrowerOperationsAddress() external view returns (address); - function interestRouter() external view returns (IInterestRouter); function BOOTSTRAP_PERIOD() external view returns (uint256); // function BOLD_GAS_COMPENSATION() external view returns (uint256); - function lastAggUpdateTime() external view returns (uint256); - function aggRecordedDebt() external view returns (uint256); - function aggWeightedDebtSum() external view returns (uint256); - function getTroveIdsCount() external view returns (uint); function getTroveFromTroveIdsArray(uint _index) external view returns (uint256); @@ -59,8 +52,6 @@ interface ITroveManager is IERC721, ILiquityBase { function addTroveIdToArray(uint256 _troveId) external returns (uint index); - function mintAggInterest(int256 _debtChange) external; - function getPendingETHReward(uint256 _troveId) external view returns (uint); function getPendingBoldDebtReward(uint256 _troveId) external view returns (uint); @@ -68,8 +59,8 @@ interface ITroveManager is IERC721, ILiquityBase { function hasRedistributionGains(address _borrower) external view returns (bool); function getEntireDebtAndColl(uint256 _troveId) external view returns ( - uint debt, - uint coll, + uint entireDebt, + uint entireColl, uint pendingBoldDebtReward, uint pendingETHReward, uint pendingBoldInterest @@ -77,7 +68,7 @@ interface ITroveManager is IERC721, ILiquityBase { function getAndApplyRedistributionGains(address _borrower) external; - function closeTrove(address _troveId) external; + function closeTrove(uint256 _troveId, uint256 _weightedRecordedTroveDebt) external; function removeStake(uint256 _troveId) external; @@ -92,14 +83,17 @@ interface ITroveManager is IERC721, ILiquityBase { function getTroveDebt(uint256 _troveId) external view returns (uint); + function getTroveWeightedRecordedDebt(address _borrower) external returns (uint256); + function getTroveColl(uint256 _troveId) external view returns (uint); + function calcPendingTroveInterest(uint256 _troveId) external view returns (uint256); function getTroveAnnualInterestRate(uint256 _troveId) external view returns (uint); function TroveAddManagers(uint256 _troveId) external view returns (address); function TroveRemoveManagers(uint256 _troveId) external view returns (address); - function getTroveLastDebtUpdateTime(address _borrower) external view returns (uint); + function getTroveLastDebtUpdateTime(uint256 _troveId) external view returns (uint); function setTrovePropertiesOnOpen(address _owner, uint256 _troveId, uint256 _coll, uint256 _debt, uint256 _annualInterestRate) external returns (uint256); @@ -121,6 +115,4 @@ interface ITroveManager is IERC721, ILiquityBase { function checkRecoveryMode(uint _price) external view returns (bool); function checkTroveIsActive(uint256 _troveId) external view returns (bool); - - function calcPendingAggInterest() external view returns (uint256); } diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 17f4bfdf..28372023 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -285,7 +285,7 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { function provideToSP(uint _amount) external override { _requireNonZeroAmount(_amount); - troveManager.mintAggInterest(0); + activePool.mintAggInterest(0); uint initialDeposit = deposits[msg.sender].initialValue; @@ -316,7 +316,7 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { uint initialDeposit = deposits[msg.sender].initialValue; _requireUserHasDeposit(initialDeposit); - troveManager.mintAggInterest(0); + activePool.mintAggInterest(0); uint depositorETHGain = getDepositorETHGain(msg.sender); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index e18edc9b..600abfb2 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -9,7 +9,6 @@ import "./Interfaces/IStabilityPool.sol"; import "./Interfaces/ICollSurplusPool.sol"; import "./Interfaces/IBoldToken.sol"; import "./Interfaces/ISortedTroves.sol"; -import "./Interfaces/IInterestRouter.sol"; import "./Dependencies/LiquityBase.sol"; import "./Dependencies/Ownable.sol"; import "./Dependencies/CheckContract.sol"; @@ -35,12 +34,9 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // A doubly linked list of Troves, sorted by their sorted by their collateral ratios ISortedTroves public sortedTroves; - IInterestRouter public interestRouter; - // --- Data structures --- uint constant public SECONDS_IN_ONE_MINUTE = 60; - uint256 constant public SECONDS_IN_ONE_YEAR = 31536000; // 60 * 60 * 24 * 365, /* @@ -136,14 +132,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint public lastETHError_Redistribution; uint public lastBoldDebtError_Redistribution; - uint256 public aggRecordedDebt; - /* Sum of individual recorded Trove debts, weighted by their respective chosen interest rates. - * Updated at all Trove operations. - */ - uint256 public aggWeightedDebtSum; - - // Last time at which the aggregate recorded debt and weighted sum were updated - uint256 public lastAggUpdateTime; /* * --- Variable container structs for liquidations --- * @@ -163,6 +151,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint collToLiquidate; uint pendingDebtReward; uint pendingCollReward; + uint256 weightedRecordedTroveDebt; } struct LocalVariables_LiquidationSequence { @@ -270,8 +259,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana address _collSurplusPoolAddress, address _priceFeedAddress, address _boldTokenAddress, - address _sortedTrovesAddress, - address _interestRouterAddress + address _sortedTrovesAddress ) external override @@ -286,7 +274,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana checkContract(_priceFeedAddress); checkContract(_boldTokenAddress); checkContract(_sortedTrovesAddress); - checkContract(_interestRouterAddress); borrowerOperationsAddress = _borrowerOperationsAddress; activePool = IActivePool(_activePoolAddress); @@ -297,7 +284,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana priceFeed = IPriceFeed(_priceFeedAddress); boldToken = IBoldToken(_boldTokenAddress); sortedTroves = ISortedTroves(_sortedTrovesAddress); - interestRouter = IInterestRouter(_interestRouterAddress); emit BorrowerOperationsAddressChanged(_borrowerOperationsAddress); emit ActivePoolAddressChanged(_activePoolAddress); @@ -352,6 +338,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana vars.pendingDebtReward, vars.pendingCollReward, ) = getEntireDebtAndColl(_troveId); + vars.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_borrower); + _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward); _removeStake(_troveId); @@ -364,7 +352,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana singleLiquidation.debtToRedistribute, singleLiquidation.collToRedistribute) = _getOffsetAndRedistributionVals(singleLiquidation.entireTroveDebt, collToLiquidate, _boldInStabPool); - _closeTrove(_troveId, Status.closedByLiquidation); + _closeTrove(_troveId, Status.closedByLiquidation, vars.weightedRecordedTroveDebt); emit TroveLiquidated(_troveId, singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, TroveManagerOperation.liquidateInNormalMode); emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.liquidateInNormalMode); return singleLiquidation; @@ -390,6 +378,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana vars.pendingDebtReward, vars.pendingCollReward, ) = getEntireDebtAndColl(_troveId); + vars.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_borrower); + singleLiquidation.collGasCompensation = _getCollGasCompensation(singleLiquidation.entireTroveColl); singleLiquidation.BoldGasCompensation = BOLD_GAS_COMPENSATION; vars.collToLiquidate = singleLiquidation.entireTroveColl - singleLiquidation.collGasCompensation; @@ -404,7 +394,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana singleLiquidation.debtToRedistribute = singleLiquidation.entireTroveDebt; singleLiquidation.collToRedistribute = vars.collToLiquidate; - _closeTrove(_troveId, Status.closedByLiquidation); + _closeTrove(_troveId, Status.closedByLiquidation, vars.weightedRecordedTroveDebt); emit TroveLiquidated(_troveId, singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, TroveManagerOperation.liquidateInRecoveryMode); emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.liquidateInRecoveryMode); @@ -418,7 +408,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana singleLiquidation.debtToRedistribute, singleLiquidation.collToRedistribute) = _getOffsetAndRedistributionVals(singleLiquidation.entireTroveDebt, vars.collToLiquidate, _boldInStabPool); - _closeTrove(_troveId, Status.closedByLiquidation); + _closeTrove(_troveId, Status.closedByLiquidation, vars.weightedRecordedTroveDebt); emit TroveLiquidated(_troveId, singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, TroveManagerOperation.liquidateInRecoveryMode); emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.liquidateInRecoveryMode); /* @@ -434,7 +424,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana _removeStake(_troveId); singleLiquidation = _getCappedOffsetVals(singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, _price); - _closeTrove(_troveId, Status.closedByLiquidation); + _closeTrove(_troveId, Status.closedByLiquidation, vars.weightedRecordedTroveDebt); if (singleLiquidation.collSurplus > 0) { collSurplusPool.accountSurplus(_troveId, singleLiquidation.collSurplus); } @@ -794,6 +784,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint _price ) internal returns (SingleRedemptionValues memory singleRedemption) + { // 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); @@ -807,9 +798,10 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // TODO: zombi troves if (newDebt == BOLD_GAS_COMPENSATION) { + uint256 weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_borrower); // No debt left in the Trove (except for the liquidation reserve), therefore the trove gets closed _removeStake(_troveId); - _closeTrove(_troveId, Status.closedByRedemption); + _closeTrove(_troveId, Status.closedByRedemption, weightedRecordedTroveDebt); _redeemCloseTrove(_contractsCache, _troveId, BOLD_GAS_COMPENSATION, newColl); emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.redeemCollateral); @@ -990,6 +982,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana _updateTroveRewardSnapshots(_troveId); + //TODO: Update agg. recorded debt and agg. weighted debt sum with the redistribution gains + // Transfer redistribution gains from DefaultPool to ActivePool _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, pendingBoldDebtReward, pendingETHReward); @@ -1055,17 +1049,17 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana public view override - returns (uint debt, uint coll, uint pendingBoldDebtReward, uint pendingETHReward, uint pendingBoldInterest) + returns (uint entireDebt, uint entireColl, uint pendingBoldDebtReward, uint pendingETHReward, uint pendingBoldInterest) { - debt = Troves[_troveId].debt; - coll = Troves[_troveId].coll; + uint256 recordedDebt = Troves[_troveId].debt; + uint256 recordedColl = Troves[_troveId].coll; pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); - pendingBoldInterest = _calcPendingTroveInterest(_troveId); + pendingBoldInterest = calcPendingTroveInterest(_troveId); pendingETHReward = getPendingETHReward(_troveId); - debt = debt + pendingBoldDebtReward + pendingBoldInterest; - coll = coll + pendingETHReward; + entireDebt = recordedDebt + pendingBoldDebtReward + pendingBoldInterest; + entireColl = recordedColl + pendingETHReward; } function removeStake(uint256 _troveId) external override { @@ -1151,25 +1145,30 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana _activePool.sendETHToDefaultPool(_coll); } - function closeTrove(uint256 _troveId) external override { + function closeTrove(uint256 _troveId, uint256 _weightedRecordedTroveDebt) external override { _requireCallerIsBorrowerOperations(); - return _closeTrove(_troveId, Status.closedByOwner); + return _closeTrove(_troveId, Status.closedByOwner, _weightedRecordedTroveDebt); } - function _closeTrove(uint256 _troveId, Status closedStatus) internal { + function _closeTrove(uint256 _troveId, Status closedStatus, uint256 _weightedRecordedTroveDebt) internal { assert(closedStatus != Status.nonExistent && closedStatus != Status.active); uint TroveIdsArrayLength = TroveIds.length; _requireMoreThanOneTroveInSystem(TroveIdsArrayLength); + // Zero Trove properties Troves[_troveId].status = closedStatus; Troves[_troveId].coll = 0; Troves[_troveId].debt = 0; Troves[_troveId].annualInterestRate = 0; + // Zero Trove snapshots rewardSnapshots[_troveId].ETH = 0; rewardSnapshots[_troveId].boldDebt = 0; + // Remove Trove's weighted debt from the weighted sum + activePool.decreaseAggWeightedDebtSum(_weightedRecordedTroveDebt); + _removeTroveId(_troveId, TroveIdsArrayLength); sortedTroves.remove(_troveId); @@ -1356,50 +1355,13 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // TODO: analyze precision loss in interest functions and decide upon the minimum granularity // (per-second, per-block, etc) - function _calcPendingTroveInterest(address _borrower) internal view returns (uint256) { + function calcPendingTroveInterest(address _borrower) public view returns (uint256) { uint256 recordedDebt = Troves[_borrower].debt; // convert annual interest to per-second and multiply by the principal uint256 annualInterestRate = Troves[_borrower].annualInterestRate; uint256 lastDebtUpdateTime = Troves[_borrower].lastDebtUpdateTime; - return recordedDebt * annualInterestRate * (block.timestamp - lastDebtUpdateTime) / SECONDS_IN_ONE_YEAR; - } - - function calcPendingAggInterest() public view returns (uint256) { - return aggWeightedDebtSum * (block.timestamp - lastAggUpdateTime) / SECONDS_IN_ONE_YEAR / 1e18; - } - - // --- Aggregate interest operations --- - - // This function is called inside all state-changing user ops: borrower ops, liquidations, redemptions and SP deposits/withdrawals. - // Some user ops trigger debt changes to Trove(s), in which case _troveDebtChange will be non-zero. - // The _troveDebtChange is the sum of: - // - Any composite debt change to a Trove from a borrower op - // - Any debt increase due to a Trove's redistribution gaisn being applied. - // It does NOT include the Trove's pending individual interest. This is because aggregate interest and individual interest are tracked - // separately, in parallel. - //That is, the aggregate recorded debt is incremented by the aggregate pending interest. - - function mintAggInterest(int256 _troveDebtChange) public { - _requireCallerIsBOorSP(); - 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);} - - // TODO: cleaner way to deal with debt changes that can be positive or negative? - aggRecordedDebt = addUint256ToInt256(aggRecordedDebt + aggInterest, _troveDebtChange); - // assert(aggRecordedDebt > 0) // This should never be negative. If all principal debt were repaid, it should be 0, and if all - lastAggUpdateTime = block.timestamp; - } - - function addUint256ToInt256(uint256 _x, int256 _y) internal returns (uint256) { - // Assumption: _x + _y > 0. Will revert otherwise. - if (_y >= 0) { - return _x + uint256(_y); - } else { - return (_x - uint256(-_y)); - } + return recordedDebt * annualInterestRate * (block.timestamp - lastDebtUpdateTime) / SECONDS_IN_ONE_YEAR / 1e18; } // TODO: make this purely for existing Trove touches which alter coll or debt. // TODO: How about redemptions and liqs? @@ -1460,11 +1422,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana require(msg.sender == borrowerOperationsAddress, "TroveManager: Caller is not the BorrowerOperations contract"); } - function _requireCallerIsBOorSP() internal view { - require(msg.sender == borrowerOperationsAddress || msg.sender == address(stabilityPool), - "TroveManager: Caller is not the BO or SP"); - } - function _requireIsOwnerOrAddManager(uint256 _troveId, address _sender) internal view { assert(_sender != address(0)); // TODO: remove require( @@ -1523,6 +1480,10 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return Troves[_troveId].debt; } + function getTroveWeightedRecordedDebt(address _borrower) public returns (uint256) { + return Troves[_borrower].debt * Troves[_borrower].annualInterestRate; + } + function getTroveColl(uint256 _troveId) external view override returns (uint) { return Troves[_troveId].coll; } @@ -1558,7 +1519,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana _updateTroveRewardSnapshots(_troveId); // Update weighted debt sum with the Trove's interest-weighted debt - aggWeightedDebtSum += _debt * _annualInterestRate; + activePool.increaseAggWeightedDebtSum(_debt, _annualInterestRate); // Record the Trove's stake (for redistributions) and update the total stakes return _updateStakeAndTotalStakes(_troveId); diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index 8d17e9bd..3b68f8f0 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -128,6 +128,18 @@ contract BaseTest is Test { vm.stopPrank(); } + function closeTrove(address _account) public { + vm.startPrank(_account); + borrowerOperations.closeTrove(); + vm.stopPrank(); + } + + function transferBold(address _from, address _to, uint256 _amount) public { + vm.startPrank(_from); + boldToken.transfer(_to, boldToken.balanceOf(_from)); + 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 2c2e34cc..dadf6dba 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -65,7 +65,7 @@ contract DevTestSetup is BaseTest { sortedTroves = new SortedTroves(); stabilityPool = new StabilityPool(address(WETH)); troveManager = new TroveManager(); - boldToken = new BoldToken(address(troveManager), address(stabilityPool), address(borrowerOperations)); + boldToken = new BoldToken(address(troveManager), address(stabilityPool), address(borrowerOperations), address(activePool)); mockInterestRouter = new MockInterestRouter(); // Connect contracts @@ -85,8 +85,7 @@ contract DevTestSetup is BaseTest { address(collSurplusPool), address(priceFeed), address(boldToken), - address(sortedTroves), - address(mockInterestRouter) + address(sortedTroves) ); // set contracts in BorrowerOperations @@ -116,7 +115,9 @@ contract DevTestSetup is BaseTest { address(borrowerOperations), address(troveManager), address(stabilityPool), - address(defaultPool) + address(defaultPool), + address(boldToken), + address(mockInterestRouter) ); defaultPool.setAddresses( diff --git a/contracts/src/test/deployment.t.sol b/contracts/src/test/deployment.t.sol index 86cdcf11..f3504774 100644 --- a/contracts/src/test/deployment.t.sol +++ b/contracts/src/test/deployment.t.sol @@ -64,11 +64,11 @@ contract Deployment is DevTestSetup { assertEq(stabilityPoolAddress, recordedStabilityPoolAddress); } - function testTroveManagerHasCorrectInterestRouterAddress() public { - address interestRouter = address(mockInterestRouter); - address recordedInterestRouterAddress = address(troveManager.interestRouter()); - assertEq(interestRouter, recordedInterestRouterAddress); - } + // function testTroveManagerHasCorrectInterestRouterAddress() public { + // address interestRouter = address(mockInterestRouter); + // address recordedInterestRouterAddress = address(troveManager.interestRouter()); + // assertEq(interestRouter, recordedInterestRouterAddress); + // } // Active Pool diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index a2d11ba2..631b1861 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -8,30 +8,30 @@ contract InterestRateAggregate is DevTestSetup { function testCalcPendingAggInterestReturns0For0TimePassedSinceLastUpdate() public { priceFeed.setPrice(2000e18); - assertEq(troveManager.lastAggUpdateTime(), 0); - assertEq(troveManager.calcPendingAggInterest(), 0); + assertEq(activePool.lastAggUpdateTime(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); - assertEq(troveManager.lastAggUpdateTime(), block.timestamp); - assertEq(troveManager.calcPendingAggInterest(), 0); + // assertEq(activePool.lastAggUpdateTime(), block.timestamp); + // assertEq(activePool.calcPendingAggInterest(), 0); - openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 5e17); - assertEq(troveManager.lastAggUpdateTime(), block.timestamp); - assertEq(troveManager.calcPendingAggInterest(), 0); + // openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 5e17); + // assertEq(activePool.lastAggUpdateTime(), block.timestamp); + // assertEq(activePool.calcPendingAggInterest(), 0); } // calcPendingAggInterest returns 0 with no recorded aggregate debt function testCalcPendingAggInterestReturns0When0AggRecordedDebt() public { priceFeed.setPrice(2000e18); - assertEq(troveManager.aggRecordedDebt(), 0); - assertEq(troveManager.aggWeightedDebtSum(), 0); - assertEq(troveManager.calcPendingAggInterest(), 0); + assertEq(activePool.aggRecordedDebt(), 0); + assertEq(activePool.aggWeightedDebtSum(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); vm.warp(block.timestamp + 1000); - assertEq(troveManager.aggRecordedDebt(), 0); - assertEq(troveManager.aggWeightedDebtSum(), 0); - assertEq(troveManager.calcPendingAggInterest(), 0); + assertEq(activePool.aggRecordedDebt(), 0); + assertEq(activePool.aggWeightedDebtSum(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); } // calcPendingAggInterest returns 0 when all troves have 0 interest rate @@ -40,19 +40,19 @@ contract InterestRateAggregate is DevTestSetup { openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 0); - assertEq(troveManager.calcPendingAggInterest(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); vm.warp(block.timestamp + 1000); - assertEq(troveManager.calcPendingAggInterest(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 0); - assertEq(troveManager.calcPendingAggInterest(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); vm.warp(block.timestamp + 1000); - assertEq(troveManager.calcPendingAggInterest(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); } // TODO: create additional fuzz test @@ -74,7 +74,7 @@ contract InterestRateAggregate is DevTestSetup { // Expect weighted average of 2 * troveDebt debt at 50% interest uint256 expectedPendingAggInterest = expectedTroveDebt * 2 * 5e17 * _duration / SECONDS_IN_1_YEAR / 1e18; - assertEq(expectedPendingAggInterest, troveManager.calcPendingAggInterest()); + assertEq(expectedPendingAggInterest, activePool.calcPendingAggInterest()); } // --- mintAggInterest --- @@ -84,44 +84,44 @@ contract InterestRateAggregate is DevTestSetup { int256 debtChange = 37e18; vm.startPrank(A); vm.expectRevert(); - troveManager.mintAggInterest(debtChange); + activePool.mintAggInterest(debtChange); vm.stopPrank(); vm.startPrank(address(borrowerOperations)); - troveManager.mintAggInterest(debtChange); + activePool.mintAggInterest(debtChange); vm.stopPrank(); vm.startPrank(address(stabilityPool)); - troveManager.mintAggInterest(debtChange); + activePool.mintAggInterest(debtChange); vm.stopPrank(); // pass negative debt change debtChange = -37e18; vm.startPrank(A); vm.expectRevert(); - troveManager.mintAggInterest(debtChange); + activePool.mintAggInterest(debtChange); vm.stopPrank(); vm.startPrank(address(borrowerOperations)); - troveManager.mintAggInterest(debtChange); + activePool.mintAggInterest(debtChange); vm.stopPrank(); vm.startPrank(address(stabilityPool)); - troveManager.mintAggInterest(debtChange); + activePool.mintAggInterest(debtChange); vm.stopPrank(); // pass 0 debt change vm.startPrank(A); vm.expectRevert(); - troveManager.mintAggInterest(0); + activePool.mintAggInterest(0); vm.stopPrank(); vm.startPrank(address(borrowerOperations)); - troveManager.mintAggInterest(0); + activePool.mintAggInterest(0); vm.stopPrank(); vm.startPrank(address(stabilityPool)); - troveManager.mintAggInterest(0); + activePool.mintAggInterest(0); vm.stopPrank(); } @@ -130,20 +130,20 @@ contract InterestRateAggregate is DevTestSetup { // openTrove increases recorded aggregate debt by correct amount function testOpenTroveIncreasesRecordedAggDebtByAggPendingInterestPlusNewDebt() public { priceFeed.setPrice(2000e18); - assertEq(troveManager.aggRecordedDebt(), 0); + assertEq(activePool.aggRecordedDebt(), 0); uint256 troveDebtRequest = 2000e18; openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest // Check aggregate recorded debt increased to non-zero - uint256 aggREcordedDebt_1 = troveManager.aggRecordedDebt(); + uint256 aggREcordedDebt_1 = activePool.aggRecordedDebt(); assertGt(aggREcordedDebt_1, 0); // fast-forward time vm.warp(block.timestamp + 1 days); // check there's pending interest - uint256 pendingInterest = troveManager.calcPendingAggInterest(); + uint256 pendingInterest = activePool.calcPendingAggInterest(); assertGt(pendingInterest, 0); uint256 expectedTroveDebt_B = troveDebtRequest + troveManager.BOLD_GAS_COMPENSATION(); @@ -151,7 +151,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(troveManager.getTroveDebt(B), expectedTroveDebt_B); // check that opening Trove B increased the agg. recorded debt by the pending agg. interest plus Trove B's debt - assertEq(troveManager.aggRecordedDebt(), aggREcordedDebt_1 + pendingInterest + expectedTroveDebt_B); + assertEq(activePool.aggRecordedDebt(), aggREcordedDebt_1 + pendingInterest + expectedTroveDebt_B); } function testOpenTroveReducesPendingAggInterestTo0() public { @@ -164,28 +164,28 @@ contract InterestRateAggregate is DevTestSetup { vm.warp(block.timestamp + 1 days); // check there's pending agg. interest - assertGt(troveManager.calcPendingAggInterest(), 0); + assertGt(activePool.calcPendingAggInterest(), 0); openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 25e16); // Check pending agg. interest reduced to 0 - assertEq(troveManager.calcPendingAggInterest(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); } function testOpenTroveUpdatesTheLastAggUpdateTime() public { priceFeed.setPrice(2000e18); - assertEq(troveManager.lastAggUpdateTime(), 0); + assertEq(activePool.lastAggUpdateTime(), 0); vm.warp(block.timestamp + 1 days); openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); // 25% annual interest - assertEq(troveManager.lastAggUpdateTime(), block.timestamp); + assertEq(activePool.lastAggUpdateTime(), block.timestamp); vm.warp(block.timestamp + 1 days); openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 25e16); // 25% annual interest - assertEq(troveManager.lastAggUpdateTime(), block.timestamp); + assertEq(activePool.lastAggUpdateTime(), block.timestamp); } function testOpenTroveMintsInterestToInterestRouter() public { @@ -197,7 +197,7 @@ contract InterestRateAggregate is DevTestSetup { vm.warp(block.timestamp + 1 days); - uint256 pendingAggInterest_1 = troveManager.calcPendingAggInterest(); + uint256 pendingAggInterest_1 = activePool.calcPendingAggInterest(); assertGt(pendingAggInterest_1, 0); // Open 2nd trove @@ -209,7 +209,7 @@ contract InterestRateAggregate is DevTestSetup { vm.warp(block.timestamp + 1 days); - uint256 pendingAggInterest_2 = troveManager.calcPendingAggInterest(); + uint256 pendingAggInterest_2 = activePool.calcPendingAggInterest(); // Open 3rd trove openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 25e16); @@ -226,7 +226,7 @@ contract InterestRateAggregate is DevTestSetup { uint256 troveDebtRequest_B = 2000e18; uint256 annualInterest_B = 25e16; - assertEq(troveManager.aggWeightedDebtSum(), 0); + assertEq(activePool.aggWeightedDebtSum(), 0); // A opens trove openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, annualInterest_A); @@ -236,9 +236,9 @@ 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(troveManager.aggWeightedDebtSum(), "troveManager.aggWeightedDebtSum()"); + console.log(activePool.aggWeightedDebtSum(), "activePool.aggWeightedDebtSum()"); - assertEq(troveManager.aggWeightedDebtSum(), expectedWeightedDebt_A); + assertEq(activePool.aggWeightedDebtSum(), expectedWeightedDebt_A); vm.warp(block.timestamp + 1000); @@ -249,7 +249,7 @@ contract InterestRateAggregate is DevTestSetup { uint256 expectedWeightedDebt_B = troveDebt_B * annualInterest_B; - assertEq(troveManager.aggWeightedDebtSum(), expectedWeightedDebt_A + expectedWeightedDebt_B); + assertEq(activePool.aggWeightedDebtSum(), expectedWeightedDebt_A + expectedWeightedDebt_B); } // --- SP deposits --- @@ -265,13 +265,13 @@ contract InterestRateAggregate is DevTestSetup { vm.warp(block.timestamp + 1 days); // // check there's pending agg. interest - assertGt(troveManager.calcPendingAggInterest(), 0); + assertGt(activePool.calcPendingAggInterest(), 0); // A deposits to SP makeSPDeposit(A, sPdeposit); // Check pending agg. interest reduced to 0 - assertEq(troveManager.calcPendingAggInterest(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); } function testSPDepositIncreasesAggRecordedDebtByPendingAggInterest() public { @@ -285,17 +285,17 @@ contract InterestRateAggregate is DevTestSetup { // fast-forward time vm.warp(block.timestamp + 1 days); - uint256 pendingInterest = troveManager.calcPendingAggInterest(); + uint256 pendingInterest = activePool.calcPendingAggInterest(); assertGt(pendingInterest, 0); - uint256 aggRecordedDebt_1 = troveManager.aggRecordedDebt(); + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); assertGt(aggRecordedDebt_1, 0); // A deposits to SP makeSPDeposit(A, sPdeposit); // Check pending agg. debt increased - uint256 aggRecordedDebt_2 = troveManager.aggRecordedDebt(); + uint256 aggRecordedDebt_2 = activePool.aggRecordedDebt(); assertEq(aggRecordedDebt_2, aggRecordedDebt_1 + pendingInterest); } @@ -310,14 +310,14 @@ contract InterestRateAggregate is DevTestSetup { // fast-forward time vm.warp(block.timestamp + 1 days); - assertGt(troveManager.lastAggUpdateTime(), 0); - assertLt(troveManager.lastAggUpdateTime(), block.timestamp); + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); // A deposits to SP makeSPDeposit(A, sPdeposit); // Check last agg update time increased to now - assertEq(troveManager.lastAggUpdateTime(), block.timestamp); + assertEq(activePool.lastAggUpdateTime(), block.timestamp); } function testSPDepositMintsInterestToInterestRouter() public { @@ -335,7 +335,7 @@ contract InterestRateAggregate is DevTestSetup { uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); assertEq(boldBalRouter_1, 0); - uint256 pendingAggInterest = troveManager.calcPendingAggInterest(); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); assertGt(pendingAggInterest, 0); // Make SP deposit @@ -359,17 +359,17 @@ contract InterestRateAggregate is DevTestSetup { vm.warp(block.timestamp + 1 days); // Get weighted sum before - uint256 weightedDebtSum_1 = troveManager.aggWeightedDebtSum(); + uint256 weightedDebtSum_1 = activePool.aggWeightedDebtSum(); assertGt(weightedDebtSum_1, 0); - uint256 pendingAggInterest = troveManager.calcPendingAggInterest(); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); assertGt(pendingAggInterest, 0); // Make SP deposit makeSPDeposit(A, sPdeposit); // Get weighted sum after, check no change - uint256 weightedDebtSum_2 = troveManager.aggWeightedDebtSum(); + uint256 weightedDebtSum_2 = activePool.aggWeightedDebtSum(); assertEq(weightedDebtSum_2, weightedDebtSum_1); } @@ -387,13 +387,13 @@ contract InterestRateAggregate is DevTestSetup { vm.warp(block.timestamp + 1 days); // check there's pending agg. interest - assertGt(troveManager.calcPendingAggInterest(), 0); + assertGt(activePool.calcPendingAggInterest(), 0); // A withdraws deposit makeSPWithdrawal(A, sPdeposit); // Check pending agg. interest reduced to 0 - assertEq(troveManager.calcPendingAggInterest(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); } function testSPWithdrawalIncreasesAggRecordedDebtByPendingAggInterest() public { @@ -407,17 +407,17 @@ contract InterestRateAggregate is DevTestSetup { // fast-forward time vm.warp(block.timestamp + 1 days); - uint256 pendingInterest = troveManager.calcPendingAggInterest(); + uint256 pendingInterest = activePool.calcPendingAggInterest(); assertGt(pendingInterest, 0); - uint256 aggRecordedDebt_1 = troveManager.aggRecordedDebt(); + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); assertGt(aggRecordedDebt_1, 0); // A withdraws deposit makeSPWithdrawal(A, sPdeposit); // Check pending agg. debt increased - uint256 aggRecordedDebt_2 = troveManager.aggRecordedDebt(); + uint256 aggRecordedDebt_2 = activePool.aggRecordedDebt(); assertEq(aggRecordedDebt_2, aggRecordedDebt_1 + pendingInterest); } @@ -432,14 +432,14 @@ contract InterestRateAggregate is DevTestSetup { // fast-forward time vm.warp(block.timestamp + 1 days); - assertGt(troveManager.lastAggUpdateTime(), 0); - assertLt(troveManager.lastAggUpdateTime(), block.timestamp); + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); // A withdraws from SP makeSPWithdrawal(A, sPdeposit); // Check last agg update time increased to now - assertEq(troveManager.lastAggUpdateTime(), block.timestamp); + assertEq(activePool.lastAggUpdateTime(), block.timestamp); } function testSPWithdrawalMintsInterestToInterestRouter() public { @@ -457,7 +457,7 @@ contract InterestRateAggregate is DevTestSetup { uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); assertEq(boldBalRouter_1, 0); - uint256 pendingAggInterest = troveManager.calcPendingAggInterest(); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); assertGt(pendingAggInterest, 0); // A withdraws from SP @@ -480,17 +480,168 @@ contract InterestRateAggregate is DevTestSetup { vm.warp(block.timestamp + 1 days); // Get weighted sum before - uint256 weightedDebtSum_1 = troveManager.aggWeightedDebtSum(); + uint256 weightedDebtSum_1 = activePool.aggWeightedDebtSum(); assertGt(weightedDebtSum_1, 0); - uint256 pendingAggInterest = troveManager.calcPendingAggInterest(); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); assertGt(pendingAggInterest, 0); // Make SP deposit makeSPDeposit(A, sPdeposit); // Get weighted sum after, check no change - uint256 weightedDebtSum_2 = troveManager.aggWeightedDebtSum(); + uint256 weightedDebtSum_2 = activePool.aggWeightedDebtSum(); assertEq(weightedDebtSum_2, weightedDebtSum_1); } + + // --- closeTrove --- + + // Reduces pending agg interest to 0 + function testcloseTroveReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + // A, B opens Trove to obtain BOLD and makes SP deposit + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + + // A sends Bold to B so B can cover their interest and close their Trove + transferBold(A, B, boldToken.balanceOf(A)); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // check there's pending agg. interest + assertGt(activePool.calcPendingAggInterest(), 0); + + // B closes Trove + closeTrove(B); + + // // Check pending agg. interest reduced to 0 + assertEq(activePool.calcPendingAggInterest(), 0); + } + + // Increases agg recorded debt by pending agg interest + + function testCloseTroveAddsPendingAggInterestAndSubtractsEntireTroveDebtFromAggRecordedDebt() public { + uint256 troveDebtRequest = 2000e18; + // A, B opens Trove to obtain BOLD and makes SP deposit + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + + // A sends Bold to B so B can cover their interest and close their Trove + transferBold(A, B, boldToken.balanceOf(A)); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // Check the agg recorded debt is non-zero + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + + // Check there's pending agg. interest + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(activePool.calcPendingAggInterest(), 0); + + // Check Trove's entire debt is larger than their recorded debt: + (uint256 entireTroveDebt_B, , , , )= troveManager.getEntireDebtAndColl(B); + assertGt(entireTroveDebt_B, troveManager.getTroveDebt(B)); + + // B closes Trove + closeTrove(B); + + // // Check agg. recorded debt increased by pending agg. interest less the closed Trove's entire debt + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - entireTroveDebt_B); + } + + + // Updates last agg update time to now + function testCloseTroveUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + // A, B opens Trove to obtain BOLD and makes SP deposit + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + + // A sends Bold to B so B can cover their interest and close their Trove + transferBold(A, B, boldToken.balanceOf(A)); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // B closes Trove + closeTrove(B); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + // mints interest to interest router + function testCloseTroveMintsInterestToInterestRouter() public { + uint256 troveDebtRequest = 2000e18; + // A, B opens Trove to obtain BOLD and makes SP deposit + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + + // A sends Bold to B so B can cover their interest and close their Trove + transferBold(A, B, boldToken.balanceOf(A)); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // Get I-router balance + uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_1, 0); + + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // B closes Trove + closeTrove(B); + + // Check I-router Bold bal has increased as expected from 3rd trove opening + uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_2, pendingAggInterest); + } + + // Reduces agg. weighted sum by the Trove's recorded debt + + function testCloseTroveReducesAggWeightedDebtSumByTrovesWeightedRecordedDebt() public { + uint256 troveDebtRequest = 2000e18; + // A, B opens Trove to obtain BOLD and makes SP deposit + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + + // A sends Bold to B so B can cover their interest and close their Trove + transferBold(A, B, boldToken.balanceOf(A)); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_B = troveManager.getTroveDebt(B); + uint256 annualInterestRate_B = troveManager.getTroveAnnualInterestRate(B); + assertGt(recordedTroveDebt_B, 0); + assertGt(annualInterestRate_B, 0); + uint256 weightedTroveDebt = recordedTroveDebt_B * annualInterestRate_B; + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // B closes Trove + closeTrove(B); + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - weightedTroveDebt); + } + + // TODO: + // getEntireDebt and getTCR basic tests, for pending agg interest + + + + + } From 7a8b8fca315e41acaca7fc66beaa2d6b8cb9b5ae Mon Sep 17 00:00:00 2001 From: RickGriff Date: Wed, 14 Feb 2024 16:51:48 +0700 Subject: [PATCH 05/18] Add tests for calcPendingTroveInterest --- contracts/src/BorrowerOperations.sol | 2 +- .../src/test/interestRateAggregate.t.sol | 92 +++++++++++++++++-- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 351dba92..4f241a66 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -287,7 +287,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe troveManagerCached.changeAnnualInterestRate(_troveId, _newAnnualInterestRate); } - + /* * _adjustTrove(): Alongside a debt change, this function can perform either a collateral top-up or a collateral withdrawal. */ diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index 631b1861..13f96d8c 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -12,12 +12,12 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.calcPendingAggInterest(), 0); openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); - // assertEq(activePool.lastAggUpdateTime(), block.timestamp); - // assertEq(activePool.calcPendingAggInterest(), 0); + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + assertEq(activePool.calcPendingAggInterest(), 0); - // openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 5e17); - // assertEq(activePool.lastAggUpdateTime(), block.timestamp); - // assertEq(activePool.calcPendingAggInterest(), 0); + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 5e17); + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + assertEq(activePool.calcPendingAggInterest(), 0); } // calcPendingAggInterest returns 0 with no recorded aggregate debt @@ -56,7 +56,7 @@ contract InterestRateAggregate is DevTestSetup { } // TODO: create additional fuzz test - function testCalcPendingInterestReturnsCorrectInterestForGivenPeriod() public { + function testCalcPendingAggInterestReturnsCorrectInterestForGivenPeriod() public { priceFeed.setPrice(2000e18); uint256 _duration = 1 days; @@ -77,6 +77,84 @@ contract InterestRateAggregate is DevTestSetup { assertEq(expectedPendingAggInterest, activePool.calcPendingAggInterest()); } + // --- calcPendingTroveInterest + + // returns 0 for non-existent trove + function testCalcPendingTroveInterestReturns0When0AggRecordedDebt() public { + priceFeed.setPrice(2000e18); + + assertEq(troveManager.calcPendingTroveInterest(A), 0); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 75e16); + + vm.warp(block.timestamp + 1 days); + + // A sends Bold to B so B can cover their interest and close their Trove + transferBold(A, B, boldToken.balanceOf(A)); + + closeTrove(B); + + assertEq(troveManager.calcPendingTroveInterest(B), 0); + } + // returns 0 for 0 time passed + + function testCalcPendingTroveInterestReturns0For0TimePassed() public { + priceFeed.setPrice(2000e18); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); + assertEq(troveManager.calcPendingTroveInterest(A), 0); + + vm.warp(block.timestamp + 1 days); + + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 75e16); + assertEq(troveManager.calcPendingTroveInterest(B), 0); + } + + function testCalcPendingTroveInterestReturns0For0InterestRate() public { + priceFeed.setPrice(2000e18); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); + + assertEq(troveManager.calcPendingTroveInterest(A), 0); + + vm.warp(block.timestamp + 1 days); + + assertEq(troveManager.calcPendingTroveInterest(A), 0); + } + + // TODO: create additional corresponding fuzz test + function testCalcPendingTroveInterestReturnsCorrectInterestForGivenPeriod() public { + priceFeed.setPrice(2000e18); + + uint256 annualRate_A = 1e18; + uint256 annualRate_B = 37e16; + uint256 debtRequest_A = 2000e18; + uint256 debtRequest_B = 2500e18; + + uint256 duration = 42 days; + + openTroveNoHints100pctMaxFee(A, 2 ether, debtRequest_A, annualRate_A); + uint256 debt_A = troveManager.getTroveDebt(A); + assertGt(debt_A, 0); + assertEq(troveManager.calcPendingTroveInterest(A), 0); + + vm.warp(block.timestamp + duration); + + uint256 expectedInterest_A = annualRate_A * debt_A * duration / 1e18 / SECONDS_IN_1_YEAR; + assertEq(troveManager.calcPendingTroveInterest(A), expectedInterest_A); + + openTroveNoHints100pctMaxFee(B, 2 ether, debtRequest_B, annualRate_B); + uint256 debt_B = troveManager.getTroveDebt(B); + assertGt(debt_B, 0); + assertEq(troveManager.calcPendingTroveInterest(B), 0); + + vm.warp(block.timestamp + duration); + + uint256 expectedInterest_B = annualRate_B * debt_B * duration / 1e18 / SECONDS_IN_1_YEAR; + assertEq(troveManager.calcPendingTroveInterest(B), expectedInterest_B); + } + // --- mintAggInterest --- function testMintAggInterestRevertsWhenNotCalledByBOorSP() public { @@ -640,6 +718,8 @@ contract InterestRateAggregate is DevTestSetup { // TODO: // getEntireDebt and getTCR basic tests, for pending agg interest + // get + From b894aca899c0e80e22817d45f73ea23e37996715 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Mon, 19 Feb 2024 17:46:26 +0700 Subject: [PATCH 06/18] Apply aggregate interest when adjusting Trove interest rate --- contracts/src/ActivePool.sol | 43 ++-- contracts/src/BorrowerOperations.sol | 67 +++--- contracts/src/Interfaces/IActivePool.sol | 15 +- contracts/src/Interfaces/IDefaultPool.sol | 12 +- contracts/src/Interfaces/IPool.sol | 16 -- contracts/src/Interfaces/ITroveManager.sol | 11 +- contracts/src/StabilityPool.sol | 3 +- contracts/src/TroveManager.sol | 53 +++-- .../src/test/interestRateAggregate.t.sol | 218 ++++++++++++++++-- contracts/src/test/interestRateBasic.t.sol | 17 +- 10 files changed, 336 insertions(+), 119 deletions(-) delete mode 100644 contracts/src/Interfaces/IPool.sol diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index a51cf486..4efb2ad7 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -43,7 +43,7 @@ contract ActivePool is Ownable, CheckContract, IActivePool { uint256 internal ETHBalance; // deposited ether tracker // Sum of individual recorded Trove debts. Updated only at individual Trove operations. - uint256 internal boldDebt; + uint256 internal recordedDebtSum; // Aggregate recorded debt tracker. Updated whenever a Trove's debt is touched AND whenever the aggregate pending interest is minted. uint256 public aggRecordedDebt; @@ -63,7 +63,7 @@ contract ActivePool is Ownable, CheckContract, IActivePool { event EtherSent(address _to, uint _amount); event BorrowerOperationsAddressChanged(address _newBorrowerOperationsAddress); event TroveManagerAddressChanged(address _newTroveManagerAddress); - event ActivePoolBoldDebtUpdated(uint _boldDebt); + event ActivePoolBoldDebtUpdated(uint _recordedDebtSum); event ActivePoolETHBalanceUpdated(uint _ETHBalance); constructor(address _ETHAddress) { @@ -121,8 +121,8 @@ contract ActivePool is Ownable, CheckContract, IActivePool { return ETHBalance; } - function getBoldDebt() external view override returns (uint) { - return boldDebt; + function getRecordedDebtSum() external view override returns (uint) { + return recordedDebtSum; } function calcPendingAggInterest() public view returns (uint256) { @@ -172,37 +172,33 @@ contract ActivePool is Ownable, CheckContract, IActivePool { emit ActivePoolETHBalanceUpdated(newETHBalance); } - function increaseBoldDebt(uint _amount) external override { + function increaseRecordedDebtSum(uint _amount) external override { _requireCallerIsBOorTroveM(); - boldDebt = boldDebt + _amount; - emit ActivePoolBoldDebtUpdated(boldDebt); + uint256 newRecordedDebtSum = recordedDebtSum + _amount; + recordedDebtSum = newRecordedDebtSum; + emit ActivePoolBoldDebtUpdated(newRecordedDebtSum); } - function decreaseBoldDebt(uint _amount) external override { + function decreaseRecordedDebtSum(uint _amount) external override { _requireCallerIsBOorTroveMorSP(); - boldDebt = boldDebt - _amount; - emit ActivePoolBoldDebtUpdated(boldDebt); + uint256 newRecordedDebtSum = recordedDebtSum - _amount; + recordedDebtSum = newRecordedDebtSum; + emit ActivePoolBoldDebtUpdated(newRecordedDebtSum); } - function increaseAggWeightedDebtSum(uint256 _debt, uint256 _annualInterestRate) external { + function changeAggWeightedDebtSum(uint256 _oldWeightedRecordedTroveDebt, uint256 _newTroveWeightedRecordedTroveDebt) external { _requireCallerIsTroveManager(); - aggWeightedDebtSum += _debt * _annualInterestRate; - } - - function decreaseAggWeightedDebtSum(uint256 _weightedRecordedTroveDebt) external { - aggWeightedDebtSum -= _weightedRecordedTroveDebt; + // TODO: Currently 2 SLOADs - more gas efficient way? Use ints? + aggWeightedDebtSum -= _oldWeightedRecordedTroveDebt; + aggWeightedDebtSum += _newTroveWeightedRecordedTroveDebt; } // --- Aggregate interest operations --- // This function is called inside all state-changing user ops: borrower ops, liquidations, redemptions and SP deposits/withdrawals. // Some user ops trigger debt changes to Trove(s), in which case _troveDebtChange will be non-zero. - // The _troveDebtChange is the sum of: - // - Any composite debt change to a Trove from a borrower op - // - Any debt increase due to a Trove's redistribution gaisn being applied. - // It does NOT include the Trove's pending individual interest. This is because aggregate interest and individual interest are tracked - // separately, in parallel. - //That is, the aggregate recorded debt is incremented by the aggregate pending interest. + // The aggregate recorded debt is incremented by the aggregate pending interest, plus the _troveDebtChange. + // The _troveDebtChange results from the borrower repaying/drawing debt and redistribution gains being applied. function mintAggInterest(int256 _troveDebtChange) public { _requireCallerIsBOorSP(); uint256 aggInterest = calcPendingAggInterest(); @@ -212,8 +208,9 @@ contract ActivePool is Ownable, CheckContract, IActivePool { // TODO: cleaner way to deal with debt changes that can be positive or negative? aggRecordedDebt = addUint256ToInt256(aggRecordedDebt + aggInterest, _troveDebtChange); + // assert(aggRecordedDebt >= 0) // This should never be negative. If all aggregate interest was applied + // and all Trove debts were repaid, it should become 0. - // assert(aggRecordedDebt > 0) // This should never be negative. If all principal debt were repaid, it should be 0, and if all lastAggUpdateTime = block.timestamp; } diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 4f241a66..f700982c 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -278,16 +278,26 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe ITroveManager troveManagerCached = troveManager; _requireTroveisActive(troveManagerCached, _troveId); - // TODO: apply individual and aggregate pending interest, and take snapshots of current timestamp. - // TODO: determine how applying pending interest should interact / be sequenced with applying pending rewards from redistributions. + uint256 initialWeightedRecordedTroveDebt = troveManager.getTroveWeightedRecordedDebt(msg.sender); + + (, uint256 redistDebtGain) = troveManagerCached.getAndApplyRedistributionGains(msg.sender); + + // No debt is issued/repaid, so ActivePool.aggRecordedDebt increases only the redistribution gain + activePool.mintAggInterest(int256(redistDebtGain)); - troveManagerCached.getAndApplyRedistributionGains(msg.sender); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(msg.sender); + uint256 recordedTroveDebt = troveManager.getTroveDebt(msg.sender); + uint256 entireTroveDebt = recordedTroveDebt + accruedTroveInterest; sortedTroves.reInsert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); - troveManagerCached.changeAnnualInterestRate(_troveId, _newAnnualInterestRate); + // Update Trove recorded debt and interest-weighted debt sum + troveManager.updateTroveDebtAndInterest(_troveId, initialWeightedRecordedTroveDebt, entireTroveDebt, _newAnnualInterestRate); + + // Increase the active Pool's recorded debt only by the Trove's accrued interest, since we have already applied pending redist. gains. + activePool.increaseRecordedDebtSum(accruedTroveInterest); } - + /* * _adjustTrove(): Alongside a debt change, this function can perform either a collateral top-up or a collateral withdrawal. */ @@ -321,7 +331,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // Confirm the operation is an ETH transfer if coming from the Stability Pool to a trove assert((msg.sender != stabilityPoolAddress || (_isCollIncrease && _boldChange == 0))); - // TODO: apply individual and aggregate pending interest, and take snapshots of current timestamp. + // TODO: apply individual and aggregate accrued interest, and take snapshots of current timestamp. contractsCache.troveManager.getAndApplyRedistributionGains(_borrower); @@ -407,40 +417,44 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _requireNotInRecoveryMode(price); uint256 initialWeightedRecordedTroveDebt = troveManager.getTroveWeightedRecordedDebt(msg.sender); + uint256 initialRecordedTroveDebt = troveManager.getTroveDebt(msg.sender); // TODO: gas optimization of pending rewards. We don't need to actually update stored Trove debt & coll properties here, since we'll // zero them at the end. Currently, since we apply gains first, the following calls to getTroveColl and getTroveDebt // return values with redistribution gains included. When we gas-optimize, this won't be true and we - // should change the total debt and coll calculations. - troveManagerCached.getAndApplyRedistributionGains(msg.sender); + // may need to change the total debt and coll calculations. + ( , uint256 redistDebtGain) = troveManagerCached.getAndApplyRedistributionGains(msg.sender); - // TODO: cav we optimize calls to TM? uint entireTroveColl = troveManagerCached.getTroveColl(_troveId); - uint256 pendingTroveInterest = troveManager.calcPendingTroveInterest(_troveId); - uint256 recordedTroveDebt = troveManager.getTroveDebt(_troveId); - uint256 entireTroveDebt = recordedTroveDebt + pendingTroveInterest; + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(_troveId); + uint256 entireTroveDebt = initialRecordedTroveDebt + redistDebtGain + accruedTroveInterest; - // The borrower must repay their entire debt, including pending interest and redist. gains + // The borrower must repay their entire debt including accrued interest and redist. gains (and less the gas comp.) _requireSufficientBoldBalance(boldTokenCached, msg.sender, entireTroveDebt - BOLD_GAS_COMPENSATION); - // The TCR always includes this Trove's pending interest, so we must subtract the Trove's entire debt here + // The TCR always includes A Trove's redist. gain and accrued interest, so we must use the Trove's entire debt here uint newTCR = _getNewTCRFromTroveChange(entireTroveColl, false, entireTroveDebt, false, price); _requireNewTCRisAboveCCR(newTCR); // --- Effects and interactions --- - // The aggregate recorded debt increases by the pending aggregate interest, and decrease by the Trove's entire debt (including its pending interest), - // since the pending aggregate interest also includes the pending interest for this individual Trove. - activePool.mintAggInterest(-int256(entireTroveDebt)); + // 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. + activePool.mintAggInterest(-int256(initialRecordedTroveDebt + accruedTroveInterest)); troveManagerCached.removeStake(_troveId); troveManagerCached.closeTrove(_troveId, initialWeightedRecordedTroveDebt); emit TroveUpdated(_troveId, 0, 0, 0, BorrowerOperation.closeTrove); - // Remove only the Trove's *recorded* debt here, excluding the pending interest, since the state variable updated by this function (ActivePool.boldDebt) - // tracks only the sum of recorded debts. - _repayBold(activePoolCached, boldTokenCached, msg.sender, recordedTroveDebt - BOLD_GAS_COMPENSATION); - _repayBold(activePoolCached, boldTokenCached, gasPoolAddress, BOLD_GAS_COMPENSATION); + // Remove only the Trove's latest recorded debt (inc. redist. gains) from ActivePool.recordedDebtSum, + // i.e. exclude the accrued interest. + // TODO: If/when redist. gains are gas-optimized, exclude these too. + activePool.decreaseRecordedDebtSum(initialRecordedTroveDebt + redistDebtGain); + // Burn the 200 BOLD gas compensation + boldToken.burn(gasPoolAddress, BOLD_GAS_COMPENSATION); + // Burn the remainder of the Trove's entire debt from the user + boldToken.burn(msg.sender, entireTroveDebt - BOLD_GAS_COMPENSATION); + // Send the collateral back to the user activePoolCached.sendETH(msg.sender, entireTroveColl); } @@ -527,7 +541,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe address borrower = _troveManager.ownerOf(_troveId); _withdrawBold(_activePool, _boldToken, borrower, _boldChange, _netDebtChange); } else { - _repayBold(_activePool, _boldToken, msg.sender, _boldChange); + // TODO: burn entire debt, not recorded debt + _repayBold(_activePool, _boldToken, msg.sender, _boldChange, _boldChange); } if (_isCollIncrease) { @@ -549,14 +564,14 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // Issue the specified amount of Bold to _account and increases the total active debt (_netDebtIncrease potentially includes a BoldFee) function _withdrawBold(IActivePool _activePool, IBoldToken _boldToken, address _account, uint _boldAmount, uint _netDebtIncrease) internal { - _activePool.increaseBoldDebt(_netDebtIncrease); + _activePool.increaseRecordedDebtSum(_netDebtIncrease); _boldToken.mint(_account, _boldAmount); } // Burn the specified amount of Bold from _account and decreases the total active debt - function _repayBold(IActivePool _activePool, IBoldToken _boldToken, address _account, uint _bold) internal { - _activePool.decreaseBoldDebt(_bold); - _boldToken.burn(_account, _bold); + function _repayBold(IActivePool _activePool, IBoldToken _boldToken, address _account, uint _recordedTroveDebt, uint256 _entireTroveDebt) internal { + _activePool.decreaseRecordedDebtSum(_recordedTroveDebt); + _boldToken.burn(_account, _entireTroveDebt); } // --- 'Require' wrapper functions --- diff --git a/contracts/src/Interfaces/IActivePool.sol b/contracts/src/Interfaces/IActivePool.sol index 43ee304e..1844c53d 100644 --- a/contracts/src/Interfaces/IActivePool.sol +++ b/contracts/src/Interfaces/IActivePool.sol @@ -2,26 +2,29 @@ pragma solidity 0.8.18; -import "./IPool.sol"; import "./IInterestRouter.sol"; - -interface IActivePool is IPool { +interface IActivePool { function stabilityPoolAddress() external view returns (address); function defaultPoolAddress() external view returns (address); function borrowerOperationsAddress() external view returns (address); function troveManagerAddress() external view returns (address); function interestRouter() external view returns (IInterestRouter); + function getETH() external view returns (uint256); + function getRecordedDebtSum() external view returns (uint256); function getTotalActiveDebt() external view returns (uint256); function lastAggUpdateTime() external view returns (uint256); function aggRecordedDebt() external view returns (uint256); function aggWeightedDebtSum() external view returns (uint256); function calcPendingAggInterest() external view returns (uint256); - function mintAggInterest(int256 _troveDebtChange) external; - function increaseAggWeightedDebtSum(uint256 _debt, uint256 _annualInterestRate) external; - function decreaseAggWeightedDebtSum(uint256 _weightedRecordedTroveDebt) external; + function changeAggWeightedDebtSum( + uint256 _oldWeightedRecordedTroveDebt, + uint256 _newTroveWeightedRecordedTroveDebt + ) external; + function increaseRecordedDebtSum(uint256 _amount) external; + function decreaseRecordedDebtSum(uint256 _amount) external; function sendETH(address _account, uint _amount) external; function sendETHToDefaultPool(uint _amount) external; function setAddresses( diff --git a/contracts/src/Interfaces/IDefaultPool.sol b/contracts/src/Interfaces/IDefaultPool.sol index a819b167..c11383d5 100644 --- a/contracts/src/Interfaces/IDefaultPool.sol +++ b/contracts/src/Interfaces/IDefaultPool.sol @@ -2,13 +2,17 @@ pragma solidity 0.8.18; -import "./IPool.sol"; - - -interface IDefaultPool is IPool { +interface IDefaultPool { function troveManagerAddress() external view returns (address); function activePoolAddress() external view returns (address); // --- Functions --- + function getETH() external view returns (uint256); + function getBoldDebt() external view returns (uint256); function sendETHToActivePool(uint _amount) external; function setAddresses(address _troveManagerAddress, address _activePoolAddress) external; + + function increaseBoldDebt(uint _amount) external; + function decreaseBoldDebt(uint _amount) external; + + } diff --git a/contracts/src/Interfaces/IPool.sol b/contracts/src/Interfaces/IPool.sol deleted file mode 100644 index 5d525f22..00000000 --- a/contracts/src/Interfaces/IPool.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.18; - -// Common interface for the Pools. -interface IPool { - function getETHBalance() external view returns (uint); - - function getBoldDebt() external view returns (uint); - - function increaseBoldDebt(uint _amount) external; - - function decreaseBoldDebt(uint _amount) external; - - function receiveETH(uint256 _amount) external; -} diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 3dd0b9cb..c478e254 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -66,7 +66,7 @@ interface ITroveManager is IERC721, ILiquityBase { uint pendingBoldInterest ); - function getAndApplyRedistributionGains(address _borrower) external; + function getAndApplyRedistributionGains(address _borrower) external returns (uint256, uint256); function closeTrove(uint256 _troveId, uint256 _weightedRecordedTroveDebt) external; @@ -90,6 +90,8 @@ interface ITroveManager is IERC721, ILiquityBase { function calcPendingTroveInterest(uint256 _troveId) external view returns (uint256); function getTroveAnnualInterestRate(uint256 _troveId) external view returns (uint); + function calcTroveAccruedInterest(address _borrower) external view returns (uint256); + function TroveAddManagers(uint256 _troveId) external view returns (address); function TroveRemoveManagers(uint256 _troveId) external view returns (address); @@ -107,6 +109,13 @@ interface ITroveManager is IERC721, ILiquityBase { function changeAnnualInterestRate(uint256 _troveId, uint256 _newAnnualInterestRate) external; + function updateTroveDebtAndInterest( + uint256 _troveId, + uint256 _oldWeightedRecordedTroveDebt, + uint256 _entireTroveDebt, + uint256 _newAnnualInterestRate + ) external; + function setAddManager(address _sender, uint256 _troveId, address _manager) external; function setRemoveManager(address _sender, uint256 _troveId, address _manager) external; diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 28372023..f032fd7d 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -486,7 +486,8 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { IActivePool activePoolCached = activePool; // Cancel the liquidated Bold debt with the Bold in the stability pool - activePoolCached.decreaseBoldDebt(_debtToOffset); + // TODO: double-check we are removing the right debt sum here in a liquidation + activePoolCached.decreaseRecordedDebtSum(_debtToOffset); _decreaseBold(_debtToOffset); // Burn the debt that was successfully offset diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 600abfb2..c5d2cffd 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -770,7 +770,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // Move a Trove's pending debt and collateral rewards from distributions, from the Default Pool to the Active Pool function _movePendingTroveRewardsToActivePool(IActivePool _activePool, IDefaultPool _defaultPool, uint _bold, uint _ETH) internal { _defaultPool.decreaseBoldDebt(_bold); - _activePool.increaseBoldDebt(_bold); + _activePool.increaseRecordedDebtSum(_bold); _defaultPool.sendETHToActivePool(_ETH); } @@ -831,7 +831,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana 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.decreaseBoldDebt(_bold); + _contractsCache.activePool.decreaseRecordedDebtSum(_bold); // send ETH from Active Pool to CollSurplus Pool _contractsCache.collSurplusPool.accountSurplus(_troveId, _ETH); @@ -938,7 +938,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // 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.decreaseBoldDebt(totals.totalBoldToRedeem); + contractsCache.activePool.decreaseRecordedDebtSum(totals.totalBoldToRedeem); contractsCache.activePool.sendETH(msg.sender, totals.ETHToSendToRedeemer); } @@ -962,19 +962,22 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return (currentETH, currentBoldDebt); } - function getAndApplyRedistributionGains(address _troveId) external override { + function getAndApplyRedistributionGains(uint256 _troveId) external returns (uint256, uint256) { _requireCallerIsBorrowerOperations(); return _getAndApplyRedistributionGains(activePool, defaultPool, _troveId); } // Add the borrowers's coll and debt rewards earned from redistributions, to their Trove - function _getAndApplyRedistributionGains(IActivePool _activePool, IDefaultPool _defaultPool, address _troveId) internal { + function _getAndApplyRedistributionGains(IActivePool _activePool, IDefaultPool _defaultPool, uint256 _troveId) internal returns (uint256, uint256) { + uint256 pendingETHReward; + uint256 pendingBoldDebtReward; + if (hasRedistributionGains(_troveId)) { _requireTroveIsActive(_troveId); // Compute redistribution gains - uint pendingETHReward = getPendingETHReward(_troveId); - uint pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); + pendingETHReward = getPendingETHReward(_troveId); + pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); // Apply redistribution gains to trove's state Troves[_troveId].coll = Troves[_troveId].coll + pendingETHReward; @@ -995,6 +998,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana TroveManagerOperation.getAndApplyRedistributionGains ); } + + return (pendingETHReward, pendingBoldDebtReward); } function _updateTroveRewardSnapshots(uint256 _troveId) internal { @@ -1055,7 +1060,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint256 recordedColl = Troves[_troveId].coll; pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); - pendingBoldInterest = calcPendingTroveInterest(_troveId); + pendingBoldInterest = calcTroveAccruedInterest(_troveId); pendingETHReward = getPendingETHReward(_troveId); entireDebt = recordedDebt + pendingBoldDebtReward + pendingBoldInterest; @@ -1080,6 +1085,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana } // Update borrower's stake based on their latest collateral value + // TODO: Gas: can we pass current coll as a param here and remove an SLOAD? function _updateStakeAndTotalStakes(uint256 _troveId) internal returns (uint) { uint newStake = _computeNewStake(Troves[_troveId].coll); uint oldStake = Troves[_troveId].stake; @@ -1140,7 +1146,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana emit LTermsUpdated(L_ETH, L_boldDebt); // Transfer coll and debt from ActivePool to DefaultPool - _activePool.decreaseBoldDebt(_debt); + _activePool.decreaseRecordedDebtSum(_debt); _defaultPool.increaseBoldDebt(_debt); _activePool.sendETHToDefaultPool(_coll); } @@ -1167,7 +1173,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana rewardSnapshots[_troveId].boldDebt = 0; // Remove Trove's weighted debt from the weighted sum - activePool.decreaseAggWeightedDebtSum(_weightedRecordedTroveDebt); + activePool.changeAggWeightedDebtSum(_weightedRecordedTroveDebt, 0); _removeTroveId(_troveId, TroveIdsArrayLength); sortedTroves.remove(_troveId); @@ -1355,7 +1361,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // TODO: analyze precision loss in interest functions and decide upon the minimum granularity // (per-second, per-block, etc) - function calcPendingTroveInterest(address _borrower) public view returns (uint256) { + function calcTroveAccruedInterest(address _borrower) public view returns (uint256) { uint256 recordedDebt = Troves[_borrower].debt; // convert annual interest to per-second and multiply by the principal uint256 annualInterestRate = Troves[_borrower].annualInterestRate; @@ -1378,7 +1384,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // { // uint256 oldRecordedDebt = Troves[_borrower].debt; - // // uint256 pendingTroveInterest = _calcPendingTroveInterest(oldRecordedDebt, annualInterestRate, Troves[_borrower].lastDebtUpdateTime); + // // uint256 pendingTroveInterest = _calcTroveAccruedInterest(oldRecordedDebt, annualInterestRate, Troves[_borrower].lastDebtUpdateTime); // // Apply all changes to the Trove's state // Troves[_borrower].coll = Troves[_borrower].coll + _pendingCollGain; @@ -1392,7 +1398,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // // Update aggregate recorded debt // aggRecordedDebt += debtChange + _pendingDebtGain + _mintedAggInterest; // we don't add the Trove's fresh interest here, since this aggregate recorded debt gets updated with minted interest separately. - // assert(lastAggUpdateTime == block.timestamp); // Confirm there's no aggregate pending interest + // assert(lastAggUpdateTime == block.timestamp); // Confirm there's no aggregate accrued interest // // Update aggregate weighted debt sum // uint256 annualInterestRate = Troves[borrower].annualInterestRate; @@ -1512,19 +1518,28 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // TODO: optimize gas for writing to this struct Troves[_troveId].status = Status.active; Troves[_troveId].coll = _coll; - Troves[_troveId].debt = _debt; - Troves[_troveId].annualInterestRate = _annualInterestRate; - Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp); - _updateTroveRewardSnapshots(_troveId); + _updateTroveDebtAndInterest(_troveId, 0, _debt, _annualInterestRate); - // Update weighted debt sum with the Trove's interest-weighted debt - activePool.increaseAggWeightedDebtSum(_debt, _annualInterestRate); + _updateTroveRewardSnapshots(_borrower); // Record the Trove's stake (for redistributions) and update the total stakes return _updateStakeAndTotalStakes(_troveId); } + function updateTroveDebtAndInterest(address _borrower, uint256 _oldWeightedRecordedTroveDebt, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) external { + _requireCallerIsBorrowerOperations(); + _updateTroveDebtAndInterest(_borrower, _oldWeightedRecordedTroveDebt, _entireTroveDebt, _newAnnualInterestRate); + } + + function _updateTroveDebtAndInterest(address _borrower, uint256 _oldWeightedRecordedTroveDebt, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) public { + Troves[_borrower].debt = _entireTroveDebt; + Troves[_borrower].annualInterestRate = _newAnnualInterestRate; + Troves[_borrower].lastDebtUpdateTime = uint64(block.timestamp); + + activePool.changeAggWeightedDebtSum(_oldWeightedRecordedTroveDebt, _entireTroveDebt * _newAnnualInterestRate); + } + function increaseTroveColl(address _sender, uint256 _troveId, uint _collIncrease) external override returns (uint) { _requireCallerIsBorrowerOperations(); _requireIsOwnerOrAddManager(_troveId, _sender); diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index 13f96d8c..201f1059 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -77,13 +77,13 @@ contract InterestRateAggregate is DevTestSetup { assertEq(expectedPendingAggInterest, activePool.calcPendingAggInterest()); } - // --- calcPendingTroveInterest + // --- calcTroveAccruedInterest // returns 0 for non-existent trove function testCalcPendingTroveInterestReturns0When0AggRecordedDebt() public { priceFeed.setPrice(2000e18); - assertEq(troveManager.calcPendingTroveInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(A), 0); openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 75e16); @@ -95,7 +95,7 @@ contract InterestRateAggregate is DevTestSetup { closeTrove(B); - assertEq(troveManager.calcPendingTroveInterest(B), 0); + assertEq(troveManager.calcTroveAccruedInterest(B), 0); } // returns 0 for 0 time passed @@ -103,12 +103,12 @@ contract InterestRateAggregate is DevTestSetup { priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); - assertEq(troveManager.calcPendingTroveInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(A), 0); vm.warp(block.timestamp + 1 days); openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 75e16); - assertEq(troveManager.calcPendingTroveInterest(B), 0); + assertEq(troveManager.calcTroveAccruedInterest(B), 0); } function testCalcPendingTroveInterestReturns0For0InterestRate() public { @@ -116,11 +116,11 @@ contract InterestRateAggregate is DevTestSetup { openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); - assertEq(troveManager.calcPendingTroveInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(A), 0); vm.warp(block.timestamp + 1 days); - assertEq(troveManager.calcPendingTroveInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(A), 0); } // TODO: create additional corresponding fuzz test @@ -137,22 +137,22 @@ contract InterestRateAggregate is DevTestSetup { openTroveNoHints100pctMaxFee(A, 2 ether, debtRequest_A, annualRate_A); uint256 debt_A = troveManager.getTroveDebt(A); assertGt(debt_A, 0); - assertEq(troveManager.calcPendingTroveInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(A), 0); vm.warp(block.timestamp + duration); uint256 expectedInterest_A = annualRate_A * debt_A * duration / 1e18 / SECONDS_IN_1_YEAR; - assertEq(troveManager.calcPendingTroveInterest(A), expectedInterest_A); + assertEq(troveManager.calcTroveAccruedInterest(A), expectedInterest_A); openTroveNoHints100pctMaxFee(B, 2 ether, debtRequest_B, annualRate_B); uint256 debt_B = troveManager.getTroveDebt(B); assertGt(debt_B, 0); - assertEq(troveManager.calcPendingTroveInterest(B), 0); + assertEq(troveManager.calcTroveAccruedInterest(B), 0); vm.warp(block.timestamp + duration); uint256 expectedInterest_B = annualRate_B * debt_B * duration / 1e18 / SECONDS_IN_1_YEAR; - assertEq(troveManager.calcPendingTroveInterest(B), expectedInterest_B); + assertEq(troveManager.calcTroveAccruedInterest(B), expectedInterest_B); } // --- mintAggInterest --- @@ -575,9 +575,9 @@ contract InterestRateAggregate is DevTestSetup { // --- closeTrove --- // Reduces pending agg interest to 0 - function testcloseTroveReducesPendingAggInterestTo0() public { + function testCloseTroveReducesPendingAggInterestTo0() public { uint256 troveDebtRequest = 2000e18; - // A, B opens Trove to obtain BOLD and makes SP deposit + // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); @@ -600,9 +600,9 @@ contract InterestRateAggregate is DevTestSetup { // Increases agg recorded debt by pending agg interest - function testCloseTroveAddsPendingAggInterestAndSubtractsEntireTroveDebtFromAggRecordedDebt() public { + function testCloseTroveAddsPendingAggInterestAndSubtractsRecordedDebtPlusInterestFromAggRecordedDebt() public { uint256 troveDebtRequest = 2000e18; - // A, B opens Trove to obtain BOLD and makes SP deposit + // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); @@ -636,7 +636,7 @@ contract InterestRateAggregate is DevTestSetup { // Updates last agg update time to now function testCloseTroveUpdatesLastAggUpdateTimeToNow() public { uint256 troveDebtRequest = 2000e18; - // A, B opens Trove to obtain BOLD and makes SP deposit + // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); @@ -660,7 +660,7 @@ contract InterestRateAggregate is DevTestSetup { // mints interest to interest router function testCloseTroveMintsInterestToInterestRouter() public { uint256 troveDebtRequest = 2000e18; - // A, B opens Trove to obtain BOLD and makes SP deposit + // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); @@ -690,7 +690,7 @@ contract InterestRateAggregate is DevTestSetup { function testCloseTroveReducesAggWeightedDebtSumByTrovesWeightedRecordedDebt() public { uint256 troveDebtRequest = 2000e18; - // A, B opens Trove to obtain BOLD and makes SP deposit + // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); @@ -715,13 +715,187 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - weightedTroveDebt); } - // TODO: - // getEntireDebt and getTCR basic tests, for pending agg interest + function testCloseTroveReducesRecordedDebtSumByInitialRecordedDebt() public { + uint256 troveDebtRequest = 2000e18; + // A, B open Troves + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + uint256 recordedDebt_B = troveManager.getTroveDebt(B); + + uint256 activePoolRecordedDebt_1 = activePool.getRecordedDebtSum(); + assertGt(activePoolRecordedDebt_1, 0); + // A sends Bold to B so B can cover their interest and close their Trove + transferBold(A, B, boldToken.balanceOf(A)); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // B closes Trove + closeTrove(B); + + // Check recorded debt sum reduced by B's recorded debt + assertEq(activePool.getRecordedDebtSum(), activePoolRecordedDebt_1 - recordedDebt_B); + } + + function testCloseTroveReducesBorrowerBoldBalByEntireTroveDebtLessGasComp() public { + uint256 troveDebtRequest = 2000e18; + // A, B opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + + // A sends Bold to B so B can cover their interest and close their Trove + transferBold(A, B, boldToken.balanceOf(A)); + uint256 bal_B = boldToken.balanceOf(B); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // Get the up-to-date entire debt + (uint256 entireDebt_B, , , , ) = troveManager.getEntireDebtAndColl(B); - // get + // B closes Trove + closeTrove(B); + // Check balance of B reduces by the Trove's entire debt less gas comp + assertEq(boldToken.balanceOf(B), bal_B - (entireDebt_B - troveManager.BOLD_GAS_COMPENSATION())); + } + + // --- adjustTroveInterestRate --- + function testAdjustTroveInterestRateWithNoPendingDebtGainIncreasesAggRecordedDebtByPendingAggInterest() public { + uint256 troveDebtRequest = 2000e18; + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + changeInterestRateNoHints(A, 75e16); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); + } + + function testAdjustTroveInterestRateReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + changeInterestRateNoHints(A, 75e16); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + // Update last agg. update time to now + function testAdjustTroveInterestRateUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // A changes interest rate + changeInterestRateNoHints(A, 75e16); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + // mints interest to router + function testAdjustTroveInterestRateMintsAggInterestToRouter() public { + uint256 troveDebtRequest = 2000e18; + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // Get I-router balance + uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_1, 0); + + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A changes interest rate + changeInterestRateNoHints(A, 75e16); + + // Check I-router Bold bal has increased as expected from SP deposit + uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_2, pendingAggInterest); + } + + // updates weighted debt sum: removes old and adds new + function testAdjustTroveInterestRateAdjustsWeightedDebtSumCorrectly() public { + uint256 troveDebtRequest = 2000e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); + + uint256 newAnnualInterestRate = 75e16; + uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * newAnnualInterestRate; + + // A changes interest rate + changeInterestRateNoHints(A, newAnnualInterestRate); + + // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); + } + + function testAdustTroveInterestRateWithNoPendingDebtRewardIncreasesByPendingInterest() public { + uint256 troveDebtRequest = 2000e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(A); + assertEq(pendingRedistDebtGain, 0); + uint256 pendingInterest = troveManager.calcTroveAccruedInterest(A); + assertGt(pendingInterest, 0); + + // Get current recorded active debt + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // A changes interest rate + changeInterestRateNoHints(A, 75e16); + + // Check redorded debt sum increases by the pending interest + assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + pendingInterest); + } - + // TODO: getEntireDebt and getTCR basic tests + // TODO: adjustTrove tests + // TODO: tests with pending debt redist. gain >0 } diff --git a/contracts/src/test/interestRateBasic.t.sol b/contracts/src/test/interestRateBasic.t.sol index 5b73e9ce..d7b7420c 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -99,6 +99,8 @@ contract InterestRateBasic is DevTestSetup { borrowerOperations.adjustTroveInterestRate(A_Id, 42e18, 0, 0); } + // --- adjustTroveInterestRate --- + function testAdjustTroveInterestRateSetsCorrectNewRate() public { priceFeed.setPrice(2000e18); @@ -120,6 +122,20 @@ contract InterestRateBasic is DevTestSetup { assertEq(troveManager.getTroveAnnualInterestRate(C_Id), 1e18); } + function testAdjustTroveInterestRateSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + + changeInterestRateNoHints(A, 75e16); + + assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + } + function testAdjustTroveInterestRateInsertsToCorrectPositionInSortedList() public { priceFeed.setPrice(2000e18); uint256 A_Id = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); @@ -164,7 +180,6 @@ contract InterestRateBasic is DevTestSetup { function testAdjustTroveDoesNotChangeListPositions() public { priceFeed.setPrice(2000e18); - // Troves opened in ascending order of interest rate uint256 A_Id = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); uint256 B_Id = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 2e17); From af19db2fb87c85108d4fa02fdaee7bd026413504 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Sun, 25 Feb 2024 15:23:18 +0700 Subject: [PATCH 07/18] Apply aggregate interest when adjusting debt and/or coll --- contracts/src/ActivePool.sol | 38 +-- contracts/src/BorrowerOperations.sol | 252 ++++++++-------- contracts/src/Interfaces/IActivePool.sol | 2 +- contracts/src/Interfaces/ITroveManager.sol | 9 +- contracts/src/StabilityPool.sol | 4 +- contracts/src/TroveManager.sol | 79 ++---- contracts/src/test/TestContracts/BaseTest.sol | 15 +- .../src/test/interestRateAggregate.t.sol | 268 +++++++++++++++++- contracts/src/test/interestRateBasic.t.sol | 17 +- 9 files changed, 459 insertions(+), 225 deletions(-) diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index 4efb2ad7..a074c3e5 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -43,13 +43,16 @@ contract ActivePool is Ownable, CheckContract, IActivePool { uint256 internal ETHBalance; // deposited ether tracker // Sum of individual recorded Trove debts. Updated only at individual Trove operations. + // "G" in the spec. uint256 internal recordedDebtSum; // Aggregate recorded debt tracker. Updated whenever a Trove's debt is touched AND whenever the aggregate pending interest is minted. + // "D" in the spec. uint256 public aggRecordedDebt; /* Sum of individual recorded Trove debts weighted by their respective chosen interest rates. * Updated at individual Trove operations. + * "S" in the spec. */ uint256 public aggWeightedDebtSum; @@ -187,42 +190,39 @@ contract ActivePool is Ownable, CheckContract, IActivePool { } function changeAggWeightedDebtSum(uint256 _oldWeightedRecordedTroveDebt, uint256 _newTroveWeightedRecordedTroveDebt) external { - _requireCallerIsTroveManager(); - // TODO: Currently 2 SLOADs - more gas efficient way? Use ints? - aggWeightedDebtSum -= _oldWeightedRecordedTroveDebt; - aggWeightedDebtSum += _newTroveWeightedRecordedTroveDebt; + _requireCallerIsBOorTroveM(); + // Do the arithmetic in 2 steps here to avoid overflow from the decrease + uint256 newAggWeightedDebtSum = aggWeightedDebtSum + _newTroveWeightedRecordedTroveDebt; // 1 SLOAD + newAggWeightedDebtSum -= _oldWeightedRecordedTroveDebt; + aggWeightedDebtSum = newAggWeightedDebtSum; // 1 SSTORE } // --- Aggregate interest operations --- // This function is called inside all state-changing user ops: borrower ops, liquidations, redemptions and SP deposits/withdrawals. // Some user ops trigger debt changes to Trove(s), in which case _troveDebtChange will be non-zero. - // The aggregate recorded debt is incremented by the aggregate pending interest, plus the _troveDebtChange. - // The _troveDebtChange results from the borrower repaying/drawing debt and redistribution gains being applied. - function mintAggInterest(int256 _troveDebtChange) public { + // The aggregate recorded debt is incremented by the aggregate pending interest, plus the net Trove Debt Change. + // The net Trove debt change consists of the sum of a) any debt issued/repaid and b) any redistribution debt gain applied in the encapsulating operation. + // 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 { _requireCallerIsBOorSP(); 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);} - // TODO: cleaner way to deal with debt changes that can be positive or negative? - aggRecordedDebt = addUint256ToInt256(aggRecordedDebt + aggInterest, _troveDebtChange); - // assert(aggRecordedDebt >= 0) // This should never be negative. If all aggregate interest was applied + // Do the arithmetic in 2 steps here to avoid overflow from the decrease + uint256 newAggRecordedDebt = aggRecordedDebt + aggInterest + _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. lastAggUpdateTime = block.timestamp; } - function addUint256ToInt256(uint256 _x, int256 _y) internal returns (uint256) { - // Assumption: _x + _y > 0. Will revert otherwise. - if (_y >= 0) { - return _x + uint256(_y); - } else { - return (_x - uint256(-_y)); - } - } - // --- 'require' functions --- function _requireCallerIsBorrowerOperationsOrDefaultPool() internal view { diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index f700982c..ec526418 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -39,9 +39,10 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe struct LocalVariables_adjustTrove { uint price; - uint netDebtChange; uint entireDebt; uint entireColl; + uint256 redistDebtGain; + uint256 accruedTroveInterest; uint oldICR; uint newICR; uint newTCR; @@ -61,7 +62,12 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint arrayIndex; } - struct ContractsCache { + struct ContractsCacheTMAP { + ITroveManager troveManager; + IActivePool activePool; + } + + struct ContractsCacheTMAPBT { ITroveManager troveManager; IActivePool activePool; IBoldToken boldToken; @@ -164,7 +170,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe override returns (uint256) { - ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, boldToken); + ContractsCacheTMAPBT memory contractsCache = ContractsCacheTMAPBT(troveManager, activePool, boldToken); LocalVariables_openTrove memory vars; vars.price = priceFeed.fetchPrice(); @@ -180,15 +186,14 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _requireTroveisNotActive(contractsCache.troveManager, troveId); vars.BoldFee; - vars.netDebt = _boldAmount; if (!isRecoveryMode) { // TODO: implement interest rate charges } - _requireAtLeastMinNetDebt(vars.netDebt); + _requireAtLeastMinNetDebt(_boldAmount); - // ICR is based on the composite debt, i.e. the requested Bold amount + Bold borrowing fee + Bold gas comp. - vars.compositeDebt = _getCompositeDebt(vars.netDebt); + // ICR is based on the composite debt, i.e. the requested Bold amount + Bold gas comp. + vars.compositeDebt = _getCompositeDebt(_boldAmount); assert(vars.compositeDebt > 0); vars.ICR = LiquityMath._computeCR(_ETHAmount, vars.compositeDebt, vars.price); @@ -203,7 +208,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // --- Effects & interactions --- - activePool.mintAggInterest(int256(vars.compositeDebt)); + contractsCache.activePool.mintAggInterest(vars.compositeDebt, 0); // Set the stored Trove properties and mint the NFT vars.stake = contractsCache.troveManager.setTrovePropertiesOnOpen( @@ -220,10 +225,15 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // Pull ETH tokens from sender and move them to the Active Pool _pullETHAndSendToActivePool(contractsCache.activePool, _ETHAmount); - // Mint Bold to borrower - _withdrawBold(contractsCache.activePool, contractsCache.boldToken, msg.sender, _boldAmount, vars.netDebt); - // Move the Bold gas compensation to the Gas Pool - _withdrawBold(contractsCache.activePool, contractsCache.boldToken, gasPoolAddress, BOLD_GAS_COMPENSATION, BOLD_GAS_COMPENSATION); + + // Mint the requested _boldAmount to the borrower and mint the gas comp to the GasPool + 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); @@ -274,28 +284,35 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe function adjustTroveInterestRate(uint256 _troveId, uint _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint) external { // TODO: Delegation functionality + + ContractsCacheTMAP memory contractsCache = ContractsCacheTMAP(troveManager, activePool); + // --- Checks --- _requireValidAnnualInterestRate(_newAnnualInterestRate); - ITroveManager troveManagerCached = troveManager; - _requireTroveisActive(troveManagerCached, _troveId); + _requireTroveisActive(contractsCache.troveManager, _troveId); + + uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(_troveId); - uint256 initialWeightedRecordedTroveDebt = troveManager.getTroveWeightedRecordedDebt(msg.sender); + // --- Effects --- - (, uint256 redistDebtGain) = troveManagerCached.getAndApplyRedistributionGains(msg.sender); + (, uint256 redistDebtGain) = contractsCache.troveManager.getAndApplyRedistributionGains(_troveId); - // No debt is issued/repaid, so ActivePool.aggRecordedDebt increases only the redistribution gain - activePool.mintAggInterest(int256(redistDebtGain)); + // No debt is issued/repaid, so the net Trove debt change is purely the redistribution gain + contractsCache.activePool.mintAggInterest(redistDebtGain, 0); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(msg.sender); - uint256 recordedTroveDebt = troveManager.getTroveDebt(msg.sender); + uint256 accruedTroveInterest = contractsCache.troveManager.calcTroveAccruedInterest(msg.sender); + uint256 recordedTroveDebt = contractsCache.troveManager.getTroveDebt(msg.sender); uint256 entireTroveDebt = recordedTroveDebt + accruedTroveInterest; sortedTroves.reInsert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); // Update Trove recorded debt and interest-weighted debt sum - troveManager.updateTroveDebtAndInterest(_troveId, initialWeightedRecordedTroveDebt, entireTroveDebt, _newAnnualInterestRate); + contractsCache.troveManager.updateTroveDebtAndInterest(_troveId, entireTroveDebt, _newAnnualInterestRate); - // Increase the active Pool's recorded debt only by the Trove's accrued interest, since we have already applied pending redist. gains. - activePool.increaseRecordedDebtSum(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 + 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); } /* @@ -312,10 +329,16 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe ) internal { - ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, boldToken); + ContractsCacheTMAPBT memory contractsCache = ContractsCacheTMAPBT(troveManager, activePool, boldToken); LocalVariables_adjustTrove memory vars; vars.price = priceFeed.fetchPrice(); + + uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(msg.sender); + uint256 annualInterestRate = contractsCache.troveManager.getTroveAnnualInterestRate(msg.sender); + + // --- Checks --- + bool isRecoveryMode = _checkRecoveryMode(vars.price); if (_isCollIncrease) { @@ -331,21 +354,10 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // Confirm the operation is an ETH transfer if coming from the Stability Pool to a trove assert((msg.sender != stabilityPoolAddress || (_isCollIncrease && _boldChange == 0))); - // TODO: apply individual and aggregate accrued interest, and take snapshots of current timestamp. - - contractsCache.troveManager.getAndApplyRedistributionGains(_borrower); - // Get the collChange based on whether or not ETH was sent in the transaction (vars.collChange, vars.isCollIncrease) = _getCollChange(msg.value, _collWithdrawal); - vars.netDebtChange = _boldChange; - - // If the adjustment incorporates a debt increase and system is in Normal Mode, then trigger a borrowing fee - if (_isDebtIncrease && !isRecoveryMode) { - // TODO: implement interest rate charges - } - - (vars.entireDebt, vars.entireColl, , , ) = contractsCache.troveManager.getEntireDebtAndColl(_troveId); + (vars.entireDebt, vars.entireColl, vars.redistDebtGain, , vars.accruedTroveInterest) = contractsCache.troveManager.getEntireDebtAndColl(_troveId); // Get the trove's old ICR before the adjustment, and what its new ICR will be after the adjustment vars.oldICR = LiquityMath._computeCR(vars.entireColl, vars.entireDebt, vars.price); @@ -354,43 +366,45 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe vars.entireDebt, _collChange, _isCollIncrease, - vars.netDebtChange, + _boldChange, _isDebtIncrease, vars.price ); assert(_isCollIncrease || _collChange <= vars.entireColl); // TODO: do we still need this? // Check the adjustment satisfies all conditions for the current system mode - _requireValidAdjustmentInCurrentMode(isRecoveryMode, _collChange, _isCollIncrease, _isDebtIncrease, vars); + _requireValidAdjustmentInCurrentMode(isRecoveryMode, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, vars); // When the adjustment is a debt repayment, check it's a valid amount and that the caller has enough Bold if (!_isDebtIncrease && _boldChange > 0) { - _requireAtLeastMinNetDebt(_getNetDebt(vars.entireDebt) - vars.netDebtChange); - _requireValidBoldRepayment(vars.entireDebt, vars.netDebtChange); - _requireSufficientBoldBalance(contractsCache.boldToken, msg.sender, vars.netDebtChange); + _requireAtLeastMinNetDebt(_getNetDebt(vars.entireDebt) - _boldChange); + _requireValidBoldRepayment(vars.entireDebt, _boldChange); + _requireSufficientBoldBalance(contractsCache.boldToken, msg.sender, _boldChange); } - // Finally actually update the Trove's recorded debt and coll - // TODO: use the composite update function - (vars.newColl, vars.newDebt) = _updateTroveFromAdjustment( - contractsCache.troveManager, - _sender, - _troveId, - vars.coll, - _collChange, - _isCollIncrease, - vars.debt, - vars.netDebtChange, - _isDebtIncrease - ); + // --- Effects and interactions --- + + contractsCache.troveManager.getAndApplyRedistributionGains(_borrower); + + if (_isDebtIncrease) { + // Incresae 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 debt and coll + // TODO: Can we optimize calls to TM? + vars.newColl = _updateTroveCollFromAdjustment(contractsCache.troveManager, _troveId, vars.collChange, vars.isCollIncrease); + uint256 newEntireDebt = vars.entireDebt + _boldChange; + contractsCache.troveManager.updateTroveDebt(_troveId, newEntireDebt); + vars.stake = contractsCache.troveManager.updateStakeAndTotalStakes(_troveId); emit TroveUpdated(_troveId, vars.newDebt, vars.newColl, vars.stake, BorrowerOperation.adjustTrove); emit BoldBorrowingFeePaid(_troveId, vars.BoldFee); - // Use the unmodified _boldChange here, as we don't send the fee to the user - //TODO: the _boldChange passed here should include both the borrower's debt adjustment plus interest applied. i.e. - // it should be the change in their recorded debt. _moveTokensAndETHfromAdjustment( contractsCache.activePool, contractsCache.boldToken, @@ -400,37 +414,33 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _isCollIncrease, _boldChange, _isDebtIncrease, - vars.netDebtChange + vars.accruedTroveInterest ); + + contractsCache.activePool.changeAggWeightedDebtSum(initialWeightedRecordedTroveDebt, newEntireDebt * annualInterestRate); } function closeTrove(uint256 _troveId) external override { - ITroveManager troveManagerCached = troveManager; - IActivePool activePoolCached = activePool; - IBoldToken boldTokenCached = boldToken; + ContractsCacheTMAPBT memory contractsCache = ContractsCacheTMAPBT(troveManager, activePool, boldToken); // --- Checks --- - _requireCallerIsBorrower(troveManagerCached, _troveId); - _requireTroveisActive(troveManagerCached, _troveId); + _requireCallerIsBorrower(contractsCache.troveManager, _troveId); + _requireTroveisActive(contractsCache.troveManager, _troveId); uint price = priceFeed.fetchPrice(); _requireNotInRecoveryMode(price); - uint256 initialWeightedRecordedTroveDebt = troveManager.getTroveWeightedRecordedDebt(msg.sender); - uint256 initialRecordedTroveDebt = troveManager.getTroveDebt(msg.sender); - - // TODO: gas optimization of pending rewards. We don't need to actually update stored Trove debt & coll properties here, since we'll - // zero them at the end. Currently, since we apply gains first, the following calls to getTroveColl and getTroveDebt - // return values with redistribution gains included. When we gas-optimize, this won't be true and we - // may need to change the total debt and coll calculations. - ( , uint256 redistDebtGain) = troveManagerCached.getAndApplyRedistributionGains(msg.sender); + uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(_troveId); + uint256 initialRecordedTroveDebt = contractsCache.troveManager.getTroveDebt(_troveId); - uint entireTroveColl = troveManagerCached.getTroveColl(_troveId); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(_troveId); - uint256 entireTroveDebt = initialRecordedTroveDebt + redistDebtGain + accruedTroveInterest; + (uint256 entireTroveDebt, + uint256 entireTroveColl, + uint256 debtRedistGain, + , // ETHredist gain + uint256 accruedTroveInterest) = contractsCache.troveManager.getEntireDebtAndColl(_troveId); // The borrower must repay their entire debt including accrued interest and redist. gains (and less the gas comp.) - _requireSufficientBoldBalance(boldTokenCached, msg.sender, entireTroveDebt - BOLD_GAS_COMPENSATION); + _requireSufficientBoldBalance(contractsCache.boldToken, msg.sender, entireTroveDebt - BOLD_GAS_COMPENSATION); // The TCR always includes A Trove's redist. gain and accrued interest, so we must use the Trove's entire debt here uint newTCR = _getNewTCRFromTroveChange(entireTroveColl, false, entireTroveDebt, false, price); @@ -438,25 +448,30 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // --- Effects and interactions --- + // TODO: gas optimization of redistribution gains. We don't need to actually update stored Trove debt & coll properties here, since we'll + // zero them at the end. + contractsCache.troveManager.getAndApplyRedistributionGains(_troveId); + // 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. - activePool.mintAggInterest(-int256(initialRecordedTroveDebt + accruedTroveInterest)); + contractsCache.activePool.mintAggInterest(0, initialRecordedTroveDebt + accruedTroveInterest); - troveManagerCached.removeStake(_troveId); - troveManagerCached.closeTrove(_troveId, initialWeightedRecordedTroveDebt); + contractsCache.troveManager.removeStake(_troveId); + contractsCache.troveManager.closeTrove(_troveId, initialWeightedRecordedTroveDebt); emit TroveUpdated(_troveId, 0, 0, 0, BorrowerOperation.closeTrove); - // Remove only the Trove's latest recorded debt (inc. redist. gains) from ActivePool.recordedDebtSum, - // i.e. exclude the accrued interest. - // TODO: If/when redist. gains are gas-optimized, exclude these too. - activePool.decreaseRecordedDebtSum(initialRecordedTroveDebt + redistDebtGain); + // 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); + // Burn the 200 BOLD gas compensation - boldToken.burn(gasPoolAddress, BOLD_GAS_COMPENSATION); + contractsCache.boldToken.burn(gasPoolAddress, BOLD_GAS_COMPENSATION); // Burn the remainder of the Trove's entire debt from the user - boldToken.burn(msg.sender, entireTroveDebt - BOLD_GAS_COMPENSATION); + contractsCache.boldToken.burn(msg.sender, entireTroveDebt - BOLD_GAS_COMPENSATION); // Send the collateral back to the user - activePoolCached.sendETH(msg.sender, entireTroveColl); + contractsCache.activePool.sendETH(msg.sender, entireTroveColl); } function setAddManager(uint256 _troveId, address _manager) external { @@ -486,24 +501,36 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe return usdValue; } - // Update trove's coll and debt based on whether they increase or decrease - function _updateTroveFromAdjustment + function _getCollChange( + uint _collReceived, + uint _requestedCollWithdrawal + ) + internal + pure + returns(uint collChange, bool isCollIncrease) + { + if (_collReceived != 0) { + collChange = _collReceived; + isCollIncrease = true; + } else { + collChange = _requestedCollWithdrawal; + } + } + + // Update trove's coll whether they added or removed collateral + function _updateTroveCollFromAdjustment ( ITroveManager _troveManager, address _sender, uint256 _troveId, uint256 _coll, uint _collChange, - bool _isCollIncrease, - uint256 _debt, - uint _debtChange, - bool _isDebtIncrease + bool _isCollIncrease ) internal - returns (uint, uint) + returns (uint256) { uint256 newColl; - uint256 newDebt; if (_collChange > 0) { newColl = (_isCollIncrease) ? @@ -512,17 +539,13 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } else { newColl = _coll; } - if (_debtChange > 0) { - newDebt = (_isDebtIncrease) ? - _troveManager.increaseTroveDebt(_sender, _troveId, _debtChange) : - _troveManager.decreaseTroveDebt(_sender, _troveId, _debtChange); - } else { - newDebt = _debt; - } - return (newColl, newDebt); + return newColl; } + // 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. function _moveTokensAndETHfromAdjustment ( IActivePool _activePool, @@ -533,16 +556,20 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe bool _isCollIncrease, uint _boldChange, bool _isDebtIncrease, - uint _netDebtChange + uint256 _accruedTroveInterest ) internal { if (_isDebtIncrease) { + _activePool.increaseRecordedDebtSum(_boldChange + _accruedTroveInterest); address borrower = _troveManager.ownerOf(_troveId); - _withdrawBold(_activePool, _boldToken, borrower, _boldChange, _netDebtChange); + _boldToken.mint(borrower, _boldChange); } else { - // TODO: burn entire debt, not recorded debt - _repayBold(_activePool, _boldToken, msg.sender, _boldChange, _boldChange); + // TODO: Gas optimize this + _activePool.increaseRecordedDebtSum(_accruedTroveInterest); + _activePool.decreaseRecordedDebtSum(_boldChange); + + _boldToken.burn(msg.sender, _boldChange); } if (_isCollIncrease) { @@ -562,18 +589,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _activePool.receiveETH(_amount); } - // Issue the specified amount of Bold to _account and increases the total active debt (_netDebtIncrease potentially includes a BoldFee) - function _withdrawBold(IActivePool _activePool, IBoldToken _boldToken, address _account, uint _boldAmount, uint _netDebtIncrease) internal { - _activePool.increaseRecordedDebtSum(_netDebtIncrease); - _boldToken.mint(_account, _boldAmount); - } - - // Burn the specified amount of Bold from _account and decreases the total active debt - function _repayBold(IActivePool _activePool, IBoldToken _boldToken, address _account, uint _recordedTroveDebt, uint256 _entireTroveDebt) internal { - _activePool.decreaseRecordedDebtSum(_recordedTroveDebt); - _boldToken.burn(_account, _entireTroveDebt); - } - // --- 'Require' wrapper functions --- function _requireCallerIsBorrower(ITroveManager _troveManager , uint256 _troveId) internal view { @@ -615,6 +630,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe bool _isRecoveryMode, uint _collChange, bool _isCollIncrease, + uint256 _boldChange, bool _isDebtIncrease, LocalVariables_adjustTrove memory _vars ) @@ -642,7 +658,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } } else { // if Normal Mode _requireICRisAboveMCR(_vars.newICR); - _vars.newTCR = _getNewTCRFromTroveChange(_collChange, _isCollIncrease, _vars.netDebtChange, _isDebtIncrease, _vars.price); + _vars.newTCR = _getNewTCRFromTroveChange(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _vars.price); _requireNewTCRisAboveCCR(_vars.newTCR); } } diff --git a/contracts/src/Interfaces/IActivePool.sol b/contracts/src/Interfaces/IActivePool.sol index 1844c53d..70177701 100644 --- a/contracts/src/Interfaces/IActivePool.sol +++ b/contracts/src/Interfaces/IActivePool.sol @@ -18,7 +18,7 @@ interface IActivePool { function aggWeightedDebtSum() external view returns (uint256); function calcPendingAggInterest() external view returns (uint256); - function mintAggInterest(int256 _troveDebtChange) external; + function mintAggInterest(uint256 _troveDebtIncrease, uint256 _troveDebtDecrease) external; function changeAggWeightedDebtSum( uint256 _oldWeightedRecordedTroveDebt, uint256 _newTroveWeightedRecordedTroveDebt diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index c478e254..86d77c94 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -109,12 +109,9 @@ interface ITroveManager is IERC721, ILiquityBase { function changeAnnualInterestRate(uint256 _troveId, uint256 _newAnnualInterestRate) external; - function updateTroveDebtAndInterest( - uint256 _troveId, - uint256 _oldWeightedRecordedTroveDebt, - uint256 _entireTroveDebt, - uint256 _newAnnualInterestRate - ) external; + function updateTroveDebtAndInterest(uint256 _troveId, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) external; + + function updateTroveDebt(uint256 _troveId, uint256 _entireTroveDebt) external; function setAddManager(address _sender, uint256 _troveId, address _manager) external; function setRemoveManager(address _sender, uint256 _troveId, address _manager) external; diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index f032fd7d..38feed85 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -285,7 +285,7 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { function provideToSP(uint _amount) external override { _requireNonZeroAmount(_amount); - activePool.mintAggInterest(0); + activePool.mintAggInterest(0, 0); uint initialDeposit = deposits[msg.sender].initialValue; @@ -316,7 +316,7 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { uint initialDeposit = deposits[msg.sender].initialValue; _requireUserHasDeposit(initialDeposit); - activePool.mintAggInterest(0); + activePool.mintAggInterest(0, 0); uint depositorETHGain = getDepositorETHGain(msg.sender); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index c5d2cffd..3551d06d 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -1370,58 +1370,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return recordedDebt * annualInterestRate * (block.timestamp - lastDebtUpdateTime) / SECONDS_IN_ONE_YEAR / 1e18; } - // TODO: make this purely for existing Trove touches which alter coll or debt. // TODO: How about redemptions and liqs? - // function _applyInterestAndRedistributionGains( - // address _borrower, - // uint256 _debtChange, - // uint256 _mintedAggInterest, - // uint256, _pendingTroveInterest, - // uint256 _annualInterestRate, - // uint256 _pendingCollGain, - // uint256 _pendingDebtGain - // ) - // internal - // { - // uint256 oldRecordedDebt = Troves[_borrower].debt; - - // // uint256 pendingTroveInterest = _calcTroveAccruedInterest(oldRecordedDebt, annualInterestRate, Troves[_borrower].lastDebtUpdateTime); - - // // Apply all changes to the Trove's state - // Troves[_borrower].coll = Troves[_borrower].coll + _pendingCollGain; - // uint256 newRecordedDebt = oldRecordedDebt + pendingTroveInterest + debtChange + _pendingDebtGain; - // Troves[borrower].debt = newRecordedDebt; - - // _updateTroveRewardSnapshots(_borrower); - - // // Record the Trove’s latest individual update time - // Troves[borrower].lastDebtUpdateTime = block.timestamp; - - // // Update aggregate recorded debt - // aggRecordedDebt += debtChange + _pendingDebtGain + _mintedAggInterest; // we don't add the Trove's fresh interest here, since this aggregate recorded debt gets updated with minted interest separately. - // assert(lastAggUpdateTime == block.timestamp); // Confirm there's no aggregate accrued interest - - // // Update aggregate weighted debt sum - // uint256 annualInterestRate = Troves[borrower].annualInterestRate; - // uint256 oldWeightedRecordedDebt = oldTroveRecordedDebt * annualInterestRate; - // uint256 newWeightedRecordedDebt = newRecordedDebt * annualInterestRate; - // aggWeightedDebtSum = aggWeightedDebtSum - oldWeightedRecordedDebt + newWeightedRecordedDebt; - - // // Transfer redistribution gains from DefaultPool to ActivePool - // _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, _pendingDebtGain, _pendingCollGain); - - // emit TroveUpdated( - // _borrower, - // Troves[_borrower].debt, - // Troves[_borrower].coll, - // Troves[_borrower].stake, - // Troves[_borrower].lastDebtUpdateTime - // TroveManagerOperation.getAndApplyRedistributionGains - // ); - // } - - // function updateAggAndTroveRecordedDebt - // // TODO: bake in getAndApplyRedistributionGains in gas-efficient way - // --- 'require' wrapper functions --- function _requireCallerIsBorrowerOperations() internal view { @@ -1486,7 +1434,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return Troves[_troveId].debt; } - function getTroveWeightedRecordedDebt(address _borrower) public returns (uint256) { + function getTroveWeightedRecordedDebt(address _borrower) public view returns (uint256) { return Troves[_borrower].debt * Troves[_borrower].annualInterestRate; } @@ -1519,25 +1467,32 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana Troves[_troveId].status = Status.active; Troves[_troveId].coll = _coll; - _updateTroveDebtAndInterest(_troveId, 0, _debt, _annualInterestRate); + _updateTroveDebtAndInterest(_troveId, _debt, _annualInterestRate); - _updateTroveRewardSnapshots(_borrower); + _updateTroveRewardSnapshots(_troveId); // Record the Trove's stake (for redistributions) and update the total stakes return _updateStakeAndTotalStakes(_troveId); } - function updateTroveDebtAndInterest(address _borrower, uint256 _oldWeightedRecordedTroveDebt, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) external { + function updateTroveDebtAndInterest(uint256 _troveId, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) external { _requireCallerIsBorrowerOperations(); - _updateTroveDebtAndInterest(_borrower, _oldWeightedRecordedTroveDebt, _entireTroveDebt, _newAnnualInterestRate); + _updateTroveDebtAndInterest(_troveId, _entireTroveDebt, _newAnnualInterestRate); + } + + function _updateTroveDebtAndInterest(uint256 _troveId, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) internal { + _updateTroveDebt(_troveId, _entireTroveDebt); + Troves[_troveId].annualInterestRate = _newAnnualInterestRate; } - function _updateTroveDebtAndInterest(address _borrower, uint256 _oldWeightedRecordedTroveDebt, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) public { - Troves[_borrower].debt = _entireTroveDebt; - Troves[_borrower].annualInterestRate = _newAnnualInterestRate; - Troves[_borrower].lastDebtUpdateTime = uint64(block.timestamp); + function updateTroveDebt(uint256 _troveId, uint256 _entireTroveDebt) external { + _requireCallerIsBorrowerOperations(); + _updateTroveDebt(_troveId, _entireTroveDebt); + } - activePool.changeAggWeightedDebtSum(_oldWeightedRecordedTroveDebt, _entireTroveDebt * _newAnnualInterestRate); + function _updateTroveDebt(uint256 _troveId, uint256 _entireTroveDebt) internal { + Troves[_troveId].debt = _entireTroveDebt; + Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp); } function increaseTroveColl(address _sender, uint256 _troveId, uint _collIncrease) external override returns (uint) { diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index 3b68f8f0..693eb0e9 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -134,9 +134,22 @@ contract BaseTest is Test { vm.stopPrank(); } + function withdrawBold100pctMaxFee(address _account, uint256 _debtIncrease) public { + vm.startPrank(_account); + borrowerOperations.withdrawBold(1e18, _debtIncrease); + vm.stopPrank(); + } + + function repayBold(address _account, uint256 _debtDecrease) public { + vm.startPrank(_account); + borrowerOperations.repayBold(_debtDecrease); + vm.stopPrank(); + } + + function transferBold(address _from, address _to, uint256 _amount) public { vm.startPrank(_from); - boldToken.transfer(_to, boldToken.balanceOf(_from)); + boldToken.transfer(_to, _amount); vm.stopPrank(); } diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index 201f1059..169be58a 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -159,47 +159,46 @@ contract InterestRateAggregate is DevTestSetup { function testMintAggInterestRevertsWhenNotCalledByBOorSP() public { // pass positive debt change - int256 debtChange = 37e18; + uint256 debtChange = 37e18; vm.startPrank(A); vm.expectRevert(); - activePool.mintAggInterest(debtChange); + activePool.mintAggInterest(debtChange, 0); vm.stopPrank(); vm.startPrank(address(borrowerOperations)); - activePool.mintAggInterest(debtChange); + activePool.mintAggInterest(debtChange, 0); vm.stopPrank(); vm.startPrank(address(stabilityPool)); - activePool.mintAggInterest(debtChange); + activePool.mintAggInterest(debtChange, 0); vm.stopPrank(); // pass negative debt change - debtChange = -37e18; vm.startPrank(A); vm.expectRevert(); - activePool.mintAggInterest(debtChange); + activePool.mintAggInterest(0, debtChange); vm.stopPrank(); vm.startPrank(address(borrowerOperations)); - activePool.mintAggInterest(debtChange); + activePool.mintAggInterest(0, debtChange); vm.stopPrank(); vm.startPrank(address(stabilityPool)); - activePool.mintAggInterest(debtChange); + activePool.mintAggInterest(0, debtChange); vm.stopPrank(); // pass 0 debt change vm.startPrank(A); vm.expectRevert(); - activePool.mintAggInterest(0); + activePool.mintAggInterest(0, 0); vm.stopPrank(); vm.startPrank(address(borrowerOperations)); - activePool.mintAggInterest(0); + activePool.mintAggInterest(0, 0); vm.stopPrank(); vm.startPrank(address(stabilityPool)); - activePool.mintAggInterest(0); + activePool.mintAggInterest(0, 0); vm.stopPrank(); } @@ -632,7 +631,6 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - entireTroveDebt_B); } - // Updates last agg update time to now function testCloseTroveUpdatesLastAggUpdateTimeToNow() public { uint256 troveDebtRequest = 2000e18; @@ -869,7 +867,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); } - function testAdustTroveInterestRateWithNoPendingDebtRewardIncreasesByPendingInterest() public { + function testAdustTroveInterestRateWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { uint256 troveDebtRequest = 2000e18; // A opens Trove @@ -890,12 +888,252 @@ contract InterestRateAggregate is DevTestSetup { // A changes interest rate changeInterestRateNoHints(A, 75e16); - // Check redorded debt sum increases by the pending interest + // Check recorded debt sum increases by the pending interest assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + pendingInterest); } // TODO: getEntireDebt and getTCR basic tests - // TODO: adjustTrove tests + + // --- withdrawBold tests --- + + function testWithdrawBoldWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterestPlusBorrowerDebtChange() public { + uint256 troveDebtRequest = 2000e18; + uint256 debtIncrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, debtIncrease); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest + debtIncrease); + } + + function testWithdrawBoldReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + uint256 debtIncrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, debtIncrease); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + // Updates last agg update time to now + function testWithdrawBoldUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + uint256 debtIncrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // A draws more debt + withdrawBold100pctMaxFee(A, debtIncrease); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + // With no redist gain, increases recorded debt sum by the borrower's debt change plus Trove's accrued interest + + function testWithdrawBoldWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterestPlusDebtChange() public { + uint256 troveDebtRequest = 2000e18; + uint256 debtIncrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + 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(); + + // A draws more debt + withdrawBold100pctMaxFee(A, debtIncrease); + + // Check recorded debt sum increases by the accrued interest plus debt change + assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest + debtIncrease); + } + + function testWithdrawBoldAdjustsWeightedDebtSumCorrectly() public { + uint256 troveDebtRequest = 2000e18; + uint256 debtIncrease = 500e18; + uint256 interestRate = 25e16; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, debtIncrease); + + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); + uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * interestRate; + + // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); + } + + // --- repayBold tests --- + + function testRepayBoldWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterestMinusBorrowerDebtChange() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A repays bold + repayBold(A, debtDecrease); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - debtDecrease); + } + + function testRepayBoldReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A repays debt + repayBold(A, debtDecrease); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testRepayBoldUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // A repays debt + repayBold(A, debtDecrease); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + function testRepayBolddWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterestMinusDebtChange() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + 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(); + + // A repays debt + repayBold(A, debtDecrease); + + // Check recorded debt sum increases by the accrued interest plus debt change + assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest - debtDecrease); + } + + function testRepayBoldAdjustsWeightedDebtSumCorrectly() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + uint256 interestRate = 25e16; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, debtDecrease); + + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); + uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * interestRate; + + // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); + } + + + // TODO: pure collateral adjustment opps + // TODO: mixed collateral & debt adjustment opps // TODO: tests with pending debt redist. gain >0 + + } diff --git a/contracts/src/test/interestRateBasic.t.sol b/contracts/src/test/interestRateBasic.t.sol index d7b7420c..34759a46 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -21,7 +21,7 @@ contract InterestRateBasic is DevTestSetup { assertEq(troveManager.getTroveAnnualInterestRate(D_Id), 1e18); } - function testOpenTroveSetsTroveLastDebtUpdateTime() public { + function testOpenTroveSetsTroveLastDebtUpdateTimeToNow() public { priceFeed.setPrice(2000e18); assertEq(troveManager.getTroveLastDebtUpdateTime(A), 0); assertEq(troveManager.getTroveLastDebtUpdateTime(B), 0); @@ -220,4 +220,19 @@ contract InterestRateBasic is DevTestSetup { assertEq(sortedTroves.getNext(E_Id), D_Id); assertEq(sortedTroves.getPrev(E_Id), 0); // head } + + // --- withdrawBold --- + + function testWithdrawBoldSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, 2000e18, 0); + + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + + // // A draws more debt + withdrawBold100pctMaxFee(ATroveId, 500e18); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + } } From a0d2bdd2026003354511830305c715d325811d69 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Sun, 25 Feb 2024 16:07:28 +0700 Subject: [PATCH 08/18] Add aggregate interest tests for addColl and withdrawColl --- contracts/src/test/TestContracts/BaseTest.sol | 11 + .../src/test/interestRateAggregate.t.sol | 240 +++++++++++++++++- 2 files changed, 248 insertions(+), 3 deletions(-) diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index 693eb0e9..24ae197b 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -146,6 +146,17 @@ contract BaseTest is Test { vm.stopPrank(); } + function addColl(address _account, uint256 _collIncrease) public { + vm.startPrank(_account); + borrowerOperations.addColl{value: _collIncrease}(); + vm.stopPrank(); + } + + function withdrawColl(address _account, uint256 _collDecrease) public { + vm.startPrank(_account); + borrowerOperations.withdrawColl(_collDecrease); + vm.stopPrank(); + } function transferBold(address _from, address _to, uint256 _amount) public { vm.startPrank(_from); diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index 169be58a..c6b4dd4a 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -1130,10 +1130,244 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); } + // --- addColl tests --- - // TODO: pure collateral adjustment opps - // TODO: mixed collateral & debt adjustment opps - // TODO: tests with pending debt redist. gain >0 + function testAddCollWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + uint256 troveDebtRequest = 3000e18; + uint256 collIncrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A adds coll + addColl(A, collIncrease); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); + } + + function testAddCollReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 3000e18; + uint256 collIncrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A adds coll + addColl(A, collIncrease); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testAddCollUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 3000e18; + uint256 collIncrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // A adds coll + addColl(A, collIncrease); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + function testAddCollWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { + uint256 troveDebtRequest = 3000e18; + uint256 collIncrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + 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(); + + // A adds coll + addColl(A, collIncrease); + + // Check recorded debt sum increases by the accrued interest plus debt change + assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest); + } + + function testAddCollAdjustsWeightedDebtSumCorrectly() public { + uint256 troveDebtRequest = 3000e18; + uint256 collIncrease = 1 ether; + uint256 interestRate = 25e16; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // A adds coll + addColl(A, collIncrease); + + (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); + } + + // --- withdrawColl --- + + function testWithdrawCollWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + uint256 troveDebtRequest = 2000e18; + uint256 collDecrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A withdraws coll + withdrawColl(A, collDecrease); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); + } + + function testWithdrawCollReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + uint256 collDecrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A withdraws coll + withdrawColl(A, collDecrease); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testWithdrawCollUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + uint256 collDecrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // A withdraw coll + withdrawColl(A, collDecrease); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + function testWithdrawCollWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { + uint256 troveDebtRequest = 2000e18; + uint256 collDecrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + 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(); + + // A withdraw coll + withdrawColl(A, collDecrease); + + // Check recorded debt sum increases by the accrued interest plus debt change + assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest); + } + + function testWithdrawCollAdjustsWeightedDebtSumCorrectly() public { + uint256 troveDebtRequest = 2000e18; + uint256 collDecrease = 1 ether; + uint256 interestRate = 25e16; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // A withdraw coll + withdrawColl(A, collDecrease); + + (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 } From aaabd9c0bab22d7a8ae9cc928b74252203ae494e Mon Sep 17 00:00:00 2001 From: RickGriff Date: Mon, 26 Feb 2024 19:22:20 +0700 Subject: [PATCH 09/18] Add permisionless Trove interest update func --- contracts/src/BorrowerOperations.sol | 124 +++-- .../src/Interfaces/IBorrowerOperations.sol | 2 + contracts/src/Interfaces/ITroveManager.sol | 2 + contracts/src/TroveManager.sol | 5 + contracts/src/test/TestContracts/BaseTest.sol | 6 + .../src/test/interestRateAggregate.t.sol | 133 +++++- contracts/src/test/interestRateBasic.t.sol | 449 +++++++++++++++++- 7 files changed, 678 insertions(+), 43 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index ec526418..c7cc093e 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -47,8 +47,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint newICR; uint newTCR; uint BoldFee; - uint newDebt; - uint newColl; + uint newEntireDebt; + uint newEntireColl; uint stake; } @@ -187,9 +187,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. @@ -286,33 +283,16 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // TODO: Delegation functionality ContractsCacheTMAP memory contractsCache = ContractsCacheTMAP(troveManager, activePool); - // --- Checks --- + _requireValidAnnualInterestRate(_newAnnualInterestRate); _requireTroveisActive(contractsCache.troveManager, _troveId); - uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(_troveId); - - // --- Effects --- - - (, uint256 redistDebtGain) = contractsCache.troveManager.getAndApplyRedistributionGains(_troveId); - - // 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, _troveId, _newAnnualInterestRate); sortedTroves.reInsert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); // Update Trove recorded debt and interest-weighted debt sum contractsCache.troveManager.updateTroveDebtAndInterest(_troveId, 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); } /* @@ -395,14 +375,12 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } // Update the Trove's recorded debt and coll - // TODO: Can we optimize calls to TM? - vars.newColl = _updateTroveCollFromAdjustment(contractsCache.troveManager, _troveId, vars.collChange, vars.isCollIncrease); - uint256 newEntireDebt = vars.entireDebt + _boldChange; - contractsCache.troveManager.updateTroveDebt(_troveId, newEntireDebt); + vars.newEntireColl = _updateTroveCollFromAdjustment(contractsCache.troveManager, _troveId, vars.collChange, vars.isCollIncrease); + vars.newEntireDebt = _updateTroveDebtFromAdjustment(contractsCache.troveManager, _troveId, vars.entireDebt, _boldChange, _isDebtIncrease); vars.stake = contractsCache.troveManager.updateStakeAndTotalStakes(_troveId); - emit TroveUpdated(_troveId, vars.newDebt, vars.newColl, vars.stake, BorrowerOperation.adjustTrove); + emit TroveUpdated(_troveId, vars.newEntireDebt, vars.newEntireColl, vars.stake, BorrowerOperation.adjustTrove); emit BoldBorrowingFeePaid(_troveId, vars.BoldFee); _moveTokensAndETHfromAdjustment( @@ -417,7 +395,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(uint256 _troveId) external override { @@ -474,6 +452,20 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe contractsCache.activePool.sendETH(msg.sender, entireTroveColl); } + function applyTroveInterestPermissionless(uint256 _troveId) external { + ContractsCacheTMAP memory contractsCache = ContractsCacheTMAP(troveManager, activePool); + + _requireTroveIsStale(contractsCache.troveManager, _troveId); + _requireTroveisActive(contractsCache.troveManager, _troveId); + + uint256 annualInterestRate = contractsCache.troveManager.getTroveAnnualInterestRate(_troveId); + + uint256 entireTroveDebt = _updateActivePoolTrackersNoDebtChange(contractsCache.troveManager, contractsCache.activePool, _troveId, annualInterestRate); + + // Update Trove recorded debt and interest-weighted debt sum + contractsCache.troveManager.updateTroveDebt(_troveId, entireTroveDebt); + } + function setAddManager(uint256 _troveId, address _manager) external { troveManager.setAddManager(msg.sender, _troveId, _manager); } @@ -517,7 +509,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, @@ -530,17 +523,40 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe internal returns (uint256) { - uint256 newColl; + uint256 newEntireColl; if (_collChange > 0) { - newColl = (_isCollIncrease) ? + newEntireColl = (_isCollIncrease) ? _troveManager.increaseTroveColl(_sender, _troveId, _collChange) : _troveManager.decreaseTroveColl(_sender, _troveId, _collChange); } else { - newColl = _coll; + newEntireColl = _coll; } - 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; + if (_debtChange > 0) { + newEntireDebt= _isDebtIncrease ? _oldEntireDebt + _debtChange : _oldEntireDebt - _debtChange; + _troveManager.updateTroveDebt(_troveId, newEntireDebt); + } else { + newEntireDebt = _oldEntireDebt; + } + + return newEntireDebt; } // This function incorporates both the Trove's net debt change (repaid/drawn) and its accrued interest. @@ -589,6 +605,37 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _activePool.receiveETH(_amount); } + function _updateActivePoolTrackersNoDebtChange + ( + ITroveManager _troveManager, + IActivePool _activePool, + uint256 _troveId, + 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 _requireCallerIsBorrower(ITroveManager _troveManager , uint256 _troveId) internal view { @@ -709,6 +756,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. @@ -767,14 +818,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 08cd4448..86efee1c 100644 --- a/contracts/src/Interfaces/IBorrowerOperations.sol +++ b/contracts/src/Interfaces/IBorrowerOperations.sol @@ -50,4 +50,6 @@ interface IBorrowerOperations is ILiquityBase { function getCompositeDebt(uint _debt) external pure returns (uint); function adjustTroveInterestRate(uint256 _troveId, uint _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint) external; + + function applyTroveInterestPermissionless(uint256 _troveId) external; } diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 86d77c94..2a87931c 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -99,6 +99,8 @@ interface ITroveManager is IERC721, ILiquityBase { function setTrovePropertiesOnOpen(address _owner, uint256 _troveId, uint256 _coll, uint256 _debt, uint256 _annualInterestRate) external returns (uint256); + function troveIsStale(address _borrower) external view returns (bool); + function increaseTroveColl(address _sender, uint256 _troveId, uint _collIncrease) external returns (uint); function decreaseTroveColl(address _sender, uint256 _troveId, uint _collDecrease) external returns (uint); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 3551d06d..5ae99c25 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -38,6 +38,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana 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 @@ -1450,6 +1451,10 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana 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( diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index 24ae197b..d009a3e2 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -158,6 +158,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 c6b4dd4a..78c6bba8 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 34759a46..49dfb8f8 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -136,6 +136,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); uint256 A_Id = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); @@ -225,14 +273,409 @@ contract InterestRateBasic is DevTestSetup { function testWithdrawBoldSetsTroveLastDebtUpdateTimeToNow() public { priceFeed.setPrice(2000e18); - uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, 2000e18, 0); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 boldWithdrawal = 500e18; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + + // A draws more debt + withdrawBold100pctMaxFee(ATroveId, boldWithdrawal); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), 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; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); - // // A draws more debt - withdrawBold100pctMaxFee(ATroveId, 500e18); + // A adds coll + addColl(ATroveId, collIncrease); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), 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); + } } + +// commit + next: liqs apply interest From 7ed854deedee9df894e3576673b204cc498a9942 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Thu, 29 Feb 2024 14:00:05 +0700 Subject: [PATCH 10/18] Fix individual ICR calc and add interest tests for withdrawETHGainToTrove --- contracts/src/ActivePool.sol | 2 +- contracts/src/BorrowerOperations.sol | 11 +- contracts/src/Interfaces/ILiquityBase.sol | 1 + contracts/src/StabilityPool.sol | 2 + contracts/src/TroveManager.sol | 6 +- contracts/src/test/TestContracts/BaseTest.sol | 25 ++ .../src/test/TestContracts/DevTestSetup.sol | 45 +- .../src/test/interestRateAggregate.t.sol | 402 +++++++++++++++++- contracts/src/test/interestRateBasic.t.sol | 71 +++- 9 files changed, 538 insertions(+), 27 deletions(-) diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index a074c3e5..d32ef805 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -201,7 +201,7 @@ contract ActivePool is Ownable, CheckContract, IActivePool { // This function is called inside all state-changing user ops: borrower ops, liquidations, redemptions and SP deposits/withdrawals. // Some user ops trigger debt changes to Trove(s), in which case _troveDebtChange will be non-zero. - // The aggregate recorded debt is incremented by the aggregate pending interest, plus the net Trove Debt Change. + // The aggregate recorded debt is incremented by the aggregate pending interest, plus the net Trove debt change. // The net Trove debt change consists of the sum of a) any debt issued/repaid and b) any redistribution debt gain applied in the encapsulating operation. // 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), diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index c7cc093e..0a7094f6 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -13,8 +13,7 @@ import "./Dependencies/LiquityBase.sol"; import "./Dependencies/Ownable.sol"; import "./Dependencies/CheckContract.sol"; -import "forge-std/console2.sol"; - +// import "forge-std/console2.sol"; contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOperations { using SafeERC20 for IERC20; @@ -314,8 +313,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe vars.price = priceFeed.fetchPrice(); - uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(msg.sender); - uint256 annualInterestRate = contractsCache.troveManager.getTroveAnnualInterestRate(msg.sender); + uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(_borrower); + uint256 annualInterestRate = contractsCache.troveManager.getTroveAnnualInterestRate(_borrower); // --- Checks --- @@ -367,14 +366,14 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe contractsCache.troveManager.getAndApplyRedistributionGains(_borrower); if (_isDebtIncrease) { - // Incresae Trove debt by the drawn debt + redist. gain + // 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 debt and coll + // Update the Trove's recorded coll and debt vars.newEntireColl = _updateTroveCollFromAdjustment(contractsCache.troveManager, _troveId, vars.collChange, vars.isCollIncrease); vars.newEntireDebt = _updateTroveDebtFromAdjustment(contractsCache.troveManager, _troveId, vars.entireDebt, _boldChange, _isDebtIncrease); diff --git a/contracts/src/Interfaces/ILiquityBase.sol b/contracts/src/Interfaces/ILiquityBase.sol index 709e2ec8..bd03411e 100644 --- a/contracts/src/Interfaces/ILiquityBase.sol +++ b/contracts/src/Interfaces/ILiquityBase.sol @@ -11,5 +11,6 @@ interface ILiquityBase { function defaultPool() external view returns (IDefaultPool); function priceFeed() external view returns (IPriceFeed); function BOLD_GAS_COMPENSATION() external view returns (uint256); + function MCR() external view returns (uint256); function getEntireSystemDebt() external view returns (uint256); } diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 38feed85..1958fbb7 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -14,6 +14,8 @@ import "./Dependencies/LiquityBase.sol"; import "./Dependencies/Ownable.sol"; import "./Dependencies/CheckContract.sol"; +// import "forge-std/console2.sol"; + /* * The Stability Pool holds Bold tokens deposited by Stability Pool depositors. * diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 5ae99c25..3b815833 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -13,7 +13,7 @@ import "./Dependencies/LiquityBase.sol"; import "./Dependencies/Ownable.sol"; import "./Dependencies/CheckContract.sol"; -import "forge-std/console2.sol"; +// import "forge-std/console2.sol"; contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveManager { string constant public NAME = "TroveManager"; // TODO @@ -957,8 +957,10 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint pendingETHReward = getPendingETHReward(_troveId); uint pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); + uint256 accruedTroveInterest = calcTroveAccruedInterest(_borrower); + uint currentETH = Troves[_troveId].coll + pendingETHReward; - uint currentBoldDebt = Troves[_troveId].debt + pendingBoldDebtReward; + uint currentBoldDebt = Troves[_troveId].debt + pendingBoldDebtReward + accruedTroveInterest; return (currentETH, currentBoldDebt); } diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index d009a3e2..b8399512 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -170,6 +170,18 @@ contract BaseTest is Test { vm.stopPrank(); } + function liquidate(address _from, address _borrower) public { + vm.startPrank(_from); + troveManager.liquidate(_borrower); + vm.stopPrank(); + } + + function withdrawETHGainToTrove(address _from) public { + vm.startPrank(_from); + stabilityPool.withdrawETHGainToTrove(); + vm.stopPrank(); + } + function logContractAddresses() public view { console.log("ActivePool addr: ", address(activePool)); console.log("BorrowerOps addr: ", address(borrowerOperations)); @@ -181,4 +193,17 @@ contract BaseTest is Test { console.log("TroveManager addr: ", address(troveManager)); console.log("BoldToken addr: ", address(boldToken)); } + + function abs(uint256 x, uint256 y) public pure returns (uint256) { + return x > y ? x - y : y - x; + } + + function assertApproximatelyEqual(uint256 _x, uint256 _y, uint256 _margin) public { + assertApproximatelyEqual(_x, _y, _margin, ""); + } + + function assertApproximatelyEqual(uint256 _x, uint256 _y, uint256 _margin, string memory _reason) public { + uint256 diff = abs(_x, _y); + assertLe(diff, _margin, _reason); + } } diff --git a/contracts/src/test/TestContracts/DevTestSetup.sol b/contracts/src/test/TestContracts/DevTestSetup.sol index dadf6dba..b75f32cf 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -54,7 +54,7 @@ contract DevTestSetup is BaseTest { WETH = new ERC20("Wrapped ETH", "WETH"); // TODO: optimize deployment order & constructor args & connector functions - + // Deploy all contracts activePool = new ActivePool(address(WETH)); borrowerOperations = new BorrowerOperations(address(WETH)); @@ -64,14 +64,14 @@ contract DevTestSetup is BaseTest { priceFeed = new PriceFeedTestnet(); sortedTroves = new SortedTroves(); stabilityPool = new StabilityPool(address(WETH)); - troveManager = new TroveManager(); + troveManager = new TroveManager(); boldToken = new BoldToken(address(troveManager), address(stabilityPool), address(borrowerOperations), address(activePool)); mockInterestRouter = new MockInterestRouter(); // Connect contracts sortedTroves.setParams( MAX_UINT256, - address(troveManager), + address(troveManager), address(borrowerOperations) ); @@ -88,7 +88,7 @@ contract DevTestSetup is BaseTest { address(sortedTroves) ); - // set contracts in BorrowerOperations + // set contracts in BorrowerOperations borrowerOperations.setAddresses( address(troveManager), address(activePool), @@ -137,4 +137,41 @@ contract DevTestSetup is BaseTest { giveAndApproveETH(accountsList[i], initialETHAmount); } } + + function _setupForWithdrawETHGainToTrove() internal { + uint256 troveDebtRequest_A = 2000e18; + uint256 troveDebtRequest_B = 3000e18; + uint256 troveDebtRequest_C = 4500e18; + uint256 interestRate = 5e16; // 5% + + uint256 price = 2000e18; + priceFeed.setPrice(price); + + openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, interestRate); + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, interestRate); + openTroveNoHints100pctMaxFee(C, 5 ether, troveDebtRequest_C, interestRate); + + console.log(troveManager.getTCR(price), "TCR"); + console.log(troveManager.getCurrentICR(C, price), "C CR"); + + // A and B deposit to SP + makeSPDeposit(A, troveDebtRequest_A); + makeSPDeposit(B, troveDebtRequest_B); + + // Price drops, C becomes liquidateable + price = 1025e18; + priceFeed.setPrice(price); + + console.log(troveManager.getTCR(price), "TCR before liq"); + console.log(troveManager.getCurrentICR(C, price), "C CR before liq"); + + assertFalse(troveManager.checkRecoveryMode(price)); + assertLt(troveManager.getCurrentICR(C, price), troveManager.MCR()); + + // A liquidates C + liquidate(A, C); + + // check A has an ETH gain + assertGt(stabilityPool.getDepositorETHGain(A), 0); + } } diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index 78c6bba8..d44b5653 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -205,7 +205,7 @@ contract InterestRateAggregate is DevTestSetup { // --- openTrove impact on aggregates --- // openTrove increases recorded aggregate debt by correct amount - function testOpenTroveIncreasesRecordedAggDebtByAggPendingInterestPlusNewDebt() public { + function testOpenTroveIncreasesRecordedAggDebtByAggPendingInterestPlusTroveDebt() public { priceFeed.setPrice(2000e18); assertEq(activePool.aggRecordedDebt(), 0); @@ -231,6 +231,26 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggRecordedDebt(), aggREcordedDebt_1 + pendingInterest + expectedTroveDebt_B); } + function testOpenTroveIncreasesRecordedDebtSumByTroveDebt() public { + priceFeed.setPrice(2000e18); + assertEq(activePool.aggRecordedDebt(), 0); + + uint256 troveDebtRequest = 2000e18; + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 recordedDebt_1 = activePool.getRecordedDebtSum(); + assertGt(recordedDebt_1, 0); + + openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 25e16); + uint256 troveDebt_A = troveManager.getTroveDebt(A); + assertGt(troveDebt_A, 0); + + assertEq(activePool.getRecordedDebtSum(), recordedDebt_1 + troveDebt_A); + } + function testOpenTroveReducesPendingAggInterestTo0() public { priceFeed.setPrice(2000e18); @@ -439,9 +459,6 @@ contract InterestRateAggregate is DevTestSetup { uint256 weightedDebtSum_1 = activePool.aggWeightedDebtSum(); assertGt(weightedDebtSum_1, 0); - uint256 pendingAggInterest = activePool.calcPendingAggInterest(); - assertGt(pendingAggInterest, 0); - // Make SP deposit makeSPDeposit(A, sPdeposit); @@ -450,6 +467,28 @@ contract InterestRateAggregate is DevTestSetup { assertEq(weightedDebtSum_2, weightedDebtSum_1); } + function testSPDepositDoesNotChangeRecordedDebtSum() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + + // A opens Trove to obtain BOLD + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // Get recorded sum before + uint256 recordedDebt_1 = activePool.getRecordedDebtSum(); + assertGt(recordedDebt_1, 0); + + // Make SP deposit + makeSPDeposit(A, sPdeposit); + + // Get recorded sum after, check no change + assertEq(activePool.getRecordedDebtSum(), recordedDebt_1); + } + // --- SP Withdrawals --- function testSPWithdrawalReducesPendingAggInterestTo0() public { @@ -552,6 +591,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove to obtain BOLD priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + makeSPDeposit(A, sPdeposit); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -564,13 +604,36 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // Make SP deposit - makeSPDeposit(A, sPdeposit); + makeSPWithdrawal(A, sPdeposit); // Get weighted sum after, check no change uint256 weightedDebtSum_2 = activePool.aggWeightedDebtSum(); assertEq(weightedDebtSum_2, weightedDebtSum_1); } + function testSPWithdrawalDoesNotChangeRecordedDebtSum() public { + uint256 troveDebtRequest = 2000e18; + uint256 sPdeposit = 100e18; + + // A opens Trove to obtain BOLD + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + makeSPDeposit(A, sPdeposit); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // Get recorded sum before + uint256 recordedDebt_1 = activePool.getRecordedDebtSum(); + assertGt(recordedDebt_1, 0); + + // Make SP withdrawal + makeSPWithdrawal(A, sPdeposit); + + // Get weighted sum after, check no change + assertEq(activePool.getRecordedDebtSum(), recordedDebt_1); + } + // --- closeTrove --- // Reduces pending agg interest to 0 @@ -835,7 +898,7 @@ contract InterestRateAggregate is DevTestSetup { // A changes interest rate changeInterestRateNoHints(A, 75e16); - // Check I-router Bold bal has increased as expected from SP deposit + // Check I-router Bold bal has increased as expected uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); assertEq(boldBalRouter_2, pendingAggInterest); } @@ -867,7 +930,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); } - function testAdustTroveInterestRateWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { + function testAdjustTroveInterestRateWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { uint256 troveDebtRequest = 2000e18; // A opens Trove @@ -937,6 +1000,28 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.calcPendingAggInterest(), 0); } + function testWithdrawBoldMintsAggInterestToRouter() public { + uint256 troveDebtRequest = 2000e18; + uint256 debtIncrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + // Check I-router balance is 0 + assertEq(boldToken.balanceOf(address(mockInterestRouter)), 0); + + uint256 aggInterest = activePool.calcPendingAggInterest(); + assertGt(aggInterest, 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, debtIncrease); + + assertEq(boldToken.balanceOf(address(mockInterestRouter)), aggInterest); + } + // Updates last agg update time to now function testWithdrawBoldUpdatesLastAggUpdateTimeToNow() public { uint256 troveDebtRequest = 2000e18; @@ -1056,6 +1141,28 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.calcPendingAggInterest(), 0); } + function testRepayBoldMintsAggInterestToRouter() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + // Check I-router balance is 0 + assertEq(boldToken.balanceOf(address(mockInterestRouter)), 0); + + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A repays debt + repayBold(A, debtDecrease); + + assertEq(boldToken.balanceOf(address(mockInterestRouter)), pendingAggInterest); + } + function testRepayBoldUpdatesLastAggUpdateTimeToNow() public { uint256 troveDebtRequest = 3000e18; uint256 debtDecrease = 500e18; @@ -1077,7 +1184,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.lastAggUpdateTime(), block.timestamp); } - function testRepayBolddWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterestMinusDebtChange() public { + function testRepayBoldWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterestMinusDebtChange() public { uint256 troveDebtRequest = 3000e18; uint256 debtDecrease = 500e18; @@ -1120,8 +1227,8 @@ contract InterestRateAggregate is DevTestSetup { uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); assertGt(aggWeightedDebtSum_1, 0); - // A draws more debt - withdrawBold100pctMaxFee(A, debtDecrease); + // A repays debt + repayBold(A, debtDecrease); (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * interestRate; @@ -1172,6 +1279,28 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.calcPendingAggInterest(), 0); } + function testAddCollMintsAggInterestToRouter() public { + uint256 troveDebtRequest = 3000e18; + uint256 collIncrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + // Check I-router balance is 0 + assertEq(boldToken.balanceOf(address(mockInterestRouter)), 0); + + uint256 aggInterest = activePool.calcPendingAggInterest(); + assertGt(aggInterest, 0); + + // A adds coll + addColl(A, collIncrease); + + assertEq(boldToken.balanceOf(address(mockInterestRouter)), aggInterest); + } + function testAddCollUpdatesLastAggUpdateTimeToNow() public { uint256 troveDebtRequest = 3000e18; uint256 collIncrease = 1 ether; @@ -1291,6 +1420,28 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.calcPendingAggInterest(), 0); } + function testWithdrawCollMintsAggInterestToRouter() public { + uint256 troveDebtRequest = 2000e18; + uint256 collDecrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + // Check I-router balance is 0 + assertEq(boldToken.balanceOf(address(mockInterestRouter)), 0); + + uint256 aggInterest = activePool.calcPendingAggInterest(); + assertGt(aggInterest, 0); + + // A withdraws coll + withdrawColl(A, collDecrease); + + assertEq(boldToken.balanceOf(address(mockInterestRouter)), aggInterest); + } + function testWithdrawCollUpdatesLastAggUpdateTimeToNow() public { uint256 troveDebtRequest = 2000e18; uint256 collDecrease = 1 ether; @@ -1414,6 +1565,32 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.calcPendingAggInterest(), 0); } + function testApplyTroveInterestPermissionlessMintsPendingAggInterestToRouter() 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)); + + // Check I-router balance is 0 + assertEq(boldToken.balanceOf(address(mockInterestRouter)), 0); + + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, A); + + // Check I-router Bold bal has increased by the pending agg interest + assertEq(boldToken.balanceOf(address(mockInterestRouter)), pendingAggInterest); + } + function testApplyTroveInterestPermissionlessUpdatesLastAggUpdateTimeToNow() public { uint256 troveDebtRequest = 2000e18; @@ -1494,9 +1671,210 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); } + // --- getTotalSystemDebt tests --- + + function testGetEntireSystemDebtReturns0For0TrovesOpen() public { + uint256 entireSystemDebt_1 = troveManager.getEntireSystemDebt(); + assertEq(entireSystemDebt_1, 0); + + vm.warp(block.timestamp + 1 days); + + uint256 entireSystemDebt_2 = troveManager.getEntireSystemDebt(); + assertEq(entireSystemDebt_2, 0); + } + function testGetEntireSystemDebtWithNoInterestAndNoRedistGainsReturnsSumOfTroveRecordedDebts() public { + uint256 troveDebtRequest_A = 2000e18; + uint256 troveDebtRequest_B = 3000e18; + uint256 troveDebtRequest_C = 4000e18; + uint256 interestRate = 5e17; + + priceFeed.setPrice(2000e18); + + openTroveNoHints100pctMaxFee(A, 20 ether, troveDebtRequest_A, interestRate); + openTroveNoHints100pctMaxFee(B, 20 ether, troveDebtRequest_B, interestRate); + openTroveNoHints100pctMaxFee(C, 20 ether, troveDebtRequest_C, interestRate); + + uint256 recordedDebt_A = troveManager.getTroveDebt(A); + uint256 recordedDebt_B = troveManager.getTroveDebt(B); + uint256 recordedDebt_C = troveManager.getTroveDebt(C); + assertGt(recordedDebt_A, 0); + assertGt(recordedDebt_B, 0); + 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); + } + + function testGetEntireSystemDebtWithNoRedistGainsReturnsSumOfTroveRecordedDebtsPlusIndividualInterests() public { + uint256 troveDebtRequest_A = 2000e18; + uint256 troveDebtRequest_B = 3000e18; + uint256 troveDebtRequest_C = 4000e18; + uint256 interestRate = 5e17; + + priceFeed.setPrice(2000e18); + + openTroveNoHints100pctMaxFee(A, 20 ether, troveDebtRequest_A, interestRate); + openTroveNoHints100pctMaxFee(B, 20 ether, troveDebtRequest_B, interestRate); + openTroveNoHints100pctMaxFee(C, 20 ether, troveDebtRequest_C, interestRate); + + // Fast-forward time, accrue interest + vm.warp(block.timestamp + 1 days); + + uint256 recordedDebt_A = troveManager.getTroveDebt(A); + uint256 recordedDebt_B = troveManager.getTroveDebt(B); + uint256 recordedDebt_C = troveManager.getTroveDebt(C); + assertGt(recordedDebt_A, 0); + assertGt(recordedDebt_B, 0); + assertGt(recordedDebt_C, 0); + + uint256 accruedInterest_A = troveManager.calcTroveAccruedInterest(A); + uint256 accruedInterest_B = troveManager.calcTroveAccruedInterest(B); + uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(C); + assertGt(accruedInterest_A, 0); + assertGt(accruedInterest_B, 0); + assertGt(accruedInterest_C, 0); + + uint256 entireSystemDebt = troveManager.getEntireSystemDebt(); + + uint256 sumIndividualTroveDebts = + recordedDebt_A + accruedInterest_A + + recordedDebt_B + accruedInterest_B + + recordedDebt_C + accruedInterest_C; + + console.log(entireSystemDebt, "entireSystemDebt"); + console.log(sumIndividualTroveDebts, "sumIndividualTroveDebts"); + + assertApproximatelyEqual(entireSystemDebt, sumIndividualTroveDebts, 10); + } + + // TODO: more thorough invariant test + + // --- withdrawETHGainToTrove --- + + function testWithdrawETHGainToTroveIncreasesAggRecordedDebtByAggInterest() public { + _setupForWithdrawETHGainToTrove(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); + } + + function testWithdrawETHGainToTroveReducesPendingAggInterestTo0() public { + _setupForWithdrawETHGainToTrove(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + // check there's pending agg. interest + assertGt(activePool.calcPendingAggInterest(), 0); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A); + + // Check pending agg. interest reduced to 0 + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testWithdrawETHGainToTroveMintsInterestToRouter() public { + _setupForWithdrawETHGainToTrove(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + // Get I-router balance + uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_1, 0); + + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A); + + // Check I-router Bold bal has increased as expected from SP deposit + uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_2, pendingAggInterest); + } + + function testWithdrawETHGainToTroveUpdatesLastAggUpdateTimeToNow() public { + _setupForWithdrawETHGainToTrove(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + function testWithdrawETHGainToTroveChangesAggWeightedDebtSumCorrectly() public { + _setupForWithdrawETHGainToTrove(); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // Get weighted sum before + uint256 weightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(weightedDebtSum_1, 0); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + assertGt(oldRecordedWeightedDebt, 0); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A); + + // Expect recorded weighted debt to have increased due to accrued Trove interest being applied + uint256 newRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + assertGt(newRecordedWeightedDebt, oldRecordedWeightedDebt); + + // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. + assertEq(activePool.aggWeightedDebtSum(), weightedDebtSum_1 - oldRecordedWeightedDebt + newRecordedWeightedDebt); + } + + function testWithdrawETHGainToTroveChangesRecordedDebtSumCorrectly() public { + _setupForWithdrawETHGainToTrove(); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // Get recorded sum before + uint256 recordedDebt_1 = activePool.getRecordedDebtSum(); + assertGt(recordedDebt_1, 0); + + uint256 oldTroveRecordedDebt = troveManager.getTroveDebt(A); + assertGt(oldTroveRecordedDebt, 0); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A); + + // Expect recorded debt to have increased due to accrued Trove interest being applied + uint256 newTroveRecordedDebt = troveManager.getTroveDebt(A); + assertGt(newTroveRecordedDebt, oldTroveRecordedDebt); + + // Get recorded sum after, check no change + assertEq(activePool.getRecordedDebtSum(), recordedDebt_1 - oldTroveRecordedDebt + newTroveRecordedDebt); + } + // 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: tests that show total debt change under user ops + // TODO: Basic TCR and ICR 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 49dfb8f8..15613255 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -676,6 +676,73 @@ contract InterestRateBasic is DevTestSetup { assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); } -} -// commit + next: liqs apply interest + // --- withdrawETHGainToTrove tests --- + + function testWithdrawETHGainToTroveSetsTroveLastDebtUpdateTimeToNow() public { + _setupForWithdrawETHGainToTrove(); + + // Fast-forward time + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A); + + assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + } + + function testWithdrawETHGainToTroveReducesTroveAccruedInterestTo0() public { + _setupForWithdrawETHGainToTrove(); + + // Fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(A), 0); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A); + + assertEq(troveManager.calcTroveAccruedInterest(A), 0); + } + + function testWithdrawETHGainToTroveDoesntChangeEntireTroveDebt() public { + _setupForWithdrawETHGainToTrove(); + + // // Fast-forward time + // vm.warp(block.timestamp + 90 days); + + // (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + // assertGt(entireTroveDebt_1, 0); + + // console.log(troveManager.getCurrentICR(A, 1000e18), "A ICR before"); + // // A withdraws ETH gain to Trove + // withdrawETHGainToTrove(A); + + // console.log(troveManager.getCurrentICR(A, 1000e18), "A ICR after"); + + // (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + // console.log(entireTroveDebt_2, "entireTroveDebt_2"); + // console.log(entireTroveDebt_1, "entireTroveDebt_1"); + + // assertEq(entireTroveDebt_2, entireTroveDebt_1); + } + + function testWithdrawETHGainToTroveIncreasesRecordedTroveDebtByAccruedInterest() public { + _setupForWithdrawETHGainToTrove(); + + // Fast-forward time + vm.warp(block.timestamp + 90 days + 1); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); + } +} From 8beb3ebc4f1beea1cea10e40c6d23b832453f45f Mon Sep 17 00:00:00 2001 From: RickGriff Date: Fri, 15 Mar 2024 13:49:53 +0700 Subject: [PATCH 11/18] Apply agg interest when liquidating (offset) in NM --- contracts/src/ActivePool.sol | 9 +- contracts/src/BorrowerOperations.sol | 6 +- contracts/src/Dependencies/LiquityBase.sol | 2 + contracts/src/Interfaces/ITroveManager.sol | 6 +- contracts/src/StabilityPool.sol | 2 - contracts/src/TroveManager.sol | 98 +++++++---- contracts/src/test/TestContracts/BaseTest.sol | 8 + .../src/test/TestContracts/DevTestSetup.sol | 35 ++++ contracts/src/test/deployment.t.sol | 12 +- .../src/test/interestRateAggregate.t.sol | 164 +++++++++++++++++- contracts/test/BorrowerOperationsTest.js | 56 ++++-- contracts/test/CollSurplusPool.js | 4 +- contracts/test/GasCompensationTest.js | 3 +- .../test/HintHelpers_getApproxHintTest.js | 3 +- contracts/test/PoolsTest.js | 12 +- contracts/test/SP_P_TruncationTest.js | 3 +- contracts/test/SortedTrovesTest.js | 3 +- contracts/test/StabilityPoolTest.js | 11 +- contracts/test/TroveManagerTest.js | 37 ++-- .../TroveManager_LiquidationRewardsTest.js | 5 +- .../test/TroveManager_RecoveryModeTest.js | 3 +- ...ager_RecoveryMode_Batch_Liqudation_Test.js | 3 +- contracts/test/stakeDeclineTest.js | 5 +- contracts/utils/deploymentHelpers.js | 11 +- 24 files changed, 383 insertions(+), 118 deletions(-) diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index d32ef805..8ff13fb2 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -185,7 +185,9 @@ contract ActivePool is Ownable, CheckContract, IActivePool { function decreaseRecordedDebtSum(uint _amount) external override { _requireCallerIsBOorTroveMorSP(); uint256 newRecordedDebtSum = recordedDebtSum - _amount; + recordedDebtSum = newRecordedDebtSum; + emit ActivePoolBoldDebtUpdated(newRecordedDebtSum); } @@ -207,7 +209,7 @@ contract ActivePool is Ownable, CheckContract, IActivePool { // 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 { - _requireCallerIsBOorSP(); + _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 @@ -247,11 +249,6 @@ contract ActivePool is Ownable, CheckContract, IActivePool { "ActivePool: Caller is neither BorrowerOperations nor TroveManager"); } - function _requireCallerIsBOorSP() internal view { - require(msg.sender == borrowerOperationsAddress || msg.sender == stabilityPoolAddress, - "ActivePool: Caller is not the BO or SP"); - } - function _requireCallerIsTroveManager() internal view { require( msg.sender == troveManagerAddress, diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 0a7094f6..16fb51ee 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -434,7 +434,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe contractsCache.activePool.mintAggInterest(0, initialRecordedTroveDebt + accruedTroveInterest); contractsCache.troveManager.removeStake(_troveId); - contractsCache.troveManager.closeTrove(_troveId, initialWeightedRecordedTroveDebt); + contractsCache.troveManager.closeTrove(_troveId); emit TroveUpdated(_troveId, 0, 0, 0, BorrowerOperation.closeTrove); // Remove only the Trove's latest recorded debt (inc. redist. gains) from the recorded debt tracker, @@ -442,6 +442,9 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // TODO: If/when redist. gains are gas-optimized, exclude them from here too. contractsCache.activePool.decreaseRecordedDebtSum(initialRecordedTroveDebt + debtRedistGain); + // Remove Trove's weighted debt from the weighted sum + activePool.changeAggWeightedDebtSum(initialWeightedRecordedTroveDebt, 0); + // Burn the 200 BOLD gas compensation contractsCache.boldToken.burn(gasPoolAddress, BOLD_GAS_COMPENSATION); // Burn the remainder of the Trove's entire debt from the user @@ -822,6 +825,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe totalDebt = _isDebtIncrease ? totalDebt + _debtChange : totalDebt - _debtChange; uint newTCR = LiquityMath._computeCR(totalColl, totalDebt, _price); + return newTCR; } diff --git a/contracts/src/Dependencies/LiquityBase.sol b/contracts/src/Dependencies/LiquityBase.sol index b7d8921a..1adfca6e 100644 --- a/contracts/src/Dependencies/LiquityBase.sol +++ b/contracts/src/Dependencies/LiquityBase.sol @@ -71,6 +71,8 @@ contract LiquityBase is BaseMath, ILiquityBase { function getEntireSystemDebt() public view returns (uint entireSystemDebt) { uint activeDebt = activePool.getTotalActiveDebt(); uint closedDebt = defaultPool.getBoldDebt(); + // console2.log("SYS::activeDebt", activeDebt); + // console2.log("SYS::closedDebt", closedDebt); return activeDebt + closedDebt; } diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 2a87931c..74ed6325 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -66,9 +66,13 @@ interface ITroveManager is IERC721, ILiquityBase { uint pendingBoldInterest ); + function getTroveEntireDebt(address _borrower) external view returns (uint256); + + function getTroveEntireColl(address _borrower) external view returns (uint256); + function getAndApplyRedistributionGains(address _borrower) external returns (uint256, uint256); - function closeTrove(uint256 _troveId, uint256 _weightedRecordedTroveDebt) external; + function closeTrove(uint256 _troveId) external; function removeStake(uint256 _troveId) external; diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 1958fbb7..a5f7a198 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -488,8 +488,6 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { IActivePool activePoolCached = activePool; // Cancel the liquidated Bold debt with the Bold in the stability pool - // TODO: double-check we are removing the right debt sum here in a liquidation - activePoolCached.decreaseRecordedDebtSum(_debtToOffset); _decreaseBold(_debtToOffset); // Burn the debt that was successfully offset diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 3b815833..a9ba0eed 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -152,7 +152,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint collToLiquidate; uint pendingDebtReward; uint pendingCollReward; - uint256 weightedRecordedTroveDebt; } struct LocalVariables_LiquidationSequence { @@ -175,11 +174,17 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint debtToRedistribute; uint collToRedistribute; uint collSurplus; + uint256 accruedTroveInterest; + uint256 weightedRecordedTroveDebt; + uint256 recordedTroveDebt; } struct LiquidationTotals { uint totalCollInSequence; uint totalDebtInSequence; + uint256 totalRecordedDebtInSequence; + uint256 totalWeightedRecordedDebtInSequence; + uint256 totalAccruedInterestInSequence; uint totalCollGasCompensation; uint totalBoldGasCompensation; uint totalDebtToOffset; @@ -333,13 +338,16 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana returns (LiquidationValues memory singleLiquidation) { LocalVariables_InnerSingleLiquidateFunction memory vars; - (singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, vars.pendingDebtReward, - vars.pendingCollReward, ) = getEntireDebtAndColl(_troveId); + vars.pendingCollReward, + singleLiquidation.accruedTroveInterest) = getEntireDebtAndColl(_troveId); - vars.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_borrower); + singleLiquidation.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_troveId); + + //TODO - GAS: We already read this inside getEntireDebtAndColl - so add it to the returned vals? + singleLiquidation.recordedTroveDebt = Troves[_troveId].debt; _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward); _removeStake(_troveId); @@ -353,7 +361,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana singleLiquidation.debtToRedistribute, singleLiquidation.collToRedistribute) = _getOffsetAndRedistributionVals(singleLiquidation.entireTroveDebt, collToLiquidate, _boldInStabPool); - _closeTrove(_troveId, Status.closedByLiquidation, vars.weightedRecordedTroveDebt); + _closeTrove(_troveId, Status.closedByLiquidation); emit TroveLiquidated(_troveId, singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, TroveManagerOperation.liquidateInNormalMode); emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.liquidateInNormalMode); return singleLiquidation; @@ -379,7 +387,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana vars.pendingDebtReward, vars.pendingCollReward, ) = getEntireDebtAndColl(_troveId); - vars.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_borrower); + singleLiquidation.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_borrower); singleLiquidation.collGasCompensation = _getCollGasCompensation(singleLiquidation.entireTroveColl); singleLiquidation.BoldGasCompensation = BOLD_GAS_COMPENSATION; @@ -395,7 +403,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana singleLiquidation.debtToRedistribute = singleLiquidation.entireTroveDebt; singleLiquidation.collToRedistribute = vars.collToLiquidate; - _closeTrove(_troveId, Status.closedByLiquidation, vars.weightedRecordedTroveDebt); + _closeTrove(_troveId, Status.closedByLiquidation); emit TroveLiquidated(_troveId, singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, TroveManagerOperation.liquidateInRecoveryMode); emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.liquidateInRecoveryMode); @@ -409,7 +417,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana singleLiquidation.debtToRedistribute, singleLiquidation.collToRedistribute) = _getOffsetAndRedistributionVals(singleLiquidation.entireTroveDebt, vars.collToLiquidate, _boldInStabPool); - _closeTrove(_troveId, Status.closedByLiquidation, vars.weightedRecordedTroveDebt); + _closeTrove(_troveId, Status.closedByLiquidation); emit TroveLiquidated(_troveId, singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, TroveManagerOperation.liquidateInRecoveryMode); emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.liquidateInRecoveryMode); /* @@ -425,7 +433,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana _removeStake(_troveId); singleLiquidation = _getCappedOffsetVals(singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, _price); - _closeTrove(_troveId, Status.closedByLiquidation, vars.weightedRecordedTroveDebt); + _closeTrove(_troveId, Status.closedByLiquidation); if (singleLiquidation.collSurplus > 0) { collSurplusPool.accountSurplus(_troveId, singleLiquidation.collSurplus); } @@ -446,8 +454,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana */ function _getOffsetAndRedistributionVals ( - uint _debt, - uint _coll, + uint _entireTroveDebt, + uint _collToLiquidate, uint _boldInStabPool ) internal @@ -465,15 +473,15 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana * - Send a fraction of the trove's collateral to the Stability Pool, equal to the fraction of its offset debt * */ - debtToOffset = LiquityMath._min(_debt, _boldInStabPool); - collToSendToSP = _coll * debtToOffset / _debt; - debtToRedistribute = _debt - debtToOffset; - collToRedistribute = _coll - collToSendToSP; + debtToOffset = LiquityMath._min(_entireTroveDebt, _boldInStabPool); + collToSendToSP = _collToLiquidate * debtToOffset / _entireTroveDebt; + debtToRedistribute = _entireTroveDebt - debtToOffset; + collToRedistribute = _collToLiquidate - collToSendToSP; } else { debtToOffset = 0; collToSendToSP = 0; - debtToRedistribute = _debt; - collToRedistribute = _coll; + debtToRedistribute = _entireTroveDebt; + collToRedistribute = _collToLiquidate; } } @@ -608,6 +616,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana function batchLiquidateTroves(uint256[] memory _troveArray) public override { require(_troveArray.length != 0, "TroveManager: Calldata address array must not be empty"); + IActivePool activePoolCached = activePool; IDefaultPool defaultPoolCached = defaultPool; IStabilityPool stabilityPoolCached = stabilityPool; @@ -628,6 +637,15 @@ 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); + + // TODO - Gas: combine these into one call to activePool? + // Remove the liquidated recorded debt from the sum + activePoolCached.decreaseRecordedDebtSum(totals.totalRecordedDebtInSequence); + // Remove the liqudiated weighted recorded debt from the sum + activePool.changeAggWeightedDebtSum(totals.totalWeightedRecordedDebtInSequence, 0); + // Move liquidated ETH and Bold to the appropriate pools stabilityPoolCached.offset(totals.totalDebtToOffset, totals.totalCollToSendToSP); _redistributeDebtAndColl(activePoolCached, defaultPoolCached, totals.totalDebtToRedistribute, totals.totalCollToRedistribute); @@ -749,12 +767,17 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana newTotals.totalBoldGasCompensation = oldTotals.totalBoldGasCompensation + singleLiquidation.BoldGasCompensation; newTotals.totalDebtInSequence = oldTotals.totalDebtInSequence + singleLiquidation.entireTroveDebt; newTotals.totalCollInSequence = oldTotals.totalCollInSequence + singleLiquidation.entireTroveColl; + newTotals.totalRecordedDebtInSequence = oldTotals.totalRecordedDebtInSequence + singleLiquidation.recordedTroveDebt; + newTotals.totalWeightedRecordedDebtInSequence = oldTotals.totalWeightedRecordedDebtInSequence + singleLiquidation.weightedRecordedTroveDebt; + newTotals.totalAccruedInterestInSequence = oldTotals.totalAccruedInterestInSequence + singleLiquidation.accruedTroveInterest; newTotals.totalDebtToOffset = oldTotals.totalDebtToOffset + singleLiquidation.debtToOffset; newTotals.totalCollToSendToSP = oldTotals.totalCollToSendToSP + singleLiquidation.collToSendToSP; newTotals.totalDebtToRedistribute = oldTotals.totalDebtToRedistribute + singleLiquidation.debtToRedistribute; newTotals.totalCollToRedistribute = oldTotals.totalCollToRedistribute + singleLiquidation.collToRedistribute; newTotals.totalCollSurplus = oldTotals.totalCollSurplus + singleLiquidation.collSurplus; + + return newTotals; } @@ -802,7 +825,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint256 weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_borrower); // No debt left in the Trove (except for the liquidation reserve), therefore the trove gets closed _removeStake(_troveId); - _closeTrove(_troveId, Status.closedByRedemption, weightedRecordedTroveDebt); + _closeTrove(_troveId, Status.closedByRedemption); _redeemCloseTrove(_contractsCache, _troveId, BOLD_GAS_COMPENSATION, newColl); emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.redeemCollateral); @@ -1057,19 +1080,29 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana public view override - returns (uint entireDebt, uint entireColl, uint pendingBoldDebtReward, uint pendingETHReward, uint pendingBoldInterest) + returns (uint entireDebt, uint entireColl, uint pendingBoldDebtReward, uint pendingETHReward, uint accruedTroveInterest) { uint256 recordedDebt = Troves[_troveId].debt; uint256 recordedColl = Troves[_troveId].coll; pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); - pendingBoldInterest = calcTroveAccruedInterest(_troveId); + accruedTroveInterest = calcTroveAccruedInterest(_troveId); pendingETHReward = getPendingETHReward(_troveId); - entireDebt = recordedDebt + pendingBoldDebtReward + pendingBoldInterest; + entireDebt = recordedDebt + pendingBoldDebtReward + accruedTroveInterest; entireColl = recordedColl + pendingETHReward; } + function getTroveEntireDebt(uint256 _troveId) public view returns (uint256) { + (uint256 entireTroveDebt, , , , ) = getEntireDebtAndColl(_troveId); + return entireTroveDebt; + } + + function getTroveEntireColl(uint256 _troveId) public view returns (uint256) { + ( , uint256 entireTroveColl, , , ) = getEntireDebtAndColl(_troveId); + return entireTroveColl; + } + function removeStake(uint256 _troveId) external override { _requireCallerIsBorrowerOperations(); return _removeStake(_troveId); @@ -1118,8 +1151,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return stake; } - function _redistributeDebtAndColl(IActivePool _activePool, IDefaultPool _defaultPool, uint _debt, uint _coll) internal { - if (_debt == 0) { return; } + function _redistributeDebtAndColl(IActivePool _activePool, IDefaultPool _defaultPool, uint _debtToRedistribute, uint _collToRedistribute) internal { + if (_debtToRedistribute == 0) { return; } /* * Add distributed coll and debt rewards-per-unit-staked to the running totals. Division uses a "feedback" @@ -1132,8 +1165,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana * 4) Store these errors for use in the next correction when this function is called. * 5) Note: static analysis tools complain about this "division before multiplication", however, it is intended. */ - uint ETHNumerator = _coll * DECIMAL_PRECISION + lastETHError_Redistribution; - uint boldDebtNumerator = _debt * DECIMAL_PRECISION + lastBoldDebtError_Redistribution; + uint ETHNumerator = _collToRedistribute * DECIMAL_PRECISION + lastETHError_Redistribution; + uint boldDebtNumerator = _debtToRedistribute * DECIMAL_PRECISION + lastBoldDebtError_Redistribution; // Get the per-unit-staked terms uint ETHRewardPerUnitStaked = ETHNumerator / totalStakes; @@ -1148,18 +1181,16 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana emit LTermsUpdated(L_ETH, L_boldDebt); - // Transfer coll and debt from ActivePool to DefaultPool - _activePool.decreaseRecordedDebtSum(_debt); - _defaultPool.increaseBoldDebt(_debt); - _activePool.sendETHToDefaultPool(_coll); + _defaultPool.increaseBoldDebt(_debtToRedistribute); + _activePool.sendETHToDefaultPool(_collToRedistribute); } - function closeTrove(uint256 _troveId, uint256 _weightedRecordedTroveDebt) external override { + function closeTrove(uint256 _troveId) external override { _requireCallerIsBorrowerOperations(); - return _closeTrove(_troveId, Status.closedByOwner, _weightedRecordedTroveDebt); + return _closeTrove(_troveId, Status.closedByOwner); } - function _closeTrove(uint256 _troveId, Status closedStatus, uint256 _weightedRecordedTroveDebt) internal { + function _closeTrove(uint256 _troveId, Status closedStatus) internal { assert(closedStatus != Status.nonExistent && closedStatus != Status.active); uint TroveIdsArrayLength = TroveIds.length; @@ -1175,9 +1206,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana rewardSnapshots[_troveId].ETH = 0; rewardSnapshots[_troveId].boldDebt = 0; - // Remove Trove's weighted debt from the weighted sum - activePool.changeAggWeightedDebtSum(_weightedRecordedTroveDebt, 0); - _removeTroveId(_troveId, TroveIdsArrayLength); sortedTroves.remove(_troveId); diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index b8399512..92114943 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -182,6 +182,14 @@ contract BaseTest is Test { vm.stopPrank(); } + function batchLiquidateTroves(address _from, address[] memory _trovesList) public { + vm.startPrank(_from); + console.log(_trovesList[0], "trove 0 to liq"); + console.log(_trovesList[1], "trove 1 to liq"); + troveManager.batchLiquidateTroves(_trovesList); + 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 b75f32cf..f94581ac 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -174,4 +174,39 @@ contract DevTestSetup is BaseTest { // check A has an ETH gain assertGt(stabilityPool.getDepositorETHGain(A), 0); } + + function _setupForBatchLiquidateTrovesPureOffset() internal { + uint256 troveDebtRequest_A = 2000e18; + uint256 troveDebtRequest_B = 3000e18; + uint256 troveDebtRequest_C = 2250e18; + uint256 troveDebtRequest_D = 2250e18; + uint256 interestRate = 5e16; // 5% + + uint256 price = 2000e18; + priceFeed.setPrice(price); + + openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, interestRate); + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, interestRate); + openTroveNoHints100pctMaxFee(C, 25e17, troveDebtRequest_C, interestRate); + openTroveNoHints100pctMaxFee(D, 25e17, troveDebtRequest_D, interestRate); + + // console.log(troveManager.getTCR(price), "TCR"); + // console.log(troveManager.getCurrentICR(C, price), "C CR"); + + // A and B deposit to SP + makeSPDeposit(A, troveDebtRequest_A); + makeSPDeposit(B, troveDebtRequest_B); + + // Price drops, C and D become liquidateable + price = 1050e18; + priceFeed.setPrice(price); + + // console.log(troveManager.getTCR(price), "TCR before liq"); + // console.log(troveManager.getCurrentICR(C, price), "C CR before liq"); + // console.log(troveManager.getCurrentICR(D, price), "D CR before liq"); + + assertFalse(troveManager.checkRecoveryMode(price)); + assertLt(troveManager.getCurrentICR(C, price), troveManager.MCR()); + assertLt(troveManager.getCurrentICR(D, price), troveManager.MCR()); + } } diff --git a/contracts/src/test/deployment.t.sol b/contracts/src/test/deployment.t.sol index f3504774..eaab545c 100644 --- a/contracts/src/test/deployment.t.sol +++ b/contracts/src/test/deployment.t.sol @@ -64,14 +64,14 @@ contract Deployment is DevTestSetup { assertEq(stabilityPoolAddress, recordedStabilityPoolAddress); } - // function testTroveManagerHasCorrectInterestRouterAddress() public { - // address interestRouter = address(mockInterestRouter); - // address recordedInterestRouterAddress = address(troveManager.interestRouter()); - // assertEq(interestRouter, recordedInterestRouterAddress); - // } - // Active Pool + function testActivePoolHasCorrectInterestRouterAddress() public { + address interestRouter = address(mockInterestRouter); + address recordedInterestRouterAddress = address(activePool.interestRouter()); + assertEq(interestRouter, recordedInterestRouterAddress); + } + function testActivePoolHasCorrectStabilityPoolAddress() public { address stabilityPoolAddress = address(stabilityPool); address recordedStabilityPoolAddress = activePool.stabilityPoolAddress(); diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index d44b5653..77ee586a 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -1837,14 +1837,14 @@ contract InterestRateAggregate is DevTestSetup { uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); assertGt(oldRecordedWeightedDebt, 0); - // A withdraws ETH gain to Trove + // A withdraws ETH gain to Trove withdrawETHGainToTrove(A); - // Expect recorded weighted debt to have increased due to accrued Trove interest being applied + // Expect recorded weighted debt to have increased due to accrued Trove interest being applied uint256 newRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); assertGt(newRecordedWeightedDebt, oldRecordedWeightedDebt); - // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. + // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. assertEq(activePool.aggWeightedDebtSum(), weightedDebtSum_1 - oldRecordedWeightedDebt + newRecordedWeightedDebt); } @@ -1877,4 +1877,162 @@ contract InterestRateAggregate is DevTestSetup { // TODO: tests that show total debt change under user ops // TODO: Basic TCR and ICR getter tests // TODO: Test total debt invariant holds i.e. (D + S * delta_T) == sum_of_all_entire_trove_debts. + + // --- batchLiquidateTroves (Normal Mode) --- + + function testBatchLiquidateTrovesPureOffsetChangesAggRecordedInterestCorrectly() public { + _setupForBatchLiquidateTrovesPureOffset(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + uint256 recordedDebt_C = troveManager.getTroveDebt(C); + uint256 recordedDebt_D = troveManager.getTroveDebt(D); + assertGt(recordedDebt_C, 0); + assertGt(recordedDebt_D, 0); + uint256 recordedDebtInLiq = recordedDebt_C + recordedDebt_D; + + uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(C); + uint256 accruedInterest_D = troveManager.calcTroveAccruedInterest(D); + assertGt(accruedInterest_C, 0); + assertGt(accruedInterest_D, 0); + uint256 accruedInterestInLiq = accruedInterest_C + accruedInterest_D; + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + + // Check both Troves were closed by liquidation + assertEq(troveManager.getTroveStatus(C), 3); + assertEq(troveManager.getTroveStatus(D), 3); + + // // changes agg. recorded debt by: agg_accrued_interest - liq'd_troves_recorded_trove_debts - liq'd_troves_accrued_interest + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - recordedDebtInLiq - accruedInterestInLiq); + } + + function testBatchLiquidateTrovesReducesAggPendingInterestTo0() public { + _setupForBatchLiquidateTrovesPureOffset(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + + // Mints interest to Router + function testBatchLiquidateTrovesMintsAggInterestToRouter() public { + _setupForBatchLiquidateTrovesPureOffset(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_1, 0); + + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + + // Check I-router Bold bal has increased as expected from liquidation + uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_2, pendingAggInterest); + } + + function testBatchLiquidateTrovesUpdatesLastAggInterestUpdateTimeToNow() public { + _setupForBatchLiquidateTrovesPureOffset(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + + } + + // Removes liq'd troves' weighted recorded debts from the weighted recorded debt sum + function testBatchLiquidateTrovesRemovesLiquidatedTrovesWeightedRecordedDebtsFromWeightedRecordedDebtSum() public { + _setupForBatchLiquidateTrovesPureOffset(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(C); + uint256 annualInterestRate_C = troveManager.getTroveAnnualInterestRate(C); + assertGt(recordedTroveDebt_C, 0); + assertGt(annualInterestRate_C, 0); + uint256 weightedTroveDebt_C = recordedTroveDebt_C * annualInterestRate_C; + + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(D); + uint256 annualInterestRate_D = troveManager.getTroveAnnualInterestRate(D); + assertGt(recordedTroveDebt_D, 0); + assertGt(annualInterestRate_D, 0); + uint256 weightedTroveDebt_D = recordedTroveDebt_D * annualInterestRate_D; + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + + // Check weighted recorded debt sum reduced by C and D's weighted recorded debt + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - (weightedTroveDebt_C + weightedTroveDebt_D)); + } + + function testBatchLiquidateTrovesWithNoRedistGainRemovesLiquidatedTrovesRecordedDebtsFromRecordedDebtSum() public { + _setupForBatchLiquidateTrovesPureOffset(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(C); + assertGt(recordedTroveDebt_C, 0); + + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(D); + assertGt(recordedTroveDebt_D, 0); + + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + + // Check recorded debt sum reduced by C and D's recorded debt + assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 - (recordedTroveDebt_C + recordedTroveDebt_D)); + } } diff --git a/contracts/test/BorrowerOperationsTest.js b/contracts/test/BorrowerOperationsTest.js index 274af94c..758ea470 100644 --- a/contracts/test/BorrowerOperationsTest.js +++ b/contracts/test/BorrowerOperationsTest.js @@ -1135,7 +1135,7 @@ contract("BorrowerOperations", async (accounts) => { assert.isTrue(aliceDebtBefore.gt(toBN(0))); // check before - const activePool_Bold_Before = await activePool.getBoldDebt(); + const activePool_Bold_Before = await activePool.getRecordedDebtSum(); assert.isTrue(activePool_Bold_Before.eq(aliceDebtBefore)); await borrowerOperations.withdrawBold( @@ -1146,7 +1146,7 @@ contract("BorrowerOperations", async (accounts) => { ); // check after - const activePool_Bold_After = await activePool.getBoldDebt(); + const activePool_Bold_After = await activePool.getRecordedDebtSum(); th.assertIsApproximatelyEqual( activePool_Bold_After, activePool_Bold_Before.add(toBN(dec(10000, 18))) @@ -1390,7 +1390,7 @@ contract("BorrowerOperations", async (accounts) => { assert.isTrue(aliceDebtBefore.gt(toBN("0"))); // Check before - const activePool_Bold_Before = await activePool.getBoldDebt(); + const activePool_Bold_Before = await activePool.getRecordedDebtSum(); assert.isTrue(activePool_Bold_Before.gt(toBN("0"))); await borrowerOperations.repayBold( @@ -1400,7 +1400,7 @@ contract("BorrowerOperations", async (accounts) => { ); // Repays 1/10 her debt // check after - const activePool_Bold_After = await activePool.getBoldDebt(); + const activePool_Bold_After = await activePool.getRecordedDebtSum(); th.assertIsApproximatelyEqual( activePool_Bold_After, activePool_Bold_Before.sub(aliceDebtBefore.div(toBN(10))) @@ -2122,7 +2122,7 @@ contract("BorrowerOperations", async (accounts) => { }); const aliceDebtBefore = await getTroveEntireDebt(aliceTroveId); - const activePoolDebtBefore = await activePool.getBoldDebt(); + const activePoolDebtBefore = await activePool.getRecordedDebtSum(); assert.isTrue(aliceDebtBefore.gt(toBN("0"))); assert.isTrue(aliceDebtBefore.eq(activePoolDebtBefore)); @@ -2141,7 +2141,7 @@ contract("BorrowerOperations", async (accounts) => { ); const aliceDebtAfter = await getTroveEntireDebt(aliceTroveId); - const activePoolDebtAfter = await activePool.getBoldDebt(); + const activePoolDebtAfter = await activePool.getRecordedDebtSum(); assert.isTrue(aliceDebtAfter.eq(aliceDebtBefore)); assert.isTrue(activePoolDebtAfter.eq(activePoolDebtBefore)); @@ -2576,7 +2576,7 @@ contract("BorrowerOperations", async (accounts) => { extraParams: { from: alice }, }); - const activePool_BoldDebt_Before = await activePool.getBoldDebt(); + const activePool_BoldDebt_Before = await activePool.getRecordedDebtSum(); assert.isTrue(activePool_BoldDebt_Before.gt(toBN("0"))); // Alice adjusts trove - coll increase and debt decrease @@ -2592,7 +2592,7 @@ contract("BorrowerOperations", async (accounts) => { { from: alice } ); - const activePool_BoldDebt_After = await activePool.getBoldDebt(); + const activePool_BoldDebt_After = await activePool.getRecordedDebtSum(); assert.isTrue( activePool_BoldDebt_After.eq( activePool_BoldDebt_Before.sub(toBN(dec(30, 18))) @@ -2612,7 +2612,7 @@ contract("BorrowerOperations", async (accounts) => { extraParams: { from: alice }, }); - const activePool_BoldDebt_Before = await activePool.getBoldDebt(); + const activePool_BoldDebt_Before = await activePool.getRecordedDebtSum(); assert.isTrue(activePool_BoldDebt_Before.gt(toBN("0"))); // Alice adjusts trove - coll increase and debt increase @@ -2628,7 +2628,7 @@ contract("BorrowerOperations", async (accounts) => { { from: alice } ); - const activePool_BoldDebt_After = await activePool.getBoldDebt(); + const activePool_BoldDebt_After = await activePool.getRecordedDebtSum(); th.assertIsApproximatelyEqual( activePool_BoldDebt_After, @@ -3191,7 +3191,7 @@ contract("BorrowerOperations", async (accounts) => { assert.isTrue(aliceDebt.gt("0")); // Check before - const activePool_Debt_before = await activePool.getBoldDebt(); + const activePool_Debt_before = await activePool.getRecordedDebtSum(); assert.isTrue(activePool_Debt_before.eq(aliceDebt.add(dennisDebt))); assert.isTrue(activePool_Debt_before.gt(toBN("0"))); @@ -3204,7 +3204,7 @@ contract("BorrowerOperations", async (accounts) => { await borrowerOperations.closeTrove(aliceTroveId, { from: alice }); // Check after - const activePool_Debt_After = (await activePool.getBoldDebt()).toString(); + const activePool_Debt_After = (await activePool.getRecordedDebtSum()).toString(); th.assertIsApproximatelyEqual(activePool_Debt_After, dennisDebt); }); @@ -3400,7 +3400,7 @@ contract("BorrowerOperations", async (accounts) => { assert.equal(bob_BoldDebtRewardSnapshot_Before, 0); const defaultPool_ETH = await defaultPool.getETHBalance(); - const defaultPool_BoldDebt = await defaultPool.getBoldDebt(); + const defaultPool_BoldDebt = await defaultPool.getRecordedDebtSum(); // Carol's liquidated coll (1 ETH) and drawn debt should have entered the Default Pool assert.isAtMost(th.getDifference(defaultPool_ETH, liquidatedColl_C), 100); @@ -3419,7 +3419,7 @@ contract("BorrowerOperations", async (accounts) => { const defaultPool_ETH_afterAliceCloses = await defaultPool.getETHBalance(); const defaultPool_BoldDebt_afterAliceCloses = - await defaultPool.getBoldDebt(); + await defaultPool.getRecordedDebtSum(); assert.isAtMost( th.getDifference( @@ -3452,7 +3452,7 @@ contract("BorrowerOperations", async (accounts) => { const defaultPool_ETH_afterBobCloses = await defaultPool.getETHBalance(); const defaultPool_BoldDebt_afterBobCloses = - await defaultPool.getBoldDebt(); + await defaultPool.getRecordedDebtSum(); assert.isAtMost( th.getDifference(defaultPool_ETH_afterBobCloses, 0), @@ -4114,7 +4114,7 @@ contract("BorrowerOperations", async (accounts) => { }); it("openTrove(): increases Bold debt in ActivePool by the debt of the trove", async () => { - const activePool_BoldDebt_Before = await activePool.getBoldDebt(); + const activePool_BoldDebt_Before = await activePool.getRecordedDebtSum(); assert.equal(activePool_BoldDebt_Before, 0); const { troveId: aliceTroveId } = await openTrove({ @@ -4125,7 +4125,7 @@ contract("BorrowerOperations", async (accounts) => { const aliceDebt = await getTroveEntireDebt(aliceTroveId); assert.isTrue(aliceDebt.gt(toBN("0"))); - const activePool_BoldDebt_After = await activePool.getBoldDebt(); + const activePool_BoldDebt_After = await activePool.getRecordedDebtSum(); assert.isTrue(activePool_BoldDebt_After.eq(aliceDebt)); }); @@ -4375,7 +4375,7 @@ contract("BorrowerOperations", async (accounts) => { describe("getNewTCRFromTroveChange() returns the correct TCR", async () => { // 0, 0 - it("collChange = 0, debtChange = 0", async () => { + it.only("collChange = 0, debtChange = 0", async () => { // --- SETUP --- Create a Liquity instance with an Active Pool and pending rewards (Default Pool) const troveColl = toBN(dec(1000, "ether")); const troveTotalDebt = toBN(dec(100000, 18)); @@ -4398,6 +4398,8 @@ contract("BorrowerOperations", async (accounts) => { ); await priceFeed.setPrice(dec(100, 18)); + assert.isTrue(troveManager.checkRecoveryMode() + //TODO: make test liq in normal mode const liquidationTx = await troveManager.liquidate(bobTroveId); assert.isFalse(await sortedTroves.contains(bobTroveId)); @@ -4405,9 +4407,18 @@ contract("BorrowerOperations", async (accounts) => { const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx); - await priceFeed.setPrice(dec(200, 18)); const price = await priceFeed.getPrice(); + const actualTroveEntireDebt = await troveManager.getTroveEntireDebt(alice); + const actualTroveRecordedDebt = await troveManager.getTroveDebt(alice); + const actualTroveEntireColl = await troveManager.getTroveEntireColl(alice); + + const pendingTroveInterest = await troveManager.calcTroveAccruedInterest(alice); + + console.log(th.logBN("troveTotalDebt:", troveTotalDebt)); + console.log(th.logBN("pendingTroveInterest:", pendingTroveInterest)); + console.log(th.logBN("actualTroveRecordedDebt:", actualTroveRecordedDebt)); + // --- TEST --- const collChange = 0; const debtChange = 0; @@ -4419,11 +4430,18 @@ contract("BorrowerOperations", async (accounts) => { price ); + th.logBN("newTCR:", newTCR); + const expectedTCR = troveColl .add(liquidatedColl) .mul(price) .div(troveTotalDebt.add(liquidatedDebt)); + th.logBN("expectedTCR:", expectedTCR); + th.logBN("Alice ICR:", await troveManager.getCurrentICR(alice, price)); + th.logBN("actualTroveEntireColl:", actualTroveEntireColl); + th.logBN("actualTroveEntireDebt:", actualTroveEntireDebt); + assert.isTrue(newTCR.eq(expectedTCR)); }); diff --git a/contracts/test/CollSurplusPool.js b/contracts/test/CollSurplusPool.js index afc77a86..07c0dafe 100644 --- a/contracts/test/CollSurplusPool.js +++ b/contracts/test/CollSurplusPool.js @@ -33,10 +33,10 @@ contract("CollSurplusPool", async (accounts) => { contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ); - priceFeed = contracts.priceFeedTestnet; collSurplusPool = contracts.collSurplusPool; borrowerOperations = contracts.borrowerOperations; diff --git a/contracts/test/GasCompensationTest.js b/contracts/test/GasCompensationTest.js index 0d4f0bc2..2519ae4d 100644 --- a/contracts/test/GasCompensationTest.js +++ b/contracts/test/GasCompensationTest.js @@ -76,7 +76,8 @@ contract("Gas compensation tests", async (accounts) => { contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ); priceFeed = contracts.priceFeedTestnet; diff --git a/contracts/test/HintHelpers_getApproxHintTest.js b/contracts/test/HintHelpers_getApproxHintTest.js index e9a7c6da..46e9c17e 100644 --- a/contracts/test/HintHelpers_getApproxHintTest.js +++ b/contracts/test/HintHelpers_getApproxHintTest.js @@ -99,7 +99,8 @@ contract("HintHelpers", async (accounts) => { contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ); sortedTroves = contracts.sortedTroves; diff --git a/contracts/test/PoolsTest.js b/contracts/test/PoolsTest.js index c48f7f6e..d61a9d2d 100644 --- a/contracts/test/PoolsTest.js +++ b/contracts/test/PoolsTest.js @@ -49,7 +49,7 @@ contract('ActivePool', async accounts => { activePool = await ActivePool.new(WETH.address) mockBorrowerOperations = await NonPayable.new() const dumbContractAddress = (await NonPayable.new()).address - await activePool.setAddresses(mockBorrowerOperations.address, dumbContractAddress, dumbContractAddress, dumbContractAddress) + await activePool.setAddresses(mockBorrowerOperations.address, dumbContractAddress, dumbContractAddress, dumbContractAddress, dumbContractAddress, dumbContractAddress) }) it('getETHBalance(): gets the recorded ETH balance', async () => { @@ -58,19 +58,19 @@ contract('ActivePool', async accounts => { }) it('getBoldDebt(): gets the recorded BOLD balance', async () => { - const recordedETHBalance = await activePool.getBoldDebt() + const recordedETHBalance = await activePool.getRecordedDebtSum() assert.equal(recordedETHBalance, 0) }) it('increaseBoldDebt(): increases the recorded BOLD balance by the correct amount', async () => { - const recordedBold_balanceBefore = await activePool.getBoldDebt() + const recordedBold_balanceBefore = await activePool.getRecordedDebtSum() assert.equal(recordedBold_balanceBefore, 0) // await activePool.increaseBoldDebt(100, { from: mockBorrowerOperationsAddress }) const increaseBoldDebtData = th.getTransactionData('increaseBoldDebt(uint256)', ['0x64']) const tx = await mockBorrowerOperations.forward(activePool.address, increaseBoldDebtData) assert.isTrue(tx.receipt.status) - const recordedBold_balanceAfter = await activePool.getBoldDebt() + const recordedBold_balanceAfter = await activePool.getRecordedDebtSum() assert.equal(recordedBold_balanceAfter, 100) }) // Decrease @@ -81,14 +81,14 @@ contract('ActivePool', async accounts => { const tx1 = await mockBorrowerOperations.forward(activePool.address, increaseBoldDebtData) assert.isTrue(tx1.receipt.status) - const recordedBold_balanceBefore = await activePool.getBoldDebt() + const recordedBold_balanceBefore = await activePool.getRecordedDebtSum() assert.equal(recordedBold_balanceBefore, 100) //await activePool.decreaseBoldDebt(100, { from: mockBorrowerOperationsAddress }) const decreaseBoldDebtData = th.getTransactionData('decreaseBoldDebt(uint256)', ['0x64']) const tx2 = await mockBorrowerOperations.forward(activePool.address, decreaseBoldDebtData) assert.isTrue(tx2.receipt.status) - const recordedBold_balanceAfter = await activePool.getBoldDebt() + const recordedBold_balanceAfter = await activePool.getRecordedDebtSum() assert.equal(recordedBold_balanceAfter, 0) }) diff --git a/contracts/test/SP_P_TruncationTest.js b/contracts/test/SP_P_TruncationTest.js index 5c031c71..f98bdb22 100644 --- a/contracts/test/SP_P_TruncationTest.js +++ b/contracts/test/SP_P_TruncationTest.js @@ -42,7 +42,8 @@ contract("StabilityPool Scale Factor issue tests", async (accounts) => { contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ); priceFeed = contracts.priceFeedTestnet; diff --git a/contracts/test/SortedTrovesTest.js b/contracts/test/SortedTrovesTest.js index 4fe8c0ad..edb68d3e 100644 --- a/contracts/test/SortedTrovesTest.js +++ b/contracts/test/SortedTrovesTest.js @@ -87,7 +87,8 @@ contract("SortedTroves", async (accounts) => { contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ); priceFeed = contracts.priceFeedTestnet; diff --git a/contracts/test/StabilityPoolTest.js b/contracts/test/StabilityPoolTest.js index 91b1b241..4c115d00 100644 --- a/contracts/test/StabilityPoolTest.js +++ b/contracts/test/StabilityPoolTest.js @@ -76,7 +76,8 @@ contract("StabilityPool", async (accounts) => { contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ); priceFeed = contracts.priceFeedTestnet; @@ -714,7 +715,7 @@ contract("StabilityPool", async (accounts) => { assert.isFalse(await sortedTroves.contains(defaulter_1_TroveId)); assert.isFalse(await sortedTroves.contains(defaulter_2_TroveId)); - const activeDebt_Before = (await activePool.getBoldDebt()).toString(); + const activeDebt_Before = (await activePool.getRecordedDebtSum()).toString(); const defaultedDebt_Before = (await defaultPool.getBoldDebt()).toString(); const activeColl_Before = (await activePool.getETHBalance()).toString(); const defaultedColl_Before = (await defaultPool.getETHBalance()).toString(); @@ -729,7 +730,7 @@ contract("StabilityPool", async (accounts) => { dec(1000, 18) ); - const activeDebt_After = (await activePool.getBoldDebt()).toString(); + const activeDebt_After = (await activePool.getRecordedDebtSum()).toString(); const defaultedDebt_After = (await defaultPool.getBoldDebt()).toString(); const activeColl_After = (await activePool.getETHBalance()).toString(); const defaultedColl_After = (await defaultPool.getETHBalance()).toString(); @@ -2026,7 +2027,7 @@ contract("StabilityPool", async (accounts) => { // Price rises await priceFeed.setPrice(dec(200, 18)); - const activeDebt_Before = (await activePool.getBoldDebt()).toString(); + const activeDebt_Before = (await activePool.getRecordedDebtSum()).toString(); const defaultedDebt_Before = (await defaultPool.getBoldDebt()).toString(); const activeColl_Before = (await activePool.getETHBalance()).toString(); const defaultedColl_Before = (await defaultPool.getETHBalance()).toString(); @@ -2040,7 +2041,7 @@ contract("StabilityPool", async (accounts) => { await stabilityPool.withdrawFromSP(dec(30000, 18), { from: carol }); assert.equal((await stabilityPool.deposits(carol)).toString(), "0"); - const activeDebt_After = (await activePool.getBoldDebt()).toString(); + const activeDebt_After = (await activePool.getRecordedDebtSum()).toString(); const defaultedDebt_After = (await defaultPool.getBoldDebt()).toString(); const activeColl_After = (await activePool.getETHBalance()).toString(); const defaultedColl_After = (await defaultPool.getETHBalance()).toString(); diff --git a/contracts/test/TroveManagerTest.js b/contracts/test/TroveManagerTest.js index 127665e3..7dc5b225 100644 --- a/contracts/test/TroveManagerTest.js +++ b/contracts/test/TroveManagerTest.js @@ -79,7 +79,8 @@ contract("TroveManager", async (accounts) => { contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ) priceFeed = contracts.priceFeedTestnet; @@ -169,7 +170,7 @@ contract("TroveManager", async (accounts) => { await contracts.WETH.balanceOf(activePool.address) ).toString(); const activePool_BoldDebt_Before = ( - await activePool.getBoldDebt() + await activePool.getRecordedDebtSum() ).toString(); assert.equal(activePool_ETH_Before, A_collateral.add(B_collateral)); @@ -195,7 +196,7 @@ contract("TroveManager", async (accounts) => { await contracts.WETH.balanceOf(activePool.address) ).toString(); const activePool_BoldDebt_After = ( - await activePool.getBoldDebt() + await activePool.getRecordedDebtSum() ).toString(); assert.equal(activePool_ETH_After, A_collateral); @@ -218,7 +219,7 @@ contract("TroveManager", async (accounts) => { await contracts.WETH.balanceOf(defaultPool.address) ).toString(); const defaultPool_BoldDebt_Before = ( - await defaultPool.getBoldDebt() + await defaultPool.getRecordedDebtSum() ).toString(); assert.equal(defaultPool_ETH_Before, "0"); @@ -240,7 +241,7 @@ contract("TroveManager", async (accounts) => { await contracts.WETH.balanceOf(defaultPool.address) ).toString(); const defaultPool_BoldDebt_After = ( - await defaultPool.getBoldDebt() + await defaultPool.getRecordedDebtSum() ).toString(); const defaultPool_ETH = th.applyLiquidationFee(B_collateral); @@ -1134,16 +1135,16 @@ contract("TroveManager", async (accounts) => { assert.isFalse(await th.checkRecoveryMode(contracts)); // Liquidate A, B and C - const activeBoldDebt_0 = await activePool.getBoldDebt(); - const defaultBoldDebt_0 = await defaultPool.getBoldDebt(); + const activeBoldDebt_0 = await activePool.getRecordedDebtSum(); + const defaultBoldDebt_0 = await defaultPool.getRecordedDebtSum(); await troveManager.liquidate(aliceTroveId); - const activeBoldDebt_A = await activePool.getBoldDebt(); - const defaultBoldDebt_A = await defaultPool.getBoldDebt(); + const activeBoldDebt_A = await activePool.getRecordedDebtSum(); + const defaultBoldDebt_A = await defaultPool.getRecordedDebtSum(); await troveManager.liquidate(bobTroveId); - const activeBoldDebt_B = await activePool.getBoldDebt(); - const defaultBoldDebt_B = await defaultPool.getBoldDebt(); + const activeBoldDebt_B = await activePool.getRecordedDebtSum(); + const defaultBoldDebt_B = await defaultPool.getRecordedDebtSum(); await troveManager.liquidate(carolTroveId); @@ -1848,7 +1849,7 @@ contract("TroveManager", async (accounts) => { const pendingETH_C = await troveManager.getPendingETHReward(CTroveId); const pendingBoldDebt_C = await troveManager.getPendingBoldDebtReward(CTroveId); const defaultPoolETH = await defaultPool.getETHBalance(); - const defaultPoolBoldDebt = await defaultPool.getBoldDebt(); + const defaultPoolBoldDebt = await defaultPool.getRecordedDebtSum(); assert.isTrue(pendingETH_C.lte(defaultPoolETH)); assert.isTrue(pendingBoldDebt_C.lte(defaultPoolBoldDebt)); //Check only difference is dust @@ -3836,7 +3837,7 @@ contract("TroveManager", async (accounts) => { const totalColl = W_coll.add(A_coll).add(B_coll).add(C_coll).add(D_coll); // Get active debt and coll before redemption - const activePool_debt_before = await activePool.getBoldDebt(); + const activePool_debt_before = await activePool.getRecordedDebtSum(); const activePool_coll_before = await activePool.getETHBalance(); th.assertIsApproximatelyEqual(activePool_debt_before, totalDebt); @@ -3873,7 +3874,7 @@ contract("TroveManager", async (accounts) => { ); // Check activePool debt reduced by 400 Bold - const activePool_debt_after = await activePool.getBoldDebt(); + const activePool_debt_after = await activePool.getRecordedDebtSum(); assert.equal( activePool_debt_before.sub(activePool_debt_after), dec(400, 18) @@ -3936,7 +3937,7 @@ contract("TroveManager", async (accounts) => { const totalColl = W_coll.add(A_coll).add(B_coll).add(C_coll).add(D_coll); // Get active debt and coll before redemption - const activePool_debt_before = await activePool.getBoldDebt(); + const activePool_debt_before = await activePool.getRecordedDebtSum(); const activePool_coll_before = (await activePool.getETHBalance()).toString(); th.assertIsApproximatelyEqual(activePool_debt_before, totalDebt); @@ -4319,7 +4320,7 @@ contract("TroveManager", async (accounts) => { const totalDebt = C_totalDebt.add(D_totalDebt); th.assertIsApproximatelyEqual( - (await activePool.getBoldDebt()).toString(), + (await activePool.getRecordedDebtSum()).toString(), totalDebt ); @@ -4394,8 +4395,8 @@ contract("TroveManager", async (accounts) => { const A_balanceBefore = toBN(await contracts.WETH.balanceOf(A)); // Check total Bold supply - const activeBold = await activePool.getBoldDebt(); - const defaultBold = await defaultPool.getBoldDebt(); + const activeBold = await activePool.getRecordedDebtSum(); + const defaultBold = await defaultPool.getRecordedDebtSum(); const totalBoldSupply = activeBold.add(defaultBold); th.assertIsApproximatelyEqual(totalBoldSupply, totalDebt); diff --git a/contracts/test/TroveManager_LiquidationRewardsTest.js b/contracts/test/TroveManager_LiquidationRewardsTest.js index 5c5c620f..f0bd9766 100644 --- a/contracts/test/TroveManager_LiquidationRewardsTest.js +++ b/contracts/test/TroveManager_LiquidationRewardsTest.js @@ -67,7 +67,8 @@ contract( contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ); priceFeed = contracts.priceFeedTestnet; @@ -1539,7 +1540,7 @@ contract( .sub(withdrawnColl) .add(th.applyLiquidationFee(D_coll)) ); - const entireSystemDebt = (await activePool.getBoldDebt()).add( + const entireSystemDebt = (await activePool.getRecordedDebtSum()).add( await defaultPool.getBoldDebt() ); th.assertIsApproximatelyEqual( diff --git a/contracts/test/TroveManager_RecoveryModeTest.js b/contracts/test/TroveManager_RecoveryModeTest.js index 47cec8f6..ab4e5be2 100644 --- a/contracts/test/TroveManager_RecoveryModeTest.js +++ b/contracts/test/TroveManager_RecoveryModeTest.js @@ -81,7 +81,8 @@ contract.skip("TroveManager - in Recovery Mode", async (accounts) => { contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ); priceFeed = contracts.priceFeedTestnet; diff --git a/contracts/test/TroveManager_RecoveryMode_Batch_Liqudation_Test.js b/contracts/test/TroveManager_RecoveryMode_Batch_Liqudation_Test.js index cb6a6ee8..eb631593 100644 --- a/contracts/test/TroveManager_RecoveryMode_Batch_Liqudation_Test.js +++ b/contracts/test/TroveManager_RecoveryMode_Batch_Liqudation_Test.js @@ -54,7 +54,8 @@ contract.skip( contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ); troveManager = contracts.troveManager; diff --git a/contracts/test/stakeDeclineTest.js b/contracts/test/stakeDeclineTest.js index 16d05c78..c2cb84de 100644 --- a/contracts/test/stakeDeclineTest.js +++ b/contracts/test/stakeDeclineTest.js @@ -54,8 +54,9 @@ contract("TroveManager", async (accounts) => { contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address - ); + contracts.borrowerOperations.address, + contracts.activePool.address + ) priceFeed = contracts.priceFeedTestnet; boldToken = contracts.boldToken; diff --git a/contracts/utils/deploymentHelpers.js b/contracts/utils/deploymentHelpers.js index 0520c909..6ee7e8cf 100644 --- a/contracts/utils/deploymentHelpers.js +++ b/contracts/utils/deploymentHelpers.js @@ -45,7 +45,8 @@ class DeploymentHelper { const boldToken = await BoldToken.new( troveManager.address, stabilityPool.address, - borrowerOperations.address + borrowerOperations.address, + activePool.address ); const mockInterestRouter = await MockInterestRouter.new(); @@ -97,7 +98,8 @@ class DeploymentHelper { contracts.boldToken = await BoldToken.new( contracts.troveManager.address, contracts.stabilityPool.address, - contracts.borrowerOperations.address + contracts.borrowerOperations.address, + contracts.activePool.address ); return contracts; } @@ -114,8 +116,7 @@ class DeploymentHelper { contracts.collSurplusPool.address, contracts.priceFeedTestnet.address, contracts.boldToken.address, - contracts.sortedTroves.address, - contracts.mockInterestRouter.address + contracts.sortedTroves.address ); await contracts.stabilityPool.setAddresses( @@ -152,6 +153,8 @@ class DeploymentHelper { contracts.troveManager.address, contracts.stabilityPool.address, contracts.defaultPool.address, + contracts.boldToken.address, + contracts.mockInterestRouter.address //contracts.stETH.address, ); From a20b8b1d75f2381a8b6e2d73cc164ebba971054b Mon Sep 17 00:00:00 2001 From: RickGriff Date: Fri, 15 Mar 2024 22:02:55 +0700 Subject: [PATCH 12/18] Apply agg interest when liquidating (redist) in NM --- .../src/test/TestContracts/DevTestSetup.sol | 26 ++- .../src/test/interestRateAggregate.t.sol | 214 +++++++++++++++++- 2 files changed, 230 insertions(+), 10 deletions(-) diff --git a/contracts/src/test/TestContracts/DevTestSetup.sol b/contracts/src/test/TestContracts/DevTestSetup.sol index f94581ac..9dc80135 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -201,9 +201,29 @@ contract DevTestSetup is BaseTest { price = 1050e18; priceFeed.setPrice(price); - // console.log(troveManager.getTCR(price), "TCR before liq"); - // console.log(troveManager.getCurrentICR(C, price), "C CR before liq"); - // console.log(troveManager.getCurrentICR(D, price), "D CR before liq"); + assertFalse(troveManager.checkRecoveryMode(price)); + assertLt(troveManager.getCurrentICR(C, price), troveManager.MCR()); + assertLt(troveManager.getCurrentICR(D, price), troveManager.MCR()); + } + + function _setupForBatchLiquidateTrovesPureRedist() internal { + uint256 troveDebtRequest_A = 2000e18; + uint256 troveDebtRequest_B = 3000e18; + uint256 troveDebtRequest_C = 2250e18; + uint256 troveDebtRequest_D = 2250e18; + uint256 interestRate = 5e16; // 5% + + uint256 price = 2000e18; + priceFeed.setPrice(price); + + openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, interestRate); + openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, interestRate); + openTroveNoHints100pctMaxFee(C, 25e17, troveDebtRequest_C, interestRate); + openTroveNoHints100pctMaxFee(D, 25e17, troveDebtRequest_D, interestRate); + + // Price drops, C and D become liquidateable + price = 1050e18; + priceFeed.setPrice(price); assertFalse(troveManager.checkRecoveryMode(price)); assertLt(troveManager.getCurrentICR(C, price), troveManager.MCR()); diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index 77ee586a..b6eb0256 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -1878,7 +1878,7 @@ contract InterestRateAggregate is DevTestSetup { // TODO: Basic TCR and ICR getter tests // TODO: Test total debt invariant holds i.e. (D + S * delta_T) == sum_of_all_entire_trove_debts. - // --- batchLiquidateTroves (Normal Mode) --- + // --- batchLiquidateTroves (Normal Mode, offset) --- function testBatchLiquidateTrovesPureOffsetChangesAggRecordedInterestCorrectly() public { _setupForBatchLiquidateTrovesPureOffset(); @@ -1917,7 +1917,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - recordedDebtInLiq - accruedInterestInLiq); } - function testBatchLiquidateTrovesReducesAggPendingInterestTo0() public { + function testBatchLiquidateTrovesPureOffsetReducesAggPendingInterestTo0() public { _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues @@ -1934,9 +1934,8 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.calcPendingAggInterest(), 0); } - // Mints interest to Router - function testBatchLiquidateTrovesMintsAggInterestToRouter() public { + function testBatchLiquidateTrovesPureOffsetMintsAggInterestToRouter() public { _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues @@ -1959,7 +1958,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(boldBalRouter_2, pendingAggInterest); } - function testBatchLiquidateTrovesUpdatesLastAggInterestUpdateTimeToNow() public { + function testBatchLiquidateTrovesPureOffsetUpdatesLastAggInterestUpdateTimeToNow() public { _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues @@ -1980,7 +1979,7 @@ contract InterestRateAggregate is DevTestSetup { } // Removes liq'd troves' weighted recorded debts from the weighted recorded debt sum - function testBatchLiquidateTrovesRemovesLiquidatedTrovesWeightedRecordedDebtsFromWeightedRecordedDebtSum() public { + function testBatchLiquidateTrovesPureOffsetRemovesLiquidatedTrovesWeightedRecordedDebtsFromWeightedRecordedDebtSum() public { _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues @@ -2011,7 +2010,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - (weightedTroveDebt_C + weightedTroveDebt_D)); } - function testBatchLiquidateTrovesWithNoRedistGainRemovesLiquidatedTrovesRecordedDebtsFromRecordedDebtSum() public { + function testBatchLiquidateTrovesPureOffsetWithNoRedistGainRemovesLiquidatedTrovesRecordedDebtsFromRecordedDebtSum() public { _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues @@ -2035,4 +2034,205 @@ contract InterestRateAggregate is DevTestSetup { // Check recorded debt sum reduced by C and D's recorded debt assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 - (recordedTroveDebt_C + recordedTroveDebt_D)); } + + // --- // --- batchLiquidateTroves (Normal Mode, redistribution) --- + + function testBatchLiquidateTrovesPureRedistChangesAggRecordedInterestCorrectly() public { + _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + uint256 recordedDebt_C = troveManager.getTroveDebt(C); + uint256 recordedDebt_D = troveManager.getTroveDebt(D); + assertGt(recordedDebt_C, 0); + assertGt(recordedDebt_D, 0); + uint256 recordedDebtInLiq = recordedDebt_C + recordedDebt_D; + + uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(C); + uint256 accruedInterest_D = troveManager.calcTroveAccruedInterest(D); + assertGt(accruedInterest_C, 0); + assertGt(accruedInterest_D, 0); + uint256 accruedInterestInLiq = accruedInterest_C + accruedInterest_D; + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(A)); + + // Check both Troves were closed by liquidation + assertEq(troveManager.getTroveStatus(C), 3); + assertEq(troveManager.getTroveStatus(D), 3); + + // // changes agg. recorded debt by: agg_accrued_interest - liq'd_troves_recorded_trove_debts - liq'd_troves_accrued_interest + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - recordedDebtInLiq - accruedInterestInLiq); + } + + function testBatchLiquidateTrovesPureRedistReducesAggPendingInterestTo0() public { + _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(A)); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + // Mints interest to Router + function testBatchLiquidateTrovesPureRedistMintsAggInterestToRouter() public { + _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_1, 0); + + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(A)); + + // Check I-router Bold bal has increased as expected from liquidation + uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_2, pendingAggInterest); + } + + function testBatchLiquidateTrovesPureRedistUpdatesLastAggInterestUpdateTimeToNow() public { + _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(A)); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + + } + + // Removes liq'd troves' weighted recorded debts from the weighted recorded debt sum + function testBatchLiquidateTrovesPureRedistRemovesLiquidatedTrovesWeightedRecordedDebtsFromWeightedRecordedDebtSum() public { + _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(C); + uint256 annualInterestRate_C = troveManager.getTroveAnnualInterestRate(C); + assertGt(recordedTroveDebt_C, 0); + assertGt(annualInterestRate_C, 0); + uint256 weightedTroveDebt_C = recordedTroveDebt_C * annualInterestRate_C; + + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(D); + uint256 annualInterestRate_D = troveManager.getTroveAnnualInterestRate(D); + assertGt(recordedTroveDebt_D, 0); + assertGt(annualInterestRate_D, 0); + uint256 weightedTroveDebt_D = recordedTroveDebt_D * annualInterestRate_D; + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(A)); + + // Check weighted recorded debt sum reduced by C and D's weighted recorded debt + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - (weightedTroveDebt_C + weightedTroveDebt_D)); + } + + function testBatchLiquidateTrovesPureRedistWithNoRedistGainRemovesLiquidatedTrovesRecordedDebtsFromRecordedDebtSum() public { + _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(C); + assertGt(recordedTroveDebt_C, 0); + + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(D); + assertGt(recordedTroveDebt_D, 0); + + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(A)); + + // Check recorded debt sum reduced by C and D's recorded debt + assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 - (recordedTroveDebt_C + recordedTroveDebt_D)); + } + + function testBatchLiquidateTrovesPureRedistWithNoRedistGainAddsLiquidatedTrovesEntireDebtsToDefaultPoolDebtSum() public { + _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(C); + uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(C); + assertGt(recordedTroveDebt_C, 0); + assertGt(accruedInterest_C, 0); + + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(D); + uint256 accruedInterest_D = troveManager.calcTroveAccruedInterest(C); + assertGt(recordedTroveDebt_D, 0); + assertGt(accruedInterest_D, 0); + + uint256 debtInLiq = recordedTroveDebt_C + accruedInterest_C + recordedTroveDebt_D + accruedInterest_D; + + uint256 defaultPoolDebt = defaultPool.getBoldDebt(); + assertEq(defaultPoolDebt, 0); + + // A liquidates C and D + address[] memory trovesToLiq = new address[](2); + trovesToLiq[0] = C; + trovesToLiq[1] = D; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(A)); + + // Check recorded debt sum reduced by C and D's entire debts + assertEq(defaultPool.getBoldDebt(), debtInLiq); + } } From 10c0deab1530ad2bf58b712fcd5724a041af4327 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Mon, 25 Mar 2024 17:10:08 +0700 Subject: [PATCH 13/18] Apply interest in RM and fix hardhat tests --- contracts/src/TroveManager.sol | 17 +- contracts/test/AccessControlTest.js | 4 +- contracts/test/BorrowerOperationsTest.js | 455 ++++++++++-------- contracts/test/OwnershipTest.js | 4 +- contracts/test/PoolsTest.js | 8 +- contracts/test/TroveManagerTest.js | 2 + .../TroveManager_LiquidationRewardsTest.js | 35 +- .../test/TroveManager_RecoveryModeTest.js | 57 ++- contracts/utils/testHelpers.js | 8 +- 9 files changed, 314 insertions(+), 276 deletions(-) diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index a9ba0eed..01e4345f 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -389,6 +389,9 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana singleLiquidation.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_borrower); + //TODO - GAS: We already read this inside getEntireDebtAndColl - so add it to the returned vals? + singleLiquidation.recordedTroveDebt = Troves[_borrower].debt; + singleLiquidation.collGasCompensation = _getCollGasCompensation(singleLiquidation.entireTroveColl); singleLiquidation.BoldGasCompensation = BOLD_GAS_COMPENSATION; vars.collToLiquidate = singleLiquidation.entireTroveColl - singleLiquidation.collGasCompensation; @@ -431,7 +434,13 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana assert(_boldInStabPool != 0); _removeStake(_troveId); - singleLiquidation = _getCappedOffsetVals(singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, _price); + singleLiquidation = _getCappedOffsetVals( + singleLiquidation.entireTroveDebt, + singleLiquidation.entireTroveColl, + singleLiquidation.recordedTroveDebt, + singleLiquidation.weightedRecordedTroveDebt, + _price + ); _closeTrove(_troveId, Status.closedByLiquidation); if (singleLiquidation.collSurplus > 0) { @@ -445,7 +454,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana LiquidationValues memory zeroVals; return zeroVals; } - return singleLiquidation; } @@ -492,6 +500,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana ( uint _entireTroveDebt, uint _entireTroveColl, + uint256 _recordedTroveDebt, + uint256 _weightedRecordedTroveDebt, uint _price ) internal @@ -500,6 +510,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana { singleLiquidation.entireTroveDebt = _entireTroveDebt; singleLiquidation.entireTroveColl = _entireTroveColl; + singleLiquidation.recordedTroveDebt = _recordedTroveDebt; + singleLiquidation.weightedRecordedTroveDebt = _weightedRecordedTroveDebt; uint cappedCollPortion = _entireTroveDebt * MCR / _price; singleLiquidation.collGasCompensation = _getCollGasCompensation(cappedCollPortion); @@ -808,7 +820,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint _price ) internal returns (SingleRedemptionValues memory singleRedemption) - { // 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); diff --git a/contracts/test/AccessControlTest.js b/contracts/test/AccessControlTest.js index be843e51..1f569904 100644 --- a/contracts/test/AccessControlTest.js +++ b/contracts/test/AccessControlTest.js @@ -231,7 +231,7 @@ contract( it("increaseBoldDebt(): reverts when called by an account that is not BO nor TroveM", async () => { // Attempt call from alice try { - const txAlice = await activePool.increaseBoldDebt(100, { + const txAlice = await activePool.increaseRecordedDebtSum(100, { from: alice, }); } catch (err) { @@ -247,7 +247,7 @@ contract( it("decreaseBoldDebt(): reverts when called by an account that is not BO nor TroveM nor SP", async () => { // Attempt call from alice try { - const txAlice = await activePool.decreaseBoldDebt(100, { + const txAlice = await activePool.decreaseRecordedDebtSum(100, { from: alice, }); } catch (err) { diff --git a/contracts/test/BorrowerOperationsTest.js b/contracts/test/BorrowerOperationsTest.js index 758ea470..f0c1dd49 100644 --- a/contracts/test/BorrowerOperationsTest.js +++ b/contracts/test/BorrowerOperationsTest.js @@ -2303,6 +2303,9 @@ contract("BorrowerOperations", async (accounts) => { true, { from: alice } ); + + const debtAfter = await getTroveEntireDebt(alice); + const collAfter = await getTroveEntireColl(alice); const debtAfter = await getTroveEntireDebt(aliceTroveId); const collAfter = await getTroveEntireColl(aliceTroveId); @@ -3078,7 +3081,7 @@ contract("BorrowerOperations", async (accounts) => { from: dennis, }); - await priceFeed.setPrice(dec(200, 18)); + await priceFeed.setPrice(dec(2000, 18)); // Alice closes trove await borrowerOperations.closeTrove(aliceTroveId, { from: alice }); @@ -4375,31 +4378,37 @@ contract("BorrowerOperations", async (accounts) => { describe("getNewTCRFromTroveChange() returns the correct TCR", async () => { // 0, 0 - it.only("collChange = 0, debtChange = 0", async () => { + it("collChange = 0, debtChange = 0", async () => { // --- SETUP --- Create a Liquity instance with an Active Pool and pending rewards (Default Pool) - const troveColl = toBN(dec(1000, "ether")); - const troveTotalDebt = toBN(dec(100000, 18)); - const troveBoldAmount = await getOpenTroveBoldAmount(troveTotalDebt); + const bobColl = toBN(dec(1000, "ether")); + const whaleColl = toBN(dec(10000, "ether")); + const bobTotalDebt = toBN(dec(100000, 18)); + const whaleTotalDebt = toBN(dec(2000, 18)); + const bobBoldAmount = await getOpenTroveBoldAmount(bobTotalDebt); + const whaleBoldAmount = await getOpenTroveBoldAmount(whaleTotalDebt); + await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, - alice, - alice, + whaleBoldAmount, + whale, + whale, 0, - { from: alice, value: troveColl } + { from: whale, value: whaleColl } ); const bobTroveId = await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, + bobBoldAmount, bob, bob, 0, - { from: bob, value: troveColl } + { from: bob, value: bobColl } ); - await priceFeed.setPrice(dec(100, 18)); - assert.isTrue(troveManager.checkRecoveryMode() - //TODO: make test liq in normal mode + const liqPrice = th.toBN(dec(100,18)) + th.logBN("Bob ICR before liq", await troveManager.getCurrentICR(bob, liqPrice)) + await priceFeed.setPrice(liqPrice); + // Confirm we are in Normal Mode + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) const liquidationTx = await troveManager.liquidate(bobTroveId); assert.isFalse(await sortedTroves.contains(bobTroveId)); @@ -4407,18 +4416,6 @@ contract("BorrowerOperations", async (accounts) => { const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx); - const price = await priceFeed.getPrice(); - - const actualTroveEntireDebt = await troveManager.getTroveEntireDebt(alice); - const actualTroveRecordedDebt = await troveManager.getTroveDebt(alice); - const actualTroveEntireColl = await troveManager.getTroveEntireColl(alice); - - const pendingTroveInterest = await troveManager.calcTroveAccruedInterest(alice); - - console.log(th.logBN("troveTotalDebt:", troveTotalDebt)); - console.log(th.logBN("pendingTroveInterest:", pendingTroveInterest)); - console.log(th.logBN("actualTroveRecordedDebt:", actualTroveRecordedDebt)); - // --- TEST --- const collChange = 0; const debtChange = 0; @@ -4427,20 +4424,13 @@ contract("BorrowerOperations", async (accounts) => { true, debtChange, true, - price + liqPrice ); - - th.logBN("newTCR:", newTCR); - const expectedTCR = troveColl + const expectedTCR = whaleColl .add(liquidatedColl) - .mul(price) - .div(troveTotalDebt.add(liquidatedDebt)); - - th.logBN("expectedTCR:", expectedTCR); - th.logBN("Alice ICR:", await troveManager.getCurrentICR(alice, price)); - th.logBN("actualTroveEntireColl:", actualTroveEntireColl); - th.logBN("actualTroveEntireDebt:", actualTroveEntireDebt); + .mul(liqPrice) + .div(whaleTotalDebt.add(liquidatedDebt)); assert.isTrue(newTCR.eq(expectedTCR)); }); @@ -4448,27 +4438,36 @@ contract("BorrowerOperations", async (accounts) => { // 0, +ve it("collChange = 0, debtChange is positive", async () => { // --- SETUP --- Create a Liquity instance with an Active Pool and pending rewards (Default Pool) - const troveColl = toBN(dec(1000, "ether")); - const troveTotalDebt = toBN(dec(100000, 18)); - const troveBoldAmount = await getOpenTroveBoldAmount(troveTotalDebt); + const bobColl = toBN(dec(1000, "ether")); + const whaleColl = toBN(dec(10000, "ether")); + const bobTotalDebt = toBN(dec(100000, 18)); + const whaleTotalDebt = toBN(dec(2000, 18)); + const bobBoldAmount = await getOpenTroveBoldAmount(bobTotalDebt); + const whaleBoldAmount = await getOpenTroveBoldAmount(whaleTotalDebt); + await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, - alice, - alice, + whaleBoldAmount, + whale, + whale, 0, - { from: alice, value: troveColl } + { from: whale, value: whaleColl } ); + const bobTroveId = await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, + bobBoldAmount, bob, bob, 0, - { from: bob, value: troveColl } + { from: bob, value: bobColl } ); - await priceFeed.setPrice(dec(100, 18)); + const liqPrice = th.toBN(dec(100,18)) + th.logBN("Bob ICR before liq", await troveManager.getCurrentICR(bob, liqPrice)) + await priceFeed.setPrice(liqPrice); + // Confirm we are in Normal Mode + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) const liquidationTx = await troveManager.liquidate(bobTroveId); assert.isFalse(await sortedTroves.contains(bobTroveId)); @@ -4476,52 +4475,58 @@ contract("BorrowerOperations", async (accounts) => { const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx); - await priceFeed.setPrice(dec(200, 18)); - const price = await priceFeed.getPrice(); - // --- TEST --- - const collChange = 0; - const debtChange = dec(200, 18); + const collChange = th.toBN(0); + const debtChange = th.toBN(dec(100, 18)) const newTCR = await borrowerOperations.getNewTCRFromTroveChange( collChange, true, debtChange, true, - price + liqPrice ); - - const expectedTCR = troveColl + + const expectedTCR = whaleColl .add(liquidatedColl) - .mul(price) - .div(troveTotalDebt.add(liquidatedDebt).add(toBN(debtChange))); + .mul(liqPrice) + .div(whaleTotalDebt.add(liquidatedDebt).add(debtChange)); assert.isTrue(newTCR.eq(expectedTCR)); }); - // 0, -ve + // 0, -ve it("collChange = 0, debtChange is negative", async () => { // --- SETUP --- Create a Liquity instance with an Active Pool and pending rewards (Default Pool) - const troveColl = toBN(dec(1000, "ether")); - const troveTotalDebt = toBN(dec(100000, 18)); - const troveBoldAmount = await getOpenTroveBoldAmount(troveTotalDebt); + const bobColl = toBN(dec(1000, "ether")); + const whaleColl = toBN(dec(10000, "ether")); + const bobTotalDebt = toBN(dec(100000, 18)); + const whaleTotalDebt = toBN(dec(2000, 18)); + const bobBoldAmount = await getOpenTroveBoldAmount(bobTotalDebt); + const whaleBoldAmount = await getOpenTroveBoldAmount(whaleTotalDebt); + await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, - alice, - alice, + whaleBoldAmount, + whale, + whale, 0, - { from: alice, value: troveColl } + { from: whale, value: whaleColl } ); + const bobTroveId = await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, + bobBoldAmount, bob, bob, 0, - { from: bob, value: troveColl } + { from: bob, value: bobColl } ); - await priceFeed.setPrice(dec(100, 18)); + const liqPrice = th.toBN(dec(100,18)) + th.logBN("Bob ICR before liq", await troveManager.getCurrentICR(bob, liqPrice)) + await priceFeed.setPrice(liqPrice); + // Confirm we are in Normal Mode + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) const liquidationTx = await troveManager.liquidate(bobTroveId); assert.isFalse(await sortedTroves.contains(bobTroveId)); @@ -4529,51 +4534,57 @@ contract("BorrowerOperations", async (accounts) => { const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx); - await priceFeed.setPrice(dec(200, 18)); - const price = await priceFeed.getPrice(); // --- TEST --- - const collChange = 0; - const debtChange = dec(100, 18); + const collChange = th.toBN(0); + const debtChange = th.toBN(dec(100, 18)) const newTCR = await borrowerOperations.getNewTCRFromTroveChange( collChange, true, debtChange, false, - price + liqPrice ); - - const expectedTCR = troveColl + + const expectedTCR = whaleColl .add(liquidatedColl) - .mul(price) - .div(troveTotalDebt.add(liquidatedDebt).sub(toBN(dec(100, 18)))); + .mul(liqPrice) + .div(whaleTotalDebt.add(liquidatedDebt).sub(debtChange)); assert.isTrue(newTCR.eq(expectedTCR)); }); // +ve, 0 - it("collChange is positive, debtChange is 0", async () => { + it("collChange is positive, debtChange = 0", async () => { // --- SETUP --- Create a Liquity instance with an Active Pool and pending rewards (Default Pool) - const troveColl = toBN(dec(1000, "ether")); - const troveTotalDebt = toBN(dec(100000, 18)); - const troveBoldAmount = await getOpenTroveBoldAmount(troveTotalDebt); + const bobColl = toBN(dec(1000, "ether")); + const whaleColl = toBN(dec(10000, "ether")); + const bobTotalDebt = toBN(dec(100000, 18)); + const whaleTotalDebt = toBN(dec(2000, 18)); + const bobBoldAmount = await getOpenTroveBoldAmount(bobTotalDebt); + const whaleBoldAmount = await getOpenTroveBoldAmount(whaleTotalDebt); + await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, - alice, - alice, + whaleBoldAmount, + whale, + whale, 0, - { from: alice, value: troveColl } + { from: whale, value: whaleColl } ); const bobTroveId = await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, + bobBoldAmount, bob, bob, 0, - { from: bob, value: troveColl } + { from: bob, value: bobColl } ); - await priceFeed.setPrice(dec(100, 18)); + const liqPrice = th.toBN(dec(100,18)) + th.logBN("Bob ICR before liq", await troveManager.getCurrentICR(bob, liqPrice)) + await priceFeed.setPrice(liqPrice); + // Confirm we are in Normal Mode + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) const liquidationTx = await troveManager.liquidate(bobTroveId); assert.isFalse(await sortedTroves.contains(bobTroveId)); @@ -4581,52 +4592,58 @@ contract("BorrowerOperations", async (accounts) => { const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx); - await priceFeed.setPrice(dec(200, 18)); - const price = await priceFeed.getPrice(); // --- TEST --- - const collChange = dec(2, "ether"); - const debtChange = 0; + const collChange = th.toBN(dec(100, 18)); + const debtChange = th.toBN(0); const newTCR = await borrowerOperations.getNewTCRFromTroveChange( collChange, true, debtChange, true, - price + liqPrice ); - - const expectedTCR = troveColl - .add(liquidatedColl) - .add(toBN(collChange)) - .mul(price) - .div(troveTotalDebt.add(liquidatedDebt)); + + const expectedTCR = whaleColl + .add(liquidatedColl).add(collChange) + .mul(liqPrice) + .div(whaleTotalDebt.add(liquidatedDebt)); assert.isTrue(newTCR.eq(expectedTCR)); }); // -ve, 0 - it("collChange is negative, debtChange is 0", async () => { + it("collChange is negative, debtChange = 0", async () => { // --- SETUP --- Create a Liquity instance with an Active Pool and pending rewards (Default Pool) - const troveColl = toBN(dec(1000, "ether")); - const troveTotalDebt = toBN(dec(100000, 18)); - const troveBoldAmount = await getOpenTroveBoldAmount(troveTotalDebt); + const bobColl = toBN(dec(1000, "ether")); + const whaleColl = toBN(dec(10000, "ether")); + const bobTotalDebt = toBN(dec(100000, 18)); + const whaleTotalDebt = toBN(dec(2000, 18)); + const bobBoldAmount = await getOpenTroveBoldAmount(bobTotalDebt); + const whaleBoldAmount = await getOpenTroveBoldAmount(whaleTotalDebt); + await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, - alice, - alice, + whaleBoldAmount, + whale, + whale, 0, - { from: alice, value: troveColl } + { from: whale, value: whaleColl } ); + const bobTroveId = await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, + bobBoldAmount, bob, bob, 0, - { from: bob, value: troveColl } + { from: bob, value: bobColl } ); - await priceFeed.setPrice(dec(100, 18)); + const liqPrice = th.toBN(dec(100,18)) + th.logBN("Bob ICR before liq", await troveManager.getCurrentICR(bob, liqPrice)) + await priceFeed.setPrice(liqPrice); + // Confirm we are in Normal Mode + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) const liquidationTx = await troveManager.liquidate(bobTroveId); assert.isFalse(await sortedTroves.contains(bobTroveId)); @@ -4634,25 +4651,21 @@ contract("BorrowerOperations", async (accounts) => { const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx); - await priceFeed.setPrice(dec(200, 18)); - const price = await priceFeed.getPrice(); - // --- TEST --- - const collChange = dec(1, 18); - const debtChange = 0; + const collChange = th.toBN(dec(100, 18)); + const debtChange = th.toBN(0); const newTCR = await borrowerOperations.getNewTCRFromTroveChange( collChange, false, debtChange, true, - price + liqPrice ); - - const expectedTCR = troveColl - .add(liquidatedColl) - .sub(toBN(dec(1, "ether"))) - .mul(price) - .div(troveTotalDebt.add(liquidatedDebt)); + + const expectedTCR = whaleColl + .add(liquidatedColl).sub(collChange) + .mul(liqPrice) + .div(whaleTotalDebt.add(liquidatedDebt)); assert.isTrue(newTCR.eq(expectedTCR)); }); @@ -4660,27 +4673,36 @@ contract("BorrowerOperations", async (accounts) => { // -ve, -ve it("collChange is negative, debtChange is negative", async () => { // --- SETUP --- Create a Liquity instance with an Active Pool and pending rewards (Default Pool) - const troveColl = toBN(dec(1000, "ether")); - const troveTotalDebt = toBN(dec(100000, 18)); - const troveBoldAmount = await getOpenTroveBoldAmount(troveTotalDebt); + const bobColl = toBN(dec(1000, "ether")); + const whaleColl = toBN(dec(10000, "ether")); + const bobTotalDebt = toBN(dec(100000, 18)); + const whaleTotalDebt = toBN(dec(2000, 18)); + const bobBoldAmount = await getOpenTroveBoldAmount(bobTotalDebt); + const whaleBoldAmount = await getOpenTroveBoldAmount(whaleTotalDebt); + await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, - alice, - alice, + whaleBoldAmount, + whale, + whale, 0, - { from: alice, value: troveColl } + { from: whale, value: whaleColl } ); + const bobTroveId = await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, + bobBoldAmount, bob, bob, 0, - { from: bob, value: troveColl } + { from: bob, value: bobColl } ); - await priceFeed.setPrice(dec(100, 18)); + const liqPrice = th.toBN(dec(100,18)) + th.logBN("Bob ICR before liq", await troveManager.getCurrentICR(bob, liqPrice)) + await priceFeed.setPrice(liqPrice); + // Confirm we are in Normal Mode + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) const liquidationTx = await troveManager.liquidate(bobTroveId); assert.isFalse(await sortedTroves.contains(bobTroveId)); @@ -4688,25 +4710,21 @@ contract("BorrowerOperations", async (accounts) => { const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx); - await priceFeed.setPrice(dec(200, 18)); - const price = await priceFeed.getPrice(); - // --- TEST --- - const collChange = dec(1, 18); - const debtChange = dec(100, 18); + const collChange = th.toBN(dec(100, 18)); + const debtChange = th.toBN(dec(100, 18)); const newTCR = await borrowerOperations.getNewTCRFromTroveChange( collChange, false, debtChange, false, - price + liqPrice ); - - const expectedTCR = troveColl - .add(liquidatedColl) - .sub(toBN(dec(1, "ether"))) - .mul(price) - .div(troveTotalDebt.add(liquidatedDebt).sub(toBN(dec(100, 18)))); + + const expectedTCR = whaleColl + .add(liquidatedColl).sub(collChange) + .mul(liqPrice) + .div(whaleTotalDebt.add(liquidatedDebt).sub(debtChange)); assert.isTrue(newTCR.eq(expectedTCR)); }); @@ -4714,27 +4732,36 @@ contract("BorrowerOperations", async (accounts) => { // +ve, +ve it("collChange is positive, debtChange is positive", async () => { // --- SETUP --- Create a Liquity instance with an Active Pool and pending rewards (Default Pool) - const troveColl = toBN(dec(1000, "ether")); - const troveTotalDebt = toBN(dec(100000, 18)); - const troveBoldAmount = await getOpenTroveBoldAmount(troveTotalDebt); + const bobColl = toBN(dec(1000, "ether")); + const whaleColl = toBN(dec(10000, "ether")); + const bobTotalDebt = toBN(dec(100000, 18)); + const whaleTotalDebt = toBN(dec(2000, 18)); + const bobBoldAmount = await getOpenTroveBoldAmount(bobTotalDebt); + const whaleBoldAmount = await getOpenTroveBoldAmount(whaleTotalDebt); + await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, - alice, - alice, + whaleBoldAmount, + whale, + whale, 0, - { from: alice, value: troveColl } + { from: whale, value: whaleColl } ); + const bobTroveId = await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, + bobBoldAmount, bob, bob, 0, - { from: bob, value: troveColl } + { from: bob, value: bobColl } ); - await priceFeed.setPrice(dec(100, 18)); + const liqPrice = th.toBN(dec(100,18)) + th.logBN("Bob ICR before liq", await troveManager.getCurrentICR(bob, liqPrice)) + await priceFeed.setPrice(liqPrice); + // Confirm we are in Normal Mode + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) const liquidationTx = await troveManager.liquidate(bobTroveId); assert.isFalse(await sortedTroves.contains(bobTroveId)); @@ -4742,25 +4769,21 @@ contract("BorrowerOperations", async (accounts) => { const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx); - await priceFeed.setPrice(dec(200, 18)); - const price = await priceFeed.getPrice(); - // --- TEST --- - const collChange = dec(1, "ether"); - const debtChange = dec(100, 18); + const collChange = th.toBN(dec(100, 18)); + const debtChange = th.toBN(dec(100, 18)); const newTCR = await borrowerOperations.getNewTCRFromTroveChange( collChange, true, debtChange, true, - price + liqPrice ); - - const expectedTCR = troveColl - .add(liquidatedColl) - .add(toBN(dec(1, "ether"))) - .mul(price) - .div(troveTotalDebt.add(liquidatedDebt).add(toBN(dec(100, 18)))); + + const expectedTCR = whaleColl + .add(liquidatedColl).add(collChange) + .mul(liqPrice) + .div(whaleTotalDebt.add(liquidatedDebt).add(debtChange)); assert.isTrue(newTCR.eq(expectedTCR)); }); @@ -4768,27 +4791,36 @@ contract("BorrowerOperations", async (accounts) => { // +ve, -ve it("collChange is positive, debtChange is negative", async () => { // --- SETUP --- Create a Liquity instance with an Active Pool and pending rewards (Default Pool) - const troveColl = toBN(dec(1000, "ether")); - const troveTotalDebt = toBN(dec(100000, 18)); - const troveBoldAmount = await getOpenTroveBoldAmount(troveTotalDebt); + const bobColl = toBN(dec(1000, "ether")); + const whaleColl = toBN(dec(10000, "ether")); + const bobTotalDebt = toBN(dec(100000, 18)); + const whaleTotalDebt = toBN(dec(2000, 18)); + const bobBoldAmount = await getOpenTroveBoldAmount(bobTotalDebt); + const whaleBoldAmount = await getOpenTroveBoldAmount(whaleTotalDebt); + await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, - alice, - alice, + whaleBoldAmount, + whale, + whale, 0, - { from: alice, value: troveColl } + { from: whale, value: whaleColl } ); + const bobTroveId = await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, + bobBoldAmount, bob, bob, 0, - { from: bob, value: troveColl } + { from: bob, value: bobColl } ); - await priceFeed.setPrice(dec(100, 18)); + const liqPrice = th.toBN(dec(100,18)) + th.logBN("Bob ICR before liq", await troveManager.getCurrentICR(bob, liqPrice)) + await priceFeed.setPrice(liqPrice); + // Confirm we are in Normal Mode + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) const liquidationTx = await troveManager.liquidate(bobTroveId); assert.isFalse(await sortedTroves.contains(bobTroveId)); @@ -4796,53 +4828,58 @@ contract("BorrowerOperations", async (accounts) => { const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx); - await priceFeed.setPrice(dec(200, 18)); - const price = await priceFeed.getPrice(); - // --- TEST --- - const collChange = dec(1, "ether"); - const debtChange = dec(100, 18); + const collChange = th.toBN(dec(100, 18)); + const debtChange = th.toBN(dec(100, 18)); const newTCR = await borrowerOperations.getNewTCRFromTroveChange( collChange, true, debtChange, false, - price + liqPrice ); - - const expectedTCR = troveColl - .add(liquidatedColl) - .add(toBN(dec(1, "ether"))) - .mul(price) - .div(troveTotalDebt.add(liquidatedDebt).sub(toBN(dec(100, 18)))); + + const expectedTCR = whaleColl + .add(liquidatedColl).add(collChange) + .mul(liqPrice) + .div(whaleTotalDebt.add(liquidatedDebt).sub(debtChange)); assert.isTrue(newTCR.eq(expectedTCR)); }); - // -ve, +ve + // ive, +ve it("collChange is negative, debtChange is positive", async () => { // --- SETUP --- Create a Liquity instance with an Active Pool and pending rewards (Default Pool) - const troveColl = toBN(dec(1000, "ether")); - const troveTotalDebt = toBN(dec(100000, 18)); - const troveBoldAmount = await getOpenTroveBoldAmount(troveTotalDebt); + const bobColl = toBN(dec(1000, "ether")); + const whaleColl = toBN(dec(10000, "ether")); + const bobTotalDebt = toBN(dec(100000, 18)); + const whaleTotalDebt = toBN(dec(2000, 18)); + const bobBoldAmount = await getOpenTroveBoldAmount(bobTotalDebt); + const whaleBoldAmount = await getOpenTroveBoldAmount(whaleTotalDebt); + await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, - alice, - alice, + whaleBoldAmount, + whale, + whale, 0, - { from: alice, value: troveColl } + { from: whale, value: whaleColl } ); + const bobTroveId = await th.openTroveWrapper(contracts, th._100pct, - troveBoldAmount, + bobBoldAmount, bob, bob, 0, - { from: bob, value: troveColl } + { from: bob, value: bobColl } ); - await priceFeed.setPrice(dec(100, 18)); + const liqPrice = th.toBN(dec(100,18)) + th.logBN("Bob ICR before liq", await troveManager.getCurrentICR(bob, liqPrice)) + await priceFeed.setPrice(liqPrice); + // Confirm we are in Normal Mode + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) const liquidationTx = await troveManager.liquidate(bobTroveId); assert.isFalse(await sortedTroves.contains(bobTroveId)); @@ -4850,25 +4887,21 @@ contract("BorrowerOperations", async (accounts) => { const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx); - await priceFeed.setPrice(dec(200, 18)); - const price = await priceFeed.getPrice(); - // --- TEST --- - const collChange = dec(1, 18); - const debtChange = await getNetBorrowingAmount(dec(200, 18)); + const collChange = th.toBN(dec(100, 18)); + const debtChange = th.toBN(dec(100, 18)); const newTCR = await borrowerOperations.getNewTCRFromTroveChange( collChange, false, debtChange, true, - price + liqPrice ); - - const expectedTCR = troveColl - .add(liquidatedColl) - .sub(toBN(collChange)) - .mul(price) - .div(troveTotalDebt.add(liquidatedDebt).add(toBN(debtChange))); + + const expectedTCR = whaleColl + .add(liquidatedColl).sub(collChange) + .mul(liqPrice) + .div(whaleTotalDebt.add(liquidatedDebt).add(debtChange)); assert.isTrue(newTCR.eq(expectedTCR)); }); diff --git a/contracts/test/OwnershipTest.js b/contracts/test/OwnershipTest.js index d1687409..a5dadc52 100644 --- a/contracts/test/OwnershipTest.js +++ b/contracts/test/OwnershipTest.js @@ -68,7 +68,7 @@ contract('All Liquity functions with onlyOwner modifier', async accounts => { describe('TroveManager', async accounts => { it("setAddresses(): reverts when called by non-owner, with wrong addresses, or twice", async () => { - await testSetAddresses(troveManager, 10) + await testSetAddresses(troveManager, 9) }) }) @@ -92,7 +92,7 @@ contract('All Liquity functions with onlyOwner modifier', async accounts => { describe('ActivePool', async accounts => { it("setAddresses(): reverts when called by non-owner, with wrong addresses, or twice", async () => { - await testSetAddresses(activePool, 4) + await testSetAddresses(activePool, 6) }) }) diff --git a/contracts/test/PoolsTest.js b/contracts/test/PoolsTest.js index d61a9d2d..aedeccb0 100644 --- a/contracts/test/PoolsTest.js +++ b/contracts/test/PoolsTest.js @@ -62,12 +62,12 @@ contract('ActivePool', async accounts => { assert.equal(recordedETHBalance, 0) }) - it('increaseBoldDebt(): increases the recorded BOLD balance by the correct amount', async () => { + it('increaseRecordedDebtSum(): increases the recorded BOLD balance by the correct amount', async () => { const recordedBold_balanceBefore = await activePool.getRecordedDebtSum() assert.equal(recordedBold_balanceBefore, 0) // await activePool.increaseBoldDebt(100, { from: mockBorrowerOperationsAddress }) - const increaseBoldDebtData = th.getTransactionData('increaseBoldDebt(uint256)', ['0x64']) + const increaseBoldDebtData = th.getTransactionData('increaseRecordedDebtSum(uint256)', ['0x64']) const tx = await mockBorrowerOperations.forward(activePool.address, increaseBoldDebtData) assert.isTrue(tx.receipt.status) const recordedBold_balanceAfter = await activePool.getRecordedDebtSum() @@ -77,7 +77,7 @@ contract('ActivePool', async accounts => { it('decreaseBoldDebt(): 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('increaseBoldDebt(uint256)', ['0x64']) + const increaseBoldDebtData = th.getTransactionData('increaseRecordedDebtSum(uint256)', ['0x64']) const tx1 = await mockBorrowerOperations.forward(activePool.address, increaseBoldDebtData) assert.isTrue(tx1.receipt.status) @@ -85,7 +85,7 @@ contract('ActivePool', async accounts => { assert.equal(recordedBold_balanceBefore, 100) //await activePool.decreaseBoldDebt(100, { from: mockBorrowerOperationsAddress }) - const decreaseBoldDebtData = th.getTransactionData('decreaseBoldDebt(uint256)', ['0x64']) + const decreaseBoldDebtData = th.getTransactionData('decreaseRecordedDebtSum(uint256)', ['0x64']) const tx2 = await mockBorrowerOperations.forward(activePool.address, decreaseBoldDebtData) assert.isTrue(tx2.receipt.status) const recordedBold_balanceAfter = await activePool.getRecordedDebtSum() diff --git a/contracts/test/TroveManagerTest.js b/contracts/test/TroveManagerTest.js index 7dc5b225..d114b2a5 100644 --- a/contracts/test/TroveManagerTest.js +++ b/contracts/test/TroveManagerTest.js @@ -434,6 +434,8 @@ contract("TroveManager", async (accounts) => { 100 ); + assert.isTrue(await sortedTroves.contains(bob)); + th.logBN("bob icr", await troveManager.getCurrentICR(bob, await priceFeed.getPrice())); // Bob now withdraws Bold, bringing his ICR to 1.11 const { increasedTotalDebt: B_increasedTotalDebt } = await withdrawBold({ ICR: toBN(dec(111, 16)), diff --git a/contracts/test/TroveManager_LiquidationRewardsTest.js b/contracts/test/TroveManager_LiquidationRewardsTest.js index f0bd9766..80e4f414 100644 --- a/contracts/test/TroveManager_LiquidationRewardsTest.js +++ b/contracts/test/TroveManager_LiquidationRewardsTest.js @@ -1424,7 +1424,7 @@ contract( it("redistribution: A,B,C Open. Liq(C). B withdraws coll. D Opens. Liq(D). Distributes correct rewards.", async () => { // A, B, C open troves const { troveId: aliceTroveId, collateral: A_coll, totalDebt: A_totalDebt } = await openTrove({ - ICR: toBN(dec(400, 16)), + ICR: toBN(dec(500, 16)), extraParams: { from: alice }, }); const { troveId: bobTroveId, collateral: B_coll, totalDebt: B_totalDebt } = await openTrove({ @@ -1433,13 +1433,15 @@ contract( extraParams: { from: bob }, }); const { troveId: carolTroveId, collateral: C_coll, totalDebt: C_totalDebt } = await openTrove({ - ICR: toBN(dec(200, 16)), + ICR: toBN(dec(110, 16)), extraBoldAmount: dec(110, 18), extraParams: { from: carol }, }); - // Price drops to 100 $/E - await priceFeed.setPrice(dec(100, 18)); + // Price drops to 110 $/E + const liqPrice = th.toBN(dec(190, 18)) + await priceFeed.setPrice(liqPrice); + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) // Liquidate Carol const txC = await troveManager.liquidate(carolTroveId); @@ -1457,36 +1459,20 @@ contract( // D opens trove const { troveId: dennisTroveId, collateral: D_coll, totalDebt: D_totalDebt } = await openTrove({ - ICR: toBN(dec(200, 16)), + ICR: toBN(dec(110, 16)), extraBoldAmount: dec(110, 18), extraParams: { from: dennis }, }); - // Price drops to 100 $/E - await priceFeed.setPrice(dec(100, 18)); + // Price drops again + await priceFeed.setPrice(liqPrice); + assert.isFalse(await troveManager.checkRecoveryMode(liqPrice)) // Liquidate D const txA = await troveManager.liquidate(dennisTroveId); assert.isTrue(txA.receipt.status); assert.isFalse(await sortedTroves.contains(dennisTroveId)); - /* Bob rewards: - L1: 0.4975 ETH, 55 Bold - L2: (0.9975/2.495)*0.995 = 0.3978 ETH , 110*(0.9975/2.495)= 43.98 BoldDebt - - coll: (1 + 0.4975 - 0.5 + 0.3968) = 1.3953 ETH - debt: (110 + 55 + 43.98 = 208.98 BoldDebt - - Alice rewards: - L1 0.4975, 55 Bold - L2 (1.4975/2.495)*0.995 = 0.5972 ETH, 110*(1.4975/2.495) = 66.022 BoldDebt - - coll: (1 + 0.4975 + 0.5972) = 2.0947 ETH - debt: (50 + 55 + 66.022) = 171.022 Bold Debt - - totalColl: 3.49 ETH - totalDebt 380 Bold (Includes 50 in each trove for gas compensation) - */ const bob_Coll = (await troveManager.Troves(bobTroveId))[1] .add(await troveManager.getPendingETHReward(bobTroveId)) .toString(); @@ -1543,6 +1529,7 @@ contract( const entireSystemDebt = (await activePool.getRecordedDebtSum()).add( await defaultPool.getBoldDebt() ); + th.assertIsApproximatelyEqual( entireSystemDebt, A_totalDebt.add(B_totalDebt).add(C_totalDebt).add(D_totalDebt) diff --git a/contracts/test/TroveManager_RecoveryModeTest.js b/contracts/test/TroveManager_RecoveryModeTest.js index ab4e5be2..cde20e6c 100644 --- a/contracts/test/TroveManager_RecoveryModeTest.js +++ b/contracts/test/TroveManager_RecoveryModeTest.js @@ -1199,32 +1199,31 @@ contract.skip("TroveManager - in Recovery Mode", async (accounts) => { // Check Recovery Mode is active assert.isTrue(await th.checkRecoveryMode(contracts)); - - // Check troves A-D are in range 110% < ICR < TCR - const ICR_A = await troveManager.getCurrentICR(alice, price); - const ICR_B = await troveManager.getCurrentICR(bob, price); - const ICR_C = await troveManager.getCurrentICR(carol, price); - const ICR_D = await troveManager.getCurrentICR(dennis, price); - - assert.isTrue(ICR_A.gt(mv._MCR) && ICR_A.lt(TCR)); - assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR)); - assert.isTrue(ICR_C.gt(mv._MCR) && ICR_C.lt(TCR)); - assert.isTrue(ICR_D.gt(mv._MCR) && ICR_D.lt(TCR)); - + // Troves are ordered by ICR, low to high: A, B, C, D. - // Liquidate out of ICR order: D, B, C. Confirm Recovery Mode is active prior to each. + // Liquidate out of ICR order: D, B, C. Prior to each, confirm that: + // - Recovery Mode is active + // - MCR < ICR < TCR + assert.isTrue(await th.checkRecoveryMode(contracts)); + const ICR_D = await troveManager.getCurrentICR(dennis, price); + assert.isTrue(ICR_D.gt(mv._MCR)); + assert.isTrue(ICR_D.lt(TCR)); const liquidationTx_D = await troveManager.liquidate(dennis); + assert.isTrue(liquidationTx_D.receipt.status); assert.isTrue(await th.checkRecoveryMode(contracts)); + const ICR_B = await troveManager.getCurrentICR(bob, price); + assert.isTrue(ICR_B.gt(mv._MCR)); + assert.isTrue(ICR_B.lt(TCR)); const liquidationTx_B = await troveManager.liquidate(bob); + assert.isTrue(liquidationTx_B.receipt.status); assert.isTrue(await th.checkRecoveryMode(contracts)); + const ICR_C = await troveManager.getCurrentICR(carol, price); + assert.isTrue(ICR_C.gt(mv._MCR)); + assert.isTrue(ICR_C.lt(TCR)); const liquidationTx_C = await troveManager.liquidate(carol); - - // Check transactions all succeeded - assert.isTrue(liquidationTx_D.receipt.status); - assert.isTrue(liquidationTx_B.receipt.status); assert.isTrue(liquidationTx_C.receipt.status); // Confirm troves D, B, C removed @@ -2281,19 +2280,23 @@ contract.skip("TroveManager - in Recovery Mode", async (accounts) => { ); }); - it("liquidate(), with 110% < ICR < TCR, can claim collateral, after another claim from a redemption", async () => { + // TODO: Reassess this test once interest is correctly applied in redemptions + it.skip("liquidate(), with 110% < ICR < TCR, can claim collateral, after another claim from a redemption", async () => { // --- SETUP --- // Bob withdraws up to 90 Bold of debt, resulting in ICR of 222% const { collateral: B_coll, netDebt: B_netDebt } = await openTrove({ ICR: toBN(dec(222, 16)), extraBoldAmount: dec(90, 18), - extraParams: { from: bob }, + extraParams: { from: bob, annualInterestRate: toBN(dec(5,16)) }, // 5% interest (lowest) }); + let price = await priceFeed.getPrice(); + th.logBN("bob ICR start", await troveManager.getCurrentICR(bob, price)) + // Dennis withdraws to 150 Bold of debt, resulting in ICRs of 266%. const { collateral: D_coll } = await openTrove({ ICR: toBN(dec(266, 16)), extraBoldAmount: B_netDebt, - extraParams: { from: dennis }, + extraParams: { from: dennis, annualInterestRate: toBN(dec(10,16)) }, // 10% interest }); // --- TEST --- @@ -2303,9 +2306,9 @@ contract.skip("TroveManager - in Recovery Mode", async (accounts) => { web3.currentProvider ); - // Dennis redeems 40, so Bob has a surplus of (200 * 1 - 40) / 200 = 0.8 ETH + // Dennis redeems 40, hits Bob (lowest ICR) so Bob has a surplus of (200 * 1 - 40) / 200 = 0.8 ETH await th.redeemCollateral(dennis, contracts, B_netDebt); - let price = await priceFeed.getPrice(); + const price = await priceFeed.getPrice(); const bob_surplus = B_coll.sub(B_netDebt.mul(mv._1e18BN).div(price)); th.assertIsApproximatelyEqual( await collSurplusPool.getCollateral(bob), @@ -2321,16 +2324,16 @@ contract.skip("TroveManager - in Recovery Mode", async (accounts) => { bob_balanceBefore.add(bob_surplus) ); - // Bob re-opens the trove, price 200, total debt 250 Bold, ICR = 240% (lowest one) + // Bob re-opens the trove, price 200, total debt 250 Bold, interest = 5% (lowest one) const { collateral: B_coll_2, totalDebt: B_totalDebt_2 } = await openTrove({ ICR: toBN(dec(240, 16)), - extraParams: { from: bob, value: _3_Ether }, + extraParams: { from: bob, value: _3_Ether, annualInterestRate: th.toBN(dec(5, 16)) }, }); - // Alice deposits Bold in the Stability Pool + // Alice opens (20 % interest, highest) and deposits Bold in the Stability Pool await openTrove({ ICR: toBN(dec(266, 16)), extraBoldAmount: B_totalDebt_2, - extraParams: { from: alice }, + extraParams: { from: alice, annualInterestRate: th.toBN(dec(20, 16))}, }); await stabilityPool.provideToSP(B_totalDebt_2, { from: alice, @@ -2340,12 +2343,14 @@ contract.skip("TroveManager - in Recovery Mode", async (accounts) => { await priceFeed.setPrice("100000000000000000000"); price = await priceFeed.getPrice(); const TCR = await th.getTCR(contracts); + th.logBN("TCR", TCR); const recoveryMode = await th.checkRecoveryMode(contracts); assert.isTrue(recoveryMode); // Check Bob's ICR is between 110 and TCR const bob_ICR = await troveManager.getCurrentICR(bob, price); + th.logBN("bob_ICR", bob_ICR); assert.isTrue(bob_ICR.gt(mv._MCR) && bob_ICR.lt(TCR)); // debt is increased by fee, due to previous redemption const bob_debt = await troveManager.getTroveDebt(bob); diff --git a/contracts/utils/testHelpers.js b/contracts/utils/testHelpers.js index d0360ea5..8ea01bc4 100644 --- a/contracts/utils/testHelpers.js +++ b/contracts/utils/testHelpers.js @@ -983,14 +983,14 @@ class TestHelper { let increasedTotalDebt; if (ICR) { assert(extraParams.from, "A from account is needed"); - const { debt, coll } = await contracts.troveManager.getEntireDebtAndColl(troveId); + const { entireDebt, entireColl } = await contracts.troveManager.getEntireDebtAndColl(troveId); const price = await contracts.priceFeedTestnet.getPrice(); - const targetDebt = coll.mul(price).div(ICR); + const targetDebt = entireColl.mul(price).div(ICR); assert( - targetDebt > debt, + targetDebt > entireDebt, "ICR is already greater than or equal to target" ); - increasedTotalDebt = targetDebt.sub(debt); + increasedTotalDebt = targetDebt.sub(entireDebt); boldAmount = await this.getNetBorrowingAmount( contracts, increasedTotalDebt From e67afb923f9168a6230fc7a54a36e8f871d881fb Mon Sep 17 00:00:00 2001 From: Bingen Date: Tue, 26 Mar 2024 19:06:31 +0000 Subject: [PATCH 14/18] contracts: Fixes after rebase Rebasing apply_and_mint_interest on top of trove_ERC721. --- contracts/src/BorrowerOperations.sol | 74 ++- contracts/src/Interfaces/IActivePool.sol | 19 +- contracts/src/Interfaces/IDefaultPool.sol | 7 +- contracts/src/Interfaces/ITroveManager.sol | 29 +- contracts/src/TroveManager.sol | 84 ++- contracts/src/test/TestContracts/BaseTest.sol | 42 +- .../src/test/TestContracts/DevTestSetup.sol | 52 +- contracts/src/test/borrowerOperations.t.sol | 2 +- .../src/test/interestRateAggregate.t.sol | 546 +++++++++--------- contracts/src/test/interestRateBasic.t.sol | 411 ++++++------- contracts/test/AccessControlTest.js | 38 +- contracts/test/BorrowerOperationsTest.js | 9 +- contracts/test/TroveManagerTest.js | 16 +- .../test/TroveManager_RecoveryModeTest.js | 2 +- 14 files changed, 657 insertions(+), 674 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 16fb51ee..345275c0 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -45,7 +45,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint oldICR; uint newICR; uint newTCR; - uint BoldFee; + uint BoldFee; // TODO uint newEntireDebt; uint newEntireColl; uint stake; @@ -53,7 +53,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe struct LocalVariables_openTrove { uint price; - uint BoldFee; + uint BoldFee; // TODO uint netDebt; uint compositeDebt; uint ICR; @@ -184,7 +184,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint256 troveId = uint256(keccak256(abi.encode(_owner, _ownerIndex))); _requireTroveisNotActive(contractsCache.troveManager, troveId); - vars.BoldFee; + vars.BoldFee; // TODO _requireAtLeastMinNetDebt(_boldAmount); @@ -232,7 +232,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe contractsCache.activePool.changeAggWeightedDebtSum(0, vars.compositeDebt * _annualInterestRate); emit TroveUpdated(troveId, vars.compositeDebt, _ETHAmount, vars.stake, BorrowerOperation.openTrove); - emit BoldBorrowingFeePaid(troveId, vars.BoldFee); + emit BoldBorrowingFeePaid(troveId, vars.BoldFee); // TODO return troveId; } @@ -313,8 +313,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe vars.price = priceFeed.fetchPrice(); - uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(_borrower); - uint256 annualInterestRate = contractsCache.troveManager.getTroveAnnualInterestRate(_borrower); + uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(_troveId); + uint256 annualInterestRate = contractsCache.troveManager.getTroveAnnualInterestRate(_troveId); // --- Checks --- @@ -333,9 +333,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // Confirm the operation is an ETH transfer if coming from the Stability Pool to a trove assert((msg.sender != stabilityPoolAddress || (_isCollIncrease && _boldChange == 0))); - // Get the collChange based on whether or not ETH was sent in the transaction - (vars.collChange, vars.isCollIncrease) = _getCollChange(msg.value, _collWithdrawal); - (vars.entireDebt, vars.entireColl, vars.redistDebtGain, , vars.accruedTroveInterest) = contractsCache.troveManager.getEntireDebtAndColl(_troveId); // Get the trove's old ICR before the adjustment, and what its new ICR will be after the adjustment @@ -363,7 +360,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // --- Effects and interactions --- - contractsCache.troveManager.getAndApplyRedistributionGains(_borrower); + contractsCache.troveManager.getAndApplyRedistributionGains(_troveId); if (_isDebtIncrease) { // Increase Trove debt by the drawn debt + redist. gain @@ -374,13 +371,28 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } // Update the Trove's recorded coll and debt - vars.newEntireColl = _updateTroveCollFromAdjustment(contractsCache.troveManager, _troveId, vars.collChange, vars.isCollIncrease); - vars.newEntireDebt = _updateTroveDebtFromAdjustment(contractsCache.troveManager, _troveId, vars.entireDebt, _boldChange, _isDebtIncrease); + vars.newEntireColl = _updateTroveCollFromAdjustment( + contractsCache.troveManager, + _sender, + _troveId, + vars.entireColl, + _collChange, + _isCollIncrease + ); + vars.newEntireDebt = _updateTroveDebtFromAdjustment( + contractsCache.troveManager, + _sender, + _troveId, + vars.entireDebt, + _boldChange, + _isDebtIncrease, + vars.accruedTroveInterest + ); vars.stake = contractsCache.troveManager.updateStakeAndTotalStakes(_troveId); emit TroveUpdated(_troveId, vars.newEntireDebt, vars.newEntireColl, vars.stake, BorrowerOperation.adjustTrove); - emit BoldBorrowingFeePaid(_troveId, vars.BoldFee); + emit BoldBorrowingFeePaid(_troveId, vars.BoldFee); // TODO _moveTokensAndETHfromAdjustment( contractsCache.activePool, @@ -465,7 +477,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 - contractsCache.troveManager.updateTroveDebt(_troveId, entireTroveDebt); + contractsCache.troveManager.updateTroveDebtFromInterestApplication(_troveId, entireTroveDebt); } function setAddManager(uint256 _troveId, address _manager) external { @@ -518,7 +530,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe ITroveManager _troveManager, address _sender, uint256 _troveId, - uint256 _coll, + uint256 _oldEntireColl, uint _collChange, bool _isCollIncrease ) @@ -528,11 +540,10 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint256 newEntireColl; if (_collChange > 0) { - newEntireColl = (_isCollIncrease) ? - _troveManager.increaseTroveColl(_sender, _troveId, _collChange) : - _troveManager.decreaseTroveColl(_sender, _troveId, _collChange); + newEntireColl = _isCollIncrease ? _oldEntireColl + _collChange : _oldEntireColl - _collChange; + _troveManager.updateTroveColl(_sender, _troveId, newEntireColl, _isCollIncrease); } else { - newEntireColl = _coll; + newEntireColl = _oldEntireColl; } return newEntireColl; @@ -542,20 +553,25 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // to the Trove's debt. function _updateTroveDebtFromAdjustment( ITroveManager _troveManager, - address _borrower, + address _sender, + uint256 _troveId, uint256 _oldEntireDebt, uint256 _debtChange, - bool _isDebtIncrease + bool _isDebtIncrease, + uint256 _accruedTroveInterest ) internal returns (uint256) { uint newEntireDebt; if (_debtChange > 0) { - newEntireDebt= _isDebtIncrease ? _oldEntireDebt + _debtChange : _oldEntireDebt - _debtChange; - _troveManager.updateTroveDebt(_troveId, newEntireDebt); + newEntireDebt = _isDebtIncrease ? _oldEntireDebt + _debtChange : _oldEntireDebt - _debtChange; + _troveManager.updateTroveDebt(_sender, _troveId, newEntireDebt, _isDebtIncrease); } else { newEntireDebt = _oldEntireDebt; + if (_accruedTroveInterest > 0) { + _troveManager.updateTroveDebtFromInterestApplication(_troveId, newEntireDebt); + } } return newEntireDebt; @@ -617,16 +633,16 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe internal returns (uint256) { - uint256 initialWeightedRecordedTroveDebt = _troveManager.getTroveWeightedRecordedDebt(_borrower); + uint256 initialWeightedRecordedTroveDebt = _troveManager.getTroveWeightedRecordedDebt(_troveId); // --- Effects --- - (, uint256 redistDebtGain) = _troveManager.getAndApplyRedistributionGains(_borrower); + (, 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(_borrower); - uint256 recordedTroveDebt = _troveManager.getTroveDebt(_borrower); + uint256 accruedTroveInterest = _troveManager.calcTroveAccruedInterest(_troveId); + uint256 recordedTroveDebt = _troveManager.getTroveDebt(_troveId); uint256 entireTroveDebt = recordedTroveDebt + accruedTroveInterest; // Add only the Trove's accrued interest to the recorded debt tracker since we have already applied redist. gains. @@ -758,8 +774,8 @@ 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"); + function _requireTroveIsStale(ITroveManager _troveManager, uint256 _troveId) internal view { + require(_troveManager.troveIsStale(_troveId), "BO: Trove must be stale"); } // --- ICR and TCR getters --- diff --git a/contracts/src/Interfaces/IActivePool.sol b/contracts/src/Interfaces/IActivePool.sol index 70177701..8fdc929a 100644 --- a/contracts/src/Interfaces/IActivePool.sol +++ b/contracts/src/Interfaces/IActivePool.sol @@ -9,8 +9,16 @@ interface IActivePool { function borrowerOperationsAddress() external view returns (address); function troveManagerAddress() external view returns (address); function interestRouter() external view returns (IInterestRouter); + function setAddresses( + address _borrowerOperationsAddress, + address _troveManagerAddress, + address _stabilityPoolAddress, + address _defaultPoolAddress, + address _boldTokenAddress, + address _interestRouterAddress + ) external; - function getETH() external view returns (uint256); + function getETHBalance() external view returns (uint256); function getRecordedDebtSum() external view returns (uint256); function getTotalActiveDebt() external view returns (uint256); function lastAggUpdateTime() external view returns (uint256); @@ -27,12 +35,5 @@ interface IActivePool { function decreaseRecordedDebtSum(uint256 _amount) external; function sendETH(address _account, uint _amount) external; function sendETHToDefaultPool(uint _amount) external; - function setAddresses( - address _borrowerOperationsAddress, - address _troveManagerAddress, - address _stabilityPoolAddress, - address _defaultPoolAddress, - address _boldTokenAddress, - address _interestRouterAddress - ) external; + function receiveETH(uint256 _amount) external; } diff --git a/contracts/src/Interfaces/IDefaultPool.sol b/contracts/src/Interfaces/IDefaultPool.sol index c11383d5..313c22ee 100644 --- a/contracts/src/Interfaces/IDefaultPool.sol +++ b/contracts/src/Interfaces/IDefaultPool.sol @@ -3,16 +3,15 @@ pragma solidity 0.8.18; interface IDefaultPool { + function setAddresses(address _troveManagerAddress, address _activePoolAddress) external; function troveManagerAddress() external view returns (address); function activePoolAddress() external view returns (address); // --- Functions --- - function getETH() external view returns (uint256); + function getETHBalance() external view returns (uint256); function getBoldDebt() external view returns (uint256); function sendETHToActivePool(uint _amount) external; - function setAddresses(address _troveManagerAddress, address _activePoolAddress) external; + function receiveETH(uint256 _amount) external; function increaseBoldDebt(uint _amount) external; function decreaseBoldDebt(uint _amount) external; - - } diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 74ed6325..8c67e628 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -56,7 +56,7 @@ interface ITroveManager is IERC721, ILiquityBase { function getPendingBoldDebtReward(uint256 _troveId) external view returns (uint); - function hasRedistributionGains(address _borrower) external view returns (bool); + function hasRedistributionGains(uint256 _troveId) external view returns (bool); function getEntireDebtAndColl(uint256 _troveId) external view returns ( uint entireDebt, @@ -66,11 +66,11 @@ interface ITroveManager is IERC721, ILiquityBase { uint pendingBoldInterest ); - function getTroveEntireDebt(address _borrower) external view returns (uint256); + function getTroveEntireDebt(uint256 _troveId) external view returns (uint256); - function getTroveEntireColl(address _borrower) external view returns (uint256); + function getTroveEntireColl(uint256 _troveId) external view returns (uint256); - function getAndApplyRedistributionGains(address _borrower) external returns (uint256, uint256); + function getAndApplyRedistributionGains(uint256 _troveId) external returns (uint256, uint256); function closeTrove(uint256 _troveId) external; @@ -87,14 +87,13 @@ interface ITroveManager is IERC721, ILiquityBase { function getTroveDebt(uint256 _troveId) external view returns (uint); - function getTroveWeightedRecordedDebt(address _borrower) external returns (uint256); + function getTroveWeightedRecordedDebt(uint256 _troveId) external returns (uint256); function getTroveColl(uint256 _troveId) external view returns (uint); - function calcPendingTroveInterest(uint256 _troveId) external view returns (uint256); function getTroveAnnualInterestRate(uint256 _troveId) external view returns (uint); - function calcTroveAccruedInterest(address _borrower) external view returns (uint256); + function calcTroveAccruedInterest(uint256 _troveId) external view returns (uint256); function TroveAddManagers(uint256 _troveId) external view returns (address); function TroveRemoveManagers(uint256 _troveId) external view returns (address); @@ -103,21 +102,17 @@ interface ITroveManager is IERC721, ILiquityBase { function setTrovePropertiesOnOpen(address _owner, uint256 _troveId, uint256 _coll, uint256 _debt, uint256 _annualInterestRate) external returns (uint256); - function troveIsStale(address _borrower) external view returns (bool); - - function increaseTroveColl(address _sender, uint256 _troveId, uint _collIncrease) external returns (uint); - - function decreaseTroveColl(address _sender, uint256 _troveId, uint _collDecrease) external returns (uint); - - function increaseTroveDebt(address _sender, uint256 _troveId, uint _debtIncrease) external returns (uint); - - function decreaseTroveDebt(address _sender, uint256 _troveId, uint _collDecrease) external returns (uint); + function troveIsStale(uint256 _troveId) external view returns (bool); function changeAnnualInterestRate(uint256 _troveId, uint256 _newAnnualInterestRate) external; function updateTroveDebtAndInterest(uint256 _troveId, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) external; - function updateTroveDebt(uint256 _troveId, uint256 _entireTroveDebt) external; + function updateTroveDebtFromInterestApplication(uint256 _troveId, uint256 _entireTroveDebt) external; + + function updateTroveDebt(address _sender, uint256 _troveId, uint256 _entireTroveDebt, bool _isDebtIncrease) external; + + function updateTroveColl(address _sender, uint256 _troveId, uint256 _entireTroveColl, bool _isCollIncrease) external; function setAddManager(address _sender, uint256 _troveId, address _manager) external; function setRemoveManager(address _sender, uint256 _troveId, address _manager) external; diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 01e4345f..f53920c6 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -387,10 +387,10 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana vars.pendingDebtReward, vars.pendingCollReward, ) = getEntireDebtAndColl(_troveId); - singleLiquidation.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_borrower); + singleLiquidation.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_troveId); //TODO - GAS: We already read this inside getEntireDebtAndColl - so add it to the returned vals? - singleLiquidation.recordedTroveDebt = Troves[_borrower].debt; + singleLiquidation.recordedTroveDebt = Troves[_troveId].debt; singleLiquidation.collGasCompensation = _getCollGasCompensation(singleLiquidation.entireTroveColl); singleLiquidation.BoldGasCompensation = BOLD_GAS_COMPENSATION; @@ -833,7 +833,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // TODO: zombi troves if (newDebt == BOLD_GAS_COMPENSATION) { - uint256 weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_borrower); // No debt left in the Trove (except for the liquidation reserve), therefore the trove gets closed _removeStake(_troveId); _closeTrove(_troveId, Status.closedByRedemption); @@ -991,7 +990,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint pendingETHReward = getPendingETHReward(_troveId); uint pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); - uint256 accruedTroveInterest = calcTroveAccruedInterest(_borrower); + uint256 accruedTroveInterest = calcTroveAccruedInterest(_troveId); uint currentETH = Troves[_troveId].coll + pendingETHReward; uint currentBoldDebt = Troves[_troveId].debt + pendingBoldDebtReward + accruedTroveInterest; @@ -1073,7 +1072,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return pendingBoldDebtReward; } - function hasRedistributionGains(address _troveId) public view override returns (bool) { + function hasRedistributionGains(uint256 _troveId) public view override returns (bool) { /* * A Trove has redistribution gains if its snapshot is less than the current rewards per-unit-staked sum: * this indicates that rewards have occured since the snapshot was made, and the user therefore has @@ -1403,11 +1402,11 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // TODO: analyze precision loss in interest functions and decide upon the minimum granularity // (per-second, per-block, etc) - function calcTroveAccruedInterest(address _borrower) public view returns (uint256) { - uint256 recordedDebt = Troves[_borrower].debt; + function calcTroveAccruedInterest(uint256 _troveId) public view returns (uint256) { + uint256 recordedDebt = Troves[_troveId].debt; // convert annual interest to per-second and multiply by the principal - uint256 annualInterestRate = Troves[_borrower].annualInterestRate; - uint256 lastDebtUpdateTime = Troves[_borrower].lastDebtUpdateTime; + uint256 annualInterestRate = Troves[_troveId].annualInterestRate; + uint256 lastDebtUpdateTime = Troves[_troveId].lastDebtUpdateTime; return recordedDebt * annualInterestRate * (block.timestamp - lastDebtUpdateTime) / SECONDS_IN_ONE_YEAR / 1e18; } @@ -1476,8 +1475,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return Troves[_troveId].debt; } - function getTroveWeightedRecordedDebt(address _borrower) public view returns (uint256) { - return Troves[_borrower].debt * Troves[_borrower].annualInterestRate; + function getTroveWeightedRecordedDebt(uint256 _troveId) public view returns (uint256) { + return Troves[_troveId].debt * Troves[_troveId].annualInterestRate; } function getTroveColl(uint256 _troveId) external view override returns (uint) { @@ -1488,12 +1487,12 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return Troves[_troveId].annualInterestRate; } - function getTroveLastDebtUpdateTime(address _borrower) external view returns (uint) { - return Troves[_borrower].lastDebtUpdateTime; + function getTroveLastDebtUpdateTime(uint256 _troveId) external view returns (uint) { + return Troves[_troveId].lastDebtUpdateTime; } - function troveIsStale(address _borrower) external view returns (bool) { - return block.timestamp - Troves[_borrower].lastDebtUpdateTime > STALE_TROVE_DURATION; + function troveIsStale(uint256 _troveId) external view returns (bool) { + return block.timestamp - Troves[_troveId].lastDebtUpdateTime > STALE_TROVE_DURATION; } // --- Trove property setters, called by BorrowerOperations --- @@ -1517,6 +1516,10 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana _updateTroveRewardSnapshots(_troveId); + // mint ERC721 + // TODO: Should we use safeMint? I guess not + _mint(_owner, _troveId); + // Record the Trove's stake (for redistributions) and update the total stakes return _updateStakeAndTotalStakes(_troveId); } @@ -1531,50 +1534,35 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana Troves[_troveId].annualInterestRate = _newAnnualInterestRate; } - function updateTroveDebt(uint256 _troveId, uint256 _entireTroveDebt) external { + function updateTroveDebtFromInterestApplication(uint256 _troveId, uint256 _entireTroveDebt) external { _requireCallerIsBorrowerOperations(); _updateTroveDebt(_troveId, _entireTroveDebt); } - function _updateTroveDebt(uint256 _troveId, uint256 _entireTroveDebt) internal { - Troves[_troveId].debt = _entireTroveDebt; - Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp); - } - - function increaseTroveColl(address _sender, uint256 _troveId, uint _collIncrease) external override returns (uint) { - _requireCallerIsBorrowerOperations(); - _requireIsOwnerOrAddManager(_troveId, _sender); - - uint newColl = Troves[_troveId].coll + _collIncrease; - Troves[_troveId].coll = newColl; - return newColl; - } - - function decreaseTroveColl(address _sender, uint256 _troveId, uint _collDecrease) external override returns (uint) { + function updateTroveDebt(address _sender, uint256 _troveId, uint256 _entireTroveDebt, bool _isDebtIncrease) external { _requireCallerIsBorrowerOperations(); - _requireIsOwnerOrRemoveManager(_troveId, _sender); - - uint newColl = Troves[_troveId].coll - _collDecrease; - Troves[_troveId].coll = newColl; - return newColl; + if (_isDebtIncrease) { + _requireIsOwnerOrRemoveManager(_troveId, _sender); + } else { + _requireIsOwnerOrAddManager(_troveId, _sender); + } + _updateTroveDebt(_troveId, _entireTroveDebt); } - function increaseTroveDebt(address _sender, uint256 _troveId, uint _debtIncrease) external override returns (uint) { - _requireCallerIsBorrowerOperations(); - _requireIsOwnerOrRemoveManager(_troveId, _sender); - - uint newDebt = Troves[_troveId].debt + _debtIncrease; - Troves[_troveId].debt = newDebt; - return newDebt; + function _updateTroveDebt(uint256 _troveId, uint256 _entireTroveDebt) internal { + Troves[_troveId].debt = _entireTroveDebt; + Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp); } - function decreaseTroveDebt(address _sender, uint256 _troveId, uint _debtDecrease) external override returns (uint) { + function updateTroveColl(address _sender, uint256 _troveId, uint256 _entireTroveColl, bool _isCollIncrease) external override { _requireCallerIsBorrowerOperations(); - _requireIsOwnerOrAddManager(_troveId, _sender); + if (_isCollIncrease) { + _requireIsOwnerOrAddManager(_troveId, _sender); + } else { + _requireIsOwnerOrRemoveManager(_troveId, _sender); + } - uint newDebt = Troves[_troveId].debt - _debtDecrease; - Troves[_troveId].debt = newDebt; - return newDebt; + Troves[_troveId].coll = _entireTroveColl; } function changeAnnualInterestRate(uint256 _troveId, uint256 _newAnnualInterestRate) external { diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index 92114943..56d4bbcf 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -59,6 +59,14 @@ contract BaseTest is Test { accountsList = tempAccounts; } + function addressToTroveId(address _owner, uint256 _ownerIndex) public pure returns (uint256) { + return uint256(keccak256(abi.encode(_owner, _ownerIndex))); + } + + function addressToTroveId(address _owner) public pure returns (uint256) { + return addressToTroveId(_owner, 0); + } + function openTroveNoHints100pctMaxFee( address _account, uint256 _coll, @@ -128,39 +136,39 @@ contract BaseTest is Test { vm.stopPrank(); } - function closeTrove(address _account) public { + function closeTrove(address _account, uint256 _troveId) public { vm.startPrank(_account); - borrowerOperations.closeTrove(); + borrowerOperations.closeTrove(_troveId); vm.stopPrank(); } - function withdrawBold100pctMaxFee(address _account, uint256 _debtIncrease) public { + function withdrawBold100pctMaxFee(address _account, uint256 _troveId, uint256 _debtIncrease) public { vm.startPrank(_account); - borrowerOperations.withdrawBold(1e18, _debtIncrease); + borrowerOperations.withdrawBold(_troveId, 1e18, _debtIncrease); vm.stopPrank(); } - function repayBold(address _account, uint256 _debtDecrease) public { + function repayBold(address _account, uint256 _troveId, uint256 _debtDecrease) public { vm.startPrank(_account); - borrowerOperations.repayBold(_debtDecrease); + borrowerOperations.repayBold(_troveId, _debtDecrease); vm.stopPrank(); } - function addColl(address _account, uint256 _collIncrease) public { + function addColl(address _account, uint256 _troveId, uint256 _collIncrease) public { vm.startPrank(_account); - borrowerOperations.addColl{value: _collIncrease}(); + borrowerOperations.addColl(_troveId, _collIncrease); vm.stopPrank(); } - function withdrawColl(address _account, uint256 _collDecrease) public { + function withdrawColl(address _account, uint256 _troveId, uint256 _collDecrease) public { vm.startPrank(_account); - borrowerOperations.withdrawColl(_collDecrease); + borrowerOperations.withdrawColl(_troveId, _collDecrease); vm.stopPrank(); } - function applyTroveInterestPermissionless(address _from, address _borrower) public { + function applyTroveInterestPermissionless(address _from, uint256 _troveId) public { vm.startPrank(_from); - borrowerOperations.applyTroveInterestPermissionless(_borrower); + borrowerOperations.applyTroveInterestPermissionless(_troveId); vm.stopPrank(); } @@ -170,19 +178,19 @@ contract BaseTest is Test { vm.stopPrank(); } - function liquidate(address _from, address _borrower) public { + function liquidate(address _from, uint256 _troveId) public { vm.startPrank(_from); - troveManager.liquidate(_borrower); + troveManager.liquidate(_troveId); vm.stopPrank(); } - function withdrawETHGainToTrove(address _from) public { + function withdrawETHGainToTrove(address _from, uint256 _troveId) public { vm.startPrank(_from); - stabilityPool.withdrawETHGainToTrove(); + stabilityPool.withdrawETHGainToTrove(_troveId); vm.stopPrank(); } - function batchLiquidateTroves(address _from, address[] memory _trovesList) public { + function batchLiquidateTroves(address _from, uint256[] memory _trovesList) public { vm.startPrank(_from); console.log(_trovesList[0], "trove 0 to liq"); console.log(_trovesList[1], "trove 1 to liq"); diff --git a/contracts/src/test/TestContracts/DevTestSetup.sol b/contracts/src/test/TestContracts/DevTestSetup.sol index 9dc80135..2bd6f48c 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -138,7 +138,7 @@ contract DevTestSetup is BaseTest { } } - function _setupForWithdrawETHGainToTrove() internal { + function _setupForWithdrawETHGainToTrove() internal returns (uint256, uint256, uint256) { uint256 troveDebtRequest_A = 2000e18; uint256 troveDebtRequest_B = 3000e18; uint256 troveDebtRequest_C = 4500e18; @@ -147,12 +147,12 @@ contract DevTestSetup is BaseTest { uint256 price = 2000e18; priceFeed.setPrice(price); - openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, interestRate); - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, interestRate); - openTroveNoHints100pctMaxFee(C, 5 ether, troveDebtRequest_C, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, interestRate); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, interestRate); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, 5 ether, troveDebtRequest_C, interestRate); console.log(troveManager.getTCR(price), "TCR"); - console.log(troveManager.getCurrentICR(C, price), "C CR"); + console.log(troveManager.getCurrentICR(CTroveId, price), "C CR"); // A and B deposit to SP makeSPDeposit(A, troveDebtRequest_A); @@ -163,19 +163,21 @@ contract DevTestSetup is BaseTest { priceFeed.setPrice(price); console.log(troveManager.getTCR(price), "TCR before liq"); - console.log(troveManager.getCurrentICR(C, price), "C CR before liq"); + console.log(troveManager.getCurrentICR(CTroveId, price), "C CR before liq"); assertFalse(troveManager.checkRecoveryMode(price)); - assertLt(troveManager.getCurrentICR(C, price), troveManager.MCR()); + assertLt(troveManager.getCurrentICR(CTroveId, price), troveManager.MCR()); // A liquidates C - liquidate(A, C); + liquidate(A, CTroveId); // check A has an ETH gain assertGt(stabilityPool.getDepositorETHGain(A), 0); + + return (ATroveId, BTroveId, CTroveId); } - function _setupForBatchLiquidateTrovesPureOffset() internal { + function _setupForBatchLiquidateTrovesPureOffset() internal returns (uint256, uint256, uint256, uint256) { uint256 troveDebtRequest_A = 2000e18; uint256 troveDebtRequest_B = 3000e18; uint256 troveDebtRequest_C = 2250e18; @@ -185,13 +187,13 @@ contract DevTestSetup is BaseTest { uint256 price = 2000e18; priceFeed.setPrice(price); - openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, interestRate); - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, interestRate); - openTroveNoHints100pctMaxFee(C, 25e17, troveDebtRequest_C, interestRate); - openTroveNoHints100pctMaxFee(D, 25e17, troveDebtRequest_D, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, interestRate); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, interestRate); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, 25e17, troveDebtRequest_C, interestRate); + uint256 DTroveId = openTroveNoHints100pctMaxFee(D, 25e17, troveDebtRequest_D, interestRate); // console.log(troveManager.getTCR(price), "TCR"); - // console.log(troveManager.getCurrentICR(C, price), "C CR"); + // console.log(troveManager.getCurrentICR(CTroveId, price), "C CR"); // A and B deposit to SP makeSPDeposit(A, troveDebtRequest_A); @@ -202,11 +204,13 @@ contract DevTestSetup is BaseTest { priceFeed.setPrice(price); assertFalse(troveManager.checkRecoveryMode(price)); - assertLt(troveManager.getCurrentICR(C, price), troveManager.MCR()); - assertLt(troveManager.getCurrentICR(D, price), troveManager.MCR()); + assertLt(troveManager.getCurrentICR(CTroveId, price), troveManager.MCR()); + assertLt(troveManager.getCurrentICR(DTroveId, price), troveManager.MCR()); + + return (ATroveId, BTroveId, CTroveId, DTroveId); } - function _setupForBatchLiquidateTrovesPureRedist() internal { + function _setupForBatchLiquidateTrovesPureRedist() internal returns (uint256, uint256, uint256, uint256) { uint256 troveDebtRequest_A = 2000e18; uint256 troveDebtRequest_B = 3000e18; uint256 troveDebtRequest_C = 2250e18; @@ -216,17 +220,19 @@ contract DevTestSetup is BaseTest { uint256 price = 2000e18; priceFeed.setPrice(price); - openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, interestRate); - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, interestRate); - openTroveNoHints100pctMaxFee(C, 25e17, troveDebtRequest_C, interestRate); - openTroveNoHints100pctMaxFee(D, 25e17, troveDebtRequest_D, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, interestRate); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, interestRate); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, 25e17, troveDebtRequest_C, interestRate); + uint256 DTroveId = openTroveNoHints100pctMaxFee(D, 25e17, troveDebtRequest_D, interestRate); // Price drops, C and D become liquidateable price = 1050e18; priceFeed.setPrice(price); assertFalse(troveManager.checkRecoveryMode(price)); - assertLt(troveManager.getCurrentICR(C, price), troveManager.MCR()); - assertLt(troveManager.getCurrentICR(D, price), troveManager.MCR()); + assertLt(troveManager.getCurrentICR(CTroveId, price), troveManager.MCR()); + assertLt(troveManager.getCurrentICR(DTroveId, price), troveManager.MCR()); + + return (ATroveId, BTroveId, CTroveId, DTroveId); } } diff --git a/contracts/src/test/borrowerOperations.t.sol b/contracts/src/test/borrowerOperations.t.sol index c2a24ae3..7725f94a 100644 --- a/contracts/src/test/borrowerOperations.t.sol +++ b/contracts/src/test/borrowerOperations.t.sol @@ -14,7 +14,7 @@ contract BorrowerOperationsTest is DevTestSetup { // Check she has more Bold than her trove debt uint256 aliceBal = boldToken.balanceOf(A); - (uint256 aliceDebt,,,) = troveManager.getEntireDebtAndColl(ATroveId); + (uint256 aliceDebt,,,,) = troveManager.getEntireDebtAndColl(ATroveId); assertGe(aliceBal, aliceDebt, "Not enough balance"); // check Recovery Mode diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index b6eb0256..79ec9a3b 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -61,13 +61,13 @@ contract InterestRateAggregate is DevTestSetup { uint256 _duration = 1 days; uint256 troveDebtRequest = 2000e18; - openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest - openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 75e16); // 75% annual interest - console.log("A debt", troveManager.getTroveDebt(A)); + 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(A), expectedTroveDebt); - assertEq(troveManager.getTroveDebt(B), expectedTroveDebt); + assertEq(troveManager.getTroveDebt(ATroveId), expectedTroveDebt); + assertEq(troveManager.getTroveDebt(BTroveId), expectedTroveDebt); vm.warp(block.timestamp + _duration); @@ -83,44 +83,44 @@ contract InterestRateAggregate is DevTestSetup { function testCalcPendingTroveInterestReturns0When0AggRecordedDebt() public { priceFeed.setPrice(2000e18); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(addressToTroveId(A)), 0); openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); - openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 75e16); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 75e16); vm.warp(block.timestamp + 1 days); // A sends Bold to B so B can cover their interest and close their Trove transferBold(A, B, boldToken.balanceOf(A)); - closeTrove(B); + closeTrove(B, BTroveId); - assertEq(troveManager.calcTroveAccruedInterest(B), 0); + assertEq(troveManager.calcTroveAccruedInterest(BTroveId), 0); } // returns 0 for 0 time passed function testCalcPendingTroveInterestReturns0For0TimePassed() public { priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); vm.warp(block.timestamp + 1 days); - openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 75e16); - assertEq(troveManager.calcTroveAccruedInterest(B), 0); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 75e16); + assertEq(troveManager.calcTroveAccruedInterest(BTroveId), 0); } function testCalcPendingTroveInterestReturns0For0InterestRate() public { priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); vm.warp(block.timestamp + 1 days); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); } // TODO: create additional corresponding fuzz test @@ -134,25 +134,25 @@ contract InterestRateAggregate is DevTestSetup { uint256 duration = 42 days; - openTroveNoHints100pctMaxFee(A, 2 ether, debtRequest_A, annualRate_A); - uint256 debt_A = troveManager.getTroveDebt(A); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, debtRequest_A, annualRate_A); + uint256 debt_A = troveManager.getTroveDebt(ATroveId); assertGt(debt_A, 0); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); vm.warp(block.timestamp + duration); uint256 expectedInterest_A = annualRate_A * debt_A * duration / 1e18 / SECONDS_IN_1_YEAR; - assertEq(troveManager.calcTroveAccruedInterest(A), expectedInterest_A); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), expectedInterest_A); - openTroveNoHints100pctMaxFee(B, 2 ether, debtRequest_B, annualRate_B); - uint256 debt_B = troveManager.getTroveDebt(B); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, debtRequest_B, annualRate_B); + uint256 debt_B = troveManager.getTroveDebt(BTroveId); assertGt(debt_B, 0); - assertEq(troveManager.calcTroveAccruedInterest(B), 0); + assertEq(troveManager.calcTroveAccruedInterest(BTroveId), 0); vm.warp(block.timestamp + duration); uint256 expectedInterest_B = annualRate_B * debt_B * duration / 1e18 / SECONDS_IN_1_YEAR; - assertEq(troveManager.calcTroveAccruedInterest(B), expectedInterest_B); + assertEq(troveManager.calcTroveAccruedInterest(BTroveId), expectedInterest_B); } // --- mintAggInterest --- @@ -224,8 +224,8 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingInterest, 0); uint256 expectedTroveDebt_B = troveDebtRequest + troveManager.BOLD_GAS_COMPENSATION(); - openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 25e16); - assertEq(troveManager.getTroveDebt(B), expectedTroveDebt_B); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 25e16); + assertEq(troveManager.getTroveDebt(BTroveId), expectedTroveDebt_B); // check that opening Trove B increased the agg. recorded debt by the pending agg. interest plus Trove B's debt assertEq(activePool.aggRecordedDebt(), aggREcordedDebt_1 + pendingInterest + expectedTroveDebt_B); @@ -236,7 +236,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggRecordedDebt(), 0); uint256 troveDebtRequest = 2000e18; - openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest // fast-forward time vm.warp(block.timestamp + 1 days); @@ -245,7 +245,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(recordedDebt_1, 0); openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 25e16); - uint256 troveDebt_A = troveManager.getTroveDebt(A); + uint256 troveDebt_A = troveManager.getTroveDebt(ATroveId); assertGt(troveDebt_A, 0); assertEq(activePool.getRecordedDebtSum(), recordedDebt_1 + troveDebt_A); @@ -326,8 +326,8 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.aggWeightedDebtSum(), 0); // A opens trove - openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, annualInterest_A); - uint256 troveDebt_A = troveManager.getTroveDebt(A); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 5 ether, troveDebtRequest_A, annualInterest_A); + uint256 troveDebt_A = troveManager.getTroveDebt(ATroveId); assertGt(troveDebt_A, 0); // // Trove's debt should be weighted by its annual interest rate @@ -340,8 +340,8 @@ contract InterestRateAggregate is DevTestSetup { vm.warp(block.timestamp + 1000); // B opens Trove - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, annualInterest_B); - uint256 troveDebt_B = troveManager.getTroveDebt(A); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest_B, annualInterest_B); + uint256 troveDebt_B = troveManager.getTroveDebt(BTroveId); assertGt(troveDebt_B, 0); uint256 expectedWeightedDebt_B = troveDebt_B * annualInterest_B; @@ -642,7 +642,7 @@ contract InterestRateAggregate is DevTestSetup { // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); // A sends Bold to B so B can cover their interest and close their Trove transferBold(A, B, boldToken.balanceOf(A)); @@ -654,7 +654,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(activePool.calcPendingAggInterest(), 0); // B closes Trove - closeTrove(B); + closeTrove(B, BTroveId); // // Check pending agg. interest reduced to 0 assertEq(activePool.calcPendingAggInterest(), 0); @@ -667,7 +667,7 @@ contract InterestRateAggregate is DevTestSetup { // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); // A sends Bold to B so B can cover their interest and close their Trove transferBold(A, B, boldToken.balanceOf(A)); @@ -684,11 +684,11 @@ contract InterestRateAggregate is DevTestSetup { assertGt(activePool.calcPendingAggInterest(), 0); // Check Trove's entire debt is larger than their recorded debt: - (uint256 entireTroveDebt_B, , , , )= troveManager.getEntireDebtAndColl(B); - assertGt(entireTroveDebt_B, troveManager.getTroveDebt(B)); + (uint256 entireTroveDebt_B, , , , )= troveManager.getEntireDebtAndColl(BTroveId); + assertGt(entireTroveDebt_B, troveManager.getTroveDebt(BTroveId)); // B closes Trove - closeTrove(B); + closeTrove(B, BTroveId); // // Check agg. recorded debt increased by pending agg. interest less the closed Trove's entire debt assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - entireTroveDebt_B); @@ -700,7 +700,7 @@ contract InterestRateAggregate is DevTestSetup { // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); // A sends Bold to B so B can cover their interest and close their Trove transferBold(A, B, boldToken.balanceOf(A)); @@ -712,7 +712,7 @@ contract InterestRateAggregate is DevTestSetup { assertLt(activePool.lastAggUpdateTime(), block.timestamp); // B closes Trove - closeTrove(B); + closeTrove(B, BTroveId); // Check last agg update time increased to now assertEq(activePool.lastAggUpdateTime(), block.timestamp); @@ -724,7 +724,7 @@ contract InterestRateAggregate is DevTestSetup { // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); // A sends Bold to B so B can cover their interest and close their Trove transferBold(A, B, boldToken.balanceOf(A)); @@ -740,7 +740,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // B closes Trove - closeTrove(B); + closeTrove(B, BTroveId); // Check I-router Bold bal has increased as expected from 3rd trove opening uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); @@ -754,7 +754,7 @@ contract InterestRateAggregate is DevTestSetup { // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); // A sends Bold to B so B can cover their interest and close their Trove transferBold(A, B, boldToken.balanceOf(A)); @@ -762,8 +762,8 @@ contract InterestRateAggregate is DevTestSetup { // fast-forward time vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_B = troveManager.getTroveDebt(B); - uint256 annualInterestRate_B = troveManager.getTroveAnnualInterestRate(B); + uint256 recordedTroveDebt_B = troveManager.getTroveDebt(BTroveId); + uint256 annualInterestRate_B = troveManager.getTroveAnnualInterestRate(BTroveId); assertGt(recordedTroveDebt_B, 0); assertGt(annualInterestRate_B, 0); uint256 weightedTroveDebt = recordedTroveDebt_B * annualInterestRate_B; @@ -772,7 +772,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(aggWeightedDebtSum_1, 0); // B closes Trove - closeTrove(B); + closeTrove(B, BTroveId); assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - weightedTroveDebt); } @@ -781,8 +781,8 @@ contract InterestRateAggregate is DevTestSetup { // A, B open Troves priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); - uint256 recordedDebt_B = troveManager.getTroveDebt(B); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + uint256 recordedDebt_B = troveManager.getTroveDebt(BTroveId); uint256 activePoolRecordedDebt_1 = activePool.getRecordedDebtSum(); assertGt(activePoolRecordedDebt_1, 0); @@ -793,7 +793,7 @@ contract InterestRateAggregate is DevTestSetup { vm.warp(block.timestamp + 1 days); // B closes Trove - closeTrove(B); + closeTrove(B, BTroveId); // Check recorded debt sum reduced by B's recorded debt assertEq(activePool.getRecordedDebtSum(), activePoolRecordedDebt_1 - recordedDebt_B); @@ -804,7 +804,7 @@ contract InterestRateAggregate is DevTestSetup { // A, B opens Trove priceFeed.setPrice(2000e18); openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); - openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); // A sends Bold to B so B can cover their interest and close their Trove transferBold(A, B, boldToken.balanceOf(A)); @@ -814,10 +814,10 @@ contract InterestRateAggregate is DevTestSetup { vm.warp(block.timestamp + 1 days); // Get the up-to-date entire debt - (uint256 entireDebt_B, , , , ) = troveManager.getEntireDebtAndColl(B); + (uint256 entireDebt_B, , , , ) = troveManager.getEntireDebtAndColl(BTroveId); // B closes Trove - closeTrove(B); + closeTrove(B, BTroveId); // Check balance of B reduces by the Trove's entire debt less gas comp assertEq(boldToken.balanceOf(B), bal_B - (entireDebt_B - troveManager.BOLD_GAS_COMPENSATION())); @@ -829,7 +829,7 @@ contract InterestRateAggregate is DevTestSetup { uint256 troveDebtRequest = 2000e18; // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); vm.warp(block.timestamp + 1 days); @@ -838,7 +838,7 @@ contract InterestRateAggregate is DevTestSetup { uint256 pendingAggInterest = activePool.calcPendingAggInterest(); assertGt(pendingAggInterest, 0); - changeInterestRateNoHints(A, 75e16); + changeInterestRateNoHints(A, ATroveId, 75e16); assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); } @@ -847,13 +847,13 @@ contract InterestRateAggregate is DevTestSetup { uint256 troveDebtRequest = 2000e18; // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); vm.warp(block.timestamp + 1 days); assertGt(activePool.calcPendingAggInterest(), 0); - changeInterestRateNoHints(A, 75e16); + changeInterestRateNoHints(A, ATroveId, 75e16); assertEq(activePool.calcPendingAggInterest(), 0); } @@ -863,7 +863,7 @@ contract InterestRateAggregate is DevTestSetup { uint256 troveDebtRequest = 2000e18; // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -872,7 +872,7 @@ contract InterestRateAggregate is DevTestSetup { assertLt(activePool.lastAggUpdateTime(), block.timestamp); // A changes interest rate - changeInterestRateNoHints(A, 75e16); + changeInterestRateNoHints(A, ATroveId, 75e16); // Check last agg update time increased to now assertEq(activePool.lastAggUpdateTime(), block.timestamp); @@ -883,7 +883,7 @@ contract InterestRateAggregate is DevTestSetup { uint256 troveDebtRequest = 2000e18; // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -896,7 +896,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // A changes interest rate - changeInterestRateNoHints(A, 75e16); + changeInterestRateNoHints(A, ATroveId, 75e16); // Check I-router Bold bal has increased as expected uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); @@ -909,22 +909,22 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); - uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); // fast-forward time vm.warp(block.timestamp + 1 days); uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); assertGt(aggWeightedDebtSum_1, 0); - (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); uint256 newAnnualInterestRate = 75e16; uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * newAnnualInterestRate; // A changes interest rate - changeInterestRateNoHints(A, newAnnualInterestRate); + changeInterestRateNoHints(A, ATroveId, newAnnualInterestRate); // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); @@ -935,21 +935,21 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); - uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(A); + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); assertEq(pendingRedistDebtGain, 0); - uint256 pendingInterest = troveManager.calcTroveAccruedInterest(A); + uint256 pendingInterest = troveManager.calcTroveAccruedInterest(ATroveId); assertGt(pendingInterest, 0); // Get current recorded active debt uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); // A changes interest rate - changeInterestRateNoHints(A, 75e16); + changeInterestRateNoHints(A, ATroveId, 75e16); // Check recorded debt sum increases by the pending interest assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + pendingInterest); @@ -966,7 +966,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -977,7 +977,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // A draws more debt - withdrawBold100pctMaxFee(A, debtIncrease); + withdrawBold100pctMaxFee(A, ATroveId, debtIncrease); assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest + debtIncrease); } @@ -988,14 +988,14 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); vm.warp(block.timestamp + 1 days); assertGt(activePool.calcPendingAggInterest(), 0); // A draws more debt - withdrawBold100pctMaxFee(A, debtIncrease); + withdrawBold100pctMaxFee(A, ATroveId, debtIncrease); assertEq(activePool.calcPendingAggInterest(), 0); } @@ -1006,7 +1006,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); vm.warp(block.timestamp + 1 days); @@ -1017,7 +1017,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(aggInterest, 0); // A draws more debt - withdrawBold100pctMaxFee(A, debtIncrease); + withdrawBold100pctMaxFee(A, ATroveId, debtIncrease); assertEq(boldToken.balanceOf(address(mockInterestRouter)), aggInterest); } @@ -1029,7 +1029,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1038,7 +1038,7 @@ contract InterestRateAggregate is DevTestSetup { assertLt(activePool.lastAggUpdateTime(), block.timestamp); // A draws more debt - withdrawBold100pctMaxFee(A, debtIncrease); + withdrawBold100pctMaxFee(A, ATroveId, debtIncrease); // Check last agg update time increased to now assertEq(activePool.lastAggUpdateTime(), block.timestamp); @@ -1052,21 +1052,21 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); - uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(A); + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); assertEq(pendingRedistDebtGain, 0); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); assertGt(accruedTroveInterest, 0); // Get current recorded active debt uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); // A draws more debt - withdrawBold100pctMaxFee(A, debtIncrease); + withdrawBold100pctMaxFee(A, ATroveId, debtIncrease); // Check recorded debt sum increases by the accrued interest plus debt change assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest + debtIncrease); @@ -1079,9 +1079,9 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); - uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1090,9 +1090,9 @@ contract InterestRateAggregate is DevTestSetup { assertGt(aggWeightedDebtSum_1, 0); // A draws more debt - withdrawBold100pctMaxFee(A, debtIncrease); + withdrawBold100pctMaxFee(A, ATroveId, debtIncrease); - (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * interestRate; // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. @@ -1107,7 +1107,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1118,7 +1118,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // A repays bold - repayBold(A, debtDecrease); + repayBold(A, ATroveId, debtDecrease); assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - debtDecrease); } @@ -1129,14 +1129,14 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); vm.warp(block.timestamp + 1 days); assertGt(activePool.calcPendingAggInterest(), 0); // A repays debt - repayBold(A, debtDecrease); + repayBold(A, ATroveId, debtDecrease); assertEq(activePool.calcPendingAggInterest(), 0); } @@ -1147,7 +1147,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); vm.warp(block.timestamp + 1 days); @@ -1158,7 +1158,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // A repays debt - repayBold(A, debtDecrease); + repayBold(A, ATroveId, debtDecrease); assertEq(boldToken.balanceOf(address(mockInterestRouter)), pendingAggInterest); } @@ -1169,7 +1169,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1178,7 +1178,7 @@ contract InterestRateAggregate is DevTestSetup { assertLt(activePool.lastAggUpdateTime(), block.timestamp); // A repays debt - repayBold(A, debtDecrease); + repayBold(A, ATroveId, debtDecrease); // Check last agg update time increased to now assertEq(activePool.lastAggUpdateTime(), block.timestamp); @@ -1190,21 +1190,21 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); - uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(A); + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); assertEq(pendingRedistDebtGain, 0); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); assertGt(accruedTroveInterest, 0); // Get current recorded active debt uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); // A repays debt - repayBold(A, debtDecrease); + repayBold(A, ATroveId, debtDecrease); // Check recorded debt sum increases by the accrued interest plus debt change assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest - debtDecrease); @@ -1217,9 +1217,9 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); - uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1228,9 +1228,9 @@ contract InterestRateAggregate is DevTestSetup { assertGt(aggWeightedDebtSum_1, 0); // A repays debt - repayBold(A, debtDecrease); + repayBold(A, ATroveId, debtDecrease); - (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * interestRate; // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. @@ -1245,7 +1245,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1256,7 +1256,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // A adds coll - addColl(A, collIncrease); + addColl(A, ATroveId, collIncrease); assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); } @@ -1267,14 +1267,14 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); vm.warp(block.timestamp + 1 days); assertGt(activePool.calcPendingAggInterest(), 0); // A adds coll - addColl(A, collIncrease); + addColl(A, ATroveId, collIncrease); assertEq(activePool.calcPendingAggInterest(), 0); } @@ -1285,7 +1285,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); vm.warp(block.timestamp + 1 days); @@ -1296,7 +1296,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(aggInterest, 0); // A adds coll - addColl(A, collIncrease); + addColl(A, ATroveId, collIncrease); assertEq(boldToken.balanceOf(address(mockInterestRouter)), aggInterest); } @@ -1307,7 +1307,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1316,7 +1316,7 @@ contract InterestRateAggregate is DevTestSetup { assertLt(activePool.lastAggUpdateTime(), block.timestamp); // A adds coll - addColl(A, collIncrease); + addColl(A, ATroveId, collIncrease); // Check last agg update time increased to now assertEq(activePool.lastAggUpdateTime(), block.timestamp); @@ -1328,21 +1328,21 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); - uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(A); + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); assertEq(pendingRedistDebtGain, 0); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); assertGt(accruedTroveInterest, 0); // Get current recorded active debt uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); // A adds coll - addColl(A, collIncrease); + addColl(A, ATroveId, collIncrease); // Check recorded debt sum increases by the accrued interest assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest); @@ -1355,9 +1355,9 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); - uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1366,9 +1366,9 @@ contract InterestRateAggregate is DevTestSetup { assertGt(aggWeightedDebtSum_1, 0); // A adds coll - addColl(A, collIncrease); + addColl(A, ATroveId, collIncrease); - (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * interestRate; // Weighted debt should have increased due to interest being applied @@ -1386,7 +1386,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1397,7 +1397,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // A withdraws coll - withdrawColl(A, collDecrease); + withdrawColl(A, ATroveId, collDecrease); assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); } @@ -1408,14 +1408,14 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); vm.warp(block.timestamp + 1 days); assertGt(activePool.calcPendingAggInterest(), 0); // A withdraws coll - withdrawColl(A, collDecrease); + withdrawColl(A, ATroveId, collDecrease); assertEq(activePool.calcPendingAggInterest(), 0); } @@ -1426,7 +1426,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); vm.warp(block.timestamp + 1 days); @@ -1437,7 +1437,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(aggInterest, 0); // A withdraws coll - withdrawColl(A, collDecrease); + withdrawColl(A, ATroveId, collDecrease); assertEq(boldToken.balanceOf(address(mockInterestRouter)), aggInterest); } @@ -1448,7 +1448,7 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1457,7 +1457,7 @@ contract InterestRateAggregate is DevTestSetup { assertLt(activePool.lastAggUpdateTime(), block.timestamp); // A withdraw coll - withdrawColl(A, collDecrease); + withdrawColl(A, ATroveId, collDecrease); // Check last agg update time increased to now assertEq(activePool.lastAggUpdateTime(), block.timestamp); @@ -1469,21 +1469,21 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); // fast-forward time vm.warp(block.timestamp + 1 days); - uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(A); + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); assertEq(pendingRedistDebtGain, 0); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); assertGt(accruedTroveInterest, 0); // Get current recorded active debt uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); // A withdraw coll - withdrawColl(A, collDecrease); + withdrawColl(A, ATroveId, collDecrease); // Check recorded debt sum increases by the accrued interest assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest); @@ -1496,9 +1496,9 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); - uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1507,9 +1507,9 @@ contract InterestRateAggregate is DevTestSetup { assertGt(aggWeightedDebtSum_1, 0); // A withdraw coll - withdrawColl(A, collDecrease); + withdrawColl(A, ATroveId, collDecrease); - (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * interestRate; // Weighted debt should have increased due to interest being applied @@ -1526,12 +1526,12 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = 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)); + assertTrue(troveManager.troveIsStale(ATroveId)); uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); assertGt(aggRecordedDebt_1, 0); @@ -1539,44 +1539,42 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // B applies A's pending interest - applyTroveInterestPermissionless(B, A); + applyTroveInterestPermissionless(B, ATroveId); 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); + uint256 ATroveId = 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)); + assertTrue(troveManager.troveIsStale(ATroveId)); assertGt(activePool.calcPendingAggInterest(), 0); // B applies A's pending interest - applyTroveInterestPermissionless(B, A); + applyTroveInterestPermissionless(B, ATroveId); assertEq(activePool.calcPendingAggInterest(), 0); } function testApplyTroveInterestPermissionlessMintsPendingAggInterestToRouter() public { uint256 troveDebtRequest = 2000e18; - uint256 collDecrease = 1 ether; // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = 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)); + assertTrue(troveManager.troveIsStale(ATroveId)); // Check I-router balance is 0 assertEq(boldToken.balanceOf(address(mockInterestRouter)), 0); @@ -1585,7 +1583,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // B applies A's pending interest - applyTroveInterestPermissionless(B, A); + applyTroveInterestPermissionless(B, ATroveId); // Check I-router Bold bal has increased by the pending agg interest assertEq(boldToken.balanceOf(address(mockInterestRouter)), pendingAggInterest); @@ -1596,18 +1594,18 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = 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)); + assertTrue(troveManager.troveIsStale(ATroveId)); assertGt(activePool.lastAggUpdateTime(), 0); assertLt(activePool.lastAggUpdateTime(), block.timestamp); // B applies A's pending interest - applyTroveInterestPermissionless(B, A); + applyTroveInterestPermissionless(B, ATroveId); // Check last agg update time increased to now assertEq(activePool.lastAggUpdateTime(), block.timestamp); @@ -1618,23 +1616,23 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + uint256 ATroveId = 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)); + assertTrue(troveManager.troveIsStale(ATroveId)); - uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(A); + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); assertEq(pendingRedistDebtGain, 0); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); assertGt(accruedTroveInterest, 0); // Get current recorded active debt uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); // B applies A's pending interest - applyTroveInterestPermissionless(B, A); + applyTroveInterestPermissionless(B, ATroveId); // Check recorded debt sum increases by the accrued interest assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + accruedTroveInterest); @@ -1646,22 +1644,22 @@ contract InterestRateAggregate is DevTestSetup { // A opens Trove priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); - uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); // fast-forward time such that trove is Stale vm.warp(block.timestamp + 90 days + 1); // Confirm Trove is stale - assertTrue(troveManager.troveIsStale(A)); + assertTrue(troveManager.troveIsStale(ATroveId)); uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); assertGt(aggWeightedDebtSum_1, 0); // B applies A's pending interest - applyTroveInterestPermissionless(B, A); + applyTroveInterestPermissionless(B, ATroveId); - (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * interestRate; // Weighted debt should have increased due to interest being applied @@ -1690,13 +1688,13 @@ contract InterestRateAggregate is DevTestSetup { priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 20 ether, troveDebtRequest_A, interestRate); - openTroveNoHints100pctMaxFee(B, 20 ether, troveDebtRequest_B, interestRate); - openTroveNoHints100pctMaxFee(C, 20 ether, troveDebtRequest_C, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 20 ether, troveDebtRequest_A, interestRate); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 20 ether, troveDebtRequest_B, interestRate); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, 20 ether, troveDebtRequest_C, interestRate); - uint256 recordedDebt_A = troveManager.getTroveDebt(A); - uint256 recordedDebt_B = troveManager.getTroveDebt(B); - uint256 recordedDebt_C = troveManager.getTroveDebt(C); + uint256 recordedDebt_A = troveManager.getTroveDebt(ATroveId); + uint256 recordedDebt_B = troveManager.getTroveDebt(BTroveId); + uint256 recordedDebt_C = troveManager.getTroveDebt(CTroveId); assertGt(recordedDebt_A, 0); assertGt(recordedDebt_B, 0); assertGt(recordedDebt_C, 0); @@ -1716,23 +1714,23 @@ contract InterestRateAggregate is DevTestSetup { priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 20 ether, troveDebtRequest_A, interestRate); - openTroveNoHints100pctMaxFee(B, 20 ether, troveDebtRequest_B, interestRate); - openTroveNoHints100pctMaxFee(C, 20 ether, troveDebtRequest_C, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 20 ether, troveDebtRequest_A, interestRate); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 20 ether, troveDebtRequest_B, interestRate); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, 20 ether, troveDebtRequest_C, interestRate); // Fast-forward time, accrue interest vm.warp(block.timestamp + 1 days); - uint256 recordedDebt_A = troveManager.getTroveDebt(A); - uint256 recordedDebt_B = troveManager.getTroveDebt(B); - uint256 recordedDebt_C = troveManager.getTroveDebt(C); + uint256 recordedDebt_A = troveManager.getTroveDebt(ATroveId); + uint256 recordedDebt_B = troveManager.getTroveDebt(BTroveId); + uint256 recordedDebt_C = troveManager.getTroveDebt(CTroveId); assertGt(recordedDebt_A, 0); assertGt(recordedDebt_B, 0); assertGt(recordedDebt_C, 0); - uint256 accruedInterest_A = troveManager.calcTroveAccruedInterest(A); - uint256 accruedInterest_B = troveManager.calcTroveAccruedInterest(B); - uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(C); + uint256 accruedInterest_A = troveManager.calcTroveAccruedInterest(ATroveId); + uint256 accruedInterest_B = troveManager.calcTroveAccruedInterest(BTroveId); + uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(CTroveId); assertGt(accruedInterest_A, 0); assertGt(accruedInterest_B, 0); assertGt(accruedInterest_C, 0); @@ -1755,7 +1753,7 @@ contract InterestRateAggregate is DevTestSetup { // --- withdrawETHGainToTrove --- function testWithdrawETHGainToTroveIncreasesAggRecordedDebtByAggInterest() public { - _setupForWithdrawETHGainToTrove(); + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -1766,13 +1764,13 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // A withdraws ETH gain to Trove - withdrawETHGainToTrove(A); + withdrawETHGainToTrove(A, ATroveId); assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); } function testWithdrawETHGainToTroveReducesPendingAggInterestTo0() public { - _setupForWithdrawETHGainToTrove(); + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -1781,14 +1779,14 @@ contract InterestRateAggregate is DevTestSetup { assertGt(activePool.calcPendingAggInterest(), 0); // A withdraws ETH gain to Trove - withdrawETHGainToTrove(A); + withdrawETHGainToTrove(A, ATroveId); // Check pending agg. interest reduced to 0 assertEq(activePool.calcPendingAggInterest(), 0); } function testWithdrawETHGainToTroveMintsInterestToRouter() public { - _setupForWithdrawETHGainToTrove(); + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -1801,7 +1799,7 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // A withdraws ETH gain to Trove - withdrawETHGainToTrove(A); + withdrawETHGainToTrove(A, ATroveId); // Check I-router Bold bal has increased as expected from SP deposit uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); @@ -1809,7 +1807,7 @@ contract InterestRateAggregate is DevTestSetup { } function testWithdrawETHGainToTroveUpdatesLastAggUpdateTimeToNow() public { - _setupForWithdrawETHGainToTrove(); + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -1818,14 +1816,14 @@ contract InterestRateAggregate is DevTestSetup { assertLt(activePool.lastAggUpdateTime(), block.timestamp); // A withdraws ETH gain to Trove - withdrawETHGainToTrove(A); + withdrawETHGainToTrove(A, ATroveId); // Check last agg update time increased to now assertEq(activePool.lastAggUpdateTime(), block.timestamp); } function testWithdrawETHGainToTroveChangesAggWeightedDebtSumCorrectly() public { - _setupForWithdrawETHGainToTrove(); + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1834,14 +1832,14 @@ contract InterestRateAggregate is DevTestSetup { uint256 weightedDebtSum_1 = activePool.aggWeightedDebtSum(); assertGt(weightedDebtSum_1, 0); - uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); assertGt(oldRecordedWeightedDebt, 0); // A withdraws ETH gain to Trove - withdrawETHGainToTrove(A); + withdrawETHGainToTrove(A, ATroveId); // Expect recorded weighted debt to have increased due to accrued Trove interest being applied - uint256 newRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(A); + uint256 newRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); assertGt(newRecordedWeightedDebt, oldRecordedWeightedDebt); // Expect weighted sum decreases by the old and increases by the new individual weighted Trove debt. @@ -1849,7 +1847,7 @@ contract InterestRateAggregate is DevTestSetup { } function testWithdrawETHGainToTroveChangesRecordedDebtSumCorrectly() public { - _setupForWithdrawETHGainToTrove(); + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -1858,14 +1856,14 @@ contract InterestRateAggregate is DevTestSetup { uint256 recordedDebt_1 = activePool.getRecordedDebtSum(); assertGt(recordedDebt_1, 0); - uint256 oldTroveRecordedDebt = troveManager.getTroveDebt(A); + uint256 oldTroveRecordedDebt = troveManager.getTroveDebt(ATroveId); assertGt(oldTroveRecordedDebt, 0); // A withdraws ETH gain to Trove - withdrawETHGainToTrove(A); + withdrawETHGainToTrove(A, ATroveId); // Expect recorded debt to have increased due to accrued Trove interest being applied - uint256 newTroveRecordedDebt = troveManager.getTroveDebt(A); + uint256 newTroveRecordedDebt = troveManager.getTroveDebt(ATroveId); assertGt(newTroveRecordedDebt, oldTroveRecordedDebt); // Get recorded sum after, check no change @@ -1881,7 +1879,7 @@ contract InterestRateAggregate is DevTestSetup { // --- batchLiquidateTroves (Normal Mode, offset) --- function testBatchLiquidateTrovesPureOffsetChangesAggRecordedInterestCorrectly() public { - _setupForBatchLiquidateTrovesPureOffset(); + (,,uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -1891,34 +1889,34 @@ contract InterestRateAggregate is DevTestSetup { uint256 pendingAggInterest = activePool.calcPendingAggInterest(); assertGt(pendingAggInterest, 0); - uint256 recordedDebt_C = troveManager.getTroveDebt(C); - uint256 recordedDebt_D = troveManager.getTroveDebt(D); + uint256 recordedDebt_C = troveManager.getTroveDebt(CTroveId); + uint256 recordedDebt_D = troveManager.getTroveDebt(DTroveId); assertGt(recordedDebt_C, 0); assertGt(recordedDebt_D, 0); uint256 recordedDebtInLiq = recordedDebt_C + recordedDebt_D; - uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(C); - uint256 accruedInterest_D = troveManager.calcTroveAccruedInterest(D); + uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(CTroveId); + uint256 accruedInterest_D = troveManager.calcTroveAccruedInterest(DTroveId); assertGt(accruedInterest_C, 0); assertGt(accruedInterest_D, 0); uint256 accruedInterestInLiq = accruedInterest_C + accruedInterest_D; // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check both Troves were closed by liquidation - assertEq(troveManager.getTroveStatus(C), 3); - assertEq(troveManager.getTroveStatus(D), 3); + assertEq(troveManager.getTroveStatus(CTroveId), 3); + assertEq(troveManager.getTroveStatus(DTroveId), 3); // // changes agg. recorded debt by: agg_accrued_interest - liq'd_troves_recorded_trove_debts - liq'd_troves_accrued_interest assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - recordedDebtInLiq - accruedInterestInLiq); } function testBatchLiquidateTrovesPureOffsetReducesAggPendingInterestTo0() public { - _setupForBatchLiquidateTrovesPureOffset(); + (,,uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -1926,9 +1924,9 @@ contract InterestRateAggregate is DevTestSetup { assertGt(activePool.calcPendingAggInterest(), 0); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); assertEq(activePool.calcPendingAggInterest(), 0); @@ -1936,7 +1934,7 @@ contract InterestRateAggregate is DevTestSetup { // Mints interest to Router function testBatchLiquidateTrovesPureOffsetMintsAggInterestToRouter() public { - _setupForBatchLiquidateTrovesPureOffset(); + (,,uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -1948,9 +1946,9 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check I-router Bold bal has increased as expected from liquidation @@ -1959,7 +1957,7 @@ contract InterestRateAggregate is DevTestSetup { } function testBatchLiquidateTrovesPureOffsetUpdatesLastAggInterestUpdateTimeToNow() public { - _setupForBatchLiquidateTrovesPureOffset(); + (,,uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -1968,9 +1966,9 @@ contract InterestRateAggregate is DevTestSetup { assertLt(activePool.lastAggUpdateTime(), block.timestamp); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check last agg update time increased to now @@ -1980,19 +1978,19 @@ contract InterestRateAggregate is DevTestSetup { // Removes liq'd troves' weighted recorded debts from the weighted recorded debt sum function testBatchLiquidateTrovesPureOffsetRemovesLiquidatedTrovesWeightedRecordedDebtsFromWeightedRecordedDebtSum() public { - _setupForBatchLiquidateTrovesPureOffset(); + (,,uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_C = troveManager.getTroveDebt(C); - uint256 annualInterestRate_C = troveManager.getTroveAnnualInterestRate(C); + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(CTroveId); + uint256 annualInterestRate_C = troveManager.getTroveAnnualInterestRate(CTroveId); assertGt(recordedTroveDebt_C, 0); assertGt(annualInterestRate_C, 0); uint256 weightedTroveDebt_C = recordedTroveDebt_C * annualInterestRate_C; - uint256 recordedTroveDebt_D = troveManager.getTroveDebt(D); - uint256 annualInterestRate_D = troveManager.getTroveAnnualInterestRate(D); + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(DTroveId); + uint256 annualInterestRate_D = troveManager.getTroveAnnualInterestRate(DTroveId); assertGt(recordedTroveDebt_D, 0); assertGt(annualInterestRate_D, 0); uint256 weightedTroveDebt_D = recordedTroveDebt_D * annualInterestRate_D; @@ -2001,9 +1999,9 @@ contract InterestRateAggregate is DevTestSetup { assertGt(aggWeightedDebtSum_1, 0); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check weighted recorded debt sum reduced by C and D's weighted recorded debt @@ -2011,24 +2009,24 @@ contract InterestRateAggregate is DevTestSetup { } function testBatchLiquidateTrovesPureOffsetWithNoRedistGainRemovesLiquidatedTrovesRecordedDebtsFromRecordedDebtSum() public { - _setupForBatchLiquidateTrovesPureOffset(); + (,,uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureOffset(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_C = troveManager.getTroveDebt(C); + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(CTroveId); assertGt(recordedTroveDebt_C, 0); - uint256 recordedTroveDebt_D = troveManager.getTroveDebt(D); + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(DTroveId); assertGt(recordedTroveDebt_D, 0); uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check recorded debt sum reduced by C and D's recorded debt @@ -2038,7 +2036,7 @@ contract InterestRateAggregate is DevTestSetup { // --- // --- batchLiquidateTroves (Normal Mode, redistribution) --- function testBatchLiquidateTrovesPureRedistChangesAggRecordedInterestCorrectly() public { - _setupForBatchLiquidateTrovesPureRedist(); + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -2048,36 +2046,36 @@ contract InterestRateAggregate is DevTestSetup { uint256 pendingAggInterest = activePool.calcPendingAggInterest(); assertGt(pendingAggInterest, 0); - uint256 recordedDebt_C = troveManager.getTroveDebt(C); - uint256 recordedDebt_D = troveManager.getTroveDebt(D); + uint256 recordedDebt_C = troveManager.getTroveDebt(CTroveId); + uint256 recordedDebt_D = troveManager.getTroveDebt(DTroveId); assertGt(recordedDebt_C, 0); assertGt(recordedDebt_D, 0); uint256 recordedDebtInLiq = recordedDebt_C + recordedDebt_D; - uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(C); - uint256 accruedInterest_D = troveManager.calcTroveAccruedInterest(D); + uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(CTroveId); + uint256 accruedInterest_D = troveManager.calcTroveAccruedInterest(DTroveId); assertGt(accruedInterest_C, 0); assertGt(accruedInterest_D, 0); uint256 accruedInterestInLiq = accruedInterest_C + accruedInterest_D; // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check for redist. gains - assertTrue(troveManager.hasRedistributionGains(A)); + assertTrue(troveManager.hasRedistributionGains(ATroveId)); // Check both Troves were closed by liquidation - assertEq(troveManager.getTroveStatus(C), 3); - assertEq(troveManager.getTroveStatus(D), 3); + assertEq(troveManager.getTroveStatus(CTroveId), 3); + assertEq(troveManager.getTroveStatus(DTroveId), 3); // // changes agg. recorded debt by: agg_accrued_interest - liq'd_troves_recorded_trove_debts - liq'd_troves_accrued_interest assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - recordedDebtInLiq - accruedInterestInLiq); } function testBatchLiquidateTrovesPureRedistReducesAggPendingInterestTo0() public { - _setupForBatchLiquidateTrovesPureRedist(); + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -2085,19 +2083,19 @@ contract InterestRateAggregate is DevTestSetup { assertGt(activePool.calcPendingAggInterest(), 0); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check for redist. gains - assertTrue(troveManager.hasRedistributionGains(A)); + assertTrue(troveManager.hasRedistributionGains(ATroveId)); assertEq(activePool.calcPendingAggInterest(), 0); } // Mints interest to Router function testBatchLiquidateTrovesPureRedistMintsAggInterestToRouter() public { - _setupForBatchLiquidateTrovesPureRedist(); + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -2109,12 +2107,12 @@ contract InterestRateAggregate is DevTestSetup { assertGt(pendingAggInterest, 0); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check for redist. gains - assertTrue(troveManager.hasRedistributionGains(A)); + assertTrue(troveManager.hasRedistributionGains(ATroveId)); // Check I-router Bold bal has increased as expected from liquidation uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); @@ -2122,7 +2120,7 @@ contract InterestRateAggregate is DevTestSetup { } function testBatchLiquidateTrovesPureRedistUpdatesLastAggInterestUpdateTimeToNow() public { - _setupForBatchLiquidateTrovesPureRedist(); + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); @@ -2131,12 +2129,12 @@ contract InterestRateAggregate is DevTestSetup { assertLt(activePool.lastAggUpdateTime(), block.timestamp); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check for redist. gains - assertTrue(troveManager.hasRedistributionGains(A)); + assertTrue(troveManager.hasRedistributionGains(ATroveId)); // Check last agg update time increased to now assertEq(activePool.lastAggUpdateTime(), block.timestamp); @@ -2145,19 +2143,19 @@ contract InterestRateAggregate is DevTestSetup { // Removes liq'd troves' weighted recorded debts from the weighted recorded debt sum function testBatchLiquidateTrovesPureRedistRemovesLiquidatedTrovesWeightedRecordedDebtsFromWeightedRecordedDebtSum() public { - _setupForBatchLiquidateTrovesPureRedist(); + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_C = troveManager.getTroveDebt(C); - uint256 annualInterestRate_C = troveManager.getTroveAnnualInterestRate(C); + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(CTroveId); + uint256 annualInterestRate_C = troveManager.getTroveAnnualInterestRate(CTroveId); assertGt(recordedTroveDebt_C, 0); assertGt(annualInterestRate_C, 0); uint256 weightedTroveDebt_C = recordedTroveDebt_C * annualInterestRate_C; - uint256 recordedTroveDebt_D = troveManager.getTroveDebt(D); - uint256 annualInterestRate_D = troveManager.getTroveAnnualInterestRate(D); + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(DTroveId); + uint256 annualInterestRate_D = troveManager.getTroveAnnualInterestRate(DTroveId); assertGt(recordedTroveDebt_D, 0); assertGt(annualInterestRate_D, 0); uint256 weightedTroveDebt_D = recordedTroveDebt_D * annualInterestRate_D; @@ -2166,56 +2164,56 @@ contract InterestRateAggregate is DevTestSetup { assertGt(aggWeightedDebtSum_1, 0); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check for redist. gains - assertTrue(troveManager.hasRedistributionGains(A)); + assertTrue(troveManager.hasRedistributionGains(ATroveId)); // Check weighted recorded debt sum reduced by C and D's weighted recorded debt assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - (weightedTroveDebt_C + weightedTroveDebt_D)); } function testBatchLiquidateTrovesPureRedistWithNoRedistGainRemovesLiquidatedTrovesRecordedDebtsFromRecordedDebtSum() public { - _setupForBatchLiquidateTrovesPureRedist(); + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_C = troveManager.getTroveDebt(C); + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(CTroveId); assertGt(recordedTroveDebt_C, 0); - uint256 recordedTroveDebt_D = troveManager.getTroveDebt(D); + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(DTroveId); assertGt(recordedTroveDebt_D, 0); uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check for redist. gains - assertTrue(troveManager.hasRedistributionGains(A)); + assertTrue(troveManager.hasRedistributionGains(ATroveId)); // Check recorded debt sum reduced by C and D's recorded debt assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 - (recordedTroveDebt_C + recordedTroveDebt_D)); } function testBatchLiquidateTrovesPureRedistWithNoRedistGainAddsLiquidatedTrovesEntireDebtsToDefaultPoolDebtSum() public { - _setupForBatchLiquidateTrovesPureRedist(); + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); // fast-forward time so interest accrues vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_C = troveManager.getTroveDebt(C); - uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(C); + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(CTroveId); + uint256 accruedInterest_C = troveManager.calcTroveAccruedInterest(CTroveId); assertGt(recordedTroveDebt_C, 0); assertGt(accruedInterest_C, 0); - uint256 recordedTroveDebt_D = troveManager.getTroveDebt(D); - uint256 accruedInterest_D = troveManager.calcTroveAccruedInterest(C); + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(DTroveId); + uint256 accruedInterest_D = troveManager.calcTroveAccruedInterest(CTroveId); assertGt(recordedTroveDebt_D, 0); assertGt(accruedInterest_D, 0); @@ -2225,12 +2223,12 @@ contract InterestRateAggregate is DevTestSetup { assertEq(defaultPoolDebt, 0); // A liquidates C and D - address[] memory trovesToLiq = new address[](2); - trovesToLiq[0] = C; - trovesToLiq[1] = D; + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; batchLiquidateTroves(A, trovesToLiq); // Check for redist. gains - assertTrue(troveManager.hasRedistributionGains(A)); + assertTrue(troveManager.hasRedistributionGains(ATroveId)); // Check recorded debt sum reduced by C and D's entire debts assertEq(defaultPool.getBoldDebt(), debtInLiq); diff --git a/contracts/src/test/interestRateBasic.t.sol b/contracts/src/test/interestRateBasic.t.sol index 15613255..d3e8154f 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -8,31 +8,32 @@ contract InterestRateBasic is DevTestSetup { function testOpenTroveSetsInterestRate() public { priceFeed.setPrice(2000e18); - uint256 A_Id = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); - assertEq(troveManager.getTroveAnnualInterestRate(A_Id), 0); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); + assertEq(troveManager.getTroveAnnualInterestRate(ATroveId), 0); - uint256 B_Id = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 1); - assertEq(troveManager.getTroveAnnualInterestRate(B_Id), 1); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 1); + assertEq(troveManager.getTroveAnnualInterestRate(BTroveId), 1); - uint256 C_Id = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 37e16); - assertEq(troveManager.getTroveAnnualInterestRate(C_Id), 37e16); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 37e16); + assertEq(troveManager.getTroveAnnualInterestRate(CTroveId), 37e16); - uint256 D_Id = openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, 1e18); - assertEq(troveManager.getTroveAnnualInterestRate(D_Id), 1e18); + uint256 DTroveId = openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, 1e18); + assertEq(troveManager.getTroveAnnualInterestRate(DTroveId), 1e18); } function testOpenTroveSetsTroveLastDebtUpdateTimeToNow() public { priceFeed.setPrice(2000e18); - assertEq(troveManager.getTroveLastDebtUpdateTime(A), 0); - assertEq(troveManager.getTroveLastDebtUpdateTime(B), 0); + assertEq(troveManager.getTroveLastDebtUpdateTime(addressToTroveId(A)), 0); + assertEq(troveManager.getTroveLastDebtUpdateTime(addressToTroveId(B)), 0); - openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); - assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); vm.warp(block.timestamp + 1000); - openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 1); - assertEq(troveManager.getTroveLastDebtUpdateTime(B), block.timestamp); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 1); + assertEq(troveManager.getTroveLastDebtUpdateTime(BTroveId), block.timestamp); } + function testOpenTroveInsertsToCorrectPositionInSortedList() public { priceFeed.setPrice(2000e18); @@ -44,31 +45,31 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate_E = 4e17; // B and D open - uint256 B_Id = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, interestRate_B); - uint256 D_Id = openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, interestRate_D); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, interestRate_B); + uint256 DTroveId = openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, interestRate_D); // Check initial list order - expect [B, D] // B - assertEq(sortedTroves.getNext(B_Id), 0); // tail - assertEq(sortedTroves.getPrev(B_Id), D_Id); + assertEq(sortedTroves.getNext(BTroveId), 0); // tail + assertEq(sortedTroves.getPrev(BTroveId), DTroveId); // D - assertEq(sortedTroves.getNext(D_Id), B_Id); - assertEq(sortedTroves.getPrev(D_Id), 0); // head + assertEq(sortedTroves.getNext(DTroveId), BTroveId); + assertEq(sortedTroves.getPrev(DTroveId), 0); // head // C opens. Expect to be inserted between B and D - uint256 C_Id = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, interestRate_C); - assertEq(sortedTroves.getNext(C_Id), B_Id); - assertEq(sortedTroves.getPrev(C_Id), D_Id); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, interestRate_C); + assertEq(sortedTroves.getNext(CTroveId), BTroveId); + assertEq(sortedTroves.getPrev(CTroveId), DTroveId); // A opens. Expect to be inserted at the tail, below B - uint256 A_Id = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, interestRate_A); - assertEq(sortedTroves.getNext(A_Id), 0); - assertEq(sortedTroves.getPrev(A_Id), B_Id); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, interestRate_A); + assertEq(sortedTroves.getNext(ATroveId), 0); + assertEq(sortedTroves.getPrev(ATroveId), BTroveId); // E opens. Expect to be inserted at the head, above D - uint256 E_Id = openTroveNoHints100pctMaxFee(E, 2 ether, 2000e18, interestRate_E); - assertEq(sortedTroves.getNext(E_Id), D_Id); - assertEq(sortedTroves.getPrev(E_Id), 0); + uint256 ETroveId = openTroveNoHints100pctMaxFee(E, 2 ether, 2000e18, interestRate_E); + assertEq(sortedTroves.getNext(ETroveId), DTroveId); + assertEq(sortedTroves.getPrev(ETroveId), 0); } @@ -87,16 +88,16 @@ contract InterestRateBasic is DevTestSetup { priceFeed.setPrice(2000e18); // A opens Trove with valid annual interest rate ... - uint256 A_Id = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 37e16); - assertEq(troveManager.getTroveAnnualInterestRate(A_Id), 37e16); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 37e16); + assertEq(troveManager.getTroveAnnualInterestRate(ATroveId), 37e16); // ... then tries to adjust it to an invalid value vm.startPrank(A); vm.expectRevert(); - borrowerOperations.adjustTroveInterestRate(A_Id, 1e18 + 1, 0, 0); + borrowerOperations.adjustTroveInterestRate(ATroveId, 1e18 + 1, 0, 0); vm.expectRevert(); - borrowerOperations.adjustTroveInterestRate(A_Id, 42e18, 0, 0); + borrowerOperations.adjustTroveInterestRate(ATroveId, 42e18, 0, 0); } // --- adjustTroveInterestRate --- @@ -105,168 +106,168 @@ contract InterestRateBasic is DevTestSetup { priceFeed.setPrice(2000e18); // A, B, C opens Troves with valid annual interest rates - uint256 A_Id = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); - uint256 B_Id = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 5e17); - uint256 C_Id = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 5e17); - assertEq(troveManager.getTroveAnnualInterestRate(A_Id), 5e17); - assertEq(troveManager.getTroveAnnualInterestRate(B_Id), 5e17); - assertEq(troveManager.getTroveAnnualInterestRate(C_Id), 5e17); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 5e17); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 5e17); + assertEq(troveManager.getTroveAnnualInterestRate(ATroveId), 5e17); + assertEq(troveManager.getTroveAnnualInterestRate(BTroveId), 5e17); + assertEq(troveManager.getTroveAnnualInterestRate(CTroveId), 5e17); - changeInterestRateNoHints(A, A_Id, 0); - assertEq(troveManager.getTroveAnnualInterestRate(A_Id), 0); + changeInterestRateNoHints(A, ATroveId, 0); + assertEq(troveManager.getTroveAnnualInterestRate(ATroveId), 0); - changeInterestRateNoHints(B, B_Id, 6e17); - assertEq(troveManager.getTroveAnnualInterestRate(B_Id), 6e17); + changeInterestRateNoHints(B, BTroveId, 6e17); + assertEq(troveManager.getTroveAnnualInterestRate(BTroveId), 6e17); - changeInterestRateNoHints(C, C_Id, 1e18); - assertEq(troveManager.getTroveAnnualInterestRate(C_Id), 1e18); + changeInterestRateNoHints(C, CTroveId, 1e18); + assertEq(troveManager.getTroveAnnualInterestRate(CTroveId), 1e18); } function testAdjustTroveInterestRateSetsTroveLastDebtUpdateTimeToNow() public { priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); vm.warp(block.timestamp + 1 days); - assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); - changeInterestRateNoHints(A, 75e16); + changeInterestRateNoHints(A, ATroveId, 75e16); - assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); } function testAdjustTroveInterestRateSetsReducesPendingInterestTo0() public { priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); vm.warp(block.timestamp + 1 days); - assertGt(troveManager.calcTroveAccruedInterest(A), 0); + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); - changeInterestRateNoHints(A, 75e16); + changeInterestRateNoHints(A, ATroveId, 75e16); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); } function testAdjustTroveInterestRateDoesNotChangeEntireTroveDebt() public { priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); vm.warp(block.timestamp + 1 days); - (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertGt(entireTroveDebt_1, 0); - changeInterestRateNoHints(A, 75e16); + changeInterestRateNoHints(A, ATroveId, 75e16); - (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertEq(entireTroveDebt_1, entireTroveDebt_2); } function testAdjustTroveInterestRateNoRedistGainsIncreasesRecordedDebtByAccruedInterest() public { priceFeed.setPrice(2000e18); - openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); assertGt(recordedTroveDebt_1, 0); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); - changeInterestRateNoHints(A, 75e16); + changeInterestRateNoHints(A, ATroveId, 75e16); - uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); } function testAdjustTroveInterestRateInsertsToCorrectPositionInSortedList() public { priceFeed.setPrice(2000e18); - uint256 A_Id = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); - uint256 B_Id = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 2e17); - uint256 C_Id = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 3e17); - uint256 D_Id = openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, 4e17); - uint256 E_Id = openTroveNoHints100pctMaxFee(E, 2 ether, 2000e18, 5e17); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 2e17); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 3e17); + uint256 DTroveId = openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, 4e17); + uint256 ETroveId = openTroveNoHints100pctMaxFee(E, 2 ether, 2000e18, 5e17); // Check initial sorted list order - expect [A:10%, B:02%, C:30%, D:40%, E:50%] // A - assertEq(sortedTroves.getNext(A_Id), 0); // tail - assertEq(sortedTroves.getPrev(A_Id), B_Id); + assertEq(sortedTroves.getNext(ATroveId), 0); // tail + assertEq(sortedTroves.getPrev(ATroveId), BTroveId); // B - assertEq(sortedTroves.getNext(B_Id), A_Id); - assertEq(sortedTroves.getPrev(B_Id), C_Id); + assertEq(sortedTroves.getNext(BTroveId), ATroveId); + assertEq(sortedTroves.getPrev(BTroveId), CTroveId); // C - assertEq(sortedTroves.getNext(C_Id), B_Id); - assertEq(sortedTroves.getPrev(C_Id), D_Id); + assertEq(sortedTroves.getNext(CTroveId), BTroveId); + assertEq(sortedTroves.getPrev(CTroveId), DTroveId); // D - assertEq(sortedTroves.getNext(D_Id), C_Id); - assertEq(sortedTroves.getPrev(D_Id), E_Id); + assertEq(sortedTroves.getNext(DTroveId), CTroveId); + assertEq(sortedTroves.getPrev(DTroveId), ETroveId); // E - assertEq(sortedTroves.getNext(E_Id), D_Id); - assertEq(sortedTroves.getPrev(E_Id), 0); // head + assertEq(sortedTroves.getNext(ETroveId), DTroveId); + assertEq(sortedTroves.getPrev(ETroveId), 0); // head // C sets rate to 0%, moves to tail - expect [C:0%, A:10%, B:20%, D:40%, E:50%] - changeInterestRateNoHints(C, C_Id, 0); - assertEq(sortedTroves.getNext(C_Id), 0); - assertEq(sortedTroves.getPrev(C_Id), A_Id); + changeInterestRateNoHints(C, CTroveId, 0); + assertEq(sortedTroves.getNext(CTroveId), 0); + assertEq(sortedTroves.getPrev(CTroveId), ATroveId); // D sets rate to 7%, moves to head - expect [C:0%, A:10%, B:20%, E:50%, D:70%] - changeInterestRateNoHints(D, D_Id, 7e17); - assertEq(sortedTroves.getNext(D_Id), E_Id); - assertEq(sortedTroves.getPrev(D_Id), 0); + changeInterestRateNoHints(D, DTroveId, 7e17); + assertEq(sortedTroves.getNext(DTroveId), ETroveId); + assertEq(sortedTroves.getPrev(DTroveId), 0); // A sets rate to 6%, moves up 2 positions - expect [C:0%, B:20%, E:50%, A:60%, D:70%] - changeInterestRateNoHints(A, A_Id, 6e17); - assertEq(sortedTroves.getNext(A_Id), E_Id); - assertEq(sortedTroves.getPrev(A_Id), D_Id); + changeInterestRateNoHints(A, ATroveId, 6e17); + assertEq(sortedTroves.getNext(ATroveId), ETroveId); + assertEq(sortedTroves.getPrev(ATroveId), DTroveId); } function testAdjustTroveDoesNotChangeListPositions() public { priceFeed.setPrice(2000e18); // Troves opened in ascending order of interest rate - uint256 A_Id = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); - uint256 B_Id = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 2e17); - uint256 C_Id = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 3e17); - uint256 D_Id = openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, 4e17); - uint256 E_Id = openTroveNoHints100pctMaxFee(E, 2 ether, 2000e18, 5e17); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 2e17); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 3e17); + uint256 DTroveId = openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, 4e17); + uint256 ETroveId = openTroveNoHints100pctMaxFee(E, 2 ether, 2000e18, 5e17); // Check A's neighbors - assertEq(sortedTroves.getNext(A_Id), 0); // tail - assertEq(sortedTroves.getPrev(A_Id), B_Id); + assertEq(sortedTroves.getNext(ATroveId), 0); // tail + assertEq(sortedTroves.getPrev(ATroveId), BTroveId); // Adjust A's coll + debt - adjustTrove100pctMaxFee(A, A_Id, 10 ether, 5000e18, true, true); + adjustTrove100pctMaxFee(A, ATroveId, 10 ether, 5000e18, true, true); // Check A's neighbors unchanged - assertEq(sortedTroves.getNext(A_Id), 0); // tail - assertEq(sortedTroves.getPrev(A_Id), B_Id); + assertEq(sortedTroves.getNext(ATroveId), 0); // tail + assertEq(sortedTroves.getPrev(ATroveId), BTroveId); // Check C's neighbors - assertEq(sortedTroves.getNext(C_Id), B_Id); - assertEq(sortedTroves.getPrev(C_Id), D_Id); + assertEq(sortedTroves.getNext(CTroveId), BTroveId); + assertEq(sortedTroves.getPrev(CTroveId), DTroveId); // Adjust C's coll + debt - adjustTrove100pctMaxFee(C, C_Id, 10 ether, 5000e18, true, true); + adjustTrove100pctMaxFee(C, CTroveId, 10 ether, 5000e18, true, true); // Check C's neighbors unchanged - assertEq(sortedTroves.getNext(C_Id), B_Id); - assertEq(sortedTroves.getPrev(C_Id), D_Id); + assertEq(sortedTroves.getNext(CTroveId), BTroveId); + assertEq(sortedTroves.getPrev(CTroveId), DTroveId); // Check E's neighbors - assertEq(sortedTroves.getNext(E_Id), D_Id); - assertEq(sortedTroves.getPrev(E_Id), 0); // head + assertEq(sortedTroves.getNext(ETroveId), DTroveId); + assertEq(sortedTroves.getPrev(ETroveId), 0); // head // Adjust E's coll + debt - adjustTrove100pctMaxFee(E, E_Id, 10 ether, 5000e18, true, true); + adjustTrove100pctMaxFee(E, ETroveId, 10 ether, 5000e18, true, true); // Check E's neighbors unchanged - assertEq(sortedTroves.getNext(E_Id), D_Id); - assertEq(sortedTroves.getPrev(E_Id), 0); // head + assertEq(sortedTroves.getNext(ETroveId), DTroveId); + assertEq(sortedTroves.getPrev(ETroveId), 0); // head } // --- withdrawBold --- @@ -284,7 +285,7 @@ contract InterestRateBasic is DevTestSetup { assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); // A draws more debt - withdrawBold100pctMaxFee(ATroveId, boldWithdrawal); + withdrawBold100pctMaxFee(A, ATroveId, boldWithdrawal); assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); } @@ -294,16 +295,16 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 boldWithdrawal = 500e18; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - assertGt(troveManager.calcTroveAccruedInterest(A), 0); + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); // A draws more debt - withdrawBold100pctMaxFee(A, boldWithdrawal); + withdrawBold100pctMaxFee(A, ATroveId, boldWithdrawal); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); } function testWithdrawBoldIncreasesEntireTroveDebtByWithdrawnAmount() public { @@ -312,17 +313,17 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 boldWithdrawal = 500e18; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertGt(entireTroveDebt_1, 0); // A draws more debt - withdrawBold100pctMaxFee(A, boldWithdrawal); + withdrawBold100pctMaxFee(A, ATroveId, boldWithdrawal); - (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertEq(entireTroveDebt_2, entireTroveDebt_1 + boldWithdrawal); } @@ -333,17 +334,17 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 boldWithdrawal = 500e18; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); // A draws more debt - withdrawBold100pctMaxFee(A, boldWithdrawal); + withdrawBold100pctMaxFee(A, ATroveId, boldWithdrawal); - uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest + boldWithdrawal); } @@ -356,16 +357,16 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 boldRepayment = 500e18; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); // A repays bold - repayBold(A, boldRepayment); + repayBold(A, ATroveId, boldRepayment); - assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); } function testRepayBoldReducesTroveAccruedInterestTo0() public { @@ -374,16 +375,16 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 boldRepayment = 500e18; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - assertGt(troveManager.calcTroveAccruedInterest(A), 0); + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); // A repays bold - repayBold(A, boldRepayment); + repayBold(A, ATroveId, boldRepayment); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); } function testRepayBoldReducesEntireTroveDebtByRepaidAmount() public { @@ -392,17 +393,17 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 boldRepayment = 500e18; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertGt(entireTroveDebt_1, 0); // A repays bold - repayBold(A, boldRepayment); + repayBold(A, ATroveId, boldRepayment); - (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertEq(entireTroveDebt_2, entireTroveDebt_1 - boldRepayment); @@ -414,17 +415,17 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 boldRepayment = 500e18; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); // A repays bold - repayBold(A, boldRepayment); + repayBold(A, ATroveId, boldRepayment); - uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest - boldRepayment); } @@ -444,7 +445,7 @@ contract InterestRateBasic is DevTestSetup { assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); // A adds coll - addColl(ATroveId, collIncrease); + addColl(A, ATroveId, collIncrease); assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); } @@ -455,16 +456,16 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 collIncrease = 1 ether; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - assertGt(troveManager.calcTroveAccruedInterest(A), 0); + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); // A adds coll - addColl(A, collIncrease); + addColl(A, ATroveId, collIncrease); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); } function testAddCollDoesntChangeEntireTroveDebt() public { @@ -473,17 +474,17 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 collIncrease = 1 ether; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertGt(entireTroveDebt_1, 0); // A adds coll - addColl(A, collIncrease); + addColl(A, ATroveId, collIncrease); - (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertEq(entireTroveDebt_2, entireTroveDebt_1); } @@ -494,17 +495,17 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 collIncrease = 1 ether; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); // A adds coll - addColl(A, collIncrease); + addColl(A, ATroveId, collIncrease); - uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); } @@ -517,16 +518,16 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 collDecrease = 1 ether; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); // A withdraws coll - withdrawColl(A, collDecrease); + withdrawColl(A, ATroveId, collDecrease); - assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); } function testWithdrawCollReducesTroveAccruedInterestTo0() public { @@ -535,16 +536,16 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 collDecrease = 1 ether; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - assertGt(troveManager.calcTroveAccruedInterest(A), 0); + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); // A withdraws coll - withdrawColl(A, collDecrease); + withdrawColl(A, ATroveId, collDecrease); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); } function testWithdrawCollDoesntChangeEntireTroveDebt() public { @@ -553,17 +554,17 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 collDecrease = 1 ether; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertGt(entireTroveDebt_1, 0); // A withdraws coll - withdrawColl(A, collDecrease); + withdrawColl(A, ATroveId, collDecrease); - (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertEq(entireTroveDebt_2, entireTroveDebt_1); } @@ -574,17 +575,17 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate = 25e16; uint256 collDecrease = 1 ether; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); vm.warp(block.timestamp + 1 days); - uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); // A withdraws coll - withdrawColl(A, collDecrease); + withdrawColl(A, ATroveId, collDecrease); - uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); } @@ -596,19 +597,19 @@ contract InterestRateBasic is DevTestSetup { uint256 troveDebtRequest = 2000e18; uint256 interestRate = 25e16; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = 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)); + assertTrue(troveManager.troveIsStale(ATroveId)); - assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); // B applies A's pending interest - applyTroveInterestPermissionless(B, A); + applyTroveInterestPermissionless(B, ATroveId); - assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); } function testApplyTroveInterestPermissionlessReducesTroveAccruedInterestTo0() public { @@ -616,19 +617,19 @@ contract InterestRateBasic is DevTestSetup { uint256 troveDebtRequest = 2000e18; uint256 interestRate = 25e16; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = 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)); + assertTrue(troveManager.troveIsStale(ATroveId)); - assertGt(troveManager.calcTroveAccruedInterest(A), 0); + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); // B applies A's pending interest - applyTroveInterestPermissionless(B, A); + applyTroveInterestPermissionless(B, ATroveId); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); } function testApplyTroveInterestPermissionlessDoesntChangeEntireTroveDebt() public { @@ -636,20 +637,20 @@ contract InterestRateBasic is DevTestSetup { uint256 troveDebtRequest = 2000e18; uint256 interestRate = 25e16; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = 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)); + assertTrue(troveManager.troveIsStale(ATroveId)); - (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertGt(entireTroveDebt_1, 0); // B applies A's pending interest - applyTroveInterestPermissionless(B, A); + applyTroveInterestPermissionless(B, ATroveId); - (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); assertEq(entireTroveDebt_2, entireTroveDebt_1); } @@ -659,20 +660,20 @@ contract InterestRateBasic is DevTestSetup { uint256 troveDebtRequest = 2000e18; uint256 interestRate = 25e16; - openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + uint256 ATroveId = 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)); + assertTrue(troveManager.troveIsStale(ATroveId)); - uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); // B applies A's pending interest - applyTroveInterestPermissionless(B, A); + applyTroveInterestPermissionless(B, ATroveId); - uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); } @@ -680,31 +681,31 @@ contract InterestRateBasic is DevTestSetup { // --- withdrawETHGainToTrove tests --- function testWithdrawETHGainToTroveSetsTroveLastDebtUpdateTimeToNow() public { - _setupForWithdrawETHGainToTrove(); + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); // Fast-forward time vm.warp(block.timestamp + 1 days); - assertLt(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); // A withdraws ETH gain to Trove - withdrawETHGainToTrove(A); + withdrawETHGainToTrove(A, ATroveId); - assertEq(troveManager.getTroveLastDebtUpdateTime(A), block.timestamp); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); } function testWithdrawETHGainToTroveReducesTroveAccruedInterestTo0() public { - _setupForWithdrawETHGainToTrove(); + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); // Fast-forward time vm.warp(block.timestamp + 1 days); - assertGt(troveManager.calcTroveAccruedInterest(A), 0); + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); // A withdraws ETH gain to Trove - withdrawETHGainToTrove(A); + withdrawETHGainToTrove(A, ATroveId); - assertEq(troveManager.calcTroveAccruedInterest(A), 0); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); } function testWithdrawETHGainToTroveDoesntChangeEntireTroveDebt() public { @@ -713,16 +714,16 @@ contract InterestRateBasic is DevTestSetup { // // Fast-forward time // vm.warp(block.timestamp + 90 days); - // (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(A); + // (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); // assertGt(entireTroveDebt_1, 0); // console.log(troveManager.getCurrentICR(A, 1000e18), "A ICR before"); // // A withdraws ETH gain to Trove - // withdrawETHGainToTrove(A); + // withdrawETHGainToTrove(A, ATroveId); // console.log(troveManager.getCurrentICR(A, 1000e18), "A ICR after"); - // (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(A); + // (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); // console.log(entireTroveDebt_2, "entireTroveDebt_2"); // console.log(entireTroveDebt_1, "entireTroveDebt_1"); @@ -730,18 +731,18 @@ contract InterestRateBasic is DevTestSetup { } function testWithdrawETHGainToTroveIncreasesRecordedTroveDebtByAccruedInterest() public { - _setupForWithdrawETHGainToTrove(); + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); // Fast-forward time vm.warp(block.timestamp + 90 days + 1); - uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(A); - uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(A); + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); // A withdraws ETH gain to Trove - withdrawETHGainToTrove(A); + withdrawETHGainToTrove(A, ATroveId); - uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(A); + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); } diff --git a/contracts/test/AccessControlTest.js b/contracts/test/AccessControlTest.js index 1f569904..4e59f5a0 100644 --- a/contracts/test/AccessControlTest.js +++ b/contracts/test/AccessControlTest.js @@ -145,11 +145,11 @@ contract( } }); - // increaseTroveColl - it("increaseTroveColl(): reverts when called by an account that is not BorrowerOperations", async () => { + // updateTroveColl + it("updateTroveColl(): reverts when called by an account that is not BorrowerOperations", async () => { // Attempt call from alice try { - const txAlice = await troveManager.increaseTroveColl(bob, th.addressToTroveId(bob), 100, { + const txAlice = await troveManager.updateTroveColl(bob, th.addressToTroveId(bob), 100, true, { from: alice, }); } catch (err) { @@ -158,37 +158,11 @@ contract( } }); - // decreaseTroveColl - it("decreaseTroveColl(): reverts when called by an account that is not BorrowerOperations", async () => { + // updateTroveDebt + it("updateTroveDebt(): reverts when called by an account that is not BorrowerOperations", async () => { // Attempt call from alice try { - const txAlice = await troveManager.decreaseTroveColl(bob, th.addressToTroveId(bob), 100, { - from: alice, - }); - } catch (err) { - assert.include(err.message, "revert"); - // assert.include(err.message, "Caller is not the BorrowerOperations contract") - } - }); - - // increaseTroveDebt - it("increaseTroveDebt(): reverts when called by an account that is not BorrowerOperations", async () => { - // Attempt call from alice - try { - const txAlice = await troveManager.increaseTroveDebt(bob, th.addressToTroveId(bob), 100, { - from: alice, - }); - } catch (err) { - assert.include(err.message, "revert"); - // assert.include(err.message, "Caller is not the BorrowerOperations contract") - } - }); - - // decreaseTroveDebt - it("decreaseTroveDebt(): reverts when called by an account that is not BorrowerOperations", async () => { - // Attempt call from alice - try { - const txAlice = await troveManager.decreaseTroveDebt(bob, th.addressToTroveId(bob), 100, { + const txAlice = await troveManager.updateTroveDebt(bob, th.addressToTroveId(bob), 100, true, { from: alice, }); } catch (err) { diff --git a/contracts/test/BorrowerOperationsTest.js b/contracts/test/BorrowerOperationsTest.js index f0c1dd49..6664e73c 100644 --- a/contracts/test/BorrowerOperationsTest.js +++ b/contracts/test/BorrowerOperationsTest.js @@ -2303,9 +2303,6 @@ contract("BorrowerOperations", async (accounts) => { true, { from: alice } ); - - const debtAfter = await getTroveEntireDebt(alice); - const collAfter = await getTroveEntireColl(alice); const debtAfter = await getTroveEntireDebt(aliceTroveId); const collAfter = await getTroveEntireColl(aliceTroveId); @@ -3403,7 +3400,7 @@ contract("BorrowerOperations", async (accounts) => { assert.equal(bob_BoldDebtRewardSnapshot_Before, 0); const defaultPool_ETH = await defaultPool.getETHBalance(); - const defaultPool_BoldDebt = await defaultPool.getRecordedDebtSum(); + const defaultPool_BoldDebt = await defaultPool.getBoldDebt(); // Carol's liquidated coll (1 ETH) and drawn debt should have entered the Default Pool assert.isAtMost(th.getDifference(defaultPool_ETH, liquidatedColl_C), 100); @@ -3422,7 +3419,7 @@ contract("BorrowerOperations", async (accounts) => { const defaultPool_ETH_afterAliceCloses = await defaultPool.getETHBalance(); const defaultPool_BoldDebt_afterAliceCloses = - await defaultPool.getRecordedDebtSum(); + await defaultPool.getBoldDebt(); assert.isAtMost( th.getDifference( @@ -3455,7 +3452,7 @@ contract("BorrowerOperations", async (accounts) => { const defaultPool_ETH_afterBobCloses = await defaultPool.getETHBalance(); const defaultPool_BoldDebt_afterBobCloses = - await defaultPool.getRecordedDebtSum(); + await defaultPool.getBoldDebt(); assert.isAtMost( th.getDifference(defaultPool_ETH_afterBobCloses, 0), diff --git a/contracts/test/TroveManagerTest.js b/contracts/test/TroveManagerTest.js index d114b2a5..221fb937 100644 --- a/contracts/test/TroveManagerTest.js +++ b/contracts/test/TroveManagerTest.js @@ -219,7 +219,7 @@ contract("TroveManager", async (accounts) => { await contracts.WETH.balanceOf(defaultPool.address) ).toString(); const defaultPool_BoldDebt_Before = ( - await defaultPool.getRecordedDebtSum() + await defaultPool.getBoldDebt() ).toString(); assert.equal(defaultPool_ETH_Before, "0"); @@ -241,7 +241,7 @@ contract("TroveManager", async (accounts) => { await contracts.WETH.balanceOf(defaultPool.address) ).toString(); const defaultPool_BoldDebt_After = ( - await defaultPool.getRecordedDebtSum() + await defaultPool.getBoldDebt() ).toString(); const defaultPool_ETH = th.applyLiquidationFee(B_collateral); @@ -434,7 +434,7 @@ contract("TroveManager", async (accounts) => { 100 ); - assert.isTrue(await sortedTroves.contains(bob)); + assert.isTrue(await sortedTroves.contains(bobTroveId)); th.logBN("bob icr", await troveManager.getCurrentICR(bob, await priceFeed.getPrice())); // Bob now withdraws Bold, bringing his ICR to 1.11 const { increasedTotalDebt: B_increasedTotalDebt } = await withdrawBold({ @@ -1138,15 +1138,15 @@ contract("TroveManager", async (accounts) => { // Liquidate A, B and C const activeBoldDebt_0 = await activePool.getRecordedDebtSum(); - const defaultBoldDebt_0 = await defaultPool.getRecordedDebtSum(); + const defaultBoldDebt_0 = await defaultPool.getBoldDebt(); await troveManager.liquidate(aliceTroveId); const activeBoldDebt_A = await activePool.getRecordedDebtSum(); - const defaultBoldDebt_A = await defaultPool.getRecordedDebtSum(); + const defaultBoldDebt_A = await defaultPool.getBoldDebt(); await troveManager.liquidate(bobTroveId); const activeBoldDebt_B = await activePool.getRecordedDebtSum(); - const defaultBoldDebt_B = await defaultPool.getRecordedDebtSum(); + const defaultBoldDebt_B = await defaultPool.getBoldDebt(); await troveManager.liquidate(carolTroveId); @@ -1851,7 +1851,7 @@ contract("TroveManager", async (accounts) => { const pendingETH_C = await troveManager.getPendingETHReward(CTroveId); const pendingBoldDebt_C = await troveManager.getPendingBoldDebtReward(CTroveId); const defaultPoolETH = await defaultPool.getETHBalance(); - const defaultPoolBoldDebt = await defaultPool.getRecordedDebtSum(); + const defaultPoolBoldDebt = await defaultPool.getBoldDebt(); assert.isTrue(pendingETH_C.lte(defaultPoolETH)); assert.isTrue(pendingBoldDebt_C.lte(defaultPoolBoldDebt)); //Check only difference is dust @@ -4398,7 +4398,7 @@ contract("TroveManager", async (accounts) => { // Check total Bold supply const activeBold = await activePool.getRecordedDebtSum(); - const defaultBold = await defaultPool.getRecordedDebtSum(); + const defaultBold = await defaultPool.getBoldDebt(); const totalBoldSupply = activeBold.add(defaultBold); th.assertIsApproximatelyEqual(totalBoldSupply, totalDebt); diff --git a/contracts/test/TroveManager_RecoveryModeTest.js b/contracts/test/TroveManager_RecoveryModeTest.js index cde20e6c..e699c4d8 100644 --- a/contracts/test/TroveManager_RecoveryModeTest.js +++ b/contracts/test/TroveManager_RecoveryModeTest.js @@ -2308,7 +2308,7 @@ contract.skip("TroveManager - in Recovery Mode", async (accounts) => { // Dennis redeems 40, hits Bob (lowest ICR) so Bob has a surplus of (200 * 1 - 40) / 200 = 0.8 ETH await th.redeemCollateral(dennis, contracts, B_netDebt); - const price = await priceFeed.getPrice(); + price = await priceFeed.getPrice(); const bob_surplus = B_coll.sub(B_netDebt.mul(mv._1e18BN).div(price)); th.assertIsApproximatelyEqual( await collSurplusPool.getCollateral(bob), From 4e18bed7252029eb71d5d0e36008a6ad7bec2704 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 26 Mar 2024 19:02:28 +0700 Subject: [PATCH 15/18] Add tests for TCR and ICR getters --- contracts/src/Dependencies/LiquityMath.sol | 2 +- contracts/src/test/TestContracts/BaseTest.sol | 27 +++ .../src/test/interestRateAggregate.t.sol | 189 ++++++++++++++++-- contracts/src/test/interestRateBasic.t.sol | 26 +-- 4 files changed, 217 insertions(+), 27 deletions(-) diff --git a/contracts/src/Dependencies/LiquityMath.sol b/contracts/src/Dependencies/LiquityMath.sol index d53bc451..d7db9db7 100644 --- a/contracts/src/Dependencies/LiquityMath.sol +++ b/contracts/src/Dependencies/LiquityMath.sol @@ -80,7 +80,7 @@ library LiquityMath { return newCollRatio; } - // Return the maximal value for uint256 if the Trove has a debt of 0. Represents "infinite" CR. + // Return the maximal value for uint256 if the debt is 0. Represents "infinite" CR. else { // if (_debt == 0) return 2**256 - 1; } diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index 56d4bbcf..29a0e488 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -50,6 +50,33 @@ contract BaseTest is Test { GasPool gasPool; IInterestRouter mockInterestRouter; + // Structs for use in test where we need to bi-pass "stack-too-deep" errors + struct TroveDebtRequests { + uint256 A; + uint256 B; + uint256 C; + } + + struct TroveCollAmounts { + uint256 A; + uint256 B; + uint256 C; + } + + struct TroveInterestRates { + uint256 A; + uint256 B; + uint256 C; + } + + struct TroveAccruedInterests { + uint256 A; + uint256 B; + uint256 C; + } + + // --- functions --- + function createAccounts() public { address[10] memory tempAccounts; for (uint256 i = 0; i < accounts.getAccountsCount(); i++) { diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index 79ec9a3b..e650958e 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -80,7 +80,7 @@ contract InterestRateAggregate is DevTestSetup { // --- calcTroveAccruedInterest // returns 0 for non-existent trove - function testCalcPendingTroveInterestReturns0When0AggRecordedDebt() public { + function testCalcTroveAccruedInterestReturns0When0AggRecordedDebt() public { priceFeed.setPrice(2000e18); assertEq(troveManager.calcTroveAccruedInterest(addressToTroveId(A)), 0); @@ -99,7 +99,7 @@ contract InterestRateAggregate is DevTestSetup { } // returns 0 for 0 time passed - function testCalcPendingTroveInterestReturns0For0TimePassed() public { + function testCalcTroveAccruedInterestReturns0For0TimePassed() public { priceFeed.setPrice(2000e18); uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); @@ -111,7 +111,7 @@ contract InterestRateAggregate is DevTestSetup { assertEq(troveManager.calcTroveAccruedInterest(BTroveId), 0); } - function testCalcPendingTroveInterestReturns0For0InterestRate() public { + function testCalcTroveAccruedInterestReturns0For0InterestRate() public { priceFeed.setPrice(2000e18); uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); @@ -124,7 +124,7 @@ contract InterestRateAggregate is DevTestSetup { } // TODO: create additional corresponding fuzz test - function testCalcPendingTroveInterestReturnsCorrectInterestForGivenPeriod() public { + function testCalcTroveAccruedInterestReturnsCorrectInterestForGivenPeriod() public { priceFeed.setPrice(2000e18); uint256 annualRate_A = 1e18; @@ -955,9 +955,6 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + pendingInterest); } - // TODO: getEntireDebt and getTCR basic tests - - // --- withdrawBold tests --- function testWithdrawBoldWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterestPlusBorrowerDebtChange() public { @@ -1870,12 +1867,6 @@ contract InterestRateAggregate is DevTestSetup { assertEq(activePool.getRecordedDebtSum(), recordedDebt_1 - oldTroveRecordedDebt + newTroveRecordedDebt); } - // TODO: mixed collateral & debt adjustment opps - // TODO: tests with pending debt redist. gain >0 - // TODO: tests that show total debt change under user ops - // TODO: Basic TCR and ICR getter tests - // TODO: Test total debt invariant holds i.e. (D + S * delta_T) == sum_of_all_entire_trove_debts. - // --- batchLiquidateTroves (Normal Mode, offset) --- function testBatchLiquidateTrovesPureOffsetChangesAggRecordedInterestCorrectly() public { @@ -2233,4 +2224,176 @@ contract InterestRateAggregate is DevTestSetup { // Check recorded debt sum reduced by C and D's entire debts assertEq(defaultPool.getBoldDebt(), debtInLiq); } + + // --- TCR tests --- + + function testGetTCRReturnsMaxUint256ForEmptySystem() public { + uint256 price = priceFeed.fetchPrice(); + console.log(price); + uint256 TCR = troveManager.getTCR(price); + + assertEq(TCR, MAX_UINT256); + } + + function testGetTCRReturnsICRofTroveForSystemWithOneTrove() public { + uint256 price = priceFeed.fetchPrice(); + uint256 troveDebtRequest = 2000e18; + uint256 coll = 20 ether; + uint256 interestRate = 25e16; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, coll, troveDebtRequest, interestRate); + + uint256 compositeDebt = troveDebtRequest + borrowerOperations.BOLD_GAS_COMPENSATION(); + uint256 expectedICR = coll * price / compositeDebt; + assertEq(expectedICR, troveManager.getCurrentICR(ATroveId, price)); + + assertEq(expectedICR,troveManager.getTCR(price)); + } + + function testGetTCRReturnsSizeWeightedRatioForSystemWithMultipleTroves() public { + uint256 price = priceFeed.fetchPrice(); + console.log(price, "price"); + uint256 troveDebtRequest_A = 2000e18; + uint256 troveDebtRequest_B = 3000e18; + uint256 troveDebtRequest_C = 5000e18; + uint256 coll_A = 20 ether; + uint256 coll_B = 30 ether; + uint256 coll_C = 40 ether; + uint256 interestRate = 25e16; + + openTroveNoHints100pctMaxFee(A, coll_A, troveDebtRequest_A, interestRate); + openTroveNoHints100pctMaxFee(B, coll_B, troveDebtRequest_B, interestRate); + openTroveNoHints100pctMaxFee(C, coll_C, troveDebtRequest_C, interestRate); + + uint256 compositeDebt_A = troveDebtRequest_A + borrowerOperations.BOLD_GAS_COMPENSATION(); + uint256 compositeDebt_B = troveDebtRequest_B + borrowerOperations.BOLD_GAS_COMPENSATION(); + uint256 compositeDebt_C = troveDebtRequest_C + borrowerOperations.BOLD_GAS_COMPENSATION(); + + uint256 sizeWeightedCR = (coll_A + coll_B + coll_C) * price / (compositeDebt_A + compositeDebt_B + compositeDebt_C); + + assertEq(sizeWeightedCR, troveManager.getTCR(price)); + } + + function testGetTCRIncorporatesTroveInterestForSystemWithSingleTrove() public { + uint256 price = priceFeed.fetchPrice(); + uint256 troveDebtRequest = 2000e18; + uint256 coll = 20 ether; + uint256 interestRate = 25e16; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, coll, troveDebtRequest, interestRate); + + // Fast-forward time + vm.warp(block.timestamp + 14 days); + + uint256 troveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + assertGt(troveInterest, 0); + + uint256 compositeDebt = troveDebtRequest + borrowerOperations.BOLD_GAS_COMPENSATION() + troveInterest; + uint256 expectedICR = coll * price / compositeDebt; + assertEq(expectedICR, troveManager.getCurrentICR(ATroveId, price)); + + assertEq(expectedICR,troveManager.getTCR(price)); + } + + 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; + TroveInterestRates memory troveInterestRates; + TroveAccruedInterests memory troveInterests; + + troveDebtRequests.A = 2000e18; + troveDebtRequests.B = 4000e18; + troveDebtRequests.C = 5000e18; + troveCollAmounts.A = 20 ether; + troveCollAmounts.B = 30 ether; + troveCollAmounts.C = 40 ether; + + troveInterestRates.A = 25e16; + troveInterestRates.B = 25e16; + troveInterestRates.C = 25e16; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, troveCollAmounts.A, troveDebtRequests.A, troveInterestRates.A); + // Fast-forward time + vm.warp(block.timestamp + 14 days); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, troveCollAmounts.B, troveDebtRequests.B, troveInterestRates.B); + // Fast-forward time + vm.warp(block.timestamp + 14 days); + uint256 CTroveId = openTroveNoHints100pctMaxFee(C, troveCollAmounts.C, troveDebtRequests.C, troveInterestRates.C); + // Fast-forward time + vm.warp(block.timestamp + 14 days); + + troveInterests.A = troveManager.calcTroveAccruedInterest(ATroveId); + assertGt(troveInterests.A, 0); + troveInterests.B = troveManager.calcTroveAccruedInterest(BTroveId); + assertGt(troveInterests.B, 0); + troveInterests.C = troveManager.calcTroveAccruedInterest(CTroveId); + assertGt(troveInterests.C, 0); + + /* + * stack too deep + uint256 compositeDebt_A = troveDebtRequests.A + borrowerOperations.BOLD_GAS_COMPENSATION() + troveInterest_A; + uint256 compositeDebt_B = troveDebtRequests.B + borrowerOperations.BOLD_GAS_COMPENSATION() + troveInterest_B; + uint256 compositeDebt_C = troveDebtRequests.C + borrowerOperations.BOLD_GAS_COMPENSATION() + troveInterest_C; + + uint256 expectedTCR = (troveCollAmounts.A + troveCollAmounts.B + troveCollAmounts.C) * price / (compositeDebt_A + compositeDebt_B + compositeDebt_C); + */ + uint256 gasCompensation = borrowerOperations.BOLD_GAS_COMPENSATION(); + uint256 expectedTCR = (troveCollAmounts.A + troveCollAmounts.B + troveCollAmounts.C) * priceFeed.fetchPrice() / + (troveDebtRequests.A + troveDebtRequests.B + troveDebtRequests.C + 3 * gasCompensation + troveInterests.A + troveInterests.B + troveInterests.C); + + assertEq(expectedTCR, troveManager.getTCR(priceFeed.fetchPrice())); + } + + // --- ICR tests --- + + // - 0 for non-existent Trove + + function testGetCurrentICRReturnsInfinityForNonExistentTrove() public { + uint256 price = priceFeed.fetchPrice(); + uint256 ICR = troveManager.getCurrentICR(addressToTroveId(A), price); + + assertEq(ICR, MAX_UINT256); + } + + function testGetCurrentICRReturnsCorrectValueForNoInterest() public { + uint256 price = priceFeed.fetchPrice(); + uint256 troveDebtRequest = 2000e18; + uint256 coll = 20 ether; + uint256 interestRate = 25e16; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, coll, troveDebtRequest, interestRate); + + uint256 compositeDebt = troveDebtRequest + borrowerOperations.BOLD_GAS_COMPENSATION(); + uint256 expectedICR = coll * price / compositeDebt; + assertEq(expectedICR, troveManager.getCurrentICR(ATroveId, price)); + } + + function testGetCurrentICRReturnsCorrectValueWithAccruedInterest() public { + uint256 price = priceFeed.fetchPrice(); + uint256 troveDebtRequest = 2000e18; + uint256 coll = 20 ether; + uint256 interestRate = 25e16; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, coll, troveDebtRequest, interestRate); + + // Fast-forward time + vm.warp(block.timestamp + 14 days); + + uint256 troveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + assertGt(troveInterest, 0); + + uint256 compositeDebt = troveDebtRequest + borrowerOperations.BOLD_GAS_COMPENSATION() + troveInterest; + uint256 expectedICR = coll * price / compositeDebt; + assertEq(expectedICR, troveManager.getCurrentICR(ATroveId, price)); + } + + // TODO: mixed collateral & debt adjustment opps + // TODO: tests with pending debt redist. gain >0 + // TODO: tests that show total debt change under user ops + // TODO: Test total debt invariant holds i.e. (D + S * delta_T) == sum_of_all_entire_trove_debts in + // more complex sequences of borrower ops and time passing } diff --git a/contracts/src/test/interestRateBasic.t.sol b/contracts/src/test/interestRateBasic.t.sol index d3e8154f..e8decfcf 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -709,25 +709,25 @@ contract InterestRateBasic is DevTestSetup { } function testWithdrawETHGainToTroveDoesntChangeEntireTroveDebt() public { - _setupForWithdrawETHGainToTrove(); + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); - // // Fast-forward time - // vm.warp(block.timestamp + 90 days); + // Fast-forward time + vm.warp(block.timestamp + 90 days); - // (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); - // assertGt(entireTroveDebt_1, 0); + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + assertGt(entireTroveDebt_1, 0); - // console.log(troveManager.getCurrentICR(A, 1000e18), "A ICR before"); - // // A withdraws ETH gain to Trove - // withdrawETHGainToTrove(A, ATroveId); + console.log(troveManager.getCurrentICR(ATroveId, 1000e18), "A ICR before"); + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A, ATroveId); - // console.log(troveManager.getCurrentICR(A, 1000e18), "A ICR after"); + 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"); + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + console.log(entireTroveDebt_2, "entireTroveDebt_2"); + console.log(entireTroveDebt_1, "entireTroveDebt_1"); - // assertEq(entireTroveDebt_2, entireTroveDebt_1); + assertEq(entireTroveDebt_2, entireTroveDebt_1); } function testWithdrawETHGainToTroveIncreasesRecordedTroveDebtByAccruedInterest() public { From 2e111ce030d197376d61375478c6172648f237d2 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Wed, 27 Mar 2024 15:24:25 +0700 Subject: [PATCH 16/18] Add test for applyTroveInterestPermissionless --- contracts/src/test/interestRateBasic.t.sol | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/contracts/src/test/interestRateBasic.t.sol b/contracts/src/test/interestRateBasic.t.sol index e8decfcf..22c0bb19 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -678,6 +678,30 @@ contract InterestRateBasic is DevTestSetup { assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); } + function testRevertApplyTroveInterestPermissionlessWhenTroveIsNotStale() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + // No time passes. B tries to apply A's interest. expect revert + vm.startPrank(B); + vm.expectRevert(); + borrowerOperations.applyTroveInterestPermissionless(ATroveId); + vm.stopPrank(); + + // Fast-forward time, but less than the staleness threshold + // TODO: replace "90 days" with troveManager.STALE_TROVE_DURATION() after conflicts are resolved + vm.warp(block.timestamp + 90 days - 1); + + // B tries to apply A's interest. Expect revert + vm.startPrank(B); + vm.expectRevert(); + borrowerOperations.applyTroveInterestPermissionless(ATroveId); + vm.stopPrank(); + } + // --- withdrawETHGainToTrove tests --- function testWithdrawETHGainToTroveSetsTroveLastDebtUpdateTimeToNow() public { From 8f523650112c7dc03883fa0c505a43fe9e418545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Wed, 27 Mar 2024 13:43:08 +0000 Subject: [PATCH 17/18] contracts: Comment out console.log imports --- contracts/src/ActivePool.sol | 4 +--- contracts/src/Dependencies/LiquityBase.sol | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index 8ff13fb2..6a02d920 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -12,9 +12,7 @@ import "./Dependencies/CheckContract.sol"; import './Interfaces/IDefaultPool.sol'; import './Interfaces/IActivePool.sol'; -// import "forge-std/console.sol"; - -import "forge-std/console2.sol"; +//import "forge-std/console2.sol"; /* * The Active Pool holds the ETH collateral and Bold debt (but not Bold tokens) for all active troves. diff --git a/contracts/src/Dependencies/LiquityBase.sol b/contracts/src/Dependencies/LiquityBase.sol index 1adfca6e..ea815148 100644 --- a/contracts/src/Dependencies/LiquityBase.sol +++ b/contracts/src/Dependencies/LiquityBase.sol @@ -9,7 +9,7 @@ import "../Interfaces/IDefaultPool.sol"; import "../Interfaces/IPriceFeed.sol"; import "../Interfaces/ILiquityBase.sol"; -import "forge-std/console2.sol"; +//import "forge-std/console2.sol"; /* * Base contract for TroveManager, BorrowerOperations and StabilityPool. Contains global system constants and From 3942a9d93e3c2453d8f1b6c3ea5c2b1e94a921a6 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Thu, 4 Apr 2024 18:50:35 +0700 Subject: [PATCH 18/18] Add fixes based on review comments --- contracts/src/BorrowerOperations.sol | 2 -- contracts/src/TroveManager.sol | 29 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index e7ad753e..7ac44d59 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -184,8 +184,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint256 troveId = uint256(keccak256(abi.encode(_owner, _ownerIndex))); _requireTroveisNotActive(contractsCache.troveManager, troveId); - vars.BoldFee; // TODO - _requireAtLeastMinNetDebt(_boldAmount); // ICR is based on the composite debt, i.e. the requested Bold amount + Bold gas comp. diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index eba4e0cf..dd7cdea3 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -77,8 +77,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint stake; Status status; uint128 arrayIndex; - uint256 annualInterestRate; uint64 lastDebtUpdateTime; + uint256 annualInterestRate; // TODO: optimize this struct packing for gas reduction, which may break v1 tests that assume a certain order of properties } @@ -177,12 +177,14 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint256 accruedTroveInterest; uint256 weightedRecordedTroveDebt; uint256 recordedTroveDebt; + uint256 pendingDebtReward; } struct LiquidationTotals { uint totalCollInSequence; uint totalDebtInSequence; uint256 totalRecordedDebtInSequence; + uint256 totalRedistDebtGainsInSequence; uint256 totalWeightedRecordedDebtInSequence; uint256 totalAccruedInterestInSequence; uint totalCollGasCompensation; @@ -340,7 +342,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana LocalVariables_InnerSingleLiquidateFunction memory vars; (singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, - vars.pendingDebtReward, + singleLiquidation.pendingDebtReward, vars.pendingCollReward, singleLiquidation.accruedTroveInterest) = getEntireDebtAndColl(_troveId); @@ -349,7 +351,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana //TODO - GAS: We already read this inside getEntireDebtAndColl - so add it to the returned vals? singleLiquidation.recordedTroveDebt = Troves[_troveId].debt; - _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward); + _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, singleLiquidation.pendingDebtReward, vars.pendingCollReward); _removeStake(_troveId); singleLiquidation.collGasCompensation = _getCollGasCompensation(singleLiquidation.entireTroveColl); @@ -384,7 +386,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana if (TroveIds.length <= 1) {return singleLiquidation;} // don't liquidate if last trove (singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, - vars.pendingDebtReward, + singleLiquidation.pendingDebtReward, vars.pendingCollReward, ) = getEntireDebtAndColl(_troveId); singleLiquidation.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_troveId); @@ -398,7 +400,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // If ICR <= 100%, purely redistribute the Trove across all active Troves if (_ICR <= _100pct) { - _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward); + _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, singleLiquidation.pendingDebtReward, vars.pendingCollReward); _removeStake(_troveId); singleLiquidation.debtToOffset = 0; @@ -412,7 +414,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // If 100% < ICR < MCR, offset as much as possible, and redistribute the remainder } else if ((_ICR > _100pct) && (_ICR < MCR)) { - _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward); + _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, singleLiquidation.pendingDebtReward, vars.pendingCollReward); _removeStake(_troveId); (singleLiquidation.debtToOffset, @@ -430,7 +432,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana * The remainder due to the capped rate will be claimable as collateral surplus. */ } else if ((_ICR >= MCR) && (_ICR < _TCR) && (singleLiquidation.entireTroveDebt <= _boldInStabPool)) { - _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward); + _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, singleLiquidation.pendingDebtReward, vars.pendingCollReward); assert(_boldInStabPool != 0); _removeStake(_troveId); @@ -494,7 +496,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana } /* - * Get its offset coll/debt and ETH gas comp, and close the trove. + * Get its offset coll/debt and ETH gas comp. */ function _getCappedOffsetVals ( @@ -654,7 +656,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // TODO - Gas: combine these into one call to activePool? // Remove the liquidated recorded debt from the sum - activePoolCached.decreaseRecordedDebtSum(totals.totalRecordedDebtInSequence); + activePoolCached.decreaseRecordedDebtSum(totals.totalRecordedDebtInSequence + totals.totalRedistDebtGainsInSequence); // Remove the liqudiated weighted recorded debt from the sum activePool.changeAggWeightedDebtSum(totals.totalWeightedRecordedDebtInSequence, 0); @@ -780,6 +782,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana newTotals.totalDebtInSequence = oldTotals.totalDebtInSequence + singleLiquidation.entireTroveDebt; newTotals.totalCollInSequence = oldTotals.totalCollInSequence + singleLiquidation.entireTroveColl; newTotals.totalRecordedDebtInSequence = oldTotals.totalRecordedDebtInSequence + singleLiquidation.recordedTroveDebt; + newTotals.totalRedistDebtGainsInSequence = oldTotals.totalRedistDebtGainsInSequence + singleLiquidation.pendingDebtReward; newTotals.totalWeightedRecordedDebtInSequence = oldTotals.totalWeightedRecordedDebtInSequence + singleLiquidation.weightedRecordedTroveDebt; newTotals.totalAccruedInterestInSequence = oldTotals.totalAccruedInterestInSequence + singleLiquidation.accruedTroveInterest; newTotals.totalDebtToOffset = oldTotals.totalDebtToOffset + singleLiquidation.debtToOffset; @@ -1001,7 +1004,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return (currentETH, currentBoldDebt); } - function getAndApplyRedistributionGains(uint256 _troveId) external returns (uint256, uint256) { + function getAndApplyRedistributionGains(uint256 _troveId) external override returns (uint256, uint256) { _requireCallerIsBorrowerOperations(); return _getAndApplyRedistributionGains(activePool, defaultPool, _troveId); } @@ -1024,8 +1027,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana _updateTroveRewardSnapshots(_troveId); - //TODO: Update agg. recorded debt and agg. weighted debt sum with the redistribution gains - // Transfer redistribution gains from DefaultPool to ActivePool _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, pendingBoldDebtReward, pendingETHReward); @@ -1106,12 +1107,12 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana entireColl = recordedColl + pendingETHReward; } - function getTroveEntireDebt(uint256 _troveId) public view returns (uint256) { + function getTroveEntireDebt(uint256 _troveId) external view returns (uint256) { (uint256 entireTroveDebt, , , , ) = getEntireDebtAndColl(_troveId); return entireTroveDebt; } - function getTroveEntireColl(uint256 _troveId) public view returns (uint256) { + function getTroveEntireColl(uint256 _troveId) external view returns (uint256) { ( , uint256 entireTroveColl, , , ) = getEntireDebtAndColl(_troveId); return entireTroveColl; }