Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: double interest on pending redistribution #151

Merged
merged 2 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions contracts/src/BorrowerOperations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
// TODO: Use oldDebt and assert in fuzzing, remove before deployment
uint256 oldDebt = troveManager.getTroveEntireDebt(_troveId);
_adjustTrove(msg.sender, _troveId, 0, false, _boldAmount, false, 0, contractsCache);
assert(troveManager.getTroveEntireDebt(_troveId) < oldDebt);
assert(troveManager.getTroveEntireDebt(_troveId) < oldDebt);
}

function adjustTrove(
Expand All @@ -292,12 +292,19 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
ContractsCacheTMAPBT memory contractsCache = ContractsCacheTMAPBT(troveManager, activePool, boldToken);
_requireTroveIsActive(contractsCache.troveManager, _troveId);
_adjustTrove(
msg.sender, _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _maxFeePercentage, contractsCache
msg.sender,
_troveId,
_collChange,
_isCollIncrease,
_boldChange,
_isDebtIncrease,
_maxFeePercentage,
contractsCache
);
}

function adjustUnredeemableTrove(
uint256 _troveId,
uint256 _troveId,
uint256 _maxFeePercentage,
uint256 _collChange,
bool _isCollIncrease,
Expand All @@ -308,12 +315,21 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
) external override {
ContractsCacheTMAPBT memory contractsCache = ContractsCacheTMAPBT(troveManager, activePool, boldToken);
_requireTroveIsUnredeemable(contractsCache.troveManager, _troveId);
// TODO: Gas - pass the cached TM down here, since we fetch it again inside _adjustTrove?
// TODO: Gas - pass the cached TM down here, since we fetch it again inside _adjustTrove?
_adjustTrove(
msg.sender, _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _maxFeePercentage, contractsCache
msg.sender,
_troveId,
_collChange,
_isCollIncrease,
_boldChange,
_isDebtIncrease,
_maxFeePercentage,
contractsCache
);
troveManager.setTroveStatusToActive(_troveId);
sortedTroves.insert(_troveId, contractsCache.troveManager.getTroveAnnualInterestRate(_troveId), _upperHint, _lowerHint);
sortedTroves.insert(
_troveId, contractsCache.troveManager.getTroveAnnualInterestRate(_troveId), _upperHint, _lowerHint
);
}

function adjustTroveInterestRate(
Expand Down Expand Up @@ -665,9 +681,8 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
uint256 initialWeightedRecordedTroveDebt = _troveManager.getTroveWeightedRecordedDebt(_troveId);
// --- Effects ---

(, uint256 redistDebtGain) = _troveManager.getAndApplyRedistributionGains(_troveId);

uint256 accruedTroveInterest = _troveManager.calcTroveAccruedInterest(_troveId);
(, uint256 redistDebtGain) = _troveManager.getAndApplyRedistributionGains(_troveId);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual fix. Every other change is just auto-formatting.

uint256 recordedTroveDebt = _troveManager.getTroveDebt(_troveId);
uint256 entireTroveDebt = recordedTroveDebt + accruedTroveInterest;
uint256 newWeightedTroveDebt = entireTroveDebt * _annualInterestRate;
Expand Down Expand Up @@ -709,11 +724,13 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
}

function _requireTroveIsUnredeemable(ITroveManager _troveManager, uint256 _troveId) internal view {
require(_troveManager.checkTroveIsUnredeemable(_troveId), "BorrowerOps: Trove does not have unredeemable status");
require(
_troveManager.checkTroveIsUnredeemable(_troveId), "BorrowerOps: Trove does not have unredeemable status"
);
}

function _requireTroveIsNotOpen(ITroveManager _troveManager, uint256 _troveId) internal view {
require(!_troveManager.checkTroveIsOpen(_troveId),"BorrowerOps: Trove is open");
function _requireTroveIsNotOpen(ITroveManager _troveManager, uint256 _troveId) internal view {
require(!_troveManager.checkTroveIsOpen(_troveId), "BorrowerOps: Trove is open");
}

function _requireNonZeroCollChange(uint256 _collChange) internal pure {
Expand Down
40 changes: 39 additions & 1 deletion contracts/src/test/interestRateAggregate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2570,7 +2570,45 @@ contract InterestRateAggregate is DevTestSetup {
// Check recorded debt sum has changed correctly
assertEq(activePool.aggWeightedDebtSum(), expectedAggWeightedRecordedDebt);
}


// A bug was caught while reading the implementation of `_updateActivePoolTrackersNoDebtChange()`, wherein
// a borrower incurred double interest on redistribution gains when adjusting their interest rate.
// This test case covers that scenario.
//
// We should properly address the TODO below ("tests with pending debt redist. gain >0"), but in the meantime,
// keep this testcase.
function testNoDoubleInterestOnPendingRedistribution() public {
TroveIDs memory troveIDs;

uint256 coll = 100 ether;
uint256 borrow = 10_000 ether - 200 ether;
uint256 interestRate = 1 ether;
troveIDs.A = openTroveNoHints100pctMaxFee(A, coll, borrow, interestRate);
troveIDs.B = openTroveNoHints100pctMaxFee(B, coll, borrow, interestRate);
troveIDs.C = openTroveNoHints100pctMaxFee(C, coll, borrow, interestRate);
troveIDs.D = openTroveNoHints100pctMaxFee(D, coll, borrow, interestRate);

emit log_named_decimal_uint("Trove D debt (initial) ", troveManager.getTroveEntireDebt(troveIDs.D), 18);
vm.warp(block.timestamp + 365 days);
emit log_named_decimal_uint("Trove D debt (post-1y) ", troveManager.getTroveEntireDebt(troveIDs.D), 18);

priceFeed.setPrice(110 ether);

uint256[] memory liquidatedTroves = new uint256[](3);
liquidatedTroves[0] = troveIDs.A;
liquidatedTroves[1] = troveIDs.B;
liquidatedTroves[2] = troveIDs.C;
troveManager.batchLiquidateTroves(liquidatedTroves);

uint256 debtBefore = troveManager.getTroveEntireDebt(troveIDs.D);
emit log_named_decimal_uint("Trove D debt (post-liq) ", debtBefore, 18);
changeInterestRateNoHints(D, troveIDs.D, 0.1 ether);
uint256 debtAfter = troveManager.getTroveEntireDebt(troveIDs.D);
emit log_named_decimal_uint("Trove D debt (post-adj) ", debtAfter, 18);

assertEq(debtBefore, debtAfter, "Adjusting interest rate shouldn't change Trove's debt");
}

// TODO: mixed collateral & debt adjustment opps
// TODO: tests with pending debt redist. gain >0
// TODO: tests that show total debt change under user ops
Expand Down