Skip to content

Commit

Permalink
fix: adapt MaxBorrow to limit it considering MinCollateralLiquidity a…
Browse files Browse the repository at this point in the history
…nd MaxSupplyUtilization (#1954)

* fix MaxBorrow

* fix imports

* refactor reducing duplicated code

* fix bug in limits

* adding unit tests and minor tweaks

* changelog and readme

* lint fixes

* markdown lint fix

* Update x/leverage/README.md

Co-authored-by: Adam Moser <63419657+toteki@users.noreply.github.com>

* Update x/leverage/keeper/limits.go

Co-authored-by: Adam Moser <63419657+toteki@users.noreply.github.com>

* Update x/leverage/keeper/limits.go

Co-authored-by: Adam Moser <63419657+toteki@users.noreply.github.com>

* Update x/leverage/keeper/borrows.go

Co-authored-by: Adam Moser <63419657+toteki@users.noreply.github.com>

* Update x/leverage/keeper/borrows.go

Co-authored-by: Adam Moser <63419657+toteki@users.noreply.github.com>

* pr comments

---------

Co-authored-by: Adam Moser <63419657+toteki@users.noreply.github.com>
  • Loading branch information
kosegor and toteki authored Mar 29, 2023
1 parent 10547c8 commit f379034
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 57 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Ref: https://keepachangelog.com/en/1.0.0/
- [1929](https://github.com/umee-network/umee/pull/1929) Leverage: `MaxWithdraw` now accounts for `MinCollateralLiquidity`
- [1957](https://github.com/umee-network/umee/pull/1957) Leverage: Reserved amount per block now rounds up.
- [1956](https://github.com/umee-network/umee/pull/1956) Leverage: token liquidation threshold must be bigger than collateral_weight.
- [1954](https://github.com/umee-network/umee/pull/1954) Leverage: `MaxBorrow` now accounts for
`MinCollateralLiquidity` and `MaxSupplyUtilization`

## [v4.2.0](https://github.com/umee-network/umee/releases/tag/v4.2.0) - 2023-03-15

Expand Down
2 changes: 1 addition & 1 deletion x/leverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Users have the following actions available to them:

Interest will accrue on borrows for as long as they are not paid off, with the amount owed increasing at a rate of the asset's [Borrow APY](#borrow-apy).

- `MsgMaxBorrow` borrows assets by automatically calculating the maximum amount that can be borrowed.
- `MsgMaxBorrow` borrows assets by automatically calculating the maximum amount that can be borrowed. This amount is calculated taking into account the user's borrow limit and the module's available liquidity respecting the `min_collateral_liquidity` and `max_supply_utilization` of the `Token`.

- `MsgRepay` assets of a borrowed type, directly reducing the amount owed.

Expand Down
48 changes: 48 additions & 0 deletions x/leverage/keeper/borrows.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,51 @@ func (k Keeper) checkSupplyUtilization(ctx sdk.Context, denom string) error {
}
return nil
}

// moduleMaxBorrow calculates maximum amount of Token to borrow from the module.
// The calculation first finds the maximum amount of Token that can be borrowed from the module,
// respecting the min_collateral_liquidity parameter, then determines the maximum amount of Token that can be borrowed
// from the module, respecting the max_supply_utilization parameter. The minimum between these two values is
// selected, given that the min_collateral_liquidity and max_supply_utilization are both limiting factors.
func (k Keeper) moduleMaxBorrow(ctx sdk.Context, denom string) (sdkmath.Int, error) {
// 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 borrow anything
if !moduleAvailableLiquidity.IsPositive() {
return sdk.ZeroInt(), nil
}

// Get max_supply_utilization for the denom
token, err := k.GetTokenSettings(ctx, denom)
if err != nil {
return sdk.ZeroInt(), err
}
maxSupplyUtilization := token.MaxSupplyUtilization

// Get total_borrowed from module for the denom
totalBorrowed := k.GetTotalBorrowed(ctx, denom).Amount

// Get module liquidity for the denom
liquidity := k.AvailableLiquidity(ctx, denom)

// The formula to calculate max_borrow respecting the max_supply_utilization is as follows:
//
// max_supply_utilization = (total_borrowed + module_max_borrow) / (module_liquidity + total_borrowed)
// module_max_borrow = max_supply_utilization * module_liquidity + max_supply_utilization * total_borrowed
// - total_borrowed
moduleMaxBorrow := maxSupplyUtilization.MulInt(liquidity).Add(maxSupplyUtilization.MulInt(totalBorrowed)).Sub(
sdk.NewDec(totalBorrowed.Int64()),
)

// If module_max_borrow is zero, we cannot borrow anything
if !moduleMaxBorrow.IsPositive() {
return sdk.ZeroInt(), nil
}

// Use the minimum between module_max_borrow and module_available_liquidity
return sdk.MinInt(moduleAvailableLiquidity, moduleMaxBorrow.TruncateInt()), nil
}
83 changes: 39 additions & 44 deletions x/leverage/keeper/collateral.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,75 +206,70 @@ func (k *Keeper) checkCollateralShare(ctx sdk.Context, denom string) error {
return nil
}

// moduleMaxWithdraw calculates the maximum available amount of uToken to withdraw
// from the module given a token denom and a user's address. 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,
) {
// 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 module liquidity for the denom
liquidity := k.AvailableLiquidity(ctx, denom)

// Get module collateral for the uDenom
totalCollateral := k.GetTotalCollateral(ctx, spendableUTokens.Denom)
totalTokenCollateral, err := k.ExchangeUTokens(ctx, sdk.NewCoins(totalCollateral))
// Get the module_available_liquidity
moduleAvailableLiquidity, err := k.moduleAvailableLiquidity(ctx, denom)
if err != nil {
return sdk.ZeroInt(), err
}

// Get min_collateral_liquidity for the denom
token, err := k.GetTokenSettings(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
}
minCollateralLiquidity := token.MinCollateralLiquidity

// The formula to calculate the available_module_liquidity is as follows:
//
// min_collateral_liquidity = (module_liquidity - available_module_liquidity) / module_collateral
// available_module_liquidity = module_liquidity - min_collateral_liquidity * module_collateral
availableModuleLiquidity :=
sdk.NewDec(liquidity.Int64()).Sub(minCollateralLiquidity.MulInt(totalTokenCollateral.AmountOf(denom)))

// If available_module_liquidity is 0 or less, we cannot withdraw anything
if availableModuleLiquidity.LTE(sdk.ZeroDec()) {
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
}

// If user_spendable_utokens >= available_module_liquidity we can only withdraw
// available_module_liquidity.
if spendableUTokens.Amount.GTE(availableModuleLiquidity.TruncateInt()) {
return availableModuleLiquidity.TruncateInt(), 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 available_module_liquidity,
// 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 availableModuleLiquidity.TruncateInt().Sub(spendableUTokens.Amount).GTE(totalTokenCollateral.AmountOf(denom)) {
if moduleAvailableLiquidity.Sub(spendableUTokens.Amount).GTE(totalTokenCollateral.AmountOf(denom)) {
return spendableUTokens.Amount.Add(totalTokenCollateral.AmountOf(denom)), nil
}

// At this point we know that there is enough available_module_liquidity to withdraw user_spendable_utokens.
// Now we need to get the available_module_collateral after withdrawing user_spendable_utokens:
// 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 - available_module_collateral)
// / (module_collateral - available_module_collateral)
// min_collateral_liquidity = (module_liquidity - user_spendable_utokens - module_available_collateral)
// / (module_collateral - module_available_collateral)
//
// available_module_collateral = (module_liquidity - user_spendable_utokens - min_collateral_liquidity
// module_available_collateral = (module_liquidity - user_spendable_utokens - min_collateral_liquidity
// * module_collateral) / (1 - min_collateral_liquidity)
availableModuleCollateral :=
moduleAvailableCollateral :=
(sdk.NewDec(liquidity.Sub(spendableUTokens.Amount).Int64()).Sub(
minCollateralLiquidity.MulInt(
totalTokenCollateral.AmountOf(denom),
),
)).Quo(sdk.NewDec(1).Sub(minCollateralLiquidity))

// Adding (user_spendable_utokens + available_module_collateral) we obtain the max uTokens the account can
// Adding (user_spendable_utokens + module_available_collateral) we obtain the max uTokens the account can
// withdraw from the module.
return spendableUTokens.Amount.Add(availableModuleCollateral.TruncateInt()), nil
return spendableUTokens.Amount.Add(moduleAvailableCollateral.TruncateInt()), nil
}
2 changes: 1 addition & 1 deletion x/leverage/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ func (q Querier) MaxBorrow(
// will be nil and the resulting value will be what
// can safely be borrowed even with missing prices.
// On non-nil error here, max borrow is zero.
maxBorrow, err := q.Keeper.maxBorrow(ctx, addr, denom)
maxBorrow, err := q.Keeper.userMaxBorrow(ctx, addr, denom)
if err == nil && maxBorrow.IsPositive() {
maxTokens = maxTokens.Add(maxBorrow)
}
Expand Down
34 changes: 32 additions & 2 deletions x/leverage/keeper/limits.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ func (k *Keeper) userMaxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom str
return sdk.NewCoin(uDenom, withdrawAmount), sdk.NewCoin(uDenom, walletUtokens), nil
}

// maxBorrow calculates the maximum amount of a given token an account can currently borrow.
// userMaxBorrow calculates the maximum amount of a given token an account can currently borrow.
// input denom should be a base token. If oracle prices are missing for some of the borrower's
// collateral, computes the maximum safe borrow allowed by only the collateral whose prices are known
func (k *Keeper) maxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, error) {
func (k *Keeper) userMaxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, error) {
if types.HasUTokenPrefix(denom) {
return sdk.Coin{}, types.ErrUToken
}
Expand Down Expand Up @@ -190,3 +190,33 @@ func (k *Keeper) maxCollateralFromShare(ctx sdk.Context, denom string) (sdkmath.
// return the computed maximum or the current uToken supply, whichever is smaller
return sdk.MinInt(k.GetUTokenSupply(ctx, denom).Amount, maxUTokens.Amount), nil
}

// moduleAvailableLiquidity calculates the maximum available liquidity of a Token denom from the module can be used,
// respecting the MinCollateralLiquidity set for given Token.
func (k Keeper) moduleAvailableLiquidity(ctx sdk.Context, denom string) (sdkmath.Int, error) {
// Get module liquidity for the Token
liquidity := k.AvailableLiquidity(ctx, denom)

// Get module collateral for the associated uToken
totalCollateral := k.GetTotalCollateral(ctx, types.ToUTokenDenom(denom))
totalTokenCollateral, err := k.ExchangeUTokens(ctx, sdk.NewCoins(totalCollateral))
if err != nil {
return sdkmath.Int{}, err
}

// Get min_collateral_liquidity for the denom
token, err := k.GetTokenSettings(ctx, denom)
if err != nil {
return sdkmath.Int{}, err
}
minCollateralLiquidity := token.MinCollateralLiquidity

// The formula to calculate the module_available_liquidity is as follows:
//
// min_collateral_liquidity = (module_liquidity - module_available_liquidity) / module_collateral
// module_available_liquidity = module_liquidity - min_collateral_liquidity * module_collateral
moduleAvailableLiquidity :=
sdk.NewDec(liquidity.Int64()).Sub(minCollateralLiquidity.MulInt(totalTokenCollateral.AmountOf(denom)))

return sdk.MaxInt(moduleAvailableLiquidity.TruncateInt(), sdk.ZeroInt()), nil
}
29 changes: 21 additions & 8 deletions x/leverage/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,15 +367,28 @@ func (s msgServer) MaxBorrow(
// but not this token or any of their borrows, error
// will be nil and the resulting value will be what
// can safely be borrowed even with missing prices.
maxBorrow, err := s.keeper.maxBorrow(ctx, borrowerAddr, msg.Denom)
userMaxBorrow, err := s.keeper.userMaxBorrow(ctx, borrowerAddr, msg.Denom)
if err != nil {
return nil, err
}
if maxBorrow.IsZero() {
if userMaxBorrow.IsZero() {
return &types.MsgMaxBorrowResponse{Borrowed: coin.Zero(msg.Denom)}, nil
}

if err := s.keeper.Borrow(ctx, borrowerAddr, maxBorrow); err != nil {
// Get the max available to borrow from the module
moduleMaxBorrow, err := s.keeper.moduleMaxBorrow(ctx, msg.Denom)
if err != nil {
return nil, err
}
if moduleMaxBorrow.IsZero() {
return &types.MsgMaxBorrowResponse{Borrowed: coin.Zero(msg.Denom)}, nil
}

// Select the minimum between user_max_borrow and module_max_borrow
userMaxBorrow.Amount = sdk.MinInt(userMaxBorrow.Amount, moduleMaxBorrow)

// Proceed to borrow
if err := s.keeper.Borrow(ctx, borrowerAddr, userMaxBorrow); err != nil {
return nil, err
}

Expand All @@ -387,26 +400,26 @@ func (s msgServer) MaxBorrow(
}

// Check MaxSupplyUtilization after transaction
if err = s.keeper.checkSupplyUtilization(ctx, maxBorrow.Denom); err != nil {
if err = s.keeper.checkSupplyUtilization(ctx, userMaxBorrow.Denom); err != nil {
return nil, err
}

// Check MinCollateralLiquidity is still satisfied after the transaction
if err = s.keeper.checkCollateralLiquidity(ctx, maxBorrow.Denom); err != nil {
if err = s.keeper.checkCollateralLiquidity(ctx, userMaxBorrow.Denom); err != nil {
return nil, err
}

s.keeper.Logger(ctx).Debug(
"assets borrowed",
"borrower", msg.Borrower,
"amount", maxBorrow.String(),
"amount", moduleMaxBorrow.String(),
)
sdkutil.Emit(&ctx, &types.EventBorrow{
Borrower: msg.Borrower,
Asset: maxBorrow,
Asset: userMaxBorrow,
})
return &types.MsgMaxBorrowResponse{
Borrowed: maxBorrow,
Borrowed: userMaxBorrow,
}, nil
}

Expand Down
Loading

0 comments on commit f379034

Please sign in to comment.