Skip to content

Commit

Permalink
feat: x/metoken end block functions (#2167)
Browse files Browse the repository at this point in the history
* reserves and interest refeactored

* tests adapted (not working yet)

* endblock funcs

* Update x/metoken/keeper/redeem.go

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

* adam comments

* fix mocks

* safeguard for transferring dust

* robert comments

* fix

* safer exit when asset not found

* imports

* feat: add SupplyFromModule and WithdrawFromModule to leverage (#2170)

* add module-based supply and withdraw to leverage module

* cl++

* change return order

* godoc++ and function order

* mocks

* mocks

---------

Co-authored-by: Egor Kostetskiy <kosegor@gmail.com>

* new integration with x/leverage

* fix after merge from main

* unit mocks

* fix

* better logs

* couple more comments

---------

Co-authored-by: Adam Moser <63419657+toteki@users.noreply.github.com>
  • Loading branch information
kosegor and toteki authored Jul 27, 2023
1 parent e8bcfca commit 70e8310
Show file tree
Hide file tree
Showing 21 changed files with 931 additions and 179 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- [2159](https://github.com/umee-network/umee/pull/2159) Add hard market cap for token emission.
- [2155](https://github.com/umee-network/umee/pull/2155) `bpmath`: basis points math package.
- [2166](https://github.com/umee-network/umee/pull/2166) Basis Points: `MulDec`
- [2170](https://github.com/umee-network/umee/pull/2170) Add SupplyFromModule and WithdrawToModule to leverage keeper.

### Improvements

Expand Down
108 changes: 92 additions & 16 deletions x/leverage/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (

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

type Keeper struct {
Expand Down Expand Up @@ -85,6 +84,7 @@ func (k Keeper) ModuleBalance(ctx sdk.Context, denom string) sdk.Coin {
// Supply attempts to deposit assets into the leverage module account in
// exchange for uTokens. If asset type is invalid or account balance is
// insufficient, we return an error. Returns the amount of uTokens minted.
// Note: For supplying from a module account instead of a user, use SupplyFromModule.
func (k Keeper) Supply(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk.Coin) (sdk.Coin, error) {
if err := k.validateSupply(ctx, coin); err != nil {
return sdk.Coin{}, err
Expand Down Expand Up @@ -112,18 +112,52 @@ func (k Keeper) Supply(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk.Co
}

// The uTokens are sent to supplier address
// Only base accounts and x/metoken module account are supported.
if supplierAddr.Equals(k.meTokenAddr) {
err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, metoken.ModuleName, uTokens)
} else {
err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, supplierAddr, uTokens)
if err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, supplierAddr, uTokens); err != nil {
return sdk.Coin{}, err
}

return uToken, nil
}

// SupplyFromModule attempts to deposit assets into the leverage module account in
// exchange for uTokens on behalf of another module. In addition to the regular error
// return, also returns a boolean which indicates whether the error was recoverable.
// A recoverable = true error means SupplyFromModule was aborted without harming state.
func (k Keeper) SupplyFromModule(ctx sdk.Context, fromModule string, coin sdk.Coin) (sdk.Coin, bool, error) {
if err := k.validateSupply(ctx, coin); err != nil {
return sdk.Coin{}, true, err
}

// determine uToken amount to mint
uToken, err := k.ExchangeToken(ctx, coin)
if err != nil {
return sdk.Coin{}, err
return sdk.Coin{}, true, err
}

return uToken, nil
// All errors past this point are considered non-recoverable

// send token balance to leverage module account
err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, fromModule, types.ModuleName, sdk.NewCoins(coin))
if err != nil {
return sdk.Coin{}, false, err
}

// mint uToken and set new total uToken supply
uTokens := sdk.NewCoins(uToken)
if err = k.bankKeeper.MintCoins(ctx, types.ModuleName, uTokens); err != nil {
return sdk.Coin{}, false, err
}
if err = k.setUTokenSupply(ctx, k.GetUTokenSupply(ctx, uToken.Denom).Add(uToken)); err != nil {
return sdk.Coin{}, false, err
}

// The uTokens are sent to supplier module
if err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, fromModule, uTokens); err != nil {
return sdk.Coin{}, false, err
}

// On nil error, recoverable is set to true
return uToken, true, nil
}

// Withdraw attempts to redeem uTokens from the leverage module in exchange for base tokens.
Expand All @@ -133,6 +167,7 @@ func (k Keeper) Supply(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk.Co
// This function does NOT check that a borrower remains under their borrow limit or that
// collateral liquidity remains healthy - those assertions have been moved to MsgServer.
// Returns a boolean which is true if some or all of the withdrawn uTokens were from collateral.
// Note: For withdrawing to a module account instead of a user, use WithdrawToModule.
func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sdk.Coin) (sdk.Coin, bool, error) {
isFromCollateral := false

Expand Down Expand Up @@ -193,15 +228,8 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sd
}

// send the base assets to supplier
// Only base accounts and x/metoken module account are supported.
tokens := sdk.NewCoins(token)
if supplierAddr.Equals(k.meTokenAddr) {
err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, metoken.ModuleName, tokens)
} else {
err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, supplierAddr, tokens)
}

if err != nil {
if err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, supplierAddr, tokens); err != nil {
return sdk.Coin{}, isFromCollateral, err
}

Expand All @@ -216,6 +244,54 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sd
return token, isFromCollateral, nil
}

// WithdrawToModule attempts to redeem uTokens from the leverage module in exchange for base tokens.
// This is done on behalf of another module, not by a user account. Modules do not have collateral.
// If the uToken denom is invalid or balances are insufficient to withdraw the amount requested,
// returns an error. Returns the amount of base tokens received. In addition to the regular error
// return, also returns a boolean which indicates whether the error was recoverable.
// A recoverable = true error means WithdrawToModule was aborted without harming state.
func (k Keeper) WithdrawToModule(ctx sdk.Context, toModule string, uToken sdk.Coin) (sdk.Coin, bool, error) {
if err := validateUToken(uToken); err != nil {
return sdk.Coin{}, true, err
}

// calculate base asset amount to withdraw
token, err := k.ExchangeUToken(ctx, uToken)
if err != nil {
return sdk.Coin{}, true, err
}

// Ensure leverage module account has sufficient unreserved tokens to withdraw
availableAmount := k.AvailableLiquidity(ctx, token.Denom)
if token.Amount.GT(availableAmount) {
return sdk.Coin{}, true, types.ErrLendingPoolInsufficient.Wrap(token.String())
}

// All errors past this point are considered non-recoverable

// transfer uTokens to the leverage module account
if err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, toModule, types.ModuleName, sdk.NewCoins(uToken)); err != nil {
return sdk.Coin{}, false, err
}

// send the base assets to withdrawing module
tokens := sdk.NewCoins(token)
if err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, toModule, tokens); err != nil {
return sdk.Coin{}, false, err
}

// burn the uTokens and set the new total uToken supply
if err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(uToken)); err != nil {
return sdk.Coin{}, false, err
}
if err = k.setUTokenSupply(ctx, k.GetUTokenSupply(ctx, uToken.Denom).Sub(uToken)); err != nil {
return sdk.Coin{}, false, err
}

// On nil error, recoverable is set to true
return token, true, nil
}

