diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 204e45c51..dbdf4df1f 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -63,7 +63,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint netDebt; uint compositeDebt; uint ICR; - uint NICR; uint stake; uint arrayIndex; } @@ -154,16 +153,19 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // --- Borrower Trove Operations --- - function openTrove(uint _maxFeePercentage, uint _boldAmount, address _upperHint, address _lowerHint) external payable override { + function openTrove(uint _maxFeePercentage, uint _boldAmount, address _upperHint, address _lowerHint, uint256 _annualInterestRate) external payable override { ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, boldToken); LocalVariables_openTrove memory vars; vars.price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryMode(vars.price); + _requireValidAnnualInterestRate(_annualInterestRate); _requireValidMaxFeePercentage(_maxFeePercentage, isRecoveryMode); _requireTroveisNotActive(contractsCache.troveManager, msg.sender); + // TODO: apply aggregate pending interest, and take snapshot of current timestamp. + vars.BoldFee; vars.netDebt = _boldAmount; @@ -177,7 +179,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe assert(vars.compositeDebt > 0); vars.ICR = LiquityMath._computeCR(msg.value, vars.compositeDebt, vars.price); - vars.NICR = LiquityMath._computeNominalCR(msg.value, vars.compositeDebt); if (isRecoveryMode) { _requireICRisAboveCCR(vars.ICR); @@ -187,15 +188,15 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe _requireNewTCRisAboveCCR(newTCR); } - // Set the trove struct's properties - contractsCache.troveManager.setTroveStatus(msg.sender, 1); - contractsCache.troveManager.increaseTroveColl(msg.sender, msg.value); - contractsCache.troveManager.increaseTroveDebt(msg.sender, vars.compositeDebt); - - contractsCache.troveManager.updateTroveRewardSnapshots(msg.sender); - vars.stake = contractsCache.troveManager.updateStakeAndTotalStakes(msg.sender); + // Set the stored Trove properties + vars.stake = contractsCache.troveManager.setTrovePropertiesOnOpen( + msg.sender, + msg.value, + vars.compositeDebt, + _annualInterestRate + ); - sortedTroves.insert(msg.sender, vars.NICR, _upperHint, _lowerHint); + sortedTroves.insert(msg.sender, _annualInterestRate, _upperHint, _lowerHint); vars.arrayIndex = contractsCache.troveManager.addTroveOwnerToArray(msg.sender); emit TroveCreated(msg.sender, vars.arrayIndex); @@ -210,33 +211,48 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } // Send ETH as collateral to a trove - function addColl(address _upperHint, address _lowerHint) external payable override { - _adjustTrove(msg.sender, 0, 0, false, _upperHint, _lowerHint, 0); + function addColl() external payable override { + _adjustTrove(msg.sender, 0, 0, false, 0); } // Send ETH as collateral to a trove. Called by only the Stability Pool. - function moveETHGainToTrove(address _borrower, address _upperHint, address _lowerHint) external payable override { + function moveETHGainToTrove(address _borrower) external payable override { _requireCallerIsStabilityPool(); - _adjustTrove(_borrower, 0, 0, false, _upperHint, _lowerHint, 0); + _adjustTrove(_borrower, 0, 0, false, 0); } // Withdraw ETH collateral from a trove - function withdrawColl(uint _collWithdrawal, address _upperHint, address _lowerHint) external override { - _adjustTrove(msg.sender, _collWithdrawal, 0, false, _upperHint, _lowerHint, 0); + function withdrawColl(uint _collWithdrawal) external override { + _adjustTrove(msg.sender, _collWithdrawal, 0, false, 0); } // Withdraw Bold tokens from a trove: mint new Bold tokens to the owner, and increase the trove's debt accordingly - function withdrawBold(uint _maxFeePercentage, uint _boldAmount, address _upperHint, address _lowerHint) external override { - _adjustTrove(msg.sender, 0, _boldAmount, true, _upperHint, _lowerHint, _maxFeePercentage); + function withdrawBold(uint _maxFeePercentage, uint _boldAmount ) external override { + _adjustTrove(msg.sender, 0, _boldAmount, true, _maxFeePercentage); } // Repay Bold tokens to a Trove: Burn the repaid Bold tokens, and reduce the trove's debt accordingly - function repayBold(uint _boldAmount, address _upperHint, address _lowerHint) external override { - _adjustTrove(msg.sender, 0, _boldAmount, false, _upperHint, _lowerHint, 0); + function repayBold(uint _boldAmount) external override { + _adjustTrove(msg.sender, 0, _boldAmount, false, 0); } - function adjustTrove(uint _maxFeePercentage, uint _collWithdrawal, uint _boldChange, bool _isDebtIncrease, address _upperHint, address _lowerHint) external payable override { - _adjustTrove(msg.sender, _collWithdrawal, _boldChange, _isDebtIncrease, _upperHint, _lowerHint, _maxFeePercentage); + function adjustTrove(uint _maxFeePercentage, uint _collWithdrawal, uint _boldChange, bool _isDebtIncrease) external payable override { + _adjustTrove(msg.sender, _collWithdrawal, _boldChange, _isDebtIncrease, _maxFeePercentage); + } + + function adjustTroveInterestRate(uint _newAnnualInterestRate, address _upperHint, address _lowerHint) external { + _requireValidAnnualInterestRate(_newAnnualInterestRate); + ITroveManager troveManagerCached = troveManager; + _requireTroveisActive(troveManagerCached, msg.sender); + + // 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. + + troveManagerCached.applyPendingRewards(msg.sender); + + sortedTroves.reInsert(msg.sender, _newAnnualInterestRate, _upperHint, _lowerHint); + + troveManagerCached.changeAnnualInterestRate(msg.sender, _newAnnualInterestRate); } /* @@ -246,7 +262,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe * * If both are positive, it will revert. */ - function _adjustTrove(address _borrower, uint _collWithdrawal, uint _boldChange, bool _isDebtIncrease, address _upperHint, address _lowerHint, uint _maxFeePercentage) internal { + function _adjustTrove(address _borrower, uint _collWithdrawal, uint _boldChange, bool _isDebtIncrease, uint _maxFeePercentage) internal { ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, boldToken); LocalVariables_adjustTrove memory vars; @@ -264,6 +280,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe // Confirm the operation is either a borrower adjusting their own trove, or a pure ETH transfer from the Stability Pool to a trove assert(msg.sender == _borrower || (msg.sender == stabilityPoolAddress && msg.value > 0 && _boldChange == 0)); + // TODO: apply individual and aggregate pending interest, and take snapshots of current timestamp. + contractsCache.troveManager.applyPendingRewards(_borrower); // Get the collChange based on whether or not ETH was sent in the transaction @@ -297,10 +315,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe (vars.newColl, vars.newDebt) = _updateTroveFromAdjustment(contractsCache.troveManager, _borrower, vars.collChange, vars.isCollIncrease, vars.netDebtChange, _isDebtIncrease); vars.stake = contractsCache.troveManager.updateStakeAndTotalStakes(_borrower); - // Re-insert trove in to the sorted list - uint newNICR = _getNewNominalICRFromTroveChange(vars.coll, vars.debt, vars.collChange, vars.isCollIncrease, vars.netDebtChange, _isDebtIncrease); - sortedTroves.reInsert(_borrower, newNICR, _upperHint, _lowerHint); - emit TroveUpdated(_borrower, vars.newDebt, vars.newColl, vars.stake, BorrowerOperation.adjustTrove); emit BoldBorrowingFeePaid(msg.sender, vars.BoldFee); @@ -326,6 +340,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe uint price = priceFeed.fetchPrice(); _requireNotInRecoveryMode(price); + // TODO: apply individual and aggregate pending interest, and take snapshots of current timestamp. + troveManagerCached.applyPendingRewards(msg.sender); uint coll = troveManagerCached.getTroveColl(msg.sender); @@ -560,28 +576,12 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe } } - // --- ICR and TCR getters --- - - // Compute the new collateral ratio, considering the change in coll and debt. Assumes 0 pending rewards. - function _getNewNominalICRFromTroveChange - ( - uint _coll, - uint _debt, - uint _collChange, - bool _isCollIncrease, - uint _debtChange, - bool _isDebtIncrease - ) - pure - internal - returns (uint) - { - (uint newColl, uint newDebt) = _getNewTroveAmounts(_coll, _debt, _collChange, _isCollIncrease, _debtChange, _isDebtIncrease); - - uint newNICR = LiquityMath._computeNominalCR(newColl, newDebt); - return newNICR; + function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal pure { + require(_annualInterestRate <= MAX_ANNUAL_INTEREST_RATE, "Interest rate must not be greater than max"); } + // --- ICR and TCR getters --- + // Compute the new collateral ratio, considering the change in coll and debt. Assumes 0 pending rewards. function _getNewICRFromTroveChange ( diff --git a/contracts/src/Dependencies/LiquityBase.sol b/contracts/src/Dependencies/LiquityBase.sol index 36f7c4d9d..e30187c22 100644 --- a/contracts/src/Dependencies/LiquityBase.sol +++ b/contracts/src/Dependencies/LiquityBase.sol @@ -29,6 +29,8 @@ contract LiquityBase is BaseMath, ILiquityBase { uint constant public MIN_NET_DEBT = 1800e18; // uint constant public MIN_NET_DEBT = 0; + uint256 constant public MAX_ANNUAL_INTEREST_RATE = 1e18; // 100% + uint constant public PERCENT_DIVISOR = 200; // dividing by 200 yields 0.5% uint constant public BORROWING_FEE_FLOOR = DECIMAL_PRECISION / 1000 * 5; // 0.5% diff --git a/contracts/src/Interfaces/IBorrowerOperations.sol b/contracts/src/Interfaces/IBorrowerOperations.sol index 5db241c65..e96816c95 100644 --- a/contracts/src/Interfaces/IBorrowerOperations.sol +++ b/contracts/src/Interfaces/IBorrowerOperations.sol @@ -25,23 +25,25 @@ interface IBorrowerOperations is ILiquityBase { address _lqtyStakingAddress ) external; - function openTrove(uint _maxFee, uint _boldAmount, address _upperHint, address _lowerHint) external payable; + function openTrove(uint _maxFee, uint _boldAmount, address _upperHint, address _lowerHint, uint256 _annualInterestRate) external payable; - function addColl(address _upperHint, address _lowerHint) external payable; + function addColl() external payable; - function moveETHGainToTrove(address _user, address _upperHint, address _lowerHint) external payable; + function moveETHGainToTrove(address _user) external payable; - function withdrawColl(uint _amount, address _upperHint, address _lowerHint) external; + function withdrawColl(uint _amount) external; - function withdrawBold(uint _maxFee, uint _amount, address _upperHint, address _lowerHint) external; + function withdrawBold(uint _maxFee, uint _amount) external; - function repayBold(uint _amount, address _upperHint, address _lowerHint) external; + function repayBold(uint _amount) external; function closeTrove() external; - function adjustTrove(uint _maxFee, uint _collWithdrawal, uint _debtChange, bool isDebtIncrease, address _upperHint, address _lowerHint) external payable; + function adjustTrove(uint _maxFee, uint _collWithdrawal, uint _debtChange, bool isDebtIncrease) external payable; function claimCollateral() external; function getCompositeDebt(uint _debt) external pure returns (uint); + + function adjustTroveInterestRate(uint _newAnnualInterestRate, address _upperHint, address _lowerHint) external; } diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index 96c6f4130..efa8477c1 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -69,7 +69,7 @@ interface IStabilityPool is ILiquityBase { * - Leaves their compounded deposit in the Stability Pool * - Takes new snapshots of accumulators P and S */ - function withdrawETHGainToTrove(address _upperHint, address _lowerHint) external; + function withdrawETHGainToTrove() external; /* * Initial checks: diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index e17dc849b..2899fca7e 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -60,8 +60,6 @@ interface ITroveManager is ILiquityBase { function updateStakeAndTotalStakes(address _borrower) external returns (uint); - function updateTroveRewardSnapshots(address _borrower) external; - function addTroveOwnerToArray(address _borrower) external returns (uint index); function applyPendingRewards(address _borrower) external; @@ -104,7 +102,9 @@ interface ITroveManager is ILiquityBase { function getTroveColl(address _borrower) external view returns (uint); - function setTroveStatus(address _borrower, uint num) external; + function getTroveAnnualInterestRate(address _borrower) external view returns (uint); + + function setTrovePropertiesOnOpen(address _borrower, uint256 _coll, uint256 _debt, uint256 _annualInterestRate) external returns (uint256); function increaseTroveColl(address _borrower, uint _collIncrease) external returns (uint); @@ -114,6 +114,8 @@ interface ITroveManager is ILiquityBase { function decreaseTroveDebt(address _borrower, uint _collDecrease) external returns (uint); + function changeAnnualInterestRate(address _borrower, uint256 _newAnnualInterestRate) external returns (uint256); + function getTCR(uint _price) external view returns (uint); function checkRecoveryMode(uint _price) external view returns (bool); diff --git a/contracts/src/MultiTroveGetter.sol b/contracts/src/MultiTroveGetter.sol index 580893b21..d0c0ff66d 100644 --- a/contracts/src/MultiTroveGetter.sol +++ b/contracts/src/MultiTroveGetter.sol @@ -78,7 +78,8 @@ contract MultiTroveGetter { _troves[idx].coll, _troves[idx].stake, /* status */, - /* arrayIndex */ + /* arrayIndex */, + /* annualInterestRate */ ) = troveManager.Troves(currentTroveowner); ( _troves[idx].snapshotETH, @@ -107,7 +108,8 @@ contract MultiTroveGetter { _troves[idx].coll, _troves[idx].stake, /* status */, - /* arrayIndex */ + /* arrayIndex */, + /* annualInterestRate */ ) = troveManager.Troves(currentTroveowner); ( _troves[idx].snapshotETH, diff --git a/contracts/src/SortedTroves.sol b/contracts/src/SortedTroves.sol index 23195c767..f87ada42e 100644 --- a/contracts/src/SortedTroves.sol +++ b/contracts/src/SortedTroves.sol @@ -12,32 +12,23 @@ import "./Dependencies/CheckContract.sol"; * A sorted doubly linked list with nodes sorted in descending order. * * Nodes map to active Troves in the system - the ID property is the address of a Trove owner. -* Nodes are ordered according to their current nominal individual collateral ratio (NICR), -* which is like the ICR but without the price, i.e., just collateral / debt. +* Nodes are ordered according to the borrower's chosen annual interest rate. * * The list optionally accepts insert position hints. * -* NICRs are computed dynamically at runtime, and not stored on the Node. This is because NICRs of active Troves -* change dynamically as liquidation events occur. +* The annual interest rate is stored on the Trove struct in TroveManager, not directly on the Node. * -* The list relies on the fact that liquidation events preserve ordering: a liquidation decreases the NICRs of all active Troves, -* but maintains their order. A node inserted based on current NICR will maintain the correct position, -* relative to it's peers, as rewards accumulate, as long as it's raw collateral and debt have not changed. -* Thus, Nodes remain sorted by current NICR. -* -* Nodes need only be re-inserted upon a Trove operation - when the owner adds or removes collateral or debt -* to their position. +* A node need only be re-inserted when the borrower adjusts their interest rate. Interest rate order is preserved +* under all other system operations. * * The list is a modification of the following audited SortedDoublyLinkedList: * https://github.com/livepeer/protocol/blob/master/contracts/libraries/SortedDoublyLL.sol * -* -* Changes made in the Liquity implementation: +* Changes made in the Bold implementation: * * - Keys have been removed from nodes * -* - Ordering checks for insertion are performed by comparing an NICR argument to the current NICR, calculated at runtime. -* The list relies on the property that ordering by ICR is maintained as the ETH:USD price varies. +* - Ordering checks for insertion are performed by comparing an interest rate argument to the Trove's current interest rate. * * - Public functions with parameters have been made internal to save gas, and given an external wrapper function for external access */ @@ -46,7 +37,7 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { event TroveManagerAddressChanged(address _troveManagerAddress); event BorrowerOperationsAddressChanged(address _borrowerOperationsAddress); - event NodeAdded(address _id, uint _NICR); + event NodeAdded(address _id, uint _annualInterestRate); event NodeRemoved(address _id); address public borrowerOperationsAddress; @@ -56,14 +47,14 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { // Information for a node in the list struct Node { bool exists; - address nextId; // Id of next node (smaller NICR) in the list - address prevId; // Id of previous node (larger NICR) in the list + address nextId; // Id of next node (smaller interest rate) in the list + address prevId; // Id of previous node (larger interest rate) in the list } // Information for the list struct Data { - address head; // Head of the list. Also the node in the list with the largest NICR - address tail; // Tail of the list. Also the node in the list with the smallest NICR + address head; // Head of the list. Also the node in the list with the largest interest rate + address tail; // Tail of the list. Also the node in the list with the smallest interest rate uint256 maxSize; // Maximum size of the list uint256 size; // Current size of the list mapping (address => Node) nodes; // Track the corresponding ids for each node in the list @@ -92,35 +83,33 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { /* * @dev Add a node to the list * @param _id Node's id - * @param _NICR Node's NICR + * @param _annualInterestRate Node's annual interest rate * @param _prevId Id of previous node for the insert position * @param _nextId Id of next node for the insert position */ - function insert (address _id, uint256 _NICR, address _prevId, address _nextId) external override { + function insert (address _id, uint256 _annualInterestRate, address _prevId, address _nextId) external override { ITroveManager troveManagerCached = troveManager; _requireCallerIsBOorTroveM(troveManagerCached); - _insert(troveManagerCached, _id, _NICR, _prevId, _nextId); + _insert(troveManagerCached, _id, _annualInterestRate, _prevId, _nextId); } - function _insert(ITroveManager _troveManager, address _id, uint256 _NICR, address _prevId, address _nextId) internal { + function _insert(ITroveManager _troveManager, address _id, uint256 _annualInterestRate, address _prevId, address _nextId) internal { // List must not be full require(!isFull(), "SortedTroves: List is full"); // List must not already contain node require(!contains(_id), "SortedTroves: List already contains the node"); // Node id must not be null require(_id != address(0), "SortedTroves: Id cannot be zero"); - // NICR must be non-zero - require(_NICR > 0, "SortedTroves: NICR must be positive"); address prevId = _prevId; address nextId = _nextId; - if (!_validInsertPosition(_troveManager, _NICR, prevId, nextId)) { + if (!_validInsertPosition(_troveManager, _annualInterestRate, prevId, nextId)) { // Sender's hint was not a valid insert position // Use sender's hint to find a valid insert position - (prevId, nextId) = _findInsertPosition(_troveManager, _NICR, prevId, nextId); + (prevId, nextId) = _findInsertPosition(_troveManager, _annualInterestRate, prevId, nextId); } data.nodes[_id].exists = true; @@ -148,7 +137,7 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { } data.size = data.size + 1; - emit NodeAdded(_id, _NICR); + emit NodeAdded(_id, _annualInterestRate); } function remove(address _id) external override { @@ -198,25 +187,23 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { } /* - * @dev Re-insert the node at a new position, based on its new NICR + * @dev Re-insert the node at a new position, based on its new annual interest rate * @param _id Node's id - * @param _newNICR Node's new NICR + * @param _newAnnualInterestRate Node's new annual interest rate * @param _prevId Id of previous node for the new insert position * @param _nextId Id of next node for the new insert position */ - function reInsert(address _id, uint256 _newNICR, address _prevId, address _nextId) external override { + function reInsert(address _id, uint256 _newAnnualInterestRate, address _prevId, address _nextId) external override { ITroveManager troveManagerCached = troveManager; _requireCallerIsBOorTroveM(troveManagerCached); // List must contain the node require(contains(_id), "SortedTroves: List does not contain the id"); - // NICR must be non-zero - require(_newNICR > 0, "SortedTroves: NICR must be positive"); // Remove node from the list _remove(_id); - _insert(troveManagerCached, _id, _newNICR, _prevId, _nextId); + _insert(troveManagerCached, _id, _newAnnualInterestRate, _prevId, _nextId); } /* @@ -255,21 +242,21 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { } /* - * @dev Returns the first node in the list (node with the largest NICR) + * @dev Returns the first node in the list (node with the largest annual interest rate) */ function getFirst() external view override returns (address) { return data.head; } /* - * @dev Returns the last node in the list (node with the smallest NICR) + * @dev Returns the last node in the list (node with the smallest annual interest rate) */ function getLast() external view override returns (address) { return data.tail; } /* - * @dev Returns the next node (with a smaller NICR) in the list for a given node + * @dev Returns the next node (with a smaller interest rate) in the list for a given node * @param _id Node's id */ function getNext(address _id) external view override returns (address) { @@ -277,7 +264,7 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { } /* - * @dev Returns the previous node (with a larger NICR) in the list for a given node + * @dev Returns the previous node (with a larger interest rate) in the list for a given node * @param _id Node's id */ function getPrev(address _id) external view override returns (address) { @@ -285,42 +272,42 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { } /* - * @dev Check if a pair of nodes is a valid insertion point for a new node with the given NICR - * @param _NICR Node's NICR + * @dev Check if a pair of nodes is a valid insertion point for a new node with the given interest rate + * @param _annualInterestRate Node's annual interest rate * @param _prevId Id of previous node for the insert position * @param _nextId Id of next node for the insert position */ - function validInsertPosition(uint256 _NICR, address _prevId, address _nextId) external view override returns (bool) { - return _validInsertPosition(troveManager, _NICR, _prevId, _nextId); + function validInsertPosition(uint256 _annualInterestRate, address _prevId, address _nextId) external view override returns (bool) { + return _validInsertPosition(troveManager, _annualInterestRate, _prevId, _nextId); } - function _validInsertPosition(ITroveManager _troveManager, uint256 _NICR, address _prevId, address _nextId) internal view returns (bool) { + function _validInsertPosition(ITroveManager _troveManager, uint256 _annualInterestRate, address _prevId, address _nextId) internal view returns (bool) { if (_prevId == address(0) && _nextId == address(0)) { // `(null, null)` is a valid insert position if the list is empty return isEmpty(); } else if (_prevId == address(0)) { // `(null, _nextId)` is a valid insert position if `_nextId` is the head of the list - return data.head == _nextId && _NICR >= _troveManager.getNominalICR(_nextId); + return data.head == _nextId && _annualInterestRate >= _troveManager.getTroveAnnualInterestRate(_nextId); } else if (_nextId == address(0)) { // `(_prevId, null)` is a valid insert position if `_prevId` is the tail of the list - return data.tail == _prevId && _NICR <= _troveManager.getNominalICR(_prevId); + return data.tail == _prevId && _annualInterestRate <= _troveManager.getTroveAnnualInterestRate(_prevId); } else { - // `(_prevId, _nextId)` is a valid insert position if they are adjacent nodes and `_NICR` falls between the two nodes' NICRs + // `(_prevId, _nextId)` is a valid insert position if they are adjacent nodes and `_annualInterestRate` falls between the two nodes' interest rates return data.nodes[_prevId].nextId == _nextId && - _troveManager.getNominalICR(_prevId) >= _NICR && - _NICR >= _troveManager.getNominalICR(_nextId); + _troveManager.getTroveAnnualInterestRate(_prevId) >= _annualInterestRate && + _annualInterestRate >= _troveManager.getTroveAnnualInterestRate(_nextId); } } /* - * @dev Descend the list (larger NICRs to smaller NICRs) to find a valid insert position + * @dev Descend the list (larger interest rates to smaller interest rates) to find a valid insert position * @param _troveManager TroveManager contract, passed in as param to save SLOAD’s - * @param _NICR Node's NICR + * @param _annualInterestRate Node's annual interest rate * @param _startId Id of node to start descending the list from */ - function _descendList(ITroveManager _troveManager, uint256 _NICR, address _startId) internal view returns (address, address) { + function _descendList(ITroveManager _troveManager, uint256 _annualInterestRate, address _startId) internal view returns (address, address) { // If `_startId` is the head, check if the insert position is before the head - if (data.head == _startId && _NICR >= _troveManager.getNominalICR(_startId)) { + if (data.head == _startId && _annualInterestRate >= _troveManager.getTroveAnnualInterestRate(_startId)) { return (address(0), _startId); } @@ -328,7 +315,7 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { address nextId = data.nodes[prevId].nextId; // Descend the list until we reach the end or until we find a valid insert position - while (prevId != address(0) && !_validInsertPosition(_troveManager, _NICR, prevId, nextId)) { + while (prevId != address(0) && !_validInsertPosition(_troveManager, _annualInterestRate, prevId, nextId)) { prevId = data.nodes[prevId].nextId; nextId = data.nodes[prevId].nextId; } @@ -337,14 +324,14 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { } /* - * @dev Ascend the list (smaller NICRs to larger NICRs) to find a valid insert position + * @dev Ascend the list (smaller interest rates to larger interest rates) to find a valid insert position * @param _troveManager TroveManager contract, passed in as param to save SLOAD’s - * @param _NICR Node's NICR + * @param _annualInterestRate Node's annual interest rate * @param _startId Id of node to start ascending the list from */ - function _ascendList(ITroveManager _troveManager, uint256 _NICR, address _startId) internal view returns (address, address) { + function _ascendList(ITroveManager _troveManager, uint256 _annualInterestRate, address _startId) internal view returns (address, address) { // If `_startId` is the tail, check if the insert position is after the tail - if (data.tail == _startId && _NICR <= _troveManager.getNominalICR(_startId)) { + if (data.tail == _startId && _annualInterestRate <= _troveManager.getTroveAnnualInterestRate(_startId)) { return (_startId, address(0)); } @@ -352,7 +339,7 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { address prevId = data.nodes[nextId].prevId; // Ascend the list until we reach the end or until we find a valid insertion point - while (nextId != address(0) && !_validInsertPosition(_troveManager, _NICR, prevId, nextId)) { + while (nextId != address(0) && !_validInsertPosition(_troveManager, _annualInterestRate, prevId, nextId)) { nextId = data.nodes[nextId].prevId; prevId = data.nodes[nextId].prevId; } @@ -361,45 +348,45 @@ contract SortedTroves is Ownable, CheckContract, ISortedTroves { } /* - * @dev Find the insert position for a new node with the given NICR - * @param _NICR Node's NICR + * @dev Find the insert position for a new node with the given interest rate + * @param _annualInterestRate Node's annual interest rate * @param _prevId Id of previous node for the insert position * @param _nextId Id of next node for the insert position */ - function findInsertPosition(uint256 _NICR, address _prevId, address _nextId) external view override returns (address, address) { - return _findInsertPosition(troveManager, _NICR, _prevId, _nextId); + function findInsertPosition(uint256 _annualInterestRate, address _prevId, address _nextId) external view override returns (address, address) { + return _findInsertPosition(troveManager, _annualInterestRate, _prevId, _nextId); } - function _findInsertPosition(ITroveManager _troveManager, uint256 _NICR, address _prevId, address _nextId) internal view returns (address, address) { + function _findInsertPosition(ITroveManager _troveManager, uint256 _annualInterestRate, address _prevId, address _nextId) internal view returns (address, address) { address prevId = _prevId; address nextId = _nextId; if (prevId != address(0)) { - if (!contains(prevId) || _NICR > _troveManager.getNominalICR(prevId)) { - // `prevId` does not exist anymore or now has a smaller NICR than the given NICR + if (!contains(prevId) || _annualInterestRate > _troveManager.getTroveAnnualInterestRate(prevId)) { + // `prevId` does not exist anymore or now has a smaller interest rate than the given interest rate prevId = address(0); } } if (nextId != address(0)) { - if (!contains(nextId) || _NICR < _troveManager.getNominalICR(nextId)) { - // `nextId` does not exist anymore or now has a larger NICR than the given NICR + if (!contains(nextId) || _annualInterestRate < _troveManager.getTroveAnnualInterestRate(nextId)) { + // `nextId` does not exist anymore or now has a larger interest rate than the given interest rate nextId = address(0); } } if (prevId == address(0) && nextId == address(0)) { // No hint - descend list starting from head - return _descendList(_troveManager, _NICR, data.head); + return _descendList(_troveManager, _annualInterestRate, data.head); } else if (prevId == address(0)) { // No `prevId` for hint - ascend list starting from `nextId` - return _ascendList(_troveManager, _NICR, nextId); + return _ascendList(_troveManager, _annualInterestRate, nextId); } else if (nextId == address(0)) { // No `nextId` for hint - descend list starting from `prevId` - return _descendList(_troveManager, _NICR, prevId); + return _descendList(_troveManager, _annualInterestRate, prevId); } else { // Descend list starting from `prevId` - return _descendList(_troveManager, _NICR, prevId); + return _descendList(_troveManager, _annualInterestRate, prevId); } } diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 550ff4e53..0a1455ed3 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -326,7 +326,7 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { * - Leaves their compounded deposit in the Stability Pool * - Takes new snapshots of accumulators P and S */ - function withdrawETHGainToTrove(address _upperHint, address _lowerHint) external override { + function withdrawETHGainToTrove() external override { uint initialDeposit = deposits[msg.sender].initialValue; _requireUserHasDeposit(initialDeposit); _requireUserHasTrove(msg.sender); @@ -349,7 +349,7 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { emit StabilityPoolETHBalanceUpdated(ETH); emit EtherSent(msg.sender, depositorETHGain); - borrowerOperations.moveETHGainToTrove{ value: depositorETHGain }(msg.sender, _upperHint, _lowerHint); + borrowerOperations.moveETHGainToTrove{ value: depositorETHGain }(msg.sender); } // --- Liquidation functions --- diff --git a/contracts/src/TestContracts/BorrowerOperationsTester.sol b/contracts/src/TestContracts/BorrowerOperationsTester.sol index 142cbdade..67296a419 100644 --- a/contracts/src/TestContracts/BorrowerOperationsTester.sol +++ b/contracts/src/TestContracts/BorrowerOperationsTester.sol @@ -8,23 +8,6 @@ import "../BorrowerOperations.sol"; for testing the parent's internal functions. */ contract BorrowerOperationsTester is BorrowerOperations { - function getNewICRFromTroveChange - ( - uint _coll, - uint _debt, - uint _collChange, - bool isCollIncrease, - uint _debtChange, - bool isDebtIncrease, - uint _price - ) - external - pure - returns (uint) - { - return _getNewICRFromTroveChange(_coll, _debt, _collChange, isCollIncrease, _debtChange, isDebtIncrease, _price); - } - function getNewTCRFromTroveChange ( uint _collChange, @@ -49,12 +32,11 @@ contract BorrowerOperationsTester is BorrowerOperations { address _borrower, uint _collWithdrawal, uint _debtChange, - bool _isDebtIncrease, - address _upperHint, - address _lowerHint) + bool _isDebtIncrease + ) external { - _adjustTrove(_borrower, _collWithdrawal, _debtChange, _isDebtIncrease, _upperHint, _lowerHint, 0); + _adjustTrove(_borrower, _collWithdrawal, _debtChange, _isDebtIncrease, 0); } diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 03d5da9f8..e58a56575 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -77,6 +77,8 @@ contract TroveManager is LiquityBase, Ownable, CheckContract, ITroveManager { uint stake; Status status; uint128 arrayIndex; + uint256 annualInterestRate; + // TODO: optimize this struct packing for gas reduction, which may break v1 tests that assume a certain order of properties } mapping (address => Trove) public Troves; @@ -1079,12 +1081,6 @@ contract TroveManager is LiquityBase, Ownable, CheckContract, ITroveManager { } } - // Update borrower's snapshots of L_ETH and L_boldDebt to reflect the current values - function updateTroveRewardSnapshots(address _borrower) external override { - _requireCallerIsBorrowerOperations(); - return _updateTroveRewardSnapshots(_borrower); - } - function _updateTroveRewardSnapshots(address _borrower) internal { rewardSnapshots[_borrower].ETH = L_ETH; rewardSnapshots[_borrower].BoldDebt = L_boldDebt; @@ -1246,6 +1242,7 @@ contract TroveManager is LiquityBase, Ownable, CheckContract, ITroveManager { Troves[_borrower].status = closedStatus; Troves[_borrower].coll = 0; Troves[_borrower].debt = 0; + Troves[_borrower].annualInterestRate = 0; rewardSnapshots[_borrower].ETH = 0; rewardSnapshots[_borrower].BoldDebt = 0; @@ -1521,11 +1518,22 @@ contract TroveManager is LiquityBase, Ownable, CheckContract, ITroveManager { return Troves[_borrower].coll; } + function getTroveAnnualInterestRate(address _borrower) external view returns (uint) { + return Troves[_borrower].annualInterestRate; + } + // --- Trove property setters, called by BorrowerOperations --- - function setTroveStatus(address _borrower, uint _num) external override { + function setTrovePropertiesOnOpen(address _borrower, uint256 _coll, uint256 _debt, uint256 _annualInterestRate) external returns (uint256) { _requireCallerIsBorrowerOperations(); - Troves[_borrower].status = Status(_num); + // TODO: optimize gas for writing to this struct + Troves[_borrower].status = Status.active; + Troves[_borrower].coll = _coll; + Troves[_borrower].debt = _debt; + Troves[_borrower].annualInterestRate = _annualInterestRate; + + _updateTroveRewardSnapshots(_borrower); + return _updateStakeAndTotalStakes(_borrower); } function increaseTroveColl(address _borrower, uint _collIncrease) external override returns (uint) { @@ -1555,4 +1563,9 @@ contract TroveManager is LiquityBase, Ownable, CheckContract, ITroveManager { Troves[_borrower].debt = newDebt; return newDebt; } + + function changeAnnualInterestRate(address _borrower, uint256 _newAnnualInterestRate) external returns (uint256) { + _requireCallerIsBorrowerOperations(); + Troves[_borrower].annualInterestRate = _newAnnualInterestRate; + } } diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index bc6e82575..020210c0e 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -56,6 +56,45 @@ contract BaseTest is Test { accountsList = tempAccounts; } + function openTroveNoHints100pctMaxFee( + address _account, + uint256 _coll, + uint256 _boldAmount, + uint256 _annualInterestRate + ) + public + { + vm.startPrank(_account); + borrowerOperations.openTrove{value: _coll}(1e18, _boldAmount, ZERO_ADDRESS, ZERO_ADDRESS, _annualInterestRate); + vm.stopPrank(); + } + + + // (uint _maxFeePercentage, uint _collWithdrawal, uint _boldChange, bool _isDebtIncrease) + function adjustTrove100pctMaxFee( + address _account, + uint256 _collChange, + uint256 _boldChange, + bool _isCollIncrease, + bool _isDebtIncrease + ) + public + { + vm.startPrank(_account); + if (_isCollIncrease) { + borrowerOperations.adjustTrove{value: _collChange}(1e18, 0, _boldChange, _isDebtIncrease); + } else { + borrowerOperations.adjustTrove(1e18, _collChange, _boldChange, _isDebtIncrease); + } + vm.stopPrank(); + } + + function changeInterestRateNoHints(address _account, uint256 _newAnnualInterestRate) public { + vm.startPrank(_account); + borrowerOperations.adjustTroveInterestRate(_newAnnualInterestRate, ZERO_ADDRESS, ZERO_ADDRESS); + vm.stopPrank(); + } + function logContractAddresses() public { console.log("ActivePool addr: ", address(activePool)); console.log("BorrowerOps addr: ", address(borrowerOperations)); diff --git a/contracts/src/test/basicOps.t.sol b/contracts/src/test/basicOps.t.sol index f5644b2a6..328bd9f87 100644 --- a/contracts/src/test/basicOps.t.sol +++ b/contracts/src/test/basicOps.t.sol @@ -10,7 +10,7 @@ contract BasicOps is DevTestSetup { assertEq(trovesCount, 0); vm.startPrank(A); - borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS, 0); trovesCount = troveManager.getTroveOwnersCount(); assertEq(trovesCount, 1); @@ -19,11 +19,11 @@ contract BasicOps is DevTestSetup { function testCloseTrove() public { priceFeed.setPrice(2000e18); vm.startPrank(A); - borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS, 0); vm.stopPrank(); vm.startPrank(B); - borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS, 0); uint256 trovesCount = troveManager.getTroveOwnersCount(); assertEq(trovesCount, 2); @@ -40,7 +40,7 @@ contract BasicOps is DevTestSetup { function testAdjustTrove() public { priceFeed.setPrice(2000e18); vm.startPrank(A); - borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS, 0); // Check Trove coll and debt uint256 debt_1 = troveManager.getTroveDebt(A); @@ -49,7 +49,7 @@ contract BasicOps is DevTestSetup { assertGt(coll_1, 0); // Adjust trove - borrowerOperations.adjustTrove{value: 1 ether}(1e18, 0, 500e18, true, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.adjustTrove{value: 1 ether}(1e18, 0, 500e18, true); // Check coll and debt altered uint256 debt_2 = troveManager.getTroveDebt(A); @@ -61,7 +61,7 @@ contract BasicOps is DevTestSetup { function testRedeem() public { priceFeed.setPrice(2000e18); vm.startPrank(A); - borrowerOperations.openTrove{value: 5 ether}(1e18, 5_000e18, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.openTrove{value: 5 ether}(1e18, 5_000e18, ZERO_ADDRESS, ZERO_ADDRESS, 0); vm.stopPrank(); uint256 debt_1 = troveManager.getTroveDebt(A); @@ -70,7 +70,7 @@ contract BasicOps is DevTestSetup { assertGt(coll_1, 0); vm.startPrank(B); - borrowerOperations.openTrove{value: 5 ether}(1e18, 4_000e18, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.openTrove{value: 5 ether}(1e18, 4_000e18, ZERO_ADDRESS, ZERO_ADDRESS, 0); vm.warp(block.timestamp + troveManager.BOOTSTRAP_PERIOD() + 1); @@ -102,11 +102,11 @@ contract BasicOps is DevTestSetup { function testLiquidation() public { priceFeed.setPrice(2000e18); vm.startPrank(A); - borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS, 0); vm.stopPrank(); vm.startPrank(B); - borrowerOperations.openTrove{value: 10 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.openTrove{value: 10 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS, 0); // Price drops priceFeed.setPrice(1200e18); @@ -129,7 +129,7 @@ contract BasicOps is DevTestSetup { function testSPDeposit() public { priceFeed.setPrice(2000e18); vm.startPrank(A); - borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS, 0); // A makes an SP deposit stabilityPool.provideToSP(100e18); @@ -148,7 +148,7 @@ contract BasicOps is DevTestSetup { function testSPWithdrawal() public { priceFeed.setPrice(2000e18); vm.startPrank(A); - borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS); + borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS, 0); // A makes an SP deposit stabilityPool.provideToSP(100e18); diff --git a/contracts/src/test/interestRateBasic.t.sol b/contracts/src/test/interestRateBasic.t.sol new file mode 100644 index 000000000..e2962fe0f --- /dev/null +++ b/contracts/src/test/interestRateBasic.t.sol @@ -0,0 +1,204 @@ +pragma solidity 0.8.18; + +import "./TestContracts/DevTestSetup.sol"; + +contract InterestRateBasic is DevTestSetup { + + function testOpenTroveSetsInterestRate() public { + priceFeed.setPrice(2000e18); + assertEq(troveManager.getTroveAnnualInterestRate(A), 0); + assertEq(troveManager.getTroveAnnualInterestRate(B), 0); + assertEq(troveManager.getTroveAnnualInterestRate(C), 0); + assertEq(troveManager.getTroveAnnualInterestRate(D), 0); + + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 0); + assertEq(troveManager.getTroveAnnualInterestRate(A), 0); + + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 1); + assertEq(troveManager.getTroveAnnualInterestRate(B), 1); + + openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 37e16); + assertEq(troveManager.getTroveAnnualInterestRate(C), 37e16); + + openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, 1e18); + assertEq(troveManager.getTroveAnnualInterestRate(D), 1e18); + } + + function testOpenTroveInsertsToCorrectPositionInSortedList() public { + priceFeed.setPrice(2000e18); + + // Users A, B, C, D, E will open Troves with interest rates ascending in the alphabetical order of their names + uint256 interestRate_A = 0; + uint256 interestRate_B = 1e17; + uint256 interestRate_C = 2e17; + uint256 interestRate_D = 3e17; + uint256 interestRate_E = 4e17; + + // B and D open + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, interestRate_B); + openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, interestRate_D); + + // Check initial list order - expect [B, D] + // B + assertEq(sortedTroves.getNext(B), ZERO_ADDRESS); // tail + assertEq(sortedTroves.getPrev(B), D); + // D + assertEq(sortedTroves.getNext(D), B); + assertEq(sortedTroves.getPrev(D), ZERO_ADDRESS); // head + + + // C opens. Expect to be inserted between B and D + openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, interestRate_C); + assertEq(sortedTroves.getNext(C), B); + assertEq(sortedTroves.getPrev(C), D); + + // A opens. Expect to be inserted at the tail, below B + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, interestRate_A); + assertEq(sortedTroves.getNext(A), ZERO_ADDRESS); + assertEq(sortedTroves.getPrev(A), B); + + // E opens. Expect to be inserted at the head, above D + openTroveNoHints100pctMaxFee(E, 2 ether, 2000e18, interestRate_E); + assertEq(sortedTroves.getNext(E), D); + assertEq(sortedTroves.getPrev(E), ZERO_ADDRESS); + } + + + function testRevertWhenOpenTroveWithInterestRateGreaterThanMax() public { + priceFeed.setPrice(2000e18); + + vm.startPrank(A); + vm.expectRevert(); + borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS, 1e18 + 1); + + vm.expectRevert(); + borrowerOperations.openTrove{value: 2 ether}(1e18, 2000e18, ZERO_ADDRESS, ZERO_ADDRESS, 42e18); + } + + function testRevertWhenAdjustInterestRateGreaterThanMax() public { + priceFeed.setPrice(2000e18); + + // A opens Trove with valid annual interest rate ... + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 37e16); + assertEq(troveManager.getTroveAnnualInterestRate(A), 37e16); + + // ... then tries to adjust it to an invalid value + vm.startPrank(A); + vm.expectRevert(); + borrowerOperations.adjustTroveInterestRate(1e18 + 1, ZERO_ADDRESS, ZERO_ADDRESS); + + vm.expectRevert(); + borrowerOperations.adjustTroveInterestRate(42e18, ZERO_ADDRESS, ZERO_ADDRESS); + } + + function testAdjustTroveInterestRateSetsCorrectNewRate() public { + priceFeed.setPrice(2000e18); + + // A, B, C opens Troves with valid annual interest rates + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 5e17); + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 5e17); + openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 5e17); + assertEq(troveManager.getTroveAnnualInterestRate(A), 5e17); + assertEq(troveManager.getTroveAnnualInterestRate(B), 5e17); + assertEq(troveManager.getTroveAnnualInterestRate(C), 5e17); + + + changeInterestRateNoHints(A, 0); + assertEq(troveManager.getTroveAnnualInterestRate(A), 0); + + changeInterestRateNoHints(B, 6e17); + assertEq(troveManager.getTroveAnnualInterestRate(B), 6e17); + + changeInterestRateNoHints(C, 1e18); + assertEq(troveManager.getTroveAnnualInterestRate(C), 1e18); + } + + // TODO: test adjusting interest rates correctly orders sorted list + function testAdjustTroveInterestRateInsertsToCorrectPositionInSortedList() public { + priceFeed.setPrice(2000e18); + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 2e17); + openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 3e17); + openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, 4e17); + openTroveNoHints100pctMaxFee(E, 2 ether, 2000e18, 5e17); + + // Check initial sorted list order - expect [A:1%, B:2%, C:3%, D:4%, E:5%] + // A + assertEq(sortedTroves.getNext(A), ZERO_ADDRESS); // tail + assertEq(sortedTroves.getPrev(A), B); + // B + assertEq(sortedTroves.getNext(B), A); + assertEq(sortedTroves.getPrev(B), C); + // C + assertEq(sortedTroves.getNext(C), B); + assertEq(sortedTroves.getPrev(C), D); + // D + assertEq(sortedTroves.getNext(D), C); + assertEq(sortedTroves.getPrev(D), E); + // E + assertEq(sortedTroves.getNext(E), D); + assertEq(sortedTroves.getPrev(E), ZERO_ADDRESS); // head + + // C sets rate to 0%, moves to tail - expect [C:0%, A:1%, B:2%, D:4%, E:5%] + changeInterestRateNoHints(C, 0); + assertEq(sortedTroves.getNext(C), ZERO_ADDRESS); + assertEq(sortedTroves.getPrev(C), A); + + // D sets rate to 7%, moves to head - expect [C:0%, A:1%, B:2%, E:5%, D:7%] + changeInterestRateNoHints(D, 7e17); + assertEq(sortedTroves.getNext(D), E); + assertEq(sortedTroves.getPrev(D), ZERO_ADDRESS); + + + // A sets rate to 6%, moves up 2 positions - expect [C:0%, B:2%, E:5%, A:6%, D:7%] + changeInterestRateNoHints(A, 6e17); + assertEq(sortedTroves.getNext(A), E); + assertEq(sortedTroves.getPrev(A), D); + } + + function testAdjustTroveDoesNotChangeListPositions() public { + priceFeed.setPrice(2000e18); + + // Troves opened in ascending order of interest rate + openTroveNoHints100pctMaxFee(A, 2 ether, 2000e18, 1e17); + openTroveNoHints100pctMaxFee(B, 2 ether, 2000e18, 2e17); + openTroveNoHints100pctMaxFee(C, 2 ether, 2000e18, 3e17); + openTroveNoHints100pctMaxFee(D, 2 ether, 2000e18, 4e17); + openTroveNoHints100pctMaxFee(E, 2 ether, 2000e18, 5e17); + + + // Check A's neighbors + assertEq(sortedTroves.getNext(A), ZERO_ADDRESS); // tail + assertEq(sortedTroves.getPrev(A), B); + + // Adjust A's coll + debt + adjustTrove100pctMaxFee(A, 10 ether, 5000e18, true, true); + + // Check A's neighbors unchanged + assertEq(sortedTroves.getNext(A), ZERO_ADDRESS); // tail + assertEq(sortedTroves.getPrev(A), B); + + + // Check C's neighbors + assertEq(sortedTroves.getNext(C), B); + assertEq(sortedTroves.getPrev(C), D); + + // Adjust C's coll + debt + adjustTrove100pctMaxFee(C, 10 ether, 5000e18, true, true); + + // Check C's neighbors unchanged + assertEq(sortedTroves.getNext(C), B); + assertEq(sortedTroves.getPrev(C), D); + + // Check E's neighbors + assertEq(sortedTroves.getNext(E), D); + assertEq(sortedTroves.getPrev(E), ZERO_ADDRESS); // head + + // Adjust E's coll + debt + adjustTrove100pctMaxFee(E, 10 ether, 5000e18, true, true); + + // Check C's neighbors unchanged + assertEq(sortedTroves.getNext(E), D); + assertEq(sortedTroves.getPrev(E), ZERO_ADDRESS); // head + } +} \ No newline at end of file