Skip to content

Commit

Permalink
fix: Fix lastInterestRateAdjTime when leaving/joining batches
Browse files Browse the repository at this point in the history
Also add tests and a section to README.
  • Loading branch information
bingen committed Aug 28, 2024
1 parent dc32323 commit 3f77b6b
Show file tree
Hide file tree
Showing 6 changed files with 372 additions and 16 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,27 @@ The premature adjustment fee works as so:

- When a Trove is opened, its `lastInterestRateAdjTime` property is set equal to the current time
- When a borrower adjusts their interest rate via `adjustTroveInterestRate` the system checks that the cooldown period has passed since their last interest rate adjustment

- If the adjustment is sooner it incurs an upfront fee (equal to 7 days of average interest of the respective branch) which is added to their debt.

#### Batches and upfront fee

##### Joining a batch
When a trove joins a batch, it pays upfront fee if the last trove adjustment was done more than the cool period ago. It does’t matter if trove and batch have the same interest rate, or when was the last adjustment by the batch.

The last interest rate timestamp will be updated to the time of joining.

Batch interest rate changes only take into account global batch timestamps, so when the new batch manager changes the interest rate less than the cooldown period after the borrower moved to the new batch, but more than the cooldown period after its last adjustment, the newly joined borrower wouldn't pay the upfront fee despite the fact that his last interest rate change happened less than the cooldown period ago.

That’s why troves pay upfront fee when joining even if the interest is the same. Otherwise a trove may game it by having a batch created in advance (with no recent changens), joining it and the changing the rate of the batch.

##### Leaving a batch
When a trove leaves a batch, the user's timestamp is again reset to the current time.
No upfront fee is charged, unless the interest rate is changed in the same transaction and the batch changed the interest rate less than the cooldown period ago.

##### Switching batches
As the function to switch batches is just a wrapper that calls the functions for leaving and joining a batch, this means that switching batches always incurs in upfront fee now (unless user doesn’t use the wrapper and waits for 1 week between leaving and joining).


## BOLD Redemptions

Any BOLD holder (whether or not they have an active Trove) may redeem their BOLD directly with the system. Their BOLD is exchanged for a mixture of collaterals at face value: redeeming 1 BOLD token returns $1 worth of collaterals (minus a dynamic redemption fee), priced at their current market values according to their respective oracles. Redemptions have two purposes:
Expand Down
4 changes: 2 additions & 2 deletions contracts/src/BorrowerOperations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio
newBatchTroveChange.newWeightedRecordedDebt =
(vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate;

// TODO: We may check the old rate to see if it’s different than the new one, but then we should check the
// We may check the old rate to see if it’s different than the new one, but then we should check the
// last interest adjustment times to avoid gaming. So we decided to keep it simple and account it always
// as a change. It’s probably not so common to join a batch with the exact same interest rate.
// Apply upfront fee on premature adjustments
Expand Down Expand Up @@ -1066,7 +1066,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio
// Apply upfront fee on premature adjustments
if (
vars.batch.annualInterestRate != _newAnnualInterestRate
&& block.timestamp < vars.batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN
&& block.timestamp < vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN
) {
vars.trove.entireDebt =
_applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, batchChange, _maxUpfrontFee);
Expand Down
37 changes: 31 additions & 6 deletions contracts/src/HintHelpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,40 @@ contract HintHelpers is IHintHelpers {
return 0;
}

return _predictJoinBatchInterestRateUpfrontFee(activePool, trove, batch);
}

function forcePredictJoinBatchInterestRateUpfrontFee(uint256 _collIndex, uint256 _troveId, address _batchAddress)
external
view
returns (uint256)
{
ITroveManager troveManager = collateralRegistry.getTroveManager(_collIndex);
IActivePool activePool = troveManager.activePool();
LatestTroveData memory trove = troveManager.getLatestTroveData(_troveId);
LatestBatchData memory batch = troveManager.getLatestBatchData(_batchAddress);

return _predictJoinBatchInterestRateUpfrontFee(activePool, trove, batch);
}

function _predictJoinBatchInterestRateUpfrontFee(
IActivePool _activePool,
LatestTroveData memory _trove,
LatestBatchData memory _batch
)
internal
view
returns (uint256)
{
TroveChange memory newBatchTroveChange;
newBatchTroveChange.appliedRedistBoldDebtGain = trove.redistBoldDebtGain;
newBatchTroveChange.batchAccruedManagementFee = batch.accruedManagementFee;
newBatchTroveChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt + trove.weightedRecordedDebt;
newBatchTroveChange.appliedRedistBoldDebtGain = _trove.redistBoldDebtGain;
newBatchTroveChange.batchAccruedManagementFee = _batch.accruedManagementFee;
newBatchTroveChange.oldWeightedRecordedDebt = _batch.weightedRecordedDebt + _trove.weightedRecordedDebt;
newBatchTroveChange.newWeightedRecordedDebt =
(batch.entireDebtWithoutRedistribution + trove.entireDebt) * batch.annualInterestRate;
(_batch.entireDebtWithoutRedistribution + _trove.entireDebt) * _batch.annualInterestRate;

uint256 avgInterestRate = activePool.getNewApproxAvgInterestRateFromTroveChange(newBatchTroveChange);
return _calcUpfrontFee(trove.entireDebt, avgInterestRate);
uint256 avgInterestRate = _activePool.getNewApproxAvgInterestRateFromTroveChange(newBatchTroveChange);
return _calcUpfrontFee(_trove.entireDebt, avgInterestRate);
}

function predictRemoveFromBatchUpfrontFee(uint256 _collIndex, uint256 _troveId, uint256 _newInterestRate)
Expand Down
1 change: 1 addition & 0 deletions contracts/src/TroveManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents {
Troves[_troveId].status = Status.active;
Troves[_troveId].arrayIndex = uint64(TroveIds.length);
Troves[_troveId].interestBatchManager = _batchAddress;
Troves[_troveId].lastInterestRateAdjTime = uint64(block.timestamp);

_updateTroveRewardSnapshots(_troveId);

Expand Down
8 changes: 8 additions & 0 deletions contracts/src/test/TestContracts/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ contract BaseTest is TestAccounts, Logging {
return hintHelpers.predictJoinBatchInterestRateUpfrontFee(0, _troveId, _batchAddress);
}

function forcePredictJoinBatchInterestRateUpfrontFee(uint256 _troveId, address _batchAddress)
internal
view
returns (uint256)
{
return hintHelpers.forcePredictJoinBatchInterestRateUpfrontFee(0, _troveId, _batchAddress);
}

// Quick and dirty binary search instead of Newton's, because it's easier
function findAmountToBorrowWithOpenTrove(uint256 targetDebt, uint256 interestRate)
internal
Expand Down
Loading

0 comments on commit 3f77b6b

Please sign in to comment.