Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: adapt MaxBorrow to limit it considering MinCollateralLiquidity and MaxSupplyUtilization #1954

Merged
merged 17 commits into from
Mar 29, 2023
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Ref: https://keepachangelog.com/en/1.0.0/
### Fixes

- [1929](https://github.com/umee-network/umee/pull/1929) Leverage: `MaxWithdraw` now accounts for `MinCollateralLiquidity`
- [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 users borrows and his borrow limit, the available liquidity that can be borrowed respecting the `min_collateral_liquidity` and `max_supply_utilization` of the `Token`.
kosegor marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down
52 changes: 52 additions & 0 deletions x/leverage/keeper/borrows.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/umee-network/umee/v4/util/coin"
"github.com/umee-network/umee/v4/x/leverage/types"
)

Expand Down Expand Up @@ -218,3 +219,54 @@ func (k Keeper) checkSupplyUtilization(ctx sdk.Context, denom string) error {
}
return nil
}

// moduleMaxBorrow calculates maximum amount of Token to borrow from the module given the maximum amount of Token the
// user can borrow. 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 three values is
// selected, given that the min_collateral_liquidity and max_supply_utilization are both limiting factors.
func (k Keeper) moduleMaxBorrow(ctx sdk.Context, userMaxBorrow sdk.Coin) (sdk.Coin, error) {
// Get the module_available_liquidity
moduleAvailableLiquidity, err := k.moduleAvailableLiquidity(ctx, userMaxBorrow.Denom)
if err != nil {
return coin.Zero(userMaxBorrow.Denom), err
}

// If module_available_liquidity is 0 or less, we cannot borrow anything
if moduleAvailableLiquidity.LTE(sdkmath.ZeroInt()) {
kosegor marked this conversation as resolved.
Show resolved Hide resolved
return coin.Zero(userMaxBorrow.Denom), nil
}

// Use the minimum of the user's max borrow and the available from module liquidity
amountToBorrow := sdk.MinInt(moduleAvailableLiquidity, userMaxBorrow.Amount)

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

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

// Get module liquidity for the denom
liquidity := k.AvailableLiquidity(ctx, userMaxBorrow.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 0 or less, we cannot borrow anything
if moduleMaxBorrow.LTE(sdk.ZeroDec()) {
kosegor marked this conversation as resolved.
Show resolved Hide resolved
return coin.Zero(userMaxBorrow.Denom), nil
}

// Use the minimum between module_max_borrow and (module_available_liquidity or user_max_borrow)
return sdk.NewCoin(userMaxBorrow.Denom, sdk.MinInt(amountToBorrow, 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 0 or less, we cannot withdraw anything
if moduleAvailableLiquidity.LTE(sdkmath.ZeroInt()) {
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
37 changes: 35 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,36 @@ 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 denom
liquidity := k.AvailableLiquidity(ctx, denom)

// Get uDenom
udenom := types.ToUTokenDenom(denom)

// Get module collateral for the uDenom
totalCollateral := k.GetTotalCollateral(ctx, udenom)
kosegor marked this conversation as resolved.
Show resolved Hide resolved
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 moduleAvailableLiquidity.TruncateInt(), nil
kosegor marked this conversation as resolved.
Show resolved Hide resolved
}
12 changes: 11 additions & 1 deletion x/leverage/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,14 +367,24 @@ 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 userMaxBorrow.IsZero() {
return &types.MsgMaxBorrowResponse{Borrowed: coin.Zero(msg.Denom)}, nil
}

// Get the max available to borrow from the module
maxBorrow, err := s.keeper.moduleMaxBorrow(ctx, userMaxBorrow)
kosegor marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}
if maxBorrow.IsZero() {
return &types.MsgMaxBorrowResponse{Borrowed: coin.Zero(msg.Denom)}, nil
}

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