diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fca5ab8c5..cf6bfcf682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,11 +51,13 @@ Ref: https://keepachangelog.com/en/1.0.0/ - [1963](https://github.com/umee-network/umee/pull/1963) ICA Host integration. - [1953](https://github.com/umee-network/umee/pull/1953) IBC: accept only inflow of leverage registered tokens - [1967](https://github.com/umee-network/umee/pull/1967) Gravity Bridge phase out phase-2: disable Umee -> Ethereum transfers. +- [1967](https://github.com/umee-network/umee/pull/1967) Gravity Bridge phase out phase-2: disable Umee -> Ethereum transfers. ### Improvements - [1959](https://github.com/umee-network/umee/pull/1959) Update IBC to v6.1 - [1913](https://github.com/umee-network/umee/pull/1913), [1974](https://github.com/umee-network/umee/pull/1974) uibc: quota status check. +- [1973](https://github.com/umee-network/umee/pull/1973) UIBC: handle zero Quota Params. ### Fixes diff --git a/x/uibc/ics20/keeper/keeper.go b/x/uibc/ics20/keeper/keeper.go index b25699d28b..5024d7fa4f 100644 --- a/x/uibc/ics20/keeper/keeper.go +++ b/x/uibc/ics20/keeper/keeper.go @@ -57,7 +57,6 @@ func (k Keeper) Transfer(goCtx context.Context, msg *ibctransfertypes.MsgTransfe } return resp, err - } // OnRecvPacket delegates the OnRecvPacket call to the embedded ICS-20 transfer diff --git a/x/uibc/quota/keeper/genesis.go b/x/uibc/quota/keeper/genesis.go index 36af12af2d..faca2a086c 100644 --- a/x/uibc/quota/keeper/genesis.go +++ b/x/uibc/quota/keeper/genesis.go @@ -12,7 +12,7 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState uibc.GenesisState) { err := k.SetParams(ctx, genState.Params) util.Panic(err) - k.SetOutflows(ctx, genState.Outflows) + k.SetTokenOutflows(ctx, genState.Outflows) k.SetTotalOutflowSum(ctx, genState.TotalOutflowSum) err = k.SetExpire(ctx, genState.QuotaExpires) diff --git a/x/uibc/quota/keeper/grpc_query.go b/x/uibc/quota/keeper/grpc_query.go index ac22d192ec..17be0b846d 100644 --- a/x/uibc/quota/keeper/grpc_query.go +++ b/x/uibc/quota/keeper/grpc_query.go @@ -38,7 +38,7 @@ func (q Querier) Outflows(goCtx context.Context, req *uibc.QueryOutflows) ( if len(req.Denom) == 0 { o = q.GetTotalOutflow(ctx) } else { - d, err := q.GetOutflows(ctx, req.Denom) + d, err := q.GetTokenOutflows(ctx, req.Denom) if err != nil { return nil, err } diff --git a/x/uibc/quota/keeper/keeper_test.go b/x/uibc/quota/keeper/keeper_test.go index 8a9e69ff2f..da54690237 100644 --- a/x/uibc/quota/keeper/keeper_test.go +++ b/x/uibc/quota/keeper/keeper_test.go @@ -6,7 +6,6 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/simapp" - storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" "github.com/cosmos/cosmos-sdk/x/staking" @@ -18,7 +17,6 @@ import ( umeeapp "github.com/umee-network/umee/v4/app" appparams "github.com/umee-network/umee/v4/app/params" - "github.com/umee-network/umee/v4/tests/tsdk" "github.com/umee-network/umee/v4/x/uibc" "github.com/umee-network/umee/v4/x/uibc/quota/keeper" ) @@ -88,11 +86,3 @@ func initKeeperTestSuite(t *testing.T) *KeeperTestSuite { return s } - -// creates keeper without external dependencies (app, leverage etc...) -func initSimpleKeeper(t *testing.T) (sdk.Context, keeper.Keeper) { - storeKey := storetypes.NewMemoryStoreKey("quota") - k := keeper.NewKeeper(nil, storeKey, nil, nil, nil) - ctx, _ := tsdk.NewCtxOneStore(t, storeKey) - return ctx, k -} diff --git a/x/uibc/quota/keeper/mocks_test.go b/x/uibc/quota/keeper/mocks_test.go new file mode 100644 index 0000000000..34b6303d1c --- /dev/null +++ b/x/uibc/quota/keeper/mocks_test.go @@ -0,0 +1,56 @@ +package keeper + +import ( + "errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + + ltypes "github.com/umee-network/umee/v4/x/leverage/types" +) + +type LeverageKeeper struct { + tokenSettings map[string]ltypes.Token +} + +func (k LeverageKeeper) GetTokenSettings(ctx sdk.Context, baseDenom string) (ltypes.Token, error) { + ts, ok := k.tokenSettings[baseDenom] + if !ok { + return ts, errors.New("token settings not found") + } + return ts, nil +} +func (k LeverageKeeper) ExchangeUToken(ctx sdk.Context, uToken sdk.Coin) (sdk.Coin, error) { + panic("not implemented") +} +func (k LeverageKeeper) DeriveExchangeRate(ctx sdk.Context, denom string) sdk.Dec { + panic("not implemented") +} + +func NewLeverageKeeperMock(denoms ...string) LeverageKeeper { + tokenSettings := map[string]ltypes.Token{} + for _, d := range denoms { + tokenSettings[d] = ltypes.Token{ + BaseDenom: d, + SymbolDenom: d, + } + } + return LeverageKeeper{tokenSettings: tokenSettings} +} + +type Oracle struct { + prices map[string]sdk.Dec +} + +func (o Oracle) Price(ctx sdk.Context, denom string) (sdk.Dec, error) { + p, ok := o.prices[denom] + if !ok { + return p, ltypes.ErrNotRegisteredToken.Wrap(denom) + } + return p, nil +} + +func NewOracleMock(denom string, price sdk.Dec) Oracle { + prices := map[string]sdk.Dec{} + prices[denom] = price + return Oracle{prices: prices} +} diff --git a/x/uibc/quota/keeper/quota.go b/x/uibc/quota/keeper/quota.go index 3ea9f9aad5..a087b309f1 100644 --- a/x/uibc/quota/keeper/quota.go +++ b/x/uibc/quota/keeper/quota.go @@ -15,7 +15,7 @@ import ( var ten = sdk.MustNewDecFromStr("10") -// GetAllOutflows returns outflows of all tokens. +// GetAllOutflows returns sum of outflows of all tokens in USD value. func (k Keeper) GetAllOutflows(ctx sdk.Context) (sdk.DecCoins, error) { var outflows sdk.DecCoins store := k.PrefixStore(&ctx, uibc.KeyPrefixDenomOutflows) @@ -33,29 +33,30 @@ func (k Keeper) GetAllOutflows(ctx sdk.Context) (sdk.DecCoins, error) { return outflows, nil } -// GetOutflows retunes the rate limits of ibc denom. -func (k Keeper) GetOutflows(ctx sdk.Context, ibcDenom string) (sdk.DecCoin, error) { +// GetTokenOutflows returns sum of denom outflows in USD value in the DecCoin structure. +func (k Keeper) GetTokenOutflows(ctx sdk.Context, denom string) (sdk.DecCoin, error) { store := ctx.KVStore(k.storeKey) - bz := store.Get(uibc.KeyTotalOutflows(ibcDenom)) + bz := store.Get(uibc.KeyTotalOutflows(denom)) if bz == nil { - return coin.ZeroDec(ibcDenom), nil + return coin.ZeroDec(denom), nil } var d sdk.Dec err := d.Unmarshal(bz) - return sdk.NewDecCoinFromDec(ibcDenom, d), err + return sdk.NewDecCoinFromDec(denom, d), err } -// SetOutflows save the updated IBC outflows of by provided tokens. -func (k Keeper) SetOutflows(ctx sdk.Context, outflows sdk.DecCoins) { +// SetTokenOutflows saves provided updated IBC outflows as a pair: USD value, denom name in the +// DecCoin structure. +func (k Keeper) SetTokenOutflows(ctx sdk.Context, outflows sdk.DecCoins) { for _, q := range outflows { - k.SetDenomOutflow(ctx, q) + k.SetTokenOutflow(ctx, q) } } -// SetDenomOutflow save the outflows of denom into store. -func (k Keeper) SetDenomOutflow(ctx sdk.Context, outflow sdk.DecCoin) { +// SetTokenOutflow save the outflows of denom into store. +func (k Keeper) SetTokenOutflow(ctx sdk.Context, outflow sdk.DecCoin) { store := ctx.KVStore(k.storeKey) key := uibc.KeyTotalOutflows(outflow.Denom) bz, err := outflow.Amount.Marshal() @@ -131,12 +132,6 @@ func (k Keeper) ResetAllQuotas(ctx sdk.Context) error { // updates the current quota metrics. func (k Keeper) CheckAndUpdateQuota(ctx sdk.Context, denom string, newOutflow sdkmath.Int) error { params := k.GetParams(ctx) - - o, err := k.GetOutflows(ctx, denom) - if err != nil { - return err - } - exchangePrice, err := k.getExchangePrice(ctx, denom, newOutflow) if err != nil { if ltypes.ErrNotRegisteredToken.Is(err) { @@ -146,17 +141,21 @@ func (k Keeper) CheckAndUpdateQuota(ctx sdk.Context, denom string, newOutflow sd } } + o, err := k.GetTokenOutflows(ctx, denom) + if err != nil { + return err + } o.Amount = o.Amount.Add(exchangePrice) - if o.Amount.GT(params.TokenQuota) { + if !params.TokenQuota.IsZero() && o.Amount.GT(params.TokenQuota) { return uibc.ErrQuotaExceeded } totalOutflowSum := k.GetTotalOutflow(ctx).Add(exchangePrice) - if totalOutflowSum.GT(params.TotalQuota) { + if !params.TotalQuota.IsZero() && totalOutflowSum.GT(params.TotalQuota) { return uibc.ErrQuotaExceeded } - k.SetDenomOutflow(ctx, o) + k.SetTokenOutflow(ctx, o) k.SetTotalOutflowSum(ctx, totalOutflowSum) return nil } @@ -193,7 +192,7 @@ func (k Keeper) getExchangePrice(ctx sdk.Context, denom string, amount sdkmath.I // UndoUpdateQuota subtracts `amount` from quota metric of the ibc denom. func (k Keeper) UndoUpdateQuota(ctx sdk.Context, denom string, amount sdkmath.Int) error { - o, err := k.GetOutflows(ctx, denom) + o, err := k.GetTokenOutflows(ctx, denom) if err != nil { return err } @@ -215,7 +214,7 @@ func (k Keeper) UndoUpdateQuota(ctx sdk.Context, denom string, amount sdkmath.In return nil } - k.SetDenomOutflow(ctx, o) + k.SetTokenOutflow(ctx, o) totalOutflowSum := k.GetTotalOutflow(ctx) k.SetTotalOutflowSum(ctx, totalOutflowSum.Sub(exchangePrice)) diff --git a/x/uibc/quota/keeper/quota_test.go b/x/uibc/quota/keeper/quota_test.go index 21ce5335b1..2d96bd3d18 100644 --- a/x/uibc/quota/keeper/quota_test.go +++ b/x/uibc/quota/keeper/quota_test.go @@ -5,42 +5,15 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "gotest.tools/v3/assert" - - ibcutil "github.com/umee-network/umee/v4/util/ibc" ) -func TestGetQuotas(t *testing.T) { - ctx, k := initSimpleKeeper(t) - - quotas, err := k.GetAllOutflows(ctx) - assert.NilError(t, err) - assert.Equal(t, len(quotas), 0) - - setQuotas := sdk.DecCoins{sdk.NewInt64DecCoin("test_uumee", 10000)} - - k.SetOutflows(ctx, setQuotas) - quotas, err = k.GetAllOutflows(ctx) - assert.NilError(t, err) - assert.DeepEqual(t, setQuotas, quotas) - - // get the quota of denom - quota, err := k.GetOutflows(ctx, setQuotas[0].Denom) - assert.NilError(t, err) - assert.Equal(t, quota.Denom, setQuotas[0].Denom) -} - -func TestGetLocalDenom(t *testing.T) { - out := ibcutil.GetLocalDenom("umee") - assert.Equal(t, "umee", out) -} - func TestResetQuota(t *testing.T) { s := initKeeperTestSuite(t) ctx, k := s.ctx, s.app.UIbcQuotaKeeper umeeQuota := sdk.NewInt64DecCoin("uumee", 1000) - k.SetDenomOutflow(ctx, umeeQuota) - q, err := k.GetOutflows(ctx, umeeQuota.Denom) + k.SetTokenOutflow(ctx, umeeQuota) + q, err := k.GetTokenOutflows(ctx, umeeQuota.Denom) assert.NilError(t, err) assert.DeepEqual(t, q, umeeQuota) @@ -48,7 +21,7 @@ func TestResetQuota(t *testing.T) { k.ResetAllQuotas(ctx) // check the quota after reset - q, err = k.GetOutflows(ctx, umeeQuota.Denom) + q, err = k.GetTokenOutflows(ctx, umeeQuota.Denom) assert.NilError(t, err) assert.DeepEqual(t, q.Amount, sdk.NewDec(0)) } diff --git a/x/uibc/quota/keeper/quota_unit_test.go b/x/uibc/quota/keeper/quota_unit_test.go new file mode 100644 index 0000000000..d4d014d7a1 --- /dev/null +++ b/x/uibc/quota/keeper/quota_unit_test.go @@ -0,0 +1,108 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + ibcutil "github.com/umee-network/umee/v4/util/ibc" +) + +func TestGetQuotas(t *testing.T) { + k := initUmeeKeeper(t) + ctx := *k.ctx + + quotas, err := k.GetAllOutflows(ctx) + require.NoError(t, err) + require.Equal(t, len(quotas), 0) + + setQuotas := sdk.DecCoins{sdk.NewInt64DecCoin("test_uumee", 10000)} + + k.SetTokenOutflows(ctx, setQuotas) + quotas, err = k.GetAllOutflows(ctx) + require.NoError(t, err) + require.Equal(t, setQuotas, quotas) + + // get the quota of denom + quota, err := k.GetTokenOutflows(ctx, setQuotas[0].Denom) + require.NoError(t, err) + require.Equal(t, quota.Denom, setQuotas[0].Denom) +} + +func TestGetLocalDenom(t *testing.T) { + out := ibcutil.GetLocalDenom("umee") + require.Equal(t, "umee", out) +} + +func TestCheckAndUpdateQuota(t *testing.T) { + k := initUmeeKeeper(t) + + // initUmeeKeeper sets umee price: 2usd + + // 1. We set the quota param to 10 and existing outflow sum to 6 USD. + // Transfer of 2 USD in Umee should work, but additional transfer of 4 USD should fail. + // + k.setQuotaParams(10, 100) + k.SetTokenOutflow(*k.ctx, sdk.NewInt64DecCoin(umee, 6)) + k.SetTotalOutflowSum(*k.ctx, sdk.NewDec(50)) + + err := k.CheckAndUpdateQuota(*k.ctx, umee, sdk.NewInt(1)) + require.NoError(t, err) + k.checkOutflows(umee, 8, 52) + + // transferring 2 umee => 4USD, will exceed the quota (8+4 > 10) + err = k.CheckAndUpdateQuota(*k.ctx, umee, sdk.NewInt(2)) + require.ErrorContains(t, err, "quota") + k.checkOutflows(umee, 8, 52) + + // transferring 1 umee => 2USD, will will be still OK (8+2 <= 10) + err = k.CheckAndUpdateQuota(*k.ctx, umee, sdk.NewInt(1)) + require.NoError(t, err) + k.checkOutflows(umee, 10, 54) + + // 2. Setting TokenQuota param to 0 should unlimit the token quota check + // + k.setQuotaParams(0, 100) + + // transferring 20 umee => 40USD, will skip the token quota check, but will update outflows + err = k.CheckAndUpdateQuota(*k.ctx, umee, sdk.NewInt(20)) + require.NoError(t, err) + k.checkOutflows(umee, 50, 94) + + // transferring additional 5 umee => 10USD, will fail total quota check + err = k.CheckAndUpdateQuota(*k.ctx, umee, sdk.NewInt(5)) + require.ErrorContains(t, err, "quota") + k.checkOutflows(umee, 50, 94) + + // 3. Setting TotalQuota param to 0 should unlimit the total quota check + // + k.setQuotaParams(0, 0) + err = k.CheckAndUpdateQuota(*k.ctx, umee, sdk.NewInt(5)) + require.NoError(t, err) + k.checkOutflows(umee, 60, 104) + + // 4. Setting TokenQuota to 65 + // + k.setQuotaParams(65, 0) + err = k.CheckAndUpdateQuota(*k.ctx, umee, sdk.NewInt(1)) + require.NoError(t, err) + k.checkOutflows(umee, 62, 106) + + err = k.CheckAndUpdateQuota(*k.ctx, umee, sdk.NewInt(2)) // exceeds token quota + require.ErrorContains(t, err, "quota") +} + +func TestGetExchangePrice(t *testing.T) { + k := initUmeeKeeper(t) + p, err := k.getExchangePrice(*k.ctx, umee, sdk.NewInt(12)) + require.NoError(t, err) + require.Equal(t, sdk.NewDec(24), p) + + p, err = k.getExchangePrice(*k.ctx, atom, sdk.NewInt(3)) + require.NoError(t, err) + require.Equal(t, sdk.NewDec(30), p) + + _, err = k.getExchangePrice(*k.ctx, "notexisting", sdk.NewInt(10)) + require.ErrorContains(t, err, "not found") +} diff --git a/x/uibc/quota/keeper/unit_test.go b/x/uibc/quota/keeper/unit_test.go new file mode 100644 index 0000000000..760a3af015 --- /dev/null +++ b/x/uibc/quota/keeper/unit_test.go @@ -0,0 +1,57 @@ +package keeper + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/umee-network/umee/v4/tests/tsdk" + "github.com/umee-network/umee/v4/x/uibc" +) + +const ( + umee = "UUMEE" + atom = "ATOM" +) + +// creates keeper without external dependencies (app, leverage etc...) +func initKeeper(t *testing.T, l uibc.LeverageKeeper, o uibc.Oracle) TestKeeper { + cdc := codec.NewProtoCodec(nil) + storeKey := storetypes.NewMemoryStoreKey("quota") + k := NewKeeper(cdc, storeKey, nil, l, o) + ctx, _ := tsdk.NewCtxOneStore(t, storeKey) + return TestKeeper{k, t, &ctx} +} + +// creates keeper without simple mock of leverage and oracle, providing token settings and +// prices for umee and atom +func initUmeeKeeper(t *testing.T) TestKeeper { + lmock := NewLeverageKeeperMock(umee, atom) + omock := NewOracleMock(umee, sdk.NewDec(2)) + omock.prices[atom] = sdk.NewDec(10) + return initKeeper(t, lmock, omock) +} + +type TestKeeper struct { + Keeper + t *testing.T + ctx *sdk.Context +} + +func (k TestKeeper) checkOutflows(denom string, perToken, total int64) { + o, err := k.GetTokenOutflows(*k.ctx, denom) + require.NoError(k.t, err) + require.Equal(k.t, sdk.NewDec(perToken), o.Amount) + + d := k.GetTotalOutflow(*k.ctx) + require.Equal(k.t, sdk.NewDec(total), d) +} + +func (k TestKeeper) setQuotaParams(perToken, total int64) { + err := k.SetParams(*k.ctx, + uibc.Params{TokenQuota: sdk.NewDec(perToken), TotalQuota: sdk.NewDec(total)}) + require.NoError(k.t, err) +}