// Borrow attempts to borrow tokens from the leverage module account using
// collateral uTokens. If asset type is invalid, or module balance is insufficient,
// we return an error.
Expand Down
17 changes: 17 additions & 0 deletions x/metoken/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package errors

type LeverageError struct {
Err error
IsRecoverable bool
}

func (le *LeverageError) Error() string {
return le.Err.Error()
}

func Wrap(err error, isRecoverable bool) *LeverageError {
return &LeverageError{
Err: err,
IsRecoverable: isRecoverable,
}
}
5 changes: 3 additions & 2 deletions x/metoken/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ type LeverageKeeper interface {
GetTokenSettings(ctx sdk.Context, denom string) (ltypes.Token, error)
ExchangeToken(ctx sdk.Context, token sdk.Coin) (sdk.Coin, error)
ExchangeUToken(ctx sdk.Context, uToken sdk.Coin) (sdk.Coin, error)
Supply(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk.Coin) (sdk.Coin, error)
Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sdk.Coin) (sdk.Coin, bool, error)
SupplyFromModule(ctx sdk.Context, fromModule string, coin sdk.Coin) (sdk.Coin, bool, error)
WithdrawToModule(ctx sdk.Context, toModule string, uToken sdk.Coin) (sdk.Coin, bool, error)
ModuleMaxWithdraw(ctx sdk.Context, spendableUTokens sdk.Coin) (sdkmath.Int, error)
GetTotalSupply(ctx sdk.Context, denom string) (sdk.Coin, error)
GetAllSupplied(ctx sdk.Context, supplierAddr sdk.AccAddress) (sdk.Coins, error)
}

