diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 4a930db4..cc33e3c5 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -813,11 +813,19 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 _maxUpfrontFee ) external { _requireIsNotShutDown(); + _requireTroveIsActive(troveManager, _troveId); _requireCallerIsBorrower(_troveId); + _requireValidAnnualInterestRate(_minInterestRate); + _requireValidAnnualInterestRate(_maxInterestRate); + // With the check below, it could only be == + _requireOrderedRange(_minInterestRate, _maxInterestRate); + interestIndividualDelegateOf[_troveId] = InterestIndividualDelegate(_delegate, _minInterestRate, _maxInterestRate); // Can’t have both individual delegation and batch manager if (interestBatchManagerOf[_troveId] != address(0)) { + // Not needed, implicitly checked in removeFromBatch + //_requireValidAnnualInterestRate(_newAnnualInterestRate); removeFromBatch(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee); } } @@ -843,7 +851,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _requireValidAnnualInterestRate(_minInterestRate); _requireValidAnnualInterestRate(_maxInterestRate); // With the check below, it could only be == - if (_minInterestRate >= _maxInterestRate) revert MinGeMax(); + _requireOrderedRange(_minInterestRate, _maxInterestRate); _requireInterestRateInRange(_currentInterestRate, _minInterestRate, _maxInterestRate); // Not needed, implicitly checked in the condition above: //_requireValidAnnualInterestRate(_currentInterestRate); @@ -1292,45 +1300,6 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio return batchManager; } - function _requireInterestRateInDelegateRange(uint256 _troveId, uint256 _annualInterestRate) internal view { - InterestIndividualDelegate memory individualDelegate = interestIndividualDelegateOf[_troveId]; - if (individualDelegate.account != address(0)) { - _requireInterestRateInRange( - _annualInterestRate, individualDelegate.minInterestRate, individualDelegate.maxInterestRate - ); - } - } - - function _requireInterestRateInBatchManagerRange(address _interestBatchManagerAddress, uint256 _annualInterestRate) - internal - view - { - InterestBatchManager memory interestBatchManager = interestBatchManagers[_interestBatchManagerAddress]; - _requireInterestRateInRange( - _annualInterestRate, interestBatchManager.minInterestRate, interestBatchManager.maxInterestRate - ); - } - - function _requireInterestRateInRange( - uint256 _annualInterestRate, - uint256 _minInterestRate, - uint256 _maxInterestRate - ) internal pure { - if (_minInterestRate > _annualInterestRate || _annualInterestRate > _maxInterestRate) { - revert InterestNotInRange(); - } - } - - function _requireInterestRateChangePeriodPassed( - address _interestBatchManagerAddress, - uint256 _lastInterestRateAdjTime - ) internal view { - InterestBatchManager memory interestBatchManager = interestBatchManagers[_interestBatchManagerAddress]; - if (block.timestamp < _lastInterestRateAdjTime + uint256(interestBatchManager.minInterestRateChangePeriod)) { - revert BatchInterestRateChangePeriodNotPassed(); - } - } - function _requireTroveIsOpen(ITroveManager _troveManager, uint256 _troveId) internal view { ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); if (status != ITroveManager.Status.active && status != ITroveManager.Status.unredeemable) { @@ -1467,6 +1436,49 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } + function _requireOrderedRange(uint256 _minInterestRate, uint256 _maxInterestRate) internal pure { + if (_minInterestRate >= _maxInterestRate) revert MinGeMax(); + } + + function _requireInterestRateInDelegateRange(uint256 _troveId, uint256 _annualInterestRate) internal view { + InterestIndividualDelegate memory individualDelegate = interestIndividualDelegateOf[_troveId]; + if (individualDelegate.account != address(0)) { + _requireInterestRateInRange( + _annualInterestRate, individualDelegate.minInterestRate, individualDelegate.maxInterestRate + ); + } + } + + function _requireInterestRateInBatchManagerRange(address _interestBatchManagerAddress, uint256 _annualInterestRate) + internal + view + { + InterestBatchManager memory interestBatchManager = interestBatchManagers[_interestBatchManagerAddress]; + _requireInterestRateInRange( + _annualInterestRate, interestBatchManager.minInterestRate, interestBatchManager.maxInterestRate + ); + } + + function _requireInterestRateInRange( + uint256 _annualInterestRate, + uint256 _minInterestRate, + uint256 _maxInterestRate + ) internal pure { + if (_minInterestRate > _annualInterestRate || _annualInterestRate > _maxInterestRate) { + revert InterestNotInRange(); + } + } + + function _requireInterestRateChangePeriodPassed( + address _interestBatchManagerAddress, + uint256 _lastInterestRateAdjTime + ) internal view { + InterestBatchManager memory interestBatchManager = interestBatchManagers[_interestBatchManagerAddress]; + if (block.timestamp < _lastInterestRateAdjTime + uint256(interestBatchManager.minInterestRateChangePeriod)) { + revert BatchInterestRateChangePeriodNotPassed(); + } + } + function _requireValidInterestBatchManager(address _interestBatchManagerAddress) internal view { if (interestBatchManagers[_interestBatchManagerAddress].maxInterestRate == 0) { revert InvalidInterestBatchManager(); diff --git a/contracts/src/test/interestIndividualDelegation.t.sol b/contracts/src/test/interestIndividualDelegation.t.sol index 46843c4c..e29a1f91 100644 --- a/contracts/src/test/interestIndividualDelegation.t.sol +++ b/contracts/src/test/interestIndividualDelegation.t.sol @@ -102,6 +102,140 @@ contract InterestIndividualDelegationTest is DevTestSetup { vm.stopPrank(); } + function testSetDelegateRevertsIfTroveIsClosed() public { + vm.startPrank(B); + borrowerOperations.registerBatchManager(1e16, 20e16, 5e16, 25e14, MIN_INTEREST_RATE_CHANGE_PERIOD); + vm.stopPrank(); + + // Open trove + uint256 troveId = openTroveNoHints100pct(A, 100e18, 5000e18, 5e16); + // Open a second one, so it’s not the last one and to have BOLD for interest + openTroveNoHints100pctWithIndex(A, 1, 100e18, 5000e18, 5e16); + // Close trove + closeTrove(A, troveId); + + // Set batch manager (B) + vm.startPrank(A); + vm.expectRevert(BorrowerOperations.TroveNotActive.selector); + borrowerOperations.setInterestIndividualDelegate(troveId, C, 1e16, 20e16, 0, 0, 0, 10000e18); + vm.stopPrank(); + } + + function testSetDelegateRevertsIfTroveIsUnredeemable() public { + vm.startPrank(B); + borrowerOperations.registerBatchManager(1e16, 20e16, 5e16, 25e14, MIN_INTEREST_RATE_CHANGE_PERIOD); + vm.stopPrank(); + + // Open trove + uint256 troveId = openTroveNoHints100pct(A, 100e18, 5000e18, 5e16); + // Make trove unredeemable + redeem(A, 4000e18); + // Check A’s trove is unredeemable + assertEq(troveManager.checkTroveIsUnredeemable(troveId), true, "A trove should be unredeemable"); + + // Set batch manager (B) + vm.startPrank(A); + vm.expectRevert(BorrowerOperations.TroveNotActive.selector); + borrowerOperations.setInterestIndividualDelegate(troveId, C, 1e16, 20e16, 0, 0, 0, 10000e18); + vm.stopPrank(); + } + + function testSetDelegateRevertsIfMinTooLow() public { + vm.startPrank(B); + borrowerOperations.registerBatchManager(1e16, 20e16, 5e16, 25e14, MIN_INTEREST_RATE_CHANGE_PERIOD); + vm.stopPrank(); + + // Open trove + uint256 troveId = openTroveNoHints100pct(A, 100e18, 5000e18, 5e16); + // Set batch manager (B) + vm.startPrank(A); + vm.expectRevert(BorrowerOperations.InterestRateTooLow.selector); + borrowerOperations.setInterestIndividualDelegate(troveId, C, 1e14, 20e16, 0, 0, 0, 10000e18); + vm.stopPrank(); + } + + function testSetDelegateRevertsIfMaxTooHigh() public { + vm.startPrank(B); + borrowerOperations.registerBatchManager(1e16, 20e16, 5e16, 25e14, MIN_INTEREST_RATE_CHANGE_PERIOD); + vm.stopPrank(); + + // Open trove + uint256 troveId = openTroveNoHints100pct(A, 100e18, 5000e18, 5e16); + // Set batch manager (B) + vm.startPrank(A); + vm.expectRevert(BorrowerOperations.InterestRateTooHigh.selector); + borrowerOperations.setInterestIndividualDelegate(troveId, C, 1e16, 101e16, 0, 0, 0, 10000e18); + vm.stopPrank(); + } + + function testSetDelegateRevertsIfMinEqMax() public { + vm.startPrank(B); + borrowerOperations.registerBatchManager(1e16, 20e16, 5e16, 25e14, MIN_INTEREST_RATE_CHANGE_PERIOD); + vm.stopPrank(); + + // Open trove + uint256 troveId = openTroveNoHints100pct(A, 100e18, 5000e18, 5e16); + // Set batch manager (B) + vm.startPrank(A); + vm.expectRevert(BorrowerOperations.MinGeMax.selector); + borrowerOperations.setInterestIndividualDelegate(troveId, C, 20e16, 20e16, 0, 0, 0, 10000e18); + vm.stopPrank(); + } + + function testSetDelegateRevertsIfMinGtMax() public { + vm.startPrank(B); + borrowerOperations.registerBatchManager(1e16, 20e16, 5e16, 25e14, MIN_INTEREST_RATE_CHANGE_PERIOD); + vm.stopPrank(); + + // Open trove + uint256 troveId = openTroveNoHints100pct(A, 100e18, 5000e18, 5e16); + // Set batch manager (B) + vm.startPrank(A); + vm.expectRevert(BorrowerOperations.MinGeMax.selector); + borrowerOperations.setInterestIndividualDelegate(troveId, C, 21e16, 20e16, 0, 0, 0, 10000e18); + vm.stopPrank(); + } + + function testSetDelegateRevertsIfNewInterestRateNotInRangeBelow() public { + vm.startPrank(B); + borrowerOperations.registerBatchManager(1e16, 20e16, 5e16, 25e14, MIN_INTEREST_RATE_CHANGE_PERIOD); + vm.stopPrank(); + + // Open trove + uint256 troveId = openTroveNoHints100pct(A, 100e18, 5000e18, 5e16); + // Set batch manager (B) + vm.startPrank(A); + borrowerOperations.setInterestBatchManager(troveId, B, 0, 0, 1e24); + vm.stopPrank(); + + // Try to switch to individual delegate (C) along with new interest + uint256 newAnnualInterestRate = 1e14; + vm.startPrank(A); + vm.expectRevert(BorrowerOperations.InterestRateTooLow.selector); + borrowerOperations.setInterestIndividualDelegate(troveId, C, 1e16, 20e16, newAnnualInterestRate, 0, 0, 10000e18); + vm.stopPrank(); + } + + function testSetDelegateRevertsIfNewInterestRateNotInRangeAbove() public { + vm.startPrank(B); + borrowerOperations.registerBatchManager(1e16, 20e16, 5e16, 25e14, MIN_INTEREST_RATE_CHANGE_PERIOD); + vm.stopPrank(); + + // Open trove + uint256 troveId = openTroveNoHints100pct(A, 100e18, 5000e18, 5e16); + // Set batch manager (B) + vm.startPrank(A); + borrowerOperations.setInterestBatchManager(troveId, B, 0, 0, 1e24); + vm.stopPrank(); + + // Try to switch to individual delegate (C) along with new interest + uint256 newAnnualInterestRate = 101e16; + vm.startPrank(A); + vm.expectRevert(BorrowerOperations.InterestRateTooHigh.selector); + borrowerOperations.setInterestIndividualDelegate(troveId, C, 1e16, 20e16, newAnnualInterestRate, 0, 0, 10000e18); + vm.stopPrank(); + } + function testSetDelegateRemovesBatchManager() public { vm.startPrank(B); borrowerOperations.registerBatchManager(1e16, 20e16, 5e16, 25e14, MIN_INTEREST_RATE_CHANGE_PERIOD);