/
collateral.go
275 lines (233 loc) · 10.5 KB
/
collateral.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
package keeper
import (
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/umee-network/umee/v4/x/leverage/types"
)
// liquidateCollateral burns uToken collateral and sends the base token reward to the liquidator.
// This occurs during direct liquidation.
func (k Keeper) liquidateCollateral(ctx sdk.Context, borrower, liquidator sdk.AccAddress, uToken, token sdk.Coin,
) error {
if err := k.burnCollateral(ctx, borrower, uToken); err != nil {
return err
}
return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, liquidator, sdk.NewCoins(token))
}
// burnCollateral removes some uTokens from an account's collateral and burns them. This occurs
// during liquidations.
func (k Keeper) burnCollateral(ctx sdk.Context, addr sdk.AccAddress, uToken sdk.Coin) error {
err := k.setCollateral(ctx, addr, k.GetCollateral(ctx, addr, uToken.Denom).Sub(uToken))
if err != nil {
return err
}
if err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(uToken)); err != nil {
return err
}
return k.setUTokenSupply(ctx, k.GetUTokenSupply(ctx, uToken.Denom).Sub(uToken))
}
// decollateralize removes fromAddr's uTokens from the module and sends them to toAddr.
// It occurs when decollateralizing uTokens (in which case fromAddr and toAddr are the
// same) as well as during non-direct liquidations, where toAddr is the liquidator.
func (k Keeper) decollateralize(ctx sdk.Context, fromAddr, toAddr sdk.AccAddress, uToken sdk.Coin) error {
err := k.setCollateral(ctx, fromAddr, k.GetCollateral(ctx, fromAddr, uToken.Denom).Sub(uToken))
if err != nil {
return err
}
return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, sdk.NewCoins(uToken))
}
// GetTotalCollateral returns an sdk.Coin representing how much of a given uToken
// the x/leverage module account currently holds as collateral. Non-uTokens return zero.
func (k Keeper) GetTotalCollateral(ctx sdk.Context, denom string) sdk.Coin {
if !types.HasUTokenPrefix(denom) {
// non-uTokens cannot be collateral
return sdk.Coin{}
}
// uTokens in the module account are always from collateral
return k.ModuleBalance(ctx, denom)
}
// CalculateCollateralValue uses the price oracle to determine the value (in USD) provided by
// collateral sdk.Coins, using each token's uToken exchange rate. Always uses spot price.
// An error is returned if any input coins are not uTokens or if value calculation fails.
func (k Keeper) CalculateCollateralValue(ctx sdk.Context, collateral sdk.Coins) (sdk.Dec, error) {
total := sdk.ZeroDec()
for _, coin := range collateral {
// convert uToken collateral to base assets
baseAsset, err := k.ExchangeUToken(ctx, coin)
if err != nil {
return sdk.ZeroDec(), err
}
// get USD value of base assets
v, err := k.TokenValue(ctx, baseAsset, types.PriceModeSpot)
if err != nil {
return sdk.ZeroDec(), err
}
// add each collateral coin's weighted value to borrow limit
total = total.Add(v)
}
return total, nil
}
// VisibleCollateralValue uses the price oracle to determine the value (in USD) provided by
// collateral sdk.Coins, using each token's uToken exchange rate. Always uses spot price.
// Unlike CalculateCollateralValue, this function will not return an error if value calculation
// fails on a token - instead, that token will contribute zero value to the total.
func (k Keeper) VisibleCollateralValue(ctx sdk.Context, collateral sdk.Coins) (sdk.Dec, error) {
total := sdk.ZeroDec()
for _, coin := range collateral {
// convert uToken collateral to base assets
baseAsset, err := k.ExchangeUToken(ctx, coin)
if err != nil {
return sdk.ZeroDec(), err
}
// get USD value of base assets
v, err := k.TokenValue(ctx, baseAsset, types.PriceModeSpot)
if err == nil {
// for coins that did not error, add their value to the total
total = total.Add(v)
}
if nonOracleError(err) {
return sdk.ZeroDec(), err
}
}
return total, nil
}
// GetAllTotalCollateral returns total collateral across all uTokens.
func (k Keeper) GetAllTotalCollateral(ctx sdk.Context) sdk.Coins {
total := sdk.NewCoins()
tokens := k.GetAllRegisteredTokens(ctx)
for _, t := range tokens {
uDenom := types.ToUTokenDenom(t.BaseDenom)
total = total.Add(k.GetTotalCollateral(ctx, uDenom))
}
return total
}
// CollateralLiquidity calculates the current collateral liquidity of a token denom,
// which is defined as the token's liquidity, divided by the base token equivalent
// of associated uToken's total collateral. Ranges from 0 to 1.0
func (k Keeper) CollateralLiquidity(ctx sdk.Context, denom string) sdk.Dec {
totalCollateral := k.GetTotalCollateral(ctx, types.ToUTokenDenom(denom))
exchangeRate := k.DeriveExchangeRate(ctx, denom)
liquidity := k.AvailableLiquidity(ctx, denom)
// Zero collateral will be interpreted as full collateral liquidity. This encompasses two cases:
// - liquidity / collateral = 0/0: Empty market, system is considered healthy by default
// - liquidity / collateral = x/0: No collateral but nonzero liquidity, also considered healthy
// In both cases, "all collateral is liquid" is technically true, given that there is no collateral.
if totalCollateral.IsZero() {
return sdk.OneDec()
}
collateralLiquidity := toDec(liquidity).Quo(exchangeRate.MulInt(totalCollateral.Amount))
// Liquidity above 100% is ignored
return sdk.MinDec(collateralLiquidity, sdk.OneDec())
}
// VisibleCollateralShare calculates the portion of overall collateral (measured in USD value) that a
// given uToken denom represents. If an asset other than the denom requested is missing an oracle
// price, it ignores that asset's contribution to the system's overall collateral, thus potentially
// overestimating the requested denom's collateral share while improving availability.
func (k *Keeper) VisibleCollateralShare(ctx sdk.Context, denom string) (sdk.Dec, error) {
systemCollateral := k.GetAllTotalCollateral(ctx)
thisCollateral := sdk.NewCoins(sdk.NewCoin(denom, systemCollateral.AmountOf(denom)))
// get USD collateral value for all uTokens combined, except those experiencing price outages
totalValue, err := k.VisibleCollateralValue(ctx, systemCollateral)
if err != nil {
return sdk.ZeroDec(), err
}
// get USD collateral value for this uToken only
thisValue, err := k.CalculateCollateralValue(ctx, thisCollateral)
if err != nil {
return sdk.ZeroDec(), err
}
if !totalValue.IsPositive() {
return sdk.ZeroDec(), nil
}
return thisValue.Quo(totalValue), nil
}
// checkCollateralLiquidity returns the appropriate error if a token denom's
// collateral liquidity is below its MinCollateralLiquidity
func (k Keeper) checkCollateralLiquidity(ctx sdk.Context, denom string) error {
token, err := k.GetTokenSettings(ctx, denom)
if err != nil {
return err
}
collateralLiquidity := k.CollateralLiquidity(ctx, denom)
if collateralLiquidity.LT(token.MinCollateralLiquidity) {
return types.ErrMinCollateralLiquidity.Wrap(collateralLiquidity.String())
}
return nil
}
// checkCollateralShare returns an error if a given uToken is above its collateral share
// as calculated using only tokens whose oracle prices exist
func (k *Keeper) checkCollateralShare(ctx sdk.Context, denom string) error {
token, err := k.GetTokenSettings(ctx, types.ToTokenDenom(denom))
if err != nil {
return err
}
if token.MaxCollateralShare.Equal(sdk.OneDec()) {
// skip computation when collateral share is unrestricted
return nil
}
share, err := k.VisibleCollateralShare(ctx, denom)
if err != nil {
return err
}
if share.GT(token.MaxCollateralShare) {
return types.ErrMaxCollateralShare.Wrapf("%s share is %s", denom, share)
}
return nil
}
// moduleMaxWithdraw calculates the maximum available amount of uToken to withdraw from the module given the amount of
// user's spendable tokens. The calculation first finds the maximum amount of non-collateral uTokens the user can
// withdraw up to the amount in their wallet, then determines how much collateral can be withdrawn in addition to that.
// The returned value is the sum of the two values.
func (k Keeper) moduleMaxWithdraw(ctx sdk.Context, spendableUTokens sdk.Coin) (sdkmath.Int, error) {
denom := types.ToTokenDenom(spendableUTokens.Denom)
// Get the module_available_liquidity
moduleAvailableLiquidity, err := k.moduleAvailableLiquidity(ctx, denom)
if err != nil {
return sdk.ZeroInt(), err
}
// If module_available_liquidity is zero, we cannot withdraw anything
if !moduleAvailableLiquidity.IsPositive() {
return sdkmath.ZeroInt(), nil
}
// If user_spendable_utokens >= module_available_liquidity we can only withdraw
// module_available_liquidity.
if spendableUTokens.Amount.GTE(moduleAvailableLiquidity) {
return moduleAvailableLiquidity, nil
}
// Get module collateral for the uDenom
totalCollateral := k.GetTotalCollateral(ctx, spendableUTokens.Denom)
totalTokenCollateral, err := k.ExchangeUTokens(ctx, sdk.NewCoins(totalCollateral))
if err != nil {
return sdk.ZeroInt(), err
}
// If after subtracting all the user_spendable_utokens from the module_available_liquidity,
// the result is higher than the total module_collateral,
// we can withdraw user_spendable_utokens + module_collateral.
if moduleAvailableLiquidity.Sub(spendableUTokens.Amount).GTE(totalTokenCollateral.AmountOf(denom)) {
return spendableUTokens.Amount.Add(totalTokenCollateral.AmountOf(denom)), nil
}
// Get module liquidity for the denom
liquidity := k.AvailableLiquidity(ctx, denom)
// Get min_collateral_liquidity for the denom
token, err := k.GetTokenSettings(ctx, denom)
if err != nil {
return sdk.ZeroInt(), err
}
minCollateralLiquidity := token.MinCollateralLiquidity
// At this point we know that there is enough module_available_liquidity to withdraw user_spendable_utokens.
// Now we need to get the module_available_collateral after withdrawing user_spendable_utokens:
//
// min_collateral_liquidity = (module_liquidity - user_spendable_utokens - module_available_collateral)
// / (module_collateral - module_available_collateral)
//
// module_available_collateral = (module_liquidity - user_spendable_utokens - min_collateral_liquidity
// * module_collateral) / (1 - min_collateral_liquidity)
moduleAvailableCollateral :=
(sdk.NewDec(liquidity.Sub(spendableUTokens.Amount).Int64()).Sub(
minCollateralLiquidity.MulInt(
totalTokenCollateral.AmountOf(denom),
),
)).Quo(sdk.NewDec(1).Sub(minCollateralLiquidity))
// Adding (user_spendable_utokens + module_available_collateral) we obtain the max uTokens the account can
// withdraw from the module.
return spendableUTokens.Amount.Add(moduleAvailableCollateral.TruncateInt()), nil
}