Crate pallet_interest_accrual
source ·Expand description
§Interest Accrual Pallet
A pallet for calculating interest accrual on debt. It keeps track of different buckets of interest rates and is optimized for many loans per interest bucket. This implementation is inspired by jug.sol from Multi Collateral Dai.
It works by defining debt = normalized debt * accumulated rate. When the first loan for an interest rate is created, the accumulated rate is set to 1.0. The normalized debt is then calculated, which is the debt at t=0, using debt / accumulated rate.
Over time, the accumulated rate grows based on the interest rate per second. Any time the accumulated rate is updated for an interest rate group, this indirectly updates the debt of all loans outstanding using this interest rate.
ar = accumulated rate
nd = normalized debt
│
2.0 │ ****
│ ****
│ ****
│ ****
ar 1.5 │ ****
│ ****
│ ****
│ ****
1.0 │ **
└──────────────────────────────────
│ │
borrow 10 borrow 20
ar = 1.0 ar = 1.5
nd = 10 nd = 10 + (20 / 1.5) = 23.33
debt = 10 debt = 35
§Basics of shared rate accrual and “normalized debts”
When we want to compute the interest accrued on some value, the high-level equation is:
rate_per_second.pow(debt_age) * debt_base_value
Computing that pow for everything is expensive, so we want to only
do it once for any given rate_per_second
and share that result
across multiple debts. Because these debts might not have been
created at the same time as each other (or the rate), we must
include a correction factor to the shared interest rate accrual:
correction_factor = ???;
rate_per_second.pow(rate_age) * debt_base_value / correction_factor
This correction factor is just the accumulated interest at the time the debt was created:
correction_factor = rate_per_second.pow(rate_age_at_time_of_debt_creation);
rate_per_second.pow(rate_age) * debt_base_value / correction_factor
// Equivalent to:
rate_per_second.pow(rate_age - rate_ag_at_time_of_debt_creation) * debt_base_value
And in the classic trade-off of space vs time complexity, we precompute the correction factor applied to the base debt as the normalized debt
normalized_debt = debt_base_value / rate_per_second.pow(rate_age_at_time_of_debt_creation);
In the actual code, rate_per_second.pow(...)
will be precomputed
for us at block initialize and is just queried as the “accrued
rate”.
The case of rate_age_at_time_of_debt_creation == 0
creates a
correction factor of 1, since no debt has yet accumulated on that
rate. This leads to the behavior of normalize
apparently doing
nothing. The debt in that case is “synced” to the interest rate,
and doesn’t need any correction.
§Renormalization
Renormalization is the operation of saying “from now one, I want to accrue this debt at a new rate”. Implicit in that is that all previous debt has been accounted for. We are essentially “starting over” with a new base debt - our accrued debt from the old rate - and a new interest rate.
current_debt = normalized_debt * accrued_rate(old_interest_rate);
normalized_debt = current_debt / accrued_rate(new_interest_rate);
Two things to note here:
- If
old_interest_rate
andnew_interest_rate
are identical, this is a no-op. - If
new_interest_rate
is newly created (and thus its age is0
), the correction factor is1
just as for any other rate. See the note above regarding zero-age rates.
Re-exports§
pub use weights::WeightInfo;
pub use pallet::*;
Modules§
- The
pallet
module in each FRAME pallet hosts the most important items needed to construct this pallet.