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: x/metoken end block functions #2167

Merged
merged 21 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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 x/metoken/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type LeverageKeeper interface {
Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, 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
}
17 changes: 16 additions & 1 deletion x/metoken/keeper/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func (k Keeper) IndexBalances(meTokenDenom string) (metoken.IndexBalances, error
return *balance, nil
}

// setIndexBalances saves an Index's Balance
// setIndexBalances saves an Index's Balance.
func (k Keeper) setIndexBalances(balance metoken.IndexBalances) error {
if err := balance.Validate(); err != nil {
return err
Expand All @@ -30,3 +30,18 @@ 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
}
65 changes: 65 additions & 0 deletions x/metoken/keeper/interest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package keeper

import (
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"

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

// ClaimInterest sends accrued interest from x/leverage module to x/metoken account.
func (k Keeper) ClaimInterest() error {
interestClaimTime, err := k.getNextInterestClaimTime()
if err != nil {
return err
}

if k.ctx.BlockTime().After(interestClaimTime) {
leverageLiquidity, err := k.leverageKeeper.GetAllSupplied(*k.ctx,
authtypes.NewModuleAddress(metoken.ModuleName))
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 {
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
}
133 changes: 133 additions & 0 deletions x/metoken/keeper/intest/interest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package keeper_test

import (
"testing"
"time"

"github.com/stretchr/testify/require"

sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"

"github.com/umee-network/umee/v5/app"
"github.com/umee-network/umee/v5/util/coin"
"github.com/umee-network/umee/v5/x/metoken"
"github.com/umee-network/umee/v5/x/metoken/mocks"
)

func TestInterestClaiming(t *testing.T) {
index := mocks.StableIndex(mocks.MeUSDDenom)

s := initKeeperTestSuite(t, nil, nil)
msgServer, ctx, app := s.msgServer, s.ctx, s.app

_, err := msgServer.GovUpdateRegistry(
ctx, &metoken.MsgGovUpdateRegistry{
Authority: app.GovKeeper.GetGovernanceAccount(s.ctx).GetAddress().String(),
AddIndex: []metoken.Index{index},
UpdateIndex: nil,
},
)
require.NoError(t, err)

// create and fund a user with 1000 USDT, 1000 USDC and 1000 IST
user := s.newAccount(
t,
coin.New(mocks.USDTBaseDenom, 1000_000000),
coin.New(mocks.USDCBaseDenom, 1000_000000),
coin.New(mocks.ISTBaseDenom, 1000_000000),
)

// create a borrower with 10000 USDT
borrower := s.newAccount(t, coin.New(mocks.USDTBaseDenom, 10000_000000))

// swap 1000 USDT, 1000 USDC and 1000 IST to have an initial meUSD balance
swaps := []*metoken.MsgSwap{
{
User: user.String(),
Asset: sdk.NewCoin(mocks.USDTBaseDenom, sdkmath.NewInt(1000_000000)),
MetokenDenom: mocks.MeUSDDenom,
},
{
User: user.String(),
Asset: sdk.NewCoin(mocks.USDCBaseDenom, sdkmath.NewInt(1000_000000)),
MetokenDenom: mocks.MeUSDDenom,
},
{
User: user.String(),
Asset: sdk.NewCoin(mocks.ISTBaseDenom, sdkmath.NewInt(1000_000000)),
MetokenDenom: mocks.MeUSDDenom,
},
}

for _, swap := range swaps {
_, err := msgServer.Swap(ctx, swap)
require.NoError(t, err)
}

// supply liquidity from borrower and collateralize
borrowerSupply := sdk.NewCoin(mocks.USDTBaseDenom, sdkmath.NewInt(10000_000000))
_, err = app.LeverageKeeper.Supply(
ctx,
borrower,
borrowerSupply,
)
require.NoError(t, err)

uTokens, err := app.LeverageKeeper.ExchangeToken(ctx, borrowerSupply)
require.NoError(t, err)

err = app.LeverageKeeper.Collateralize(ctx, borrower, uTokens)
require.NoError(t, err)

// borrow 110 USDT, 200 USDC and 350 IST
err = app.LeverageKeeper.Borrow(ctx, borrower, sdk.NewCoin(mocks.USDTBaseDenom, sdkmath.NewInt(110_000000)))
require.NoError(t, err)
err = app.LeverageKeeper.Borrow(ctx, borrower, sdk.NewCoin(mocks.USDCBaseDenom, sdkmath.NewInt(200_000000)))
require.NoError(t, err)
err = app.LeverageKeeper.Borrow(ctx, borrower, sdk.NewCoin(mocks.ISTBaseDenom, sdkmath.NewInt(350_000000)))
require.NoError(t, err)

// confirm meToken account didn't receive any interest yet and the balances in meToken state and x/bank module
// are the same
checkInterest(t, ctx, app, index.Denom, true)

// create ctx in the future and generate accrued interest
err = app.LeverageKeeper.AccrueAllInterest(ctx)
require.NoError(t, err)
futureCtx := ctx.WithBlockTime(time.Now().Add(240 * time.Hour))
err = app.LeverageKeeper.AccrueAllInterest(futureCtx)
require.NoError(t, err)

err = app.MetokenKeeperB.Keeper(&ctx).ClaimInterest()
require.NoError(t, err)

// confirm meToken account received accrued interest and the balances in meToken state and x/bank module
// are the same
checkInterest(t, ctx, app, index.Denom, false)
}

func checkInterest(
t *testing.T,
ctx sdk.Context,
app *app.UmeeApp,
meTokenDenom string,
zeroInterest bool,
) {
k := app.MetokenKeeperB.Keeper(&ctx)
metokenBalances, err := k.IndexBalances(meTokenDenom)
require.NoError(t, err)
meTokenAddr := authtypes.NewModuleAddress(metoken.ModuleName)
bankBalance := app.BankKeeper.GetAllBalances(ctx, meTokenAddr)

for _, balance := range metokenBalances.AssetBalances {
// check interest
require.Equal(t, zeroInterest, balance.Interest.IsZero())

// confirm balance in x/bank and x/module state is the same
found, bBalance := bankBalance.Find(balance.Denom)
require.True(t, found)
require.True(t, bBalance.Amount.Equal(balance.Interest.Add(balance.Reserved).Add(balance.Fees)))
}
}
3 changes: 2 additions & 1 deletion x/metoken/keeper/intest/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper_test
import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -39,7 +40,7 @@ func initKeeperTestSuite(t *testing.T, registry []metoken.Index, balances []meto
ChainID: fmt.Sprintf("test-chain-%s", tmrand.Str(4)),
Height: 9,
},
)
).WithBlockTime(time.Now())

oracleMock := mocks.NewMockOracleKeeper()
oracleMock.AllMedianPricesFunc.SetDefaultHook(mocks.ValidPricesFunc())
Expand Down
Loading