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

feat: implement MaxCollateralShare #1329

Merged
merged 7 commits into from
Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- [1222](https://github.com/umee-network/umee/pull/1222) Liquidation reward_denom can now be either token or uToken.
- [1238](https://github.com/umee-network/umee/pull/1238) Added bad debts query.
- [1323](https://github.com/umee-network/umee/pull/1323) Oracle cli - Add validator address override option.
- [1329](https://github.com/umee-network/umee/pull/1329) Implement MaxCollateralShare.

### Improvements

Expand Down
61 changes: 57 additions & 4 deletions x/leverage/keeper/collateral.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package keeper

import (
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/umee-network/umee/v3/x/leverage/types"
Expand Down Expand Up @@ -89,14 +88,14 @@ func (k Keeper) setCollateralAmount(ctx sdk.Context, borrowerAddr sdk.AccAddress

// 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) sdkmath.Int {
func (k Keeper) GetTotalCollateral(ctx sdk.Context, denom string) sdk.Coin {
toteki marked this conversation as resolved.
Show resolved Hide resolved
if !types.HasUTokenPrefix(denom) {
// non-uTokens cannot be collateral
return sdk.ZeroInt()
return sdk.Coin{}
}

// uTokens in the module account are always from collateral
return k.ModuleBalance(ctx, denom)
return sdk.NewCoin(denom, k.ModuleBalance(ctx, denom))
}

// CalculateCollateralValue uses the price oracle to determine the value (in USD) provided by
Expand Down Expand Up @@ -124,3 +123,57 @@ func (k Keeper) CalculateCollateralValue(ctx sdk.Context, collateral sdk.Coins)

return limit, 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
}
toteki marked this conversation as resolved.
Show resolved Hide resolved

// CollateralShare calculates the portion of overall collateral
// (measured in USD value) that a given uToken denom represents.
func (k *Keeper) CollateralShare(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
totalValue, err := k.CalculateCollateralValue(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
}

// checkCollateralShare returns an error if a given uToken is above its collateral share
func (k *Keeper) checkCollateralShare(ctx sdk.Context, denom string) error {
token, err := k.GetTokenSettings(ctx, types.ToTokenDenom(denom))
if err != nil {
return err
}

share, err := k.CollateralShare(ctx, denom)
if err != nil {
return err
}

if share.GT(token.MaxCollateralShare) {
return types.ErrMaxCollateralShare.Wrapf("%s share is %s", denom, share)
}
return nil
}
10 changes: 7 additions & 3 deletions x/leverage/keeper/collateral_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,14 @@ func (s *IntegrationTestSuite) TestSetCollateralAmount() {
func (s *IntegrationTestSuite) TestTotalCollateral() {
app, ctx, require := s.app, s.ctx, s.Require()

// not a uToken
collateral := app.LeverageKeeper.GetTotalCollateral(ctx, umeeDenom)
require.Equal(sdk.Coin{}, collateral, "not a uToken")

// Test zero collateral
uDenom := types.ToUTokenDenom(umeeDenom)
collateral := app.LeverageKeeper.GetTotalCollateral(ctx, uDenom)
require.Equal(sdk.ZeroInt(), collateral, "zero collateral")
collateral = app.LeverageKeeper.GetTotalCollateral(ctx, uDenom)
require.Equal(coin(uDenom, 0), collateral, "zero collateral")

// create a supplier which will have 100 u/UMEE collateral
addr := s.newAccount(coin(umeeDenom, 100_000000))
Expand All @@ -113,5 +117,5 @@ func (s *IntegrationTestSuite) TestTotalCollateral() {

// Test nonzero collateral
collateral = app.LeverageKeeper.GetTotalCollateral(ctx, uDenom)
require.Equal(sdk.NewInt(100_000000), collateral, "nonzero collateral")
require.Equal(coin(uDenom, 100_000000), collateral, "nonzero collateral")
}
12 changes: 7 additions & 5 deletions x/leverage/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ func (q Querier) MarketSummary(

availableBorrow := q.Keeper.GetAvailableToBorrow(ctx, req.Denom) // TODO #1162 #1163 - update implementation

maxCollateral, _ := q.Keeper.maxCollateral(ctx, uDenom)

resp := types.QueryMarketSummaryResponse{
SymbolDenom: token.SymbolDenom,
Exponent: token.Exponent,
Expand All @@ -97,16 +99,16 @@ func (q Querier) MarketSummary(
Borrow_APY: borrowAPY,
Supplied: supplied.Amount,
Reserved: reserved,
Collateral: uCollateral,
Collateral: uCollateral.Amount,
Borrowed: borrowed.Amount,
Liquidity: balance.Sub(reserved),
MaximumBorrow: supplied.Amount, // TODO #1162 #1163 - implement limits
MaximumCollateral: uSupply.Amount, // TODO #1163 - implement limits
MinimumLiquidity: sdk.ZeroInt(), // TODO #1163 - implement limits
MaximumCollateral: maxCollateral,
MinimumLiquidity: sdk.ZeroInt(), // TODO #1163 - implement limits
UTokenSupply: uSupply.Amount,
AvailableBorrow: availableBorrow,
AvailableWithdraw: uSupply.Amount, // TODO #1163 - implement limits
AvailableCollateralize: uSupply.Amount.Sub(uCollateral), // TODO #1163 - implement limits
AvailableWithdraw: uSupply.Amount, // TODO #1163 - implement limits
AvailableCollateralize: sdk.MaxInt(maxCollateral.Sub(uCollateral.Amount), sdk.ZeroInt()),
}

// Oracle price in response will be nil if it is unavailable
Expand Down
29 changes: 17 additions & 12 deletions x/leverage/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,33 +274,38 @@ func (k Keeper) Repay(ctx sdk.Context, borrowerAddr sdk.AccAddress, payment sdk.
}

// Collateralize enables selected uTokens for use as collateral by a single borrower.
func (k Keeper) Collateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, coin sdk.Coin) error {
if err := k.validateCollateralize(ctx, coin); err != nil {
func (k Keeper) Collateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, uToken sdk.Coin) error {
if err := k.validateCollateralize(ctx, uToken); err != nil {
return err
}

currentCollateral := k.GetCollateralAmount(ctx, borrowerAddr, coin.Denom)
if err := k.setCollateralAmount(ctx, borrowerAddr, currentCollateral.Add(coin)); err != nil {
currentCollateral := k.GetCollateralAmount(ctx, borrowerAddr, uToken.Denom)
if err := k.setCollateralAmount(ctx, borrowerAddr, currentCollateral.Add(uToken)); err != nil {
return err
}

return k.bankKeeper.SendCoinsFromAccountToModule(ctx, borrowerAddr, types.ModuleName, sdk.NewCoins(coin))
err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, borrowerAddr, types.ModuleName, sdk.NewCoins(uToken))
if err != nil {
return err
}

return k.checkCollateralShare(ctx, uToken.Denom)
}

// Decollateralize disables selected uTokens for use as collateral by a single borrower.
func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, coin sdk.Coin) error {
if err := k.validateUToken(coin); err != nil {
func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, uToken sdk.Coin) error {
toteki marked this conversation as resolved.
Show resolved Hide resolved
if err := k.validateUToken(uToken); err != nil {
return err
}

// Detect where sufficient collateral exists to disable
collateral := k.GetBorrowerCollateral(ctx, borrowerAddr)
if collateral.AmountOf(coin.Denom).LT(coin.Amount) {
if collateral.AmountOf(uToken.Denom).LT(uToken.Amount) {
return types.ErrInsufficientCollateral
}

// Determine what borrow limit would be AFTER disabling this denom as collateral
newBorrowLimit, err := k.CalculateBorrowLimit(ctx, collateral.Sub(coin))
newBorrowLimit, err := k.CalculateBorrowLimit(ctx, collateral.Sub(uToken))
if err != nil {
return err
}
Expand All @@ -319,11 +324,11 @@ func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, co

// Disabling uTokens as collateral withdraws any stored collateral of the denom in question
// from the module account and returns it to the user
newCollateralAmount := collateral.AmountOf(coin.Denom).Sub(coin.Amount)
if err := k.setCollateralAmount(ctx, borrowerAddr, sdk.NewCoin(coin.Denom, newCollateralAmount)); err != nil {
newCollateralAmount := collateral.AmountOf(uToken.Denom).Sub(uToken.Amount)
if err := k.setCollateralAmount(ctx, borrowerAddr, sdk.NewCoin(uToken.Denom, newCollateralAmount)); err != nil {
return err
}
return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, borrowerAddr, sdk.NewCoins(coin))
return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, borrowerAddr, sdk.NewCoins(uToken))
}

// Liquidate attempts to repay one of an eligible borrower's borrows (in part or in full) in exchange for
Expand Down
36 changes: 36 additions & 0 deletions x/leverage/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ func (s *IntegrationTestSuite) TestCollateralize() {
supplier := s.newAccount(coin(umeeDenom, 200_000000))
s.supply(supplier, coin(umeeDenom, 100_000000))

// create and fund another supplier
otherSupplier := s.newAccount(coin(umeeDenom, 200_000000), coin(atomDenom, 200_000000))
s.supply(otherSupplier, coin(umeeDenom, 200_000000), coin(atomDenom, 200_000000))

tcs := []testCase{
{
"base token",
Expand Down Expand Up @@ -936,3 +940,35 @@ func (s *IntegrationTestSuite) TestLiquidate() {
}
}
}

func (s *IntegrationTestSuite) TestMaxCollateralShare() {
app, ctx, require := s.app, s.ctx, s.Require()

// update initial ATOM to have a limited MaxCollateralShare
atom, err := app.LeverageKeeper.GetTokenSettings(ctx, atomDenom)
require.NoError(err)
atom.MaxCollateralShare = sdk.MustNewDecFromStr("0.1")
s.registerToken(atom)

// Mock oracle prices:
// UMEE $4.21
// ATOM $39.38

// create a supplier to collateralize 100 UMEE, worth $421.00
umeeSupplier := s.newAccount(coin(umeeDenom, 100_000000))
s.supply(umeeSupplier, coin(umeeDenom, 100_000000))
s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100_000000))

// create an ATOM supplier
atomSupplier := s.newAccount(coin(atomDenom, 100_000000))
s.supply(atomSupplier, coin(atomDenom, 100_000000))

// collateralize 1.18 ATOM, worth $46.46, with no error.
// total collateral value (across all denoms) will be $467.46
// so ATOM's collateral share ($46.46 / $467.46) is barely below 10%
s.collateralize(atomSupplier, coin("u/"+atomDenom, 1_180000))

// attempt to collateralize another 0.01 ATOM, which would result in too much collateral share for ATOM
err = app.LeverageKeeper.Collateralize(ctx, atomSupplier, coin("u/"+atomDenom, 10000))
require.ErrorIs(err, types.ErrMaxCollateralShare)
}
52 changes: 52 additions & 0 deletions x/leverage/keeper/limits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package keeper

import (
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/umee-network/umee/v3/x/leverage/types"
)

// maxCollateral calculates the maximum amount of collateral a utoken denom
// is allowed to have, taking into account its associated token's MaxCollateralShare
// under current market conditions
func (k *Keeper) maxCollateral(ctx sdk.Context, denom string) (sdkmath.Int, error) {
token, err := k.GetTokenSettings(ctx, types.ToTokenDenom(denom))
if err != nil {
return sdk.ZeroInt(), err
}

// if a token's max collateral share is zero, max collateral is zero
if token.MaxCollateralShare.LTE(sdk.ZeroDec()) {
return sdk.ZeroInt(), nil
}

// if a token's max collateral share is 100%, max collateral is its uToken supply
if token.MaxCollateralShare.GTE(sdk.OneDec()) {
return k.GetUTokenSupply(ctx, denom).Amount, nil
}

// if a token's max collateral share is less than 100%, additional restrictions apply
systemCollateral := k.GetAllTotalCollateral(ctx)
thisDenomCollateral := sdk.NewCoin(denom, systemCollateral.AmountOf(denom))

// get USD collateral value for all other denoms
otherDenomsValue, err := k.CalculateCollateralValue(ctx, systemCollateral.Sub(thisDenomCollateral))
if err != nil {
return sdk.ZeroInt(), err
}

// determine the max USD value this uToken's collateral is allowed to have by MaxCollateralShare
maxValue := otherDenomsValue.Quo(sdk.OneDec().Sub(token.MaxCollateralShare)).Mul(token.MaxCollateralShare)

// determine the amount of uTokens which would be required to reach maxValue
tokenDenom := types.ToTokenDenom(denom)
tokenPrice, err := k.TokenPrice(ctx, tokenDenom)
if err != nil {
return sdk.ZeroInt(), err
}
uTokenExchangeRate := k.DeriveExchangeRate(ctx, tokenDenom)
maxCollateralAmount := maxValue.Quo(tokenPrice).Quo(uTokenExchangeRate).TruncateInt()

// return the computed maximum or the current uToken supply, whichever is smaller
return sdk.MinInt(k.GetUTokenSupply(ctx, denom).Amount, maxCollateralAmount), nil
}
5 changes: 5 additions & 0 deletions x/leverage/keeper/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ func coin(denom string, amount int64) sdk.Coin {
return sdk.NewInt64Coin(denom, amount)
}

// registerToken adds or updates a token in the token registry and requires no error.
func (s *IntegrationTestSuite) registerToken(token types.Token) {
s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, token))
}

// newAccount creates a new account for testing, and funds it with any input tokens.
func (s *IntegrationTestSuite) newAccount(funds ...sdk.Coin) sdk.AccAddress {
app, ctx := s.app, s.ctx
Expand Down