Desync between reserve and obligation interest
In the protocol interest is tracked in two places. The per-pool interest calculations are performed in reserve
. Per-user accounting happens in obligation
.
The interest for obligation
is calculated every time compound_debt()
is called. The function is shown below.
/// Compound the debt on a borrow object
fun compound_debt<P>(borrow: &mut Borrow, reserve: &Reserve<P>) {
let new_cumulative_borrow_rate = reserve::cumulative_borrow_rate(reserve);
let compounded_interest_rate = div(
new_cumulative_borrow_rate,
borrow.cumulative_borrow_rate
);
borrow.borrowed_amount = mul(
borrow.borrowed_amount,
compounded_interest_rate
);
borrow.cumulative_borrow_rate = new_cumulative_borrow_rate;
}
The function saves the cumulative interest rate to calculate the amount of interest that has accrued since the last time compound_debt()
was called. The cumulative interest rate is updated every time compound_interest()
is called in reserve
.
The functions are called with different frequencies, and in most cases, compound_interest()
in reserve
will be called more often. Depending on the length of time between calls, the rounding error will be different. Since compound_interest()
is called more often, the accounting error is in favor of the protocol. The user ends up paying slightly more interest than is accounted for in the reserve
.
Assuming that every five seconds compound_interest()
is called, and that compound_debt()
is called yearly, the difference in the interest rates is graphed below. Note that this is an extreme case, and that the scale of the y-axis is very small.