// OracleKeeper interface for price feed.
Expand Down
3 changes: 1 addition & 2 deletions x/metoken/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,9 @@ func (i Index) HasAcceptedAsset(denom string) bool {
// SetAcceptedAsset overrides an accepted asset if exists in the list, otherwise add it to the list.
func (i *Index) SetAcceptedAsset(acceptedAsset AcceptedAsset) {
index, _ := i.AcceptedAsset(acceptedAsset.Denom)
if index > 0 {
if index < 0 {
i.AcceptedAssets = append(i.AcceptedAssets, acceptedAsset)
return
}

i.AcceptedAssets[index] = acceptedAsset
}
29 changes: 28 additions & 1 deletion x/metoken/index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package metoken
import (
"testing"

"gotest.tools/v3/assert"

sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"gotest.tools/v3/assert"
)

func TestIndex_Validate(t *testing.T) {
Expand Down Expand Up @@ -92,6 +93,32 @@ func TestIndex_Validate(t *testing.T) {
}
}

func TestIndex_Update(t *testing.T) {
existingAsset := "USDT"
newAsset := "IST"
index := validIndex()
assert.Check(t, len(index.AcceptedAssets) == 1)

i, _ := index.AcceptedAsset(existingAsset)
assert.Check(t, i >= 0)

i, _ = index.AcceptedAsset(newAsset)
assert.Check(t, i == -1)

newAcceptedAsset := validAcceptedAsset(newAsset)
index.SetAcceptedAsset(newAcceptedAsset)
assert.Check(t, len(index.AcceptedAssets) == 2)

assert.Check(t, index.HasAcceptedAsset(newAsset))

newAcceptedAsset.ReservePortion = sdk.MustNewDecFromStr("0.5")
index.SetAcceptedAsset(newAcceptedAsset)

i, asset := index.AcceptedAsset(newAcceptedAsset.Denom)
assert.Check(t, i >= 0)
assert.Check(t, sdk.MustNewDecFromStr("0.5").Equal(asset.ReservePortion))
}

func TestFee_Validate(t *testing.T) {
invalidMinFee := validFee()
invalidMinFee.MinFee = sdk.MustNewDecFromStr("1.01")
Expand Down
18 changes: 16 additions & 2 deletions x/metoken/keeper/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/umee-network/umee/v5/util/store"
"github.com/umee-network/umee/v5/x/metoken"
)
Expand All @@ -16,7 +17,6 @@ func (k Keeper) IndexBalances(meTokenDenom string) (metoken.IndexBalances, error
return *balance, nil
}

// setIndexBalances saves an Index's Balance
func (k Keeper) setIndexBalances(balance metoken.IndexBalances) error {
if err := balance.Validate(); err != nil {
return err
Expand All @@ -25,8 +25,22 @@ func (k Keeper) setIndexBalances(balance metoken.IndexBalances) error {
return store.SetValue(k.store, keyBalance(balance.MetokenSupply.Denom), &balance, "balance")
}

// hasIndexBalance returns true when Index exists.
func (k Keeper) hasIndexBalance(meTokenDenom string) bool {
balance := store.GetValue[*metoken.IndexBalances](k.store, keyBalance(meTokenDenom), "balance")
return balance != nil
}

// updateBalances of the assets of an Index and save them.
func (k Keeper) updateBalances(balances metoken.IndexBalances, updatedBalances []metoken.AssetBalance) error {
if len(updatedBalances) > 0 {
for _, balance := range updatedBalances {
balances.SetAssetBalance(balance)
}
err := k.setIndexBalances(balances)
if err != nil {
return err
}
}

return nil
}
72 changes: 72 additions & 0 deletions x/metoken/keeper/interest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package keeper

import (
"errors"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/umee-network/umee/v5/x/metoken"
lerrors "github.com/umee-network/umee/v5/x/metoken/errors"
)

// ClaimLeverageInterest sends accrued interest from x/leverage module to x/metoken account.
func (k Keeper) ClaimLeverageInterest() error {
if k.ctx.BlockTime().Before(k.getNextInterestClaimTime()) {
return nil
}

leverageLiquidity, err := k.leverageKeeper.GetAllSupplied(*k.ctx, ModuleAddr())
if err != nil {
return err
}

indexes := k.GetAllRegisteredIndexes()
for _, index := range indexes {
balances, err := k.IndexBalances(index.Denom)
if err != nil {
return err
}

updatedBalances := make([]metoken.AssetBalance, 0)
for _, balance := range balances.AssetBalances {
if balance.Leveraged.IsPositive() {
found, liquidity := leverageLiquidity.Find(balance.Denom)
if !found {
continue
}

// If there is more liquidity in x/leverage than expected, we claim the delta,
// which is the accrued interest
if liquidity.Amount.GT(balance.Leveraged) {
accruedInterest := sdk.NewCoin(balance.Denom, liquidity.Amount.Sub(balance.Leveraged))
tokensWithdrawn, err := k.withdrawFromLeverage(accruedInterest)
if err != nil {
var leverageError *lerrors.LeverageError
if errors.As(err, &leverageError) && leverageError.IsRecoverable {
k.Logger().Debug(
"claiming interest: couldn't withdraw from leverage",
"error", err.Error(),
"block_time", k.ctx.BlockTime(),
)
continue
}

return err
}

balance.Interest = balance.Interest.Add(tokensWithdrawn.Amount)
updatedBalances = append(updatedBalances, balance)
}
}
}

if err = k.updateBalances(balances, updatedBalances); err != nil {
return err
}
}

k.setNextInterestClaimTime(k.ctx.BlockTime().Add(time.Duration(k.GetParams().ClaimingFrequency) * time.Second))

return nil
}
Loading

0 comments on commit 70e8310

Please sign in to comment.