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 @@ -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