-
Notifications
You must be signed in to change notification settings - Fork 364
/
interest.go
317 lines (270 loc) · 12.4 KB
/
interest.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
package keeper
import (
"math"
errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/hard/types"
)
var (
scalingFactor = 1e18
secondsPerYear = 31536000
)
// ApplyInterestRateUpdates translates the current interest rate models from the params to the store,
// with each money market accruing interest.
func (k Keeper) ApplyInterestRateUpdates(ctx sdk.Context) {
denomSet := map[string]bool{}
params := k.GetParams(ctx)
for _, mm := range params.MoneyMarkets {
// Set any new money markets in the store
moneyMarket, found := k.GetMoneyMarket(ctx, mm.Denom)
if !found {
moneyMarket = mm
k.SetMoneyMarket(ctx, mm.Denom, moneyMarket)
}
// Accrue interest according to the current money markets in the store
err := k.AccrueInterest(ctx, mm.Denom)
if err != nil {
panic(err)
}
// Update the interest rate in the store if the params have changed
if !moneyMarket.Equal(mm) {
k.SetMoneyMarket(ctx, mm.Denom, mm)
}
denomSet[mm.Denom] = true
}
// Edge case: money markets removed from params that still exist in the store
k.IterateMoneyMarkets(ctx, func(denom string, i types.MoneyMarket) bool {
if !denomSet[denom] {
// Accrue interest according to current store money market
err := k.AccrueInterest(ctx, denom)
if err != nil {
panic(err)
}
// Delete the money market from the store
k.DeleteMoneyMarket(ctx, denom)
}
return false
})
}
// AccrueInterest applies accrued interest to total borrows and reserves by calculating
// interest from the last checkpoint time and writing the updated values to the store.
func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
previousAccrualTime, found := k.GetPreviousAccrualTime(ctx, denom)
if !found {
k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
return nil
}
timeElapsed := int64(math.RoundToEven(
ctx.BlockTime().Sub(previousAccrualTime).Seconds(),
))
if timeElapsed == 0 {
return nil
}
// Get current protocol state and hold in memory as 'prior'
macc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleName)
cashPrior := k.bankKeeper.GetBalance(ctx, macc.GetAddress(), denom).Amount
borrowedPrior := sdk.NewCoin(denom, sdk.ZeroInt())
borrowedCoinsPrior, foundBorrowedCoinsPrior := k.GetBorrowedCoins(ctx)
if foundBorrowedCoinsPrior {
borrowedPrior = sdk.NewCoin(denom, borrowedCoinsPrior.AmountOf(denom))
}
if borrowedPrior.IsZero() {
k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
return nil
}
reservesPrior, foundReservesPrior := k.GetTotalReserves(ctx)
if !foundReservesPrior {
newReservesPrior := sdk.NewCoins()
k.SetTotalReserves(ctx, newReservesPrior)
reservesPrior = newReservesPrior
}
borrowInterestFactorPrior, foundBorrowInterestFactorPrior := k.GetBorrowInterestFactor(ctx, denom)
if !foundBorrowInterestFactorPrior {
newBorrowInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
k.SetBorrowInterestFactor(ctx, denom, newBorrowInterestFactorPrior)
borrowInterestFactorPrior = newBorrowInterestFactorPrior
}
supplyInterestFactorPrior, foundSupplyInterestFactorPrior := k.GetSupplyInterestFactor(ctx, denom)
if !foundSupplyInterestFactorPrior {
newSupplyInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
k.SetSupplyInterestFactor(ctx, denom, newSupplyInterestFactorPrior)
supplyInterestFactorPrior = newSupplyInterestFactorPrior
}
// Fetch money market from the store
mm, found := k.GetMoneyMarket(ctx, denom)
if !found {
return errorsmod.Wrapf(types.ErrMoneyMarketNotFound, "%s", denom)
}
// GetBorrowRate calculates the current interest rate based on utilization (the fraction of supply that has been borrowed)
borrowRateApy, err := CalculateBorrowRate(mm.InterestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowedPrior.Amount), sdk.NewDecFromInt(reservesPrior.AmountOf(denom)))
if err != nil {
return err
}
// Convert from APY to SPY, expressed as (1 + borrow rate)
borrowRateSpy, err := APYToSPY(sdk.OneDec().Add(borrowRateApy))
if err != nil {
return err
}
// Calculate borrow interest factor and update
borrowInterestFactor := CalculateBorrowInterestFactor(borrowRateSpy, sdkmath.NewInt(timeElapsed))
interestBorrowAccumulated := (borrowInterestFactor.Mul(sdk.NewDecFromInt(borrowedPrior.Amount)).TruncateInt()).Sub(borrowedPrior.Amount)
if interestBorrowAccumulated.IsZero() && borrowRateApy.IsPositive() {
// don't accumulate if borrow interest is rounding to zero
return nil
}
totalBorrowInterestAccumulated := sdk.NewCoins(sdk.NewCoin(denom, interestBorrowAccumulated))
reservesNew := sdk.NewDecFromInt(interestBorrowAccumulated).Mul(mm.ReserveFactor).TruncateInt()
borrowInterestFactorNew := borrowInterestFactorPrior.Mul(borrowInterestFactor)
k.SetBorrowInterestFactor(ctx, denom, borrowInterestFactorNew)
// Calculate supply interest factor and update
supplyInterestNew := interestBorrowAccumulated.Sub(reservesNew)
supplyInterestFactor := CalculateSupplyInterestFactor(sdk.NewDecFromInt(supplyInterestNew), sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowedPrior.Amount), sdk.NewDecFromInt(reservesPrior.AmountOf(denom)))
supplyInterestFactorNew := supplyInterestFactorPrior.Mul(supplyInterestFactor)
k.SetSupplyInterestFactor(ctx, denom, supplyInterestFactorNew)
// Update accural keys in store
k.IncrementBorrowedCoins(ctx, totalBorrowInterestAccumulated)
k.IncrementSuppliedCoins(ctx, sdk.NewCoins(sdk.NewCoin(denom, supplyInterestNew)))
k.SetTotalReserves(ctx, reservesPrior.Add(sdk.NewCoin(denom, reservesNew)))
k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
return nil
}
// CalculateBorrowRate calculates the borrow rate, which is the current APY expressed as a decimal
// based on the current utilization.
func CalculateBorrowRate(model types.InterestRateModel, cash, borrows, reserves sdk.Dec) (sdk.Dec, error) {
utilRatio := CalculateUtilizationRatio(cash, borrows, reserves)
// Calculate normal borrow rate (under kink)
if utilRatio.LTE(model.Kink) {
return utilRatio.Mul(model.BaseMultiplier).Add(model.BaseRateAPY), nil
}
// Calculate jump borrow rate (over kink)
normalRate := model.Kink.Mul(model.BaseMultiplier).Add(model.BaseRateAPY)
excessUtil := utilRatio.Sub(model.Kink)
return excessUtil.Mul(model.JumpMultiplier).Add(normalRate), nil
}
// CalculateUtilizationRatio calculates an asset's current utilization rate
func CalculateUtilizationRatio(cash, borrows, reserves sdk.Dec) sdk.Dec {
// Utilization rate is 0 when there are no borrows
if borrows.Equal(sdk.ZeroDec()) {
return sdk.ZeroDec()
}
totalSupply := cash.Add(borrows).Sub(reserves)
if totalSupply.IsNegative() {
return sdk.OneDec()
}
return sdk.MinDec(sdk.OneDec(), borrows.Quo(totalSupply))
}
// CalculateBorrowInterestFactor calculates the simple interest scaling factor,
// which is equal to: (per-second interest rate * number of seconds elapsed)
// Will return 1.000x, multiply by principal to get new principal with added interest
func CalculateBorrowInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdkmath.Int) sdk.Dec {
scalingFactorUint := sdk.NewUint(uint64(scalingFactor))
scalingFactorInt := sdkmath.NewInt(int64(scalingFactor))
// Convert per-second interest rate to a uint scaled by 1e18
interestMantissa := sdkmath.NewUintFromBigInt(perSecondInterestRate.MulInt(scalingFactorInt).RoundInt().BigInt())
// Convert seconds elapsed to uint (*not scaled*)
secondsElapsedUint := sdkmath.NewUintFromBigInt(secondsElapsed.BigInt())
// Calculate the interest factor as a uint scaled by 1e18
interestFactorMantissa := sdkmath.RelativePow(interestMantissa, secondsElapsedUint, scalingFactorUint)
// Convert interest factor to an unscaled sdk.Dec
return sdk.NewDecFromBigInt(interestFactorMantissa.BigInt()).QuoInt(scalingFactorInt)
}
// CalculateSupplyInterestFactor calculates the supply interest factor, which is the percentage of borrow interest
// that flows to each unit of supply, i.e. at 50% utilization and 0% reserve factor, a 5% borrow interest will
// correspond to a 2.5% supply interest.
func CalculateSupplyInterestFactor(newInterest, cash, borrows, reserves sdk.Dec) sdk.Dec {
totalSupply := cash.Add(borrows).Sub(reserves)
if totalSupply.IsZero() {
return sdk.OneDec()
}
return (newInterest.Quo(totalSupply)).Add(sdk.OneDec())
}
// SyncBorrowInterest updates the user's owed interest on newly borrowed coins to the latest global state
func (k Keeper) SyncBorrowInterest(ctx sdk.Context, addr sdk.AccAddress) {
totalNewInterest := sdk.Coins{}
// Update user's borrow interest factor list for each asset in the 'coins' array.
// We use a list of BorrowInterestFactors here because Amino doesn't support marshaling maps.
borrow, found := k.GetBorrow(ctx, addr)
if !found {
return
}
for _, coin := range borrow.Amount {
// Locate the borrow interest factor item by coin denom in the user's list of borrow indexes
foundAtIndex := -1
for i := range borrow.Index {
if borrow.Index[i].Denom == coin.Denom {
foundAtIndex = i
break
}
}
interestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
if foundAtIndex == -1 { // First time user has borrowed this denom
borrow.Index = append(borrow.Index, types.NewBorrowInterestFactor(coin.Denom, interestFactorValue))
} else { // User has an existing borrow index for this denom
// Calculate interest owed by user since asset's last borrow index update
storedAmount := sdk.NewDecFromInt(borrow.Amount.AmountOf(coin.Denom))
userLastInterestFactor := borrow.Index[foundAtIndex].Value
interest := (storedAmount.Quo(userLastInterestFactor).Mul(interestFactorValue)).Sub(storedAmount)
totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt()))
// We're synced up, so update user's borrow index value to match the current global borrow index value
borrow.Index[foundAtIndex].Value = interestFactorValue
}
}
// Add all pending interest to user's borrow
borrow.Amount = borrow.Amount.Add(totalNewInterest...)
// Update user's borrow in the store
k.SetBorrow(ctx, borrow)
}
// SyncSupplyInterest updates the user's earned interest on supplied coins based on the latest global state
func (k Keeper) SyncSupplyInterest(ctx sdk.Context, addr sdk.AccAddress) {
totalNewInterest := sdk.Coins{}
// Update user's supply index list for each asset in the 'coins' array.
// We use a list of SupplyInterestFactors here because Amino doesn't support marshaling maps.
deposit, found := k.GetDeposit(ctx, addr)
if !found {
return
}
for _, coin := range deposit.Amount {
// Locate the deposit index item by coin denom in the user's list of deposit indexes
foundAtIndex := -1
for i := range deposit.Index {
if deposit.Index[i].Denom == coin.Denom {
foundAtIndex = i
break
}
}
interestFactorValue, _ := k.GetSupplyInterestFactor(ctx, coin.Denom)
if foundAtIndex == -1 { // First time user has supplied this denom
deposit.Index = append(deposit.Index, types.NewSupplyInterestFactor(coin.Denom, interestFactorValue))
} else { // User has an existing supply index for this denom
// Calculate interest earned by user since asset's last deposit index update
storedAmount := sdk.NewDecFromInt(deposit.Amount.AmountOf(coin.Denom))
userLastInterestFactor := deposit.Index[foundAtIndex].Value
interest := (storedAmount.Mul(interestFactorValue).Quo(userLastInterestFactor)).Sub(storedAmount)
if interest.TruncateInt().GT(sdk.ZeroInt()) {
totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt()))
}
// We're synced up, so update user's deposit index value to match the current global deposit index value
deposit.Index[foundAtIndex].Value = interestFactorValue
}
}
// Add all pending interest to user's deposit
deposit.Amount = deposit.Amount.Add(totalNewInterest...)
// Update user's deposit in the store
k.SetDeposit(ctx, deposit)
}
// APYToSPY converts the input annual interest rate. For example, 10% apy would be passed as 1.10.
// SPY = Per second compounded interest rate is how cosmos mathematically represents APY.
func APYToSPY(apy sdk.Dec) (sdk.Dec, error) {
// Note: any APY 179 or greater will cause an out-of-bounds error
root, err := apy.ApproxRoot(uint64(secondsPerYear))
if err != nil {
return sdk.ZeroDec(), err
}
return root, nil
}
// SPYToEstimatedAPY converts the internal per second compounded interest rate into an estimated annual
// interest rate. The returned value is an estimate and should not be used for financial calculations.
func SPYToEstimatedAPY(apy sdk.Dec) sdk.Dec {
return apy.Power(uint64(secondsPerYear))
}