diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index e05894cb..6a02d920 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -4,12 +4,15 @@ 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'; 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,8 +31,31 @@ 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 - uint256 internal boldDebt; + + // 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; + + // Last time at which the aggregate recorded debt and weighted sum were updated + uint256 public lastAggUpdateTime; // --- Events --- @@ -38,7 +64,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) { @@ -46,13 +72,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 +90,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); @@ -89,8 +122,17 @@ 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) { + 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 --- @@ -131,16 +173,54 @@ contract ActivePool is Ownable, CheckContract, IActivePool { emit ActivePoolETHBalanceUpdated(newETHBalance); } - function increaseBoldDebt(uint _amount) external override { + function increaseRecordedDebtSum(uint _amount) external override { + _requireCallerIsBOorTroveM(); + uint256 newRecordedDebtSum = recordedDebtSum + _amount; + recordedDebtSum = newRecordedDebtSum; + emit ActivePoolBoldDebtUpdated(newRecordedDebtSum); + } + + function decreaseRecordedDebtSum(uint _amount) external override { + _requireCallerIsBOorTroveMorSP(); + uint256 newRecordedDebtSum = recordedDebtSum - _amount; + + recordedDebtSum = newRecordedDebtSum; + + emit ActivePoolBoldDebtUpdated(newRecordedDebtSum); + } + + function changeAggWeightedDebtSum(uint256 _oldWeightedRecordedTroveDebt, uint256 _newTroveWeightedRecordedTroveDebt) external { _requireCallerIsBOorTroveM(); - boldDebt = boldDebt + _amount; - emit ActivePoolBoldDebtUpdated(boldDebt); + // 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 } - function decreaseBoldDebt(uint _amount) external override { + // --- 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 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 { _requireCallerIsBOorTroveMorSP(); - boldDebt = boldDebt - _amount; - emit ActivePoolBoldDebtUpdated(boldDebt); + 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);} + + // 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; } // --- 'require' functions --- diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol index 29e27e34..fc2d66b0 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 { - _requireCallerIsBorrowerOperations(); + _requireCallerIsBOorAP(); _mint(_account, _amount); } @@ -258,8 +264,10 @@ contract BoldToken is CheckContract, IBoldToken { ); } - function _requireCallerIsBorrowerOperations() internal view { - require(msg.sender == borrowerOperationsAddress, "BoldToken: Caller is not BorrowerOperations"); + function _requireCallerIsBOorAP() internal view { + require(msg.sender == borrowerOperationsAddress || + 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 a98218f4..7ac44d59 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/console.sol"; - +// import "forge-std/console2.sol"; contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOperations { using SafeERC20 for IERC20; @@ -39,21 +38,22 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe struct LocalVariables_adjustTrove { uint price; - uint netDebtChange; - uint debt; - uint coll; + uint entireDebt; + uint entireColl; + uint256 redistDebtGain; + uint256 accruedTroveInterest; uint oldICR; uint newICR; uint newTCR; - uint BoldFee; - uint newDebt; - uint newColl; + uint BoldFee; // TODO + uint newEntireDebt; + uint newEntireColl; uint stake; } struct LocalVariables_openTrove { uint price; - uint BoldFee; + uint BoldFee; // TODO uint netDebt; uint compositeDebt; uint ICR; @@ -61,7 +61,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; @@ -131,7 +136,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); @@ -164,10 +169,13 @@ 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(); + + // --- Checks --- + bool isRecoveryMode = _checkRecoveryMode(vars.price); _requireValidAnnualInterestRate(_annualInterestRate); @@ -176,20 +184,12 @@ 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; - - 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); if (isRecoveryMode) { @@ -197,15 +197,19 @@ 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); } + // --- Effects & interactions --- + + contractsCache.activePool.mintAggInterest(vars.compositeDebt, 0); + // Set the stored Trove properties and mint the NFT vars.stake = contractsCache.troveManager.setTrovePropertiesOnOpen( _owner, troveId, _ETHAmount, - vars.compositeDebt, + vars.compositeDebt, _annualInterestRate ); @@ -215,13 +219,18 @@ 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); + emit BoldBorrowingFeePaid(troveId, vars.BoldFee); // TODO return troveId; } @@ -268,24 +277,27 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } function adjustTroveInterestRate(uint256 _troveId, uint _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint) external { - // TODO: Delegation functionality _requireValidAnnualInterestRate(_newAnnualInterestRate); ITroveManager troveManagerCached = troveManager; _requireTroveisActive(troveManagerCached, _troveId); + // TODO: Delegation functionality _requireIsOwner(_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. + ContractsCacheTMAP memory contractsCache = ContractsCacheTMAP(troveManager, activePool); + + _requireValidAnnualInterestRate(_newAnnualInterestRate); + _requireTroveisActive(contractsCache.troveManager, _troveId); - troveManagerCached.applyPendingRewards(_troveId); + uint256 entireTroveDebt = _updateActivePoolTrackersNoDebtChange(contractsCache.troveManager, contractsCache.activePool, _troveId, _newAnnualInterestRate); sortedTroves.reInsert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); - troveManagerCached.changeAnnualInterestRate(_troveId, _newAnnualInterestRate); + // Update Trove recorded debt and interest-weighted debt sum + contractsCache.troveManager.updateTroveDebtAndInterest(_troveId, entireTroveDebt, _newAnnualInterestRate); } /* - * _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, @@ -298,10 +310,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(_troveId); + uint256 annualInterestRate = contractsCache.troveManager.getTroveAnnualInterestRate(_troveId); + + // --- Checks --- + bool isRecoveryMode = _checkRecoveryMode(vars.price); if (_isCollIncrease) { @@ -317,52 +335,67 @@ 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. - - contractsCache.troveManager.applyPendingRewards(_troveId); - - 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, vars.redistDebtGain, , vars.accruedTroveInterest) = contractsCache.troveManager.getEntireDebtAndColl(_troveId); - vars.debt = contractsCache.troveManager.getTroveDebt(_troveId); - vars.coll = contractsCache.troveManager.getTroveColl(_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); - assert(_isCollIncrease || _collChange <= vars.coll); // TODO: do we still need this? + vars.oldICR = LiquityMath._computeCR(vars.entireColl, vars.entireDebt, vars.price); + vars.newICR = _getNewICRFromTroveChange( + vars.entireColl, + vars.entireDebt, + _collChange, + _isCollIncrease, + _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.debt) - vars.netDebtChange); - _requireValidBoldRepayment(vars.debt, vars.netDebtChange); - _requireSufficientBoldBalance(contractsCache.boldToken, msg.sender, vars.netDebtChange); + _requireAtLeastMinNetDebt(_getNetDebt(vars.entireDebt) - _boldChange); + _requireValidBoldRepayment(vars.entireDebt, _boldChange); + _requireSufficientBoldBalance(contractsCache.boldToken, msg.sender, _boldChange); + } + + // --- Effects and interactions --- + + contractsCache.troveManager.getAndApplyRedistributionGains(_troveId); + + if (_isDebtIncrease) { + // Increase Trove debt by the drawn debt + redist. gain + activePool.mintAggInterest(_boldChange + vars.redistDebtGain, 0); + } else { + // Increase Trove debt by redist. gain and decrease by the repaid debt + activePool.mintAggInterest(vars.redistDebtGain, _boldChange); } - (vars.newColl, vars.newDebt) = _updateTroveFromAdjustment( + // Update the Trove's recorded coll and debt + vars.newEntireColl = _updateTroveCollFromAdjustment( contractsCache.troveManager, _sender, _troveId, - vars.coll, + vars.entireColl, _collChange, - _isCollIncrease, - vars.debt, - vars.netDebtChange, - _isDebtIncrease + _isCollIncrease + ); + vars.newEntireDebt = _updateTroveDebtFromAdjustment( + contractsCache.troveManager, + _sender, + _troveId, + vars.entireDebt, + _boldChange, + _isDebtIncrease, + vars.accruedTroveInterest ); + vars.stake = contractsCache.troveManager.updateStakeAndTotalStakes(_troveId); - emit TroveUpdated(_troveId, vars.newDebt, vars.newColl, vars.stake, BorrowerOperation.adjustTrove); - emit BoldBorrowingFeePaid(_troveId, vars.BoldFee); + emit TroveUpdated(_troveId, vars.newEntireDebt, vars.newEntireColl, vars.stake, BorrowerOperation.adjustTrove); + emit BoldBorrowingFeePaid(_troveId, vars.BoldFee); // TODO - // Use the unmodified _boldChange here, as we don't send the fee to the user _moveTokensAndETHfromAdjustment( contractsCache.activePool, contractsCache.boldToken, @@ -372,43 +405,81 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _isCollIncrease, _boldChange, _isDebtIncrease, - vars.netDebtChange + vars.accruedTroveInterest ); + + contractsCache.activePool.changeAggWeightedDebtSum(initialWeightedRecordedTroveDebt, vars.newEntireDebt * annualInterestRate); } function closeTrove(uint256 _troveId) external override { - ITroveManager troveManagerCached = troveManager; - IActivePool activePoolCached = activePool; - IBoldToken boldTokenCached = boldToken; + ContractsCacheTMAPBT memory contractsCache = ContractsCacheTMAPBT(troveManager, activePool, boldToken); - _requireCallerIsBorrower(troveManagerCached, _troveId); - _requireTroveisActive(troveManagerCached, _troveId); + // --- Checks --- + + _requireCallerIsBorrower(contractsCache.troveManager, _troveId); + _requireTroveisActive(contractsCache.troveManager, _troveId); uint price = priceFeed.fetchPrice(); _requireNotInRecoveryMode(price); - // TODO: apply individual and aggregate pending interest, and take snapshots of current timestamp. + uint256 initialWeightedRecordedTroveDebt = contractsCache.troveManager.getTroveWeightedRecordedDebt(_troveId); + uint256 initialRecordedTroveDebt = contractsCache.troveManager.getTroveDebt(_troveId); - troveManagerCached.applyPendingRewards(_troveId); + (uint256 entireTroveDebt, + uint256 entireTroveColl, + uint256 debtRedistGain, + , // ETHredist gain + uint256 accruedTroveInterest) = contractsCache.troveManager.getEntireDebtAndColl(_troveId); - uint coll = troveManagerCached.getTroveColl(_troveId); - uint debt = troveManagerCached.getTroveDebt(_troveId); + // The borrower must repay their entire debt including accrued interest and redist. gains (and less the gas comp.) + _requireSufficientBoldBalance(contractsCache.boldToken, msg.sender, entireTroveDebt - BOLD_GAS_COMPENSATION); - _requireSufficientBoldBalance(boldTokenCached, msg.sender, debt - BOLD_GAS_COMPENSATION); - - uint newTCR = _getNewTCRFromTroveChange(coll, false, debt, false, price); + // 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); - troveManagerCached.removeStake(_troveId); - troveManagerCached.closeTrove(_troveId); + // --- 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. + contractsCache.activePool.mintAggInterest(0, initialRecordedTroveDebt + accruedTroveInterest); + contractsCache.troveManager.removeStake(_troveId); + contractsCache.troveManager.closeTrove(_troveId); 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); - _repayBold(activePoolCached, boldTokenCached, gasPoolAddress, BOLD_GAS_COMPENSATION); + // 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); + + // 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 + contractsCache.boldToken.burn(msg.sender, entireTroveDebt - BOLD_GAS_COMPENSATION); // Send the collateral back to the user - activePoolCached.sendETH(msg.sender, coll); + 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.updateTroveDebtFromInterestApplication(_troveId, entireTroveDebt); } function setAddManager(uint256 _troveId, address _manager) external { @@ -437,43 +508,79 @@ 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. Assumes any ETH redistribution gain was already applied + // to the Trove's coll. + function _updateTroveCollFromAdjustment ( ITroveManager _troveManager, address _sender, uint256 _troveId, - uint256 _coll, + uint256 _oldEntireColl, uint _collChange, - bool _isCollIncrease, - uint256 _debt, - uint _debtChange, - bool _isDebtIncrease + bool _isCollIncrease ) internal - returns (uint, uint) + returns (uint256) { - uint256 newColl; - uint256 newDebt; + uint256 newEntireColl; if (_collChange > 0) { - newColl = (_isCollIncrease) ? - _troveManager.increaseTroveColl(_sender, _troveId, _collChange) : - _troveManager.decreaseTroveColl(_sender, _troveId, _collChange); + newEntireColl = _isCollIncrease ? _oldEntireColl + _collChange : _oldEntireColl - _collChange; + _troveManager.updateTroveColl(_sender, _troveId, newEntireColl, _isCollIncrease); } else { - newColl = _coll; + newEntireColl = _oldEntireColl; } + + 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 _sender, + uint256 _troveId, + uint256 _oldEntireDebt, + uint256 _debtChange, + bool _isDebtIncrease, + uint256 _accruedTroveInterest + ) + internal + returns (uint256) + { + uint newEntireDebt; if (_debtChange > 0) { - newDebt = (_isDebtIncrease) ? - _troveManager.increaseTroveDebt(_sender, _troveId, _debtChange) : - _troveManager.decreaseTroveDebt(_sender, _troveId, _debtChange); + newEntireDebt = _isDebtIncrease ? _oldEntireDebt + _debtChange : _oldEntireDebt - _debtChange; + _troveManager.updateTroveDebt(_sender, _troveId, newEntireDebt, _isDebtIncrease); } else { - newDebt = _debt; + newEntireDebt = _oldEntireDebt; + if (_accruedTroveInterest > 0) { + _troveManager.updateTroveDebtFromInterestApplication(_troveId, newEntireDebt); + } } - return (newColl, newDebt); + return newEntireDebt; } + // This function incorporates both the Trove's net debt change (repaid/drawn) and its accrued interest. + // Redist. gains have already been applied before this is called. + // TODO: explicitly pass redist. gains too if we gas-optimize them. function _moveTokensAndETHfromAdjustment ( IActivePool _activePool, @@ -484,15 +591,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 { - _repayBold(_activePool, _boldToken, msg.sender, _boldChange); + // TODO: Gas optimize this + _activePool.increaseRecordedDebtSum(_accruedTroveInterest); + _activePool.decreaseRecordedDebtSum(_boldChange); + + _boldToken.burn(msg.sender, _boldChange); } if (_isCollIncrease) { @@ -512,16 +624,35 @@ 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.increaseBoldDebt(_netDebtIncrease); - _boldToken.mint(_account, _boldAmount); - } + function _updateActivePoolTrackersNoDebtChange + ( + ITroveManager _troveManager, + IActivePool _activePool, + uint256 _troveId, + uint256 _annualInterestRate + ) + internal + returns (uint256) + { + uint256 initialWeightedRecordedTroveDebt = _troveManager.getTroveWeightedRecordedDebt(_troveId); + // --- Effects --- + + (, uint256 redistDebtGain) = _troveManager.getAndApplyRedistributionGains(_troveId); + + // No debt is issued/repaid, so the net Trove debt change is purely the redistribution gain + _activePool.mintAggInterest(redistDebtGain, 0); + + uint256 accruedTroveInterest = _troveManager.calcTroveAccruedInterest(_troveId); + uint256 recordedTroveDebt = _troveManager.getTroveDebt(_troveId); + uint256 entireTroveDebt = recordedTroveDebt + accruedTroveInterest; + + // 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); - // 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); + return entireTroveDebt; } // --- 'Require' wrapper functions --- @@ -564,18 +695,19 @@ 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, + uint256 _boldChange, bool _isDebtIncrease, LocalVariables_adjustTrove memory _vars - ) - internal - view + ) + internal + view { - /* + /* *In Recovery Mode, only allow: * * - Pure collateral top-up @@ -593,11 +725,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); + _vars.newTCR = _getNewTCRFromTroveChange(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _vars.price); + _requireNewTCRisAboveCCR(_vars.newTCR); } } @@ -644,7 +776,11 @@ 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"); + } + + function _requireTroveIsStale(ITroveManager _troveManager, uint256 _troveId) internal view { + require(_troveManager.troveIsStale(_troveId), "BO: Trove must be stale"); } // --- ICR and TCR getters --- @@ -710,6 +846,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 ebee5b1c..ea815148 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,11 @@ contract LiquityBase is BaseMath, ILiquityBase { } function getEntireSystemDebt() public view returns (uint entireSystemDebt) { - uint activeDebt = activePool.getBoldDebt(); + 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/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/Interfaces/IActivePool.sol b/contracts/src/Interfaces/IActivePool.sol index a16e42b1..8fdc929a 100644 --- a/contracts/src/Interfaces/IActivePool.sol +++ b/contracts/src/Interfaces/IActivePool.sol @@ -2,20 +2,38 @@ pragma solidity 0.8.18; -import "./IPool.sol"; - - -interface IActivePool is IPool { +import "./IInterestRouter.sol"; +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 sendETH(address _account, uint _amount) external; - function sendETHToDefaultPool(uint _amount) external; + function interestRouter() external view returns (IInterestRouter); function setAddresses( address _borrowerOperationsAddress, address _troveManagerAddress, address _stabilityPoolAddress, - address _defaultPoolAddress + address _defaultPoolAddress, + address _boldTokenAddress, + address _interestRouterAddress ) external; + + function getETHBalance() 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(uint256 _troveDebtIncrease, uint256 _troveDebtDecrease) 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 receiveETH(uint256 _amount) external; } 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/IDefaultPool.sol b/contracts/src/Interfaces/IDefaultPool.sol index a819b167..313c22ee 100644 --- a/contracts/src/Interfaces/IDefaultPool.sol +++ b/contracts/src/Interfaces/IDefaultPool.sol @@ -2,13 +2,16 @@ pragma solidity 0.8.18; -import "./IPool.sol"; - - -interface IDefaultPool is IPool { +interface IDefaultPool { + function setAddresses(address _troveManagerAddress, address _activePoolAddress) external; function troveManagerAddress() external view returns (address); function activePoolAddress() external view returns (address); // --- Functions --- + 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/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..bd03411e 100644 --- a/contracts/src/Interfaces/ILiquityBase.sol +++ b/contracts/src/Interfaces/ILiquityBase.sol @@ -10,4 +10,7 @@ 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); + function MCR() external view returns (uint256); + function getEntireSystemDebt() external view returns (uint256); } 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 871406e2..8c67e628 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -29,7 +29,9 @@ interface ITroveManager is IERC721, ILiquityBase { function borrowerOperationsAddress() external view returns (address); function BOOTSTRAP_PERIOD() external view returns (uint256); - + + // function BOLD_GAS_COMPENSATION() external view returns (uint256); + function getTroveIdsCount() external view returns (uint); function getTroveFromTroveIdsArray(uint _index) external view returns (uint256); @@ -50,21 +52,26 @@ interface ITroveManager is IERC721, ILiquityBase { function addTroveIdToArray(uint256 _troveId) external returns (uint index); - function applyPendingRewards(uint256 _troveId) 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(uint256 _troveId) external view returns (bool); function getEntireDebtAndColl(uint256 _troveId) external view returns ( - uint debt, - uint coll, - uint pendingBoldDebtReward, - uint pendingETHReward + uint entireDebt, + uint entireColl, + uint pendingBoldDebtReward, + uint pendingETHReward, + uint pendingBoldInterest ); + function getTroveEntireDebt(uint256 _troveId) external view returns (uint256); + + function getTroveEntireColl(uint256 _troveId) external view returns (uint256); + + function getAndApplyRedistributionGains(uint256 _troveId) external returns (uint256, uint256); + function closeTrove(uint256 _troveId) external; function removeStake(uint256 _troveId) external; @@ -75,29 +82,37 @@ 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); + function getTroveWeightedRecordedDebt(uint256 _troveId) external returns (uint256); + function getTroveColl(uint256 _troveId) external view returns (uint); function getTroveAnnualInterestRate(uint256 _troveId) external view returns (uint); + function calcTroveAccruedInterest(uint256 _troveId) external view returns (uint256); + function TroveAddManagers(uint256 _troveId) external view returns (address); function TroveRemoveManagers(uint256 _troveId) external view returns (address); + function getTroveLastDebtUpdateTime(uint256 _troveId) 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); + function troveIsStale(uint256 _troveId) external view returns (bool); - function decreaseTroveColl(address _sender, uint256 _troveId, uint _collDecrease) external returns (uint); + function changeAnnualInterestRate(uint256 _troveId, uint256 _newAnnualInterestRate) external; - function increaseTroveDebt(address _sender, uint256 _troveId, uint _debtIncrease) external returns (uint); + function updateTroveDebtAndInterest(uint256 _troveId, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) external; - function decreaseTroveDebt(address _sender, uint256 _troveId, uint _collDecrease) external returns (uint); + function updateTroveDebtFromInterestApplication(uint256 _troveId, uint256 _entireTroveDebt) external; - function changeAnnualInterestRate(uint256 _troveId, uint256 _newAnnualInterestRate) 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/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/StabilityPool.sol b/contracts/src/StabilityPool.sol index f3f68b0e..a5f7a198 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. * @@ -285,6 +287,8 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { function provideToSP(uint _amount) external override { _requireNonZeroAmount(_amount); + activePool.mintAggInterest(0, 0); + uint initialDeposit = deposits[msg.sender].initialValue; uint depositorETHGain = getDepositorETHGain(msg.sender); @@ -314,6 +318,8 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { uint initialDeposit = deposits[msg.sender].initialValue; _requireUserHasDeposit(initialDeposit); + activePool.mintAggInterest(0, 0); + uint depositorETHGain = getDepositorETHGain(msg.sender); uint compoundedBoldDeposit = getCompoundedBoldDeposit(msg.sender); @@ -482,7 +488,6 @@ 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); _decreaseBold(_debtToOffset); // Burn the debt that was successfully offset diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index b64d71ea..dd7cdea3 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -37,6 +37,9 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // --- Data structures --- 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 * (1/2) = d^720 => d = (1/2)^(1/720) @@ -74,6 +77,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint stake; Status status; uint128 arrayIndex; + uint64 lastDebtUpdateTime; uint256 annualInterestRate; // TODO: optimize this struct packing for gas reduction, which may break v1 tests that assume a certain order of properties } @@ -170,11 +174,19 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint debtToRedistribute; uint collToRedistribute; uint collSurplus; + uint256 accruedTroveInterest; + uint256 weightedRecordedTroveDebt; + uint256 recordedTroveDebt; + uint256 pendingDebtReward; } struct LiquidationTotals { uint totalCollInSequence; uint totalDebtInSequence; + uint256 totalRecordedDebtInSequence; + uint256 totalRedistDebtGainsInSequence; + uint256 totalWeightedRecordedDebtInSequence; + uint256 totalAccruedInterestInSequence; uint totalCollGasCompensation; uint totalBoldGasCompensation; uint totalDebtToOffset; @@ -235,8 +247,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 @@ -328,13 +340,18 @@ 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); + singleLiquidation.pendingDebtReward, + vars.pendingCollReward, + singleLiquidation.accruedTroveInterest) = getEntireDebtAndColl(_troveId); + + singleLiquidation.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_troveId); - _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward); + //TODO - GAS: We already read this inside getEntireDebtAndColl - so add it to the returned vals? + singleLiquidation.recordedTroveDebt = Troves[_troveId].debt; + + _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, singleLiquidation.pendingDebtReward, vars.pendingCollReward); _removeStake(_troveId); singleLiquidation.collGasCompensation = _getCollGasCompensation(singleLiquidation.entireTroveColl); @@ -369,8 +386,13 @@ 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, - vars.pendingCollReward) = getEntireDebtAndColl(_troveId); + singleLiquidation.pendingDebtReward, + vars.pendingCollReward, ) = getEntireDebtAndColl(_troveId); + + singleLiquidation.weightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_troveId); + + //TODO - GAS: We already read this inside getEntireDebtAndColl - so add it to the returned vals? + singleLiquidation.recordedTroveDebt = Troves[_troveId].debt; singleLiquidation.collGasCompensation = _getCollGasCompensation(singleLiquidation.entireTroveColl); singleLiquidation.BoldGasCompensation = BOLD_GAS_COMPENSATION; @@ -378,9 +400,9 @@ 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; singleLiquidation.collToSendToSP = 0; singleLiquidation.debtToRedistribute = singleLiquidation.entireTroveDebt; @@ -389,10 +411,10 @@ 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); + _movePendingTroveRewardsToActivePool(_activePool, _defaultPool, singleLiquidation.pendingDebtReward, vars.pendingCollReward); _removeStake(_troveId); (singleLiquidation.debtToOffset, @@ -410,11 +432,17 @@ 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); - 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) { @@ -428,7 +456,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana LiquidationValues memory zeroVals; return zeroVals; } - return singleLiquidation; } @@ -437,8 +464,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana */ function _getOffsetAndRedistributionVals ( - uint _debt, - uint _coll, + uint _entireTroveDebt, + uint _collToLiquidate, uint _boldInStabPool ) internal @@ -456,25 +483,27 @@ 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; } } /* - * Get its offset coll/debt and ETH gas comp, and close the trove. + * Get its offset coll/debt and ETH gas comp. */ function _getCappedOffsetVals ( uint _entireTroveDebt, uint _entireTroveColl, + uint256 _recordedTroveDebt, + uint256 _weightedRecordedTroveDebt, uint _price ) internal @@ -483,6 +512,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); @@ -494,7 +525,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 @@ -599,6 +630,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; @@ -619,6 +651,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 + totals.totalRedistDebtGainsInSequence); + // 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); @@ -740,12 +781,18 @@ 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.totalRedistDebtGainsInSequence = oldTotals.totalRedistDebtGainsInSequence + singleLiquidation.pendingDebtReward; + 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; } @@ -762,7 +809,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); } @@ -821,7 +868,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); @@ -829,7 +876,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. @@ -895,7 +942,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana continue; } - _applyPendingRewards(contractsCache.activePool, contractsCache.defaultPool, currentTroveId); + _getAndApplyRedistributionGains(contractsCache.activePool, contractsCache.defaultPool, currentTroveId); SingleRedemptionValues memory singleRedemption = _redeemCollateralFromTrove( contractsCache, @@ -931,7 +978,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); } @@ -949,33 +996,38 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana uint pendingETHReward = getPendingETHReward(_troveId); uint pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); + uint256 accruedTroveInterest = calcTroveAccruedInterest(_troveId); + uint currentETH = Troves[_troveId].coll + pendingETHReward; - uint currentBoldDebt = Troves[_troveId].debt + pendingBoldDebtReward; + uint currentBoldDebt = Troves[_troveId].debt + pendingBoldDebtReward + accruedTroveInterest; return (currentETH, currentBoldDebt); } - function applyPendingRewards(uint256 _troveId) external override { + function getAndApplyRedistributionGains(uint256 _troveId) external override returns (uint256, uint256) { _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, uint256 _troveId) internal returns (uint256, uint256) { + uint256 pendingETHReward; + uint256 pendingBoldDebtReward; + + if (hasRedistributionGains(_troveId)) { _requireTroveIsActive(_troveId); - // Compute pending rewards - uint pendingETHReward = getPendingETHReward(_troveId); - uint pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); + // Compute redistribution gains + pendingETHReward = getPendingETHReward(_troveId); + 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( @@ -983,9 +1035,11 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana Troves[_troveId].debt, Troves[_troveId].coll, Troves[_troveId].stake, - TroveManagerOperation.applyPendingRewards + TroveManagerOperation.getAndApplyRedistributionGains ); } + + return (pendingETHReward, pendingBoldDebtReward); } function _updateTroveRewardSnapshots(uint256 _troveId) internal { @@ -1007,7 +1061,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; @@ -1022,34 +1076,45 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return pendingBoldDebtReward; } - function hasPendingRewards(uint256 _troveId) public view override returns (bool) { + function hasRedistributionGains(uint256 _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 entireDebt, uint entireColl, uint pendingBoldDebtReward, uint pendingETHReward, uint accruedTroveInterest) { - debt = Troves[_troveId].debt; - coll = Troves[_troveId].coll; + uint256 recordedDebt = Troves[_troveId].debt; + uint256 recordedColl = Troves[_troveId].coll; pendingBoldDebtReward = getPendingBoldDebtReward(_troveId); + accruedTroveInterest = calcTroveAccruedInterest(_troveId); pendingETHReward = getPendingETHReward(_troveId); - debt = debt + pendingBoldDebtReward; - coll = coll + pendingETHReward; + entireDebt = recordedDebt + pendingBoldDebtReward + accruedTroveInterest; + entireColl = recordedColl + pendingETHReward; + } + + function getTroveEntireDebt(uint256 _troveId) external view returns (uint256) { + (uint256 entireTroveDebt, , , , ) = getEntireDebtAndColl(_troveId); + return entireTroveDebt; + } + + function getTroveEntireColl(uint256 _troveId) external view returns (uint256) { + ( , uint256 entireTroveColl, , , ) = getEntireDebtAndColl(_troveId); + return entireTroveColl; } function removeStake(uint256 _troveId) external override { @@ -1070,6 +1135,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; @@ -1090,7 +1156,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); @@ -1099,8 +1165,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" @@ -1113,8 +1179,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; @@ -1129,10 +1195,8 @@ 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); - _defaultPool.increaseBoldDebt(_debt); - _activePool.sendETHToDefaultPool(_coll); + _defaultPool.increaseBoldDebt(_debtToRedistribute); + _activePool.sendETHToDefaultPool(_collToRedistribute); } function closeTrove(uint256 _troveId) external override { @@ -1146,11 +1210,13 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana 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; @@ -1271,7 +1337,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // Update the baseRate state variable baseRate = newBaseRate; emit BaseRateUpdated(newBaseRate); - + _updateLastFeeOpTime(); return newBaseRate; @@ -1336,6 +1402,19 @@ 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 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[_troveId].annualInterestRate; + uint256 lastDebtUpdateTime = Troves[_troveId].lastDebtUpdateTime; + + return recordedDebt * annualInterestRate * (block.timestamp - lastDebtUpdateTime) / SECONDS_IN_ONE_YEAR / 1e18; + } + // --- 'require' wrapper functions --- function _requireCallerIsBorrowerOperations() internal view { @@ -1400,6 +1479,10 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return Troves[_troveId].debt; } + function getTroveWeightedRecordedDebt(uint256 _troveId) public view returns (uint256) { + return Troves[_troveId].debt * Troves[_troveId].annualInterestRate; + } + function getTroveColl(uint256 _troveId) external view override returns (uint) { return Troves[_troveId].coll; } @@ -1408,6 +1491,14 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return Troves[_troveId].annualInterestRate; } + function getTroveLastDebtUpdateTime(uint256 _troveId) external view returns (uint) { + return Troves[_troveId].lastDebtUpdateTime; + } + + function troveIsStale(uint256 _troveId) external view returns (bool) { + return block.timestamp - Troves[_troveId].lastDebtUpdateTime > STALE_TROVE_DURATION; + } + // --- Trove property setters, called by BorrowerOperations --- function setTrovePropertiesOnOpen( @@ -1424,51 +1515,57 @@ 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; + + _updateTroveDebtAndInterest(_troveId, _debt, _annualInterestRate); _updateTroveRewardSnapshots(_troveId); // mint ERC721 _mint(_owner, _troveId); + // Record the Trove's stake (for redistributions) and update the total stakes return _updateStakeAndTotalStakes(_troveId); } - function increaseTroveColl(address _sender, uint256 _troveId, uint _collIncrease) external override returns (uint) { + function updateTroveDebtAndInterest(uint256 _troveId, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) external { _requireCallerIsBorrowerOperations(); - _requireIsOwnerOrAddManager(_troveId, _sender); + _updateTroveDebtAndInterest(_troveId, _entireTroveDebt, _newAnnualInterestRate); + } - uint newColl = Troves[_troveId].coll + _collIncrease; - Troves[_troveId].coll = newColl; - return newColl; + function _updateTroveDebtAndInterest(uint256 _troveId, uint256 _entireTroveDebt, uint256 _newAnnualInterestRate) internal { + _updateTroveDebt(_troveId, _entireTroveDebt); + Troves[_troveId].annualInterestRate = _newAnnualInterestRate; } - function decreaseTroveColl(address _sender, uint256 _troveId, uint _collDecrease) external override returns (uint) { + function updateTroveDebtFromInterestApplication(uint256 _troveId, uint256 _entireTroveDebt) external { _requireCallerIsBorrowerOperations(); - _requireIsOwnerOrRemoveManager(_troveId, _sender); - - uint newColl = Troves[_troveId].coll - _collDecrease; - Troves[_troveId].coll = newColl; - return newColl; + _updateTroveDebt(_troveId, _entireTroveDebt); } - function increaseTroveDebt(address _sender, uint256 _troveId, uint _debtIncrease) external override returns (uint) { + function updateTroveDebt(address _sender, uint256 _troveId, uint256 _entireTroveDebt, bool _isDebtIncrease) external { _requireCallerIsBorrowerOperations(); - _requireIsOwnerOrRemoveManager(_troveId, _sender); + if (_isDebtIncrease) { + _requireIsOwnerOrRemoveManager(_troveId, _sender); + } else { + _requireIsOwnerOrAddManager(_troveId, _sender); + } + _updateTroveDebt(_troveId, _entireTroveDebt); + } - 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 635581ea..b10d6d90 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"; @@ -37,7 +37,6 @@ contract BaseTest is Test { uint256 CCR = 150e16; address public constant ZERO_ADDRESS = address(0); - // Core contracts IActivePool activePool; IBorrowerOperations borrowerOperations; @@ -50,6 +49,34 @@ contract BaseTest is Test { IPriceFeedTestnet priceFeed; 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; @@ -60,12 +87,20 @@ 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, - uint256 _boldAmount, + address _account, + uint256 _coll, + uint256 _boldAmount, uint256 _annualInterestRate - ) + ) public returns (uint256) { @@ -94,11 +129,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); @@ -117,6 +152,80 @@ 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 closeTrove(address _account, uint256 _troveId) public { + vm.startPrank(_account); + borrowerOperations.closeTrove(_troveId); + vm.stopPrank(); + } + + function withdrawBold100pctMaxFee(address _account, uint256 _troveId, uint256 _debtIncrease) public { + vm.startPrank(_account); + borrowerOperations.withdrawBold(_troveId, 1e18, _debtIncrease); + vm.stopPrank(); + } + + function repayBold(address _account, uint256 _troveId, uint256 _debtDecrease) public { + vm.startPrank(_account); + borrowerOperations.repayBold(_troveId, _debtDecrease); + vm.stopPrank(); + } + + function addColl(address _account, uint256 _troveId, uint256 _collIncrease) public { + vm.startPrank(_account); + borrowerOperations.addColl(_troveId, _collIncrease); + vm.stopPrank(); + } + + function withdrawColl(address _account, uint256 _troveId, uint256 _collDecrease) public { + vm.startPrank(_account); + borrowerOperations.withdrawColl(_troveId, _collDecrease); + vm.stopPrank(); + } + + function applyTroveInterestPermissionless(address _from, uint256 _troveId) public { + vm.startPrank(_from); + borrowerOperations.applyTroveInterestPermissionless(_troveId); + vm.stopPrank(); + } + + function transferBold(address _from, address _to, uint256 _amount) public { + vm.startPrank(_from); + boldToken.transfer(_to, _amount); + vm.stopPrank(); + } + + function liquidate(address _from, uint256 _troveId) public { + vm.startPrank(_from); + troveManager.liquidate(_troveId); + vm.stopPrank(); + } + + function withdrawETHGainToTrove(address _from, uint256 _troveId) public { + vm.startPrank(_from); + stabilityPool.withdrawETHGainToTrove(_troveId); + vm.stopPrank(); + } + + 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"); + troveManager.batchLiquidateTroves(_trovesList); + vm.stopPrank(); + } + function logContractAddresses() public view { console.log("ActivePool addr: ", address(activePool)); console.log("BorrowerOps addr: ", address(borrowerOperations)); @@ -128,4 +237,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 4abe6a83..2bd6f48c 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"; @@ -53,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)); @@ -63,13 +64,14 @@ contract DevTestSetup is BaseTest { priceFeed = new PriceFeedTestnet(); sortedTroves = new SortedTroves(); stabilityPool = new StabilityPool(address(WETH)); - troveManager = new TroveManager(); - boldToken = new BoldToken(address(troveManager), address(stabilityPool), address(borrowerOperations)); + 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) ); @@ -86,7 +88,7 @@ contract DevTestSetup is BaseTest { address(sortedTroves) ); - // set contracts in BorrowerOperations + // set contracts in BorrowerOperations borrowerOperations.setAddresses( address(troveManager), address(activePool), @@ -113,7 +115,9 @@ contract DevTestSetup is BaseTest { address(borrowerOperations), address(troveManager), address(stabilityPool), - address(defaultPool) + address(defaultPool), + address(boldToken), + address(mockInterestRouter) ); defaultPool.setAddresses( @@ -133,4 +137,102 @@ contract DevTestSetup is BaseTest { giveAndApproveETH(accountsList[i], initialETHAmount); } } + + function _setupForWithdrawETHGainToTrove() internal returns (uint256, uint256, uint256) { + uint256 troveDebtRequest_A = 2000e18; + uint256 troveDebtRequest_B = 3000e18; + uint256 troveDebtRequest_C = 4500e18; + uint256 interestRate = 5e16; // 5% + + uint256 price = 2000e18; + priceFeed.setPrice(price); + + 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(CTroveId, 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(CTroveId, price), "C CR before liq"); + + assertFalse(troveManager.checkRecoveryMode(price)); + assertLt(troveManager.getCurrentICR(CTroveId, price), troveManager.MCR()); + + // A liquidates C + liquidate(A, CTroveId); + + // check A has an ETH gain + assertGt(stabilityPool.getDepositorETHGain(A), 0); + + return (ATroveId, BTroveId, CTroveId); + } + + function _setupForBatchLiquidateTrovesPureOffset() internal returns (uint256, uint256, uint256, uint256) { + 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); + + 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(CTroveId, 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); + + assertFalse(troveManager.checkRecoveryMode(price)); + assertLt(troveManager.getCurrentICR(CTroveId, price), troveManager.MCR()); + assertLt(troveManager.getCurrentICR(DTroveId, price), troveManager.MCR()); + + return (ATroveId, BTroveId, CTroveId, DTroveId); + } + + function _setupForBatchLiquidateTrovesPureRedist() internal returns (uint256, uint256, uint256, uint256) { + 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); + + 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(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/deployment.t.sol b/contracts/src/test/deployment.t.sol index d8992215..eaab545c 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(); } @@ -65,6 +66,12 @@ contract Deployment is DevTestSetup { // 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 new file mode 100644 index 00000000..e650958e --- /dev/null +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -0,0 +1,2399 @@ +pragma solidity 0.8.18; + +import "./TestContracts/DevTestSetup.sol"; + +contract InterestRateAggregate is DevTestSetup { + + // --- Pending aggregate interest calculator --- + + function testCalcPendingAggInterestReturns0For0TimePassedSinceLastUpdate() public { + priceFeed.setPrice(2000e18); + assertEq(activePool.lastAggUpdateTime(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 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); + } + + // calcPendingAggInterest returns 0 with no recorded aggregate debt + + function testCalcPendingAggInterestReturns0When0AggRecordedDebt() public { + priceFeed.setPrice(2000e18); + assertEq(activePool.aggRecordedDebt(), 0); + assertEq(activePool.aggWeightedDebtSum(), 0); + assertEq(activePool.calcPendingAggInterest(), 0); + + vm.warp(block.timestamp + 1000); + assertEq(activePool.aggRecordedDebt(), 0); + assertEq(activePool.aggWeightedDebtSum(), 0); + assertEq(activePool.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(activePool.calcPendingAggInterest(), 0); + + vm.warp(block.timestamp + 1000); + + assertEq(activePool.calcPendingAggInterest(), 0); + + openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 0); + + assertEq(activePool.calcPendingAggInterest(), 0); + + vm.warp(block.timestamp + 1000); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + // TODO: create additional fuzz test + function testCalcPendingAggInterestReturnsCorrectInterestForGivenPeriod() public { + priceFeed.setPrice(2000e18); + uint256 _duration = 1 days; + + uint256 troveDebtRequest = 2000e18; + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 75e16); // 75% annual interest + console.log("A debt", troveManager.getTroveDebt(ATroveId)); + + uint256 expectedTroveDebt = troveDebtRequest + troveManager.BOLD_GAS_COMPENSATION(); + assertEq(troveManager.getTroveDebt(ATroveId), expectedTroveDebt); + assertEq(troveManager.getTroveDebt(BTroveId), 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, activePool.calcPendingAggInterest()); + } + + // --- calcTroveAccruedInterest + + // returns 0 for non-existent trove + function testCalcTroveAccruedInterestReturns0When0AggRecordedDebt() public { + priceFeed.setPrice(2000e18); + + assertEq(troveManager.calcTroveAccruedInterest(addressToTroveId(A)), 0); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); + 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, BTroveId); + + assertEq(troveManager.calcTroveAccruedInterest(BTroveId), 0); + } + // returns 0 for 0 time passed + + function testCalcTroveAccruedInterestReturns0For0TimePassed() public { + priceFeed.setPrice(2000e18); + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); + + vm.warp(block.timestamp + 1 days); + + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 75e16); + assertEq(troveManager.calcTroveAccruedInterest(BTroveId), 0); + } + + function testCalcTroveAccruedInterestReturns0For0InterestRate() public { + priceFeed.setPrice(2000e18); + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); + + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); + + vm.warp(block.timestamp + 1 days); + + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); + } + + // TODO: create additional corresponding fuzz test + function testCalcTroveAccruedInterestReturnsCorrectInterestForGivenPeriod() public { + priceFeed.setPrice(2000e18); + + uint256 annualRate_A = 1e18; + uint256 annualRate_B = 37e16; + uint256 debtRequest_A = 2000e18; + uint256 debtRequest_B = 2500e18; + + uint256 duration = 42 days; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, debtRequest_A, annualRate_A); + uint256 debt_A = troveManager.getTroveDebt(ATroveId); + assertGt(debt_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(ATroveId), expectedInterest_A); + + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, debtRequest_B, annualRate_B); + uint256 debt_B = troveManager.getTroveDebt(BTroveId); + assertGt(debt_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(BTroveId), expectedInterest_B); + } + + // --- mintAggInterest --- + + function testMintAggInterestRevertsWhenNotCalledByBOorSP() public { + // pass positive debt change + uint256 debtChange = 37e18; + vm.startPrank(A); + vm.expectRevert(); + activePool.mintAggInterest(debtChange, 0); + vm.stopPrank(); + + vm.startPrank(address(borrowerOperations)); + activePool.mintAggInterest(debtChange, 0); + vm.stopPrank(); + + vm.startPrank(address(stabilityPool)); + activePool.mintAggInterest(debtChange, 0); + vm.stopPrank(); + + // pass negative debt change + vm.startPrank(A); + vm.expectRevert(); + activePool.mintAggInterest(0, debtChange); + vm.stopPrank(); + + vm.startPrank(address(borrowerOperations)); + activePool.mintAggInterest(0, debtChange); + vm.stopPrank(); + + vm.startPrank(address(stabilityPool)); + activePool.mintAggInterest(0, debtChange); + vm.stopPrank(); + + // pass 0 debt change + vm.startPrank(A); + vm.expectRevert(); + activePool.mintAggInterest(0, 0); + vm.stopPrank(); + + vm.startPrank(address(borrowerOperations)); + activePool.mintAggInterest(0, 0); + vm.stopPrank(); + + vm.startPrank(address(stabilityPool)); + activePool.mintAggInterest(0, 0); + vm.stopPrank(); + } + + // --- openTrove impact on aggregates --- + + // openTrove increases recorded aggregate debt by correct amount + function testOpenTroveIncreasesRecordedAggDebtByAggPendingInterestPlusTroveDebt() public { + priceFeed.setPrice(2000e18); + 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 = activePool.aggRecordedDebt(); + assertGt(aggREcordedDebt_1, 0); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // check there's pending interest + uint256 pendingInterest = activePool.calcPendingAggInterest(); + assertGt(pendingInterest, 0); + + uint256 expectedTroveDebt_B = troveDebtRequest + troveManager.BOLD_GAS_COMPENSATION(); + 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); + } + + function testOpenTroveIncreasesRecordedDebtSumByTroveDebt() public { + priceFeed.setPrice(2000e18); + assertEq(activePool.aggRecordedDebt(), 0); + + uint256 troveDebtRequest = 2000e18; + uint256 ATroveId = 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(ATroveId); + assertGt(troveDebt_A, 0); + + assertEq(activePool.getRecordedDebtSum(), recordedDebt_1 + troveDebt_A); + } + + function testOpenTroveReducesPendingAggInterestTo0() public { + priceFeed.setPrice(2000e18); + + uint256 troveDebtRequest = 2000e18; + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); // 25% annual interest + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // check there's pending agg. interest + assertGt(activePool.calcPendingAggInterest(), 0); + + openTroveNoHints100pctMaxFee(B, 2 ether, troveDebtRequest, 25e16); + + // Check pending agg. interest reduced to 0 + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testOpenTroveUpdatesTheLastAggUpdateTime() public { + priceFeed.setPrice(2000e18); + assertEq(activePool.lastAggUpdateTime(), 0); + + vm.warp(block.timestamp + 1 days); + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 25e16); // 25% annual interest + + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + + vm.warp(block.timestamp + 1 days); + + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 25e16); // 25% annual interest + + assertEq(activePool.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 = activePool.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 = activePool.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(activePool.aggWeightedDebtSum(), 0); + + // A opens trove + 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 + uint256 expectedWeightedDebt_A = troveDebt_A * annualInterest_A; + console.log(expectedWeightedDebt_A, "expectedWeightedDebt_A"); + console.log(activePool.aggWeightedDebtSum(), "activePool.aggWeightedDebtSum()"); + + assertEq(activePool.aggWeightedDebtSum(), expectedWeightedDebt_A); + + vm.warp(block.timestamp + 1000); + + // B opens Trove + 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; + + assertEq(activePool.aggWeightedDebtSum(), expectedWeightedDebt_A + expectedWeightedDebt_B); + } + + // --- 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(activePool.calcPendingAggInterest(), 0); + + // A deposits to SP + makeSPDeposit(A, sPdeposit); + + // Check pending agg. interest reduced to 0 + assertEq(activePool.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 = activePool.calcPendingAggInterest(); + assertGt(pendingInterest, 0); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + + // A deposits to SP + makeSPDeposit(A, sPdeposit); + + // Check pending agg. debt increased + uint256 aggRecordedDebt_2 = activePool.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(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // A deposits to SP + makeSPDeposit(A, sPdeposit); + + // Check last agg update time increased to now + assertEq(activePool.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 = activePool.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 = activePool.aggWeightedDebtSum(); + assertGt(weightedDebtSum_1, 0); + + // Make SP deposit + makeSPDeposit(A, sPdeposit); + + // Get weighted sum after, check no change + uint256 weightedDebtSum_2 = activePool.aggWeightedDebtSum(); + 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 { + 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(activePool.calcPendingAggInterest(), 0); + + // A withdraws deposit + makeSPWithdrawal(A, sPdeposit); + + // Check pending agg. interest reduced to 0 + assertEq(activePool.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 = activePool.calcPendingAggInterest(); + assertGt(pendingInterest, 0); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + + // A withdraws deposit + makeSPWithdrawal(A, sPdeposit); + + // Check pending agg. debt increased + uint256 aggRecordedDebt_2 = activePool.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(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // A withdraws from SP + makeSPWithdrawal(A, sPdeposit); + + // Check last agg update time increased to now + assertEq(activePool.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); + + // Get I-router balance + uint256 boldBalRouter_1 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_1, 0); + + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // A withdraws from SP + makeSPWithdrawal(A, sPdeposit); + + // Check I-router Bold bal has increased as expected from 3rd trove opening + uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_2, pendingAggInterest); + } + + function testSPWithdrawalDoesNotChangeAggWeightedDebtSum() 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 weighted sum before + uint256 weightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(weightedDebtSum_1, 0); + + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // Make SP deposit + 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 + function testCloseTroveReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + // A, B open Troves + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + 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)); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + // check there's pending agg. interest + assertGt(activePool.calcPendingAggInterest(), 0); + + // B closes Trove + closeTrove(B, BTroveId); + + // // Check pending agg. interest reduced to 0 + assertEq(activePool.calcPendingAggInterest(), 0); + } + + // Increases agg recorded debt by pending agg interest + + function testCloseTroveAddsPendingAggInterestAndSubtractsRecordedDebtPlusInterestFromAggRecordedDebt() public { + uint256 troveDebtRequest = 2000e18; + // A, B open Troves + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + 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)); + + // 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(BTroveId); + assertGt(entireTroveDebt_B, troveManager.getTroveDebt(BTroveId)); + + // B closes Trove + 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); + } + + // Updates last agg update time to now + function testCloseTroveUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + // A, B open Troves + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + 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)); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // B closes Trove + closeTrove(B, BTroveId); + + // 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 open Troves + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + 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)); + + // 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, BTroveId); + + // 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 open Troves + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + 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)); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + 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; + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // B closes Trove + closeTrove(B, BTroveId); + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - weightedTroveDebt); + } + + function testCloseTroveReducesRecordedDebtSumByInitialRecordedDebt() public { + uint256 troveDebtRequest = 2000e18; + // A, B open Troves + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 5 ether, troveDebtRequest, 50e16); + uint256 recordedDebt_B = troveManager.getTroveDebt(BTroveId); + + 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, BTroveId); + + // 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); + 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)); + 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(BTroveId); + + // B closes Trove + 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())); + } + + // --- adjustTroveInterestRate --- + + function testAdjustTroveInterestRateWithNoPendingDebtGainIncreasesAggRecordedDebtByPendingAggInterest() public { + uint256 troveDebtRequest = 2000e18; + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, 75e16); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); + } + + function testAdjustTroveInterestRateReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + changeInterestRateNoHints(A, ATroveId, 75e16); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + // Update last agg. update time to now + function testAdjustTroveInterestRateUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, 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); + uint256 ATroveId = 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, ATroveId, 75e16); + + // Check I-router Bold bal has increased as expected + 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); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + 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(ATroveId); + + uint256 newAnnualInterestRate = 75e16; + uint256 expectedNewRecordedWeightedDebt = entireTroveDebt * newAnnualInterestRate; + + // A changes interest rate + 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); + } + + function testAdjustTroveInterestRateWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterest() public { + uint256 troveDebtRequest = 2000e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); + assertEq(pendingRedistDebtGain, 0); + uint256 pendingInterest = troveManager.calcTroveAccruedInterest(ATroveId); + assertGt(pendingInterest, 0); + + // Get current recorded active debt + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // A changes interest rate + changeInterestRateNoHints(A, ATroveId, 75e16); + + // Check recorded debt sum increases by the pending interest + assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 + pendingInterest); + } + + // --- withdrawBold tests --- + + function testWithdrawBoldWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterestPlusBorrowerDebtChange() public { + uint256 troveDebtRequest = 2000e18; + uint256 debtIncrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, debtIncrease); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest + debtIncrease); + } + + function testWithdrawBoldReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + uint256 debtIncrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, ATroveId, debtIncrease); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testWithdrawBoldMintsAggInterestToRouter() public { + uint256 troveDebtRequest = 2000e18; + uint256 debtIncrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, debtIncrease); + + assertEq(boldToken.balanceOf(address(mockInterestRouter)), aggInterest); + } + + // Updates last agg update time to now + function testWithdrawBoldUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + uint256 debtIncrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, 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); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); + assertEq(pendingRedistDebtGain, 0); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + assertGt(accruedTroveInterest, 0); + + // Get current recorded active debt + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // A draws more debt + withdrawBold100pctMaxFee(A, ATroveId, 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); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, ATroveId, debtIncrease); + + (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. + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); + } + + // --- repayBold tests --- + + function testRepayBoldWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterestMinusBorrowerDebtChange() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, debtDecrease); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest - debtDecrease); + } + + function testRepayBoldReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A repays debt + repayBold(A, ATroveId, debtDecrease); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testRepayBoldMintsAggInterestToRouter() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, debtDecrease); + + assertEq(boldToken.balanceOf(address(mockInterestRouter)), pendingAggInterest); + } + + function testRepayBoldUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, debtDecrease); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + function testRepayBoldWithNoPendingDebtRewardIncreasesRecordedDebtSumByTrovesAccruedInterestMinusDebtChange() public { + uint256 troveDebtRequest = 3000e18; + uint256 debtDecrease = 500e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); + assertEq(pendingRedistDebtGain, 0); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + assertGt(accruedTroveInterest, 0); + + // Get current recorded active debt + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // A repays debt + repayBold(A, ATroveId, 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); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // A repays debt + repayBold(A, ATroveId, debtDecrease); + + (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. + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - oldRecordedWeightedDebt + expectedNewRecordedWeightedDebt); + } + + // --- addColl tests --- + + function testAddCollWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + uint256 troveDebtRequest = 3000e18; + uint256 collIncrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, collIncrease); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); + } + + function testAddCollReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 3000e18; + uint256 collIncrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A adds coll + addColl(A, ATroveId, collIncrease); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testAddCollMintsAggInterestToRouter() public { + uint256 troveDebtRequest = 3000e18; + uint256 collIncrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, collIncrease); + + assertEq(boldToken.balanceOf(address(mockInterestRouter)), aggInterest); + } + + function testAddCollUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 3000e18; + uint256 collIncrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, 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); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); + assertEq(pendingRedistDebtGain, 0); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + assertGt(accruedTroveInterest, 0); + + // Get current recorded active debt + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // A adds coll + addColl(A, ATroveId, collIncrease); + + // Check recorded debt sum increases by the accrued interest + 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); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // A adds coll + addColl(A, ATroveId, collIncrease); + + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + 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); + uint256 ATroveId = 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, ATroveId, collDecrease); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); + } + + function testWithdrawCollReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + uint256 collDecrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A withdraws coll + withdrawColl(A, ATroveId, collDecrease); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testWithdrawCollMintsAggInterestToRouter() public { + uint256 troveDebtRequest = 2000e18; + uint256 collDecrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, collDecrease); + + assertEq(boldToken.balanceOf(address(mockInterestRouter)), aggInterest); + } + + function testWithdrawCollUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + uint256 collDecrease = 1 ether; + + // A opens Trove + priceFeed.setPrice(2000e18); + uint256 ATroveId = 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, ATroveId, 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); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, 25e16); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); + assertEq(pendingRedistDebtGain, 0); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + assertGt(accruedTroveInterest, 0); + + // Get current recorded active debt + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // A withdraw coll + withdrawColl(A, ATroveId, collDecrease); + + // Check recorded debt sum increases by the accrued interest + 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); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + uint256 oldRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); + + // fast-forward time + vm.warp(block.timestamp + 1 days); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // A withdraw coll + withdrawColl(A, ATroveId, collDecrease); + + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + 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); + } + + // --- applyTroveInterestPermissionless --- + + function testApplyTroveInterestPermissionlessWithNoPendingRewardIncreasesAggRecordedDebtByPendingAggInterest() public { + uint256 troveDebtRequest = 2000e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + 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(ATroveId)); + + uint256 aggRecordedDebt_1 = activePool.aggRecordedDebt(); + assertGt(aggRecordedDebt_1, 0); + uint256 pendingAggInterest = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest, 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, ATroveId); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); + } + + function testApplyTroveInterestPermissionlessReducesPendingAggInterestTo0() public { + uint256 troveDebtRequest = 2000e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + 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(ATroveId)); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, ATroveId); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testApplyTroveInterestPermissionlessMintsPendingAggInterestToRouter() public { + uint256 troveDebtRequest = 2000e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + 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(ATroveId)); + + // 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, ATroveId); + + // Check I-router Bold bal has increased by the pending agg interest + assertEq(boldToken.balanceOf(address(mockInterestRouter)), pendingAggInterest); + } + + function testApplyTroveInterestPermissionlessUpdatesLastAggUpdateTimeToNow() public { + uint256 troveDebtRequest = 2000e18; + + // A opens Trove + priceFeed.setPrice(2000e18); + 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(ATroveId)); + + assertGt(activePool.lastAggUpdateTime(), 0); + assertLt(activePool.lastAggUpdateTime(), block.timestamp); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, ATroveId); + + // 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); + 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(ATroveId)); + + uint256 pendingRedistDebtGain = troveManager.getPendingBoldDebtReward(ATroveId); + assertEq(pendingRedistDebtGain, 0); + 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, ATroveId); + + // 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); + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + 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(ATroveId)); + + uint256 aggWeightedDebtSum_1 = activePool.aggWeightedDebtSum(); + assertGt(aggWeightedDebtSum_1, 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, ATroveId); + + (uint256 entireTroveDebt, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + 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); + } + + // --- 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); + + 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(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 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); + + 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(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(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); + + 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 { + (uint256 ATroveId,,) = _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, ATroveId); + + assertEq(activePool.aggRecordedDebt(), aggRecordedDebt_1 + pendingAggInterest); + } + + function testWithdrawETHGainToTroveReducesPendingAggInterestTo0() public { + (uint256 ATroveId,,) = _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, ATroveId); + + // Check pending agg. interest reduced to 0 + assertEq(activePool.calcPendingAggInterest(), 0); + } + + function testWithdrawETHGainToTroveMintsInterestToRouter() public { + (uint256 ATroveId,,) = _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, ATroveId); + + // 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 { + (uint256 ATroveId,,) = _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, ATroveId); + + // Check last agg update time increased to now + assertEq(activePool.lastAggUpdateTime(), block.timestamp); + } + + function testWithdrawETHGainToTroveChangesAggWeightedDebtSumCorrectly() public { + (uint256 ATroveId,,) = _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(ATroveId); + assertGt(oldRecordedWeightedDebt, 0); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A, ATroveId); + + // Expect recorded weighted debt to have increased due to accrued Trove interest being applied + uint256 newRecordedWeightedDebt = troveManager.getTroveWeightedRecordedDebt(ATroveId); + 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 { + (uint256 ATroveId,,) = _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(ATroveId); + assertGt(oldTroveRecordedDebt, 0); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A, ATroveId); + + // Expect recorded debt to have increased due to accrued Trove interest being applied + uint256 newTroveRecordedDebt = troveManager.getTroveDebt(ATroveId); + assertGt(newTroveRecordedDebt, oldTroveRecordedDebt); + + // Get recorded sum after, check no change + assertEq(activePool.getRecordedDebtSum(), recordedDebt_1 - oldTroveRecordedDebt + newTroveRecordedDebt); + } + + // --- batchLiquidateTroves (Normal Mode, offset) --- + + function testBatchLiquidateTrovesPureOffsetChangesAggRecordedInterestCorrectly() public { + (,,uint256 CTroveId, uint256 DTroveId) = _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(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(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 + 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(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 { + (,,uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureOffset(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A liquidates C and D + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; + batchLiquidateTroves(A, trovesToLiq); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + // Mints interest to Router + function testBatchLiquidateTrovesPureOffsetMintsAggInterestToRouter() public { + (,,uint256 CTroveId, uint256 DTroveId) = _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 + 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 + uint256 boldBalRouter_2 = boldToken.balanceOf(address(mockInterestRouter)); + assertEq(boldBalRouter_2, pendingAggInterest); + } + + function testBatchLiquidateTrovesPureOffsetUpdatesLastAggInterestUpdateTimeToNow() public { + (,,uint256 CTroveId, uint256 DTroveId) = _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 + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; + 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 testBatchLiquidateTrovesPureOffsetRemovesLiquidatedTrovesWeightedRecordedDebtsFromWeightedRecordedDebtSum() public { + (,,uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureOffset(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + 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(DTroveId); + uint256 annualInterestRate_D = troveManager.getTroveAnnualInterestRate(DTroveId); + 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 + 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 + assertEq(activePool.aggWeightedDebtSum(), aggWeightedDebtSum_1 - (weightedTroveDebt_C + weightedTroveDebt_D)); + } + + function testBatchLiquidateTrovesPureOffsetWithNoRedistGainRemovesLiquidatedTrovesRecordedDebtsFromRecordedDebtSum() public { + (,,uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureOffset(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(CTroveId); + assertGt(recordedTroveDebt_C, 0); + + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(DTroveId); + assertGt(recordedTroveDebt_D, 0); + + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // A liquidates C and 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 + assertEq(activePool.getRecordedDebtSum(), recordedDebtSum_1 - (recordedTroveDebt_C + recordedTroveDebt_D)); + } + + // --- // --- batchLiquidateTroves (Normal Mode, redistribution) --- + + function testBatchLiquidateTrovesPureRedistChangesAggRecordedInterestCorrectly() public { + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _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(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(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 + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(ATroveId)); + + // Check both Troves were closed by liquidation + 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 { + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + assertGt(activePool.calcPendingAggInterest(), 0); + + // A liquidates C and D + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(ATroveId)); + + assertEq(activePool.calcPendingAggInterest(), 0); + } + + // Mints interest to Router + function testBatchLiquidateTrovesPureRedistMintsAggInterestToRouter() public { + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _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 + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(ATroveId)); + + // 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 { + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _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 + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(ATroveId)); + + // 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 { + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + 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(DTroveId); + uint256 annualInterestRate_D = troveManager.getTroveAnnualInterestRate(DTroveId); + 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 + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + 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 { + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_C = troveManager.getTroveDebt(CTroveId); + assertGt(recordedTroveDebt_C, 0); + + uint256 recordedTroveDebt_D = troveManager.getTroveDebt(DTroveId); + assertGt(recordedTroveDebt_D, 0); + + uint256 recordedDebtSum_1 = activePool.getRecordedDebtSum(); + + // A liquidates C and D + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + 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 { + (uint256 ATroveId, , uint256 CTroveId, uint256 DTroveId) = _setupForBatchLiquidateTrovesPureRedist(); + + // fast-forward time so interest accrues + vm.warp(block.timestamp + 1 days); + + 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(DTroveId); + uint256 accruedInterest_D = troveManager.calcTroveAccruedInterest(CTroveId); + 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 + uint256[] memory trovesToLiq = new uint256[](2); + trovesToLiq[0] = CTroveId; + trovesToLiq[1] = DTroveId; + batchLiquidateTroves(A, trovesToLiq); + // Check for redist. gains + assertTrue(troveManager.hasRedistributionGains(ATroveId)); + + // 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 3647170d..f1cf0831 100644 --- a/contracts/src/test/interestRateBasic.t.sol +++ b/contracts/src/test/interestRateBasic.t.sol @@ -8,17 +8,30 @@ 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(addressToTroveId(A)), 0); + assertEq(troveManager.getTroveLastDebtUpdateTime(addressToTroveId(B)), 0); + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + + vm.warp(block.timestamp + 1000); + uint256 BTroveId = openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 1); + assertEq(troveManager.getTroveLastDebtUpdateTime(BTroveId), block.timestamp); } function testOpenTroveInsertsToCorrectPositionInSortedList() public { @@ -31,38 +44,38 @@ contract InterestRateBasic is DevTestSetup { uint256 interestRate_D = 3e17; 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); + // B and D open + 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); } function testRevertWhenOpenTroveWithInterestRateGreaterThanMax() public { priceFeed.setPrice(2000e18); - + vm.startPrank(A); vm.expectRevert(); borrowerOperations.openTrove(A, 0, 1e18, 2e18, 2000e18, 0, 0, 1e18 + 1); @@ -89,121 +102,686 @@ 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 --- + function testAdjustTroveInterestRateSetsCorrectNewRate() public { 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, ATroveId, 0); + assertEq(troveManager.getTroveAnnualInterestRate(ATroveId), 0); + + changeInterestRateNoHints(B, BTroveId, 6e17); + assertEq(troveManager.getTroveAnnualInterestRate(BTroveId), 6e17); + + changeInterestRateNoHints(C, CTroveId, 1e18); + assertEq(troveManager.getTroveAnnualInterestRate(CTroveId), 1e18); + } + + function testAdjustTroveInterestRateSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + + changeInterestRateNoHints(A, ATroveId, 75e16); + + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + } + + function testAdjustTroveInterestRateSetsReducesPendingInterestTo0() public { + priceFeed.setPrice(2000e18); + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); + + changeInterestRateNoHints(A, ATroveId, 75e16); + + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); + } + + function testAdjustTroveInterestRateDoesNotChangeEntireTroveDebt() public { + priceFeed.setPrice(2000e18); + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + + vm.warp(block.timestamp + 1 days); - changeInterestRateNoHints(A, A_Id, 0); - assertEq(troveManager.getTroveAnnualInterestRate(A_Id), 0); + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + assertGt(entireTroveDebt_1, 0); - changeInterestRateNoHints(B, B_Id, 6e17); - assertEq(troveManager.getTroveAnnualInterestRate(B_Id), 6e17); + changeInterestRateNoHints(A, ATroveId, 75e16); - changeInterestRateNoHints(C, C_Id, 1e18); - assertEq(troveManager.getTroveAnnualInterestRate(C_Id), 1e18); + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + assertEq(entireTroveDebt_1, entireTroveDebt_2); + } + + function testAdjustTroveInterestRateNoRedistGainsIncreasesRecordedDebtByAccruedInterest() public { + priceFeed.setPrice(2000e18); + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + assertGt(recordedTroveDebt_1, 0); + + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + + changeInterestRateNoHints(A, ATroveId, 75e16); + + 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 --- + + function testWithdrawBoldSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + 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(A, ATroveId, boldWithdrawal); + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + } + + function testWithdrawBoldReducesTroveAccruedInterestTo0() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 boldWithdrawal = 500e18; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, ATroveId, boldWithdrawal); + + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); + } + + function testWithdrawBoldIncreasesEntireTroveDebtByWithdrawnAmount() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 boldWithdrawal = 500e18; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + assertGt(entireTroveDebt_1, 0); + + // A draws more debt + withdrawBold100pctMaxFee(A, ATroveId, boldWithdrawal); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + + assertEq(entireTroveDebt_2, entireTroveDebt_1 + boldWithdrawal); + } + + function testWithdrawBoldIncreasesRecordedTroveDebtByAccruedInterestPlusWithdrawnAmount() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 boldWithdrawal = 500e18; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + + // A draws more debt + withdrawBold100pctMaxFee(A, ATroveId, boldWithdrawal); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); + + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest + boldWithdrawal); + } + + // --- repayBold --- + + function testRepayBoldSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 3000e18; + uint256 interestRate = 25e16; + uint256 boldRepayment = 500e18; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + + // A repays bold + repayBold(A, ATroveId, boldRepayment); + + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + } + + function testRepayBoldReducesTroveAccruedInterestTo0() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 3000e18; + uint256 interestRate = 25e16; + uint256 boldRepayment = 500e18; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); + + // A repays bold + repayBold(A, ATroveId, boldRepayment); + + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); + } + + function testRepayBoldReducesEntireTroveDebtByRepaidAmount() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 3000e18; + uint256 interestRate = 25e16; + uint256 boldRepayment = 500e18; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + assertGt(entireTroveDebt_1, 0); + + // A repays bold + repayBold(A, ATroveId, boldRepayment); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + + + assertEq(entireTroveDebt_2, entireTroveDebt_1 - boldRepayment); + } + + function testRepayBoldChangesRecordedTroveDebtByAccruedInterestMinusRepaidAmount() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 3000e18; + uint256 interestRate = 25e16; + uint256 boldRepayment = 500e18; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + + // A repays bold + repayBold(A, ATroveId, boldRepayment); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); + + 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 adds coll + addColl(A, ATroveId, collIncrease); + + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + } + + function testAddCollReducesTroveAccruedInterestTo0() 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); + + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); + + // A adds coll + addColl(A, ATroveId, collIncrease); + + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); + } + + function testAddCollDoesntChangeEntireTroveDebt() 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); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + assertGt(entireTroveDebt_1, 0); + + // A adds coll + addColl(A, ATroveId, collIncrease); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + + assertEq(entireTroveDebt_2, entireTroveDebt_1); + } + + function testAddCollIncreasesRecordedTroveDebtByAccruedInterest() 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); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + + // A adds coll + addColl(A, ATroveId, collIncrease); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); + + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); + } + + // --- withdrawColl --- + + function testWithdrawCollSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collDecrease = 1 ether; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + + // A withdraws coll + withdrawColl(A, ATroveId, collDecrease); + + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + } + + function testWithdrawCollReducesTroveAccruedInterestTo0() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collDecrease = 1 ether; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); + + // A withdraws coll + withdrawColl(A, ATroveId, collDecrease); + + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); + } + + function testWithdrawCollDoesntChangeEntireTroveDebt() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collDecrease = 1 ether; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + assertGt(entireTroveDebt_1, 0); + + // A withdraws coll + withdrawColl(A, ATroveId, collDecrease); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + + assertEq(entireTroveDebt_2, entireTroveDebt_1); + } + + function testWithdrawCollIncreasesRecordedTroveDebtByAccruedInterest() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + uint256 collDecrease = 1 ether; + + uint256 ATroveId = openTroveNoHints100pctMaxFee(A, 3 ether, troveDebtRequest, interestRate); + + vm.warp(block.timestamp + 1 days); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + + // A withdraws coll + withdrawColl(A, ATroveId, collDecrease); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); + + assertEq(recordedTroveDebt_2, recordedTroveDebt_1 + accruedTroveInterest); + } + + // --- applyTroveInterestPermissionless --- + + function testApplyTroveInterestPermissionlessSetsTroveLastDebtUpdateTimeToNow() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + + 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(ATroveId)); + + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, ATroveId); + + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + } + + function testApplyTroveInterestPermissionlessReducesTroveAccruedInterestTo0() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + + 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(ATroveId)); + + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, ATroveId); + + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); + } + + function testApplyTroveInterestPermissionlessDoesntChangeEntireTroveDebt() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + + 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(ATroveId)); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + assertGt(entireTroveDebt_1, 0); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, ATroveId); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + + assertEq(entireTroveDebt_2, entireTroveDebt_1); + } + + function testApplyTroveInterestPermissionlessIncreasesRecordedTroveDebtByAccruedInterest() public { + priceFeed.setPrice(2000e18); + uint256 troveDebtRequest = 2000e18; + uint256 interestRate = 25e16; + + 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(ATroveId)); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + + // B applies A's pending interest + applyTroveInterestPermissionless(B, ATroveId); + + uint256 recordedTroveDebt_2 = troveManager.getTroveDebt(ATroveId); + + 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 { + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); + + // Fast-forward time + vm.warp(block.timestamp + 1 days); + + assertLt(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A, ATroveId); + + assertEq(troveManager.getTroveLastDebtUpdateTime(ATroveId), block.timestamp); + } + + function testWithdrawETHGainToTroveReducesTroveAccruedInterestTo0() public { + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); + + // Fast-forward time + vm.warp(block.timestamp + 1 days); + + assertGt(troveManager.calcTroveAccruedInterest(ATroveId), 0); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A, ATroveId); + + assertEq(troveManager.calcTroveAccruedInterest(ATroveId), 0); + } + + function testWithdrawETHGainToTroveDoesntChangeEntireTroveDebt() public { + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); + + // Fast-forward time + vm.warp(block.timestamp + 90 days); + + (uint256 entireTroveDebt_1, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + assertGt(entireTroveDebt_1, 0); + + console.log(troveManager.getCurrentICR(ATroveId, 1000e18), "A ICR before"); + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A, ATroveId); + + console.log(troveManager.getCurrentICR(ATroveId, 1000e18), "A ICR after"); + + (uint256 entireTroveDebt_2, , , , ) = troveManager.getEntireDebtAndColl(ATroveId); + console.log(entireTroveDebt_2, "entireTroveDebt_2"); + console.log(entireTroveDebt_1, "entireTroveDebt_1"); + + assertEq(entireTroveDebt_2, entireTroveDebt_1); + } + + function testWithdrawETHGainToTroveIncreasesRecordedTroveDebtByAccruedInterest() public { + (uint256 ATroveId,,) = _setupForWithdrawETHGainToTrove(); + + // Fast-forward time + vm.warp(block.timestamp + 90 days + 1); + + uint256 recordedTroveDebt_1 = troveManager.getTroveDebt(ATroveId); + uint256 accruedTroveInterest = troveManager.calcTroveAccruedInterest(ATroveId); + + // A withdraws ETH gain to Trove + withdrawETHGainToTrove(A, ATroveId); + + 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 d17fa409..4e59f5a0 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) { @@ -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) { @@ -231,7 +205,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 +221,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 09b73122..9949240f 100644 --- a/contracts/test/BorrowerOperationsTest.js +++ b/contracts/test/BorrowerOperationsTest.js @@ -1134,7 +1134,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( @@ -1145,7 +1145,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))) @@ -1389,7 +1389,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( @@ -1399,7 +1399,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))) @@ -2121,7 +2121,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)); @@ -2140,7 +2140,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)); @@ -2575,7 +2575,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 @@ -2591,7 +2591,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))) @@ -2611,7 +2611,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 @@ -2627,7 +2627,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, @@ -3077,7 +3077,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 }); @@ -3190,7 +3190,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"))); @@ -3203,7 +3203,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); }); @@ -4113,7 +4113,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({ @@ -4124,7 +4124,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)); }); @@ -4376,27 +4376,35 @@ contract("BorrowerOperations", async (accounts) => { // 0, 0 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)); + 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)); @@ -4404,9 +4412,6 @@ 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 = 0; @@ -4415,13 +4420,13 @@ contract("BorrowerOperations", async (accounts) => { true, debtChange, true, - price + liqPrice ); - - const expectedTCR = troveColl + + const expectedTCR = whaleColl .add(liquidatedColl) - .mul(price) - .div(troveTotalDebt.add(liquidatedDebt)); + .mul(liqPrice) + .div(whaleTotalDebt.add(liquidatedDebt)); assert.isTrue(newTCR.eq(expectedTCR)); }); @@ -4429,27 +4434,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)); @@ -4457,52 +4471,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)); @@ -4510,51 +4530,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)); @@ -4562,52 +4588,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)); @@ -4615,25 +4647,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)); }); @@ -4641,27 +4669,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)); @@ -4669,25 +4706,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)); }); @@ -4695,27 +4728,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)); @@ -4723,25 +4765,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)); }); @@ -4749,27 +4787,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)); @@ -4777,53 +4824,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)); @@ -4831,25 +4883,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/CollSurplusPool.js b/contracts/test/CollSurplusPool.js index b12428ba..5bfe9be1 100644 --- a/contracts/test/CollSurplusPool.js +++ b/contracts/test/CollSurplusPool.js @@ -32,10 +32,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/OwnershipTest.js b/contracts/test/OwnershipTest.js index 4f5cbf88..a5dadc52 100644 --- a/contracts/test/OwnershipTest.js +++ b/contracts/test/OwnershipTest.js @@ -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 6ba4f951..414bbf33 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 NonPayableSwitch.new() const dumbContractAddress = (await NonPayableSwitch.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,37 +58,37 @@ 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() + 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.getBoldDebt() + const recordedBold_balanceAfter = await activePool.getRecordedDebtSum() assert.equal(recordedBold_balanceAfter, 100) }) // Decrease 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) - 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 decreaseBoldDebtData = th.getTransactionData('decreaseRecordedDebtSum(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 42364f5d..6d352d67 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 4777cda1..221fb937 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); @@ -433,6 +434,8 @@ contract("TroveManager", async (accounts) => { 100 ); + 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({ ICR: toBN(dec(111, 16)), @@ -1134,15 +1137,15 @@ contract("TroveManager", async (accounts) => { assert.isFalse(await th.checkRecoveryMode(contracts)); // Liquidate A, B and C - const activeBoldDebt_0 = await activePool.getBoldDebt(); + const activeBoldDebt_0 = await activePool.getRecordedDebtSum(); const defaultBoldDebt_0 = await defaultPool.getBoldDebt(); await troveManager.liquidate(aliceTroveId); - const activeBoldDebt_A = await activePool.getBoldDebt(); + const activeBoldDebt_A = await activePool.getRecordedDebtSum(); const defaultBoldDebt_A = await defaultPool.getBoldDebt(); await troveManager.liquidate(bobTroveId); - const activeBoldDebt_B = await activePool.getBoldDebt(); + const activeBoldDebt_B = await activePool.getRecordedDebtSum(); const defaultBoldDebt_B = await defaultPool.getBoldDebt(); await troveManager.liquidate(carolTroveId); @@ -1840,9 +1843,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); @@ -3836,7 +3839,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 +3876,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 +3939,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 +4322,7 @@ contract("TroveManager", async (accounts) => { const totalDebt = C_totalDebt.add(D_totalDebt); th.assertIsApproximatelyEqual( - (await activePool.getBoldDebt()).toString(), + (await activePool.getRecordedDebtSum()).toString(), totalDebt ); @@ -4394,7 +4397,7 @@ contract("TroveManager", async (accounts) => { const A_balanceBefore = toBN(await contracts.WETH.balanceOf(A)); // Check total Bold supply - const activeBold = await activePool.getBoldDebt(); + const activeBold = await activePool.getRecordedDebtSum(); const defaultBold = await defaultPool.getBoldDebt(); const totalBoldSupply = activeBold.add(defaultBold); @@ -5068,8 +5071,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/test/TroveManager_LiquidationRewardsTest.js b/contracts/test/TroveManager_LiquidationRewardsTest.js index 5c5c620f..80e4f414 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; @@ -1423,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({ @@ -1432,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); @@ -1456,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(); @@ -1539,9 +1526,10 @@ 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( 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 47cec8f6..e699c4d8 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; @@ -1198,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 @@ -2280,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 --- @@ -2302,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(); + price = await priceFeed.getPrice(); const bob_surplus = B_coll.sub(B_netDebt.mul(mv._1e18BN).div(price)); th.assertIsApproximatelyEqual( await collSurplusPool.getCollateral(bob), @@ -2320,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, @@ -2339,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/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 52151764..6ee7e8cf 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" @@ -44,8 +45,10 @@ class DeploymentHelper { const boldToken = await BoldToken.new( troveManager.address, stabilityPool.address, - borrowerOperations.address + borrowerOperations.address, + activePool.address ); + const mockInterestRouter = await MockInterestRouter.new(); const hintHelpers = await HintHelpers.new(); @@ -59,7 +62,7 @@ class DeploymentHelper { // ); // TODO: setAsDeployed all above? - + BoldToken.setAsDeployed(boldToken); DefaultPool.setAsDeployed(defaultPool); PriceFeedTestnet.setAsDeployed(priceFeedTestnet); @@ -71,6 +74,7 @@ class DeploymentHelper { CollSurplusPool.setAsDeployed(collSurplusPool); BorrowerOperations.setAsDeployed(borrowerOperations); HintHelpers.setAsDeployed(hintHelpers); + MockInterestRouter.setAsDeployed(mockInterestRouter); const coreContracts = { WETH, @@ -84,7 +88,8 @@ class DeploymentHelper { defaultPool, collSurplusPool, borrowerOperations, - hintHelpers + hintHelpers, + mockInterestRouter }; return coreContracts; } @@ -93,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; } @@ -147,6 +153,8 @@ class DeploymentHelper { contracts.troveManager.address, contracts.stabilityPool.address, contracts.defaultPool.address, + contracts.boldToken.address, + contracts.mockInterestRouter.address //contracts.stETH.address, ); 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