diff --git a/CHANGELOG.md b/CHANGELOG.md index 84018b94fa..aae929b06f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements - [1959](https://github.com/umee-network/umee/pull/1959) Update IBC to v6.1 +- [1962](https://github.com/umee-network/umee/pull/1962) Increasing unit test coverage for `x/leverage`, `x/oracle` + and `x/uibc` - [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. diff --git a/x/leverage/keeper/genesis_test.go b/x/leverage/keeper/genesis_test.go index f69a4716d5..97ece9c194 100644 --- a/x/leverage/keeper/genesis_test.go +++ b/x/leverage/keeper/genesis_test.go @@ -13,6 +13,154 @@ const ( uDenom = "u/umee" ) +func (s *IntegrationTestSuite) TestKeeper_InitGenesis() { + tcs := []struct { + name string + g types.GenesisState + expectErr bool + errMsg string + }{ + { + "invalid token", + types.GenesisState{ + Params: types.DefaultParams(), + Registry: []types.Token{ + {}, + }, + }, + true, + "invalid denom: ", + }, + { + "invalid address for borrow", + types.GenesisState{ + Params: types.DefaultParams(), + AdjustedBorrows: []types.AdjustedBorrow{ + { + Address: "", + }, + }, + }, + true, + "empty address string is not allowed", + }, + { + "invalid coin for borrow", + types.GenesisState{ + Params: types.DefaultParams(), + AdjustedBorrows: []types.AdjustedBorrow{ + { + Address: testAddr, + Amount: sdk.DecCoin{}, + }, + }, + }, + true, + "invalid denom: ", + }, + { + "invalid address for collateral", + types.GenesisState{ + Params: types.DefaultParams(), + Collateral: []types.Collateral{ + { + Address: "", + }, + }, + }, + true, + "empty address string is not allowed", + }, + { + "invalid coin for collateral", + types.GenesisState{ + Params: types.DefaultParams(), + Collateral: []types.Collateral{ + { + Address: testAddr, + Amount: sdk.Coin{}, + }, + }, + }, + true, + "invalid denom: ", + }, + { + "invalid coin for reserves", + types.GenesisState{ + Params: types.DefaultParams(), + Reserves: sdk.Coins{ + sdk.Coin{ + Denom: "", + }, + }, + }, + true, + "invalid denom: ", + }, + { + "invalid address for badDebt", + types.GenesisState{ + Params: types.DefaultParams(), + BadDebts: []types.BadDebt{ + { + Address: "", + }, + }, + }, + true, + "empty address string is not allowed", + }, + { + "invalid coin for badDebt", + types.GenesisState{ + Params: types.DefaultParams(), + BadDebts: []types.BadDebt{ + { + Address: testAddr, + Denom: "", + }, + }, + }, + true, + "invalid denom: ", + }, + { + "invalid coin for interestScalars", + types.GenesisState{ + Params: types.DefaultParams(), + InterestScalars: []types.InterestScalar{ + { + Denom: "", + }, + }, + }, + true, + "invalid denom: ", + }, + { + "valid", + types.GenesisState{ + Params: types.DefaultParams(), + }, + false, + "", + }, + } + + for _, tc := range tcs { + s.Run( + tc.name, func() { + if tc.expectErr { + s.Assertions.PanicsWithError(tc.errMsg, func() { s.app.LeverageKeeper.InitGenesis(s.ctx, tc.g) }) + } else { + s.Assertions.NotPanics(func() { s.app.LeverageKeeper.InitGenesis(s.ctx, tc.g) }) + } + }, + ) + } +} + func (s *IntegrationTestSuite) TestKeeper_ExportGenesis() { borrows := []types.AdjustedBorrow{ { diff --git a/x/leverage/types/errors.go b/x/leverage/types/errors.go index 773c9a1a2a..dde7399dbf 100644 --- a/x/leverage/types/errors.go +++ b/x/leverage/types/errors.go @@ -21,9 +21,12 @@ var ( ErrSupplyNotAllowed = errors.Register(ModuleName, 203, "supplying of Token disabled") ErrBorrowNotAllowed = errors.Register(ModuleName, 204, "borrowing of Token disabled") ErrBlacklisted = errors.Register(ModuleName, 205, "blacklisted Token") - ErrCollateralWeightZero = errors.Register(ModuleName, 206, - "collateral weight of Token is zero: can't be used as a collateral") - ErrDuplicateToken = errors.Register(ModuleName, 207, "duplicate token") + ErrCollateralWeightZero = errors.Register( + ModuleName, 206, + "collateral weight of Token is zero: can't be used as a collateral", + ) + ErrDuplicateToken = errors.Register(ModuleName, 207, "duplicate token") + ErrEmptyAddAndUpdateTokens = errors.Register(ModuleName, 208, "empty add and update tokens") // 3XX = User Positions ErrInsufficientBalance = errors.Register(ModuleName, 300, "insufficient balance") diff --git a/x/leverage/types/genesis_test.go b/x/leverage/types/genesis_test.go index b70afc9077..dd966f3f08 100644 --- a/x/leverage/types/genesis_test.go +++ b/x/leverage/types/genesis_test.go @@ -3,12 +3,146 @@ package types import ( "testing" + sdk "github.com/cosmos/cosmos-sdk/types" + "gotest.tools/v3/assert" ) func TestGenesisValidation(t *testing.T) { - genState := DefaultGenesis() - assert.NilError(t, genState.Validate()) + testAddr := "umee1s84d29zk3k20xk9f0hvczkax90l9t94g72n6wm" + validDenom := "umee" + + tcs := []struct { + name string + q GenesisState + expectErr bool + errMsg string + }{ + {"default genesis", *DefaultGenesis(), false, ""}, + { + "invalid params", + *NewGenesisState( + Params{ + CompleteLiquidationThreshold: sdk.MustNewDecFromStr("-0.4"), + }, nil, nil, nil, nil, 0, nil, nil, nil, + ), + true, + "complete liquidation threshold must be positive", + }, + { + "invalid token registry", GenesisState{ + Params: DefaultParams(), + Registry: []Token{ + {}, + }, + }, + true, + "invalid denom", + }, + { + "invalid adjusted borrows address", GenesisState{ + Params: DefaultParams(), + AdjustedBorrows: []AdjustedBorrow{ + NewAdjustedBorrow("", sdk.DecCoin{}), + }, + }, + true, + "empty address string is not allowed", + }, + { + "invalid adjusted borrows amount", GenesisState{ + Params: DefaultParams(), + AdjustedBorrows: []AdjustedBorrow{ + NewAdjustedBorrow(testAddr, sdk.DecCoin{}), + }, + }, + true, + "invalid denom", + }, + { + "invalid collateral address", GenesisState{ + Params: DefaultParams(), + Collateral: []Collateral{ + NewCollateral("", sdk.Coin{}), + }, + }, + true, + "empty address string is not allowed", + }, + { + "invalid collateral amount", GenesisState{ + Params: DefaultParams(), + Collateral: []Collateral{ + NewCollateral(testAddr, sdk.Coin{}), + }, + }, + true, + "invalid denom", + }, + { + "invalid reserves", GenesisState{ + Params: DefaultParams(), + Reserves: sdk.Coins{ + sdk.Coin{ + Denom: "", + }, + }, + }, + true, + "invalid denom", + }, + { + "invalid badDebt address", GenesisState{ + Params: DefaultParams(), + BadDebts: []BadDebt{ + NewBadDebt("", ""), + }, + }, + true, + "empty address string is not allowed", + }, + { + "invalid badDebt denom", GenesisState{ + Params: DefaultParams(), + BadDebts: []BadDebt{ + NewBadDebt(testAddr, ""), + }, + }, + true, + "invalid denom", + }, + { + "invalid interestScalar denom", GenesisState{ + Params: DefaultParams(), + InterestScalars: []InterestScalar{ + NewInterestScalar("", sdk.ZeroDec()), + }, + }, + true, + "invalid denom", + }, + { + "invalid interestScalar address", GenesisState{ + Params: DefaultParams(), + InterestScalars: []InterestScalar{ + NewInterestScalar(validDenom, sdk.ZeroDec()), + }, + }, + true, + "exchange rate less than one", + }, + } - // TODO #484: expand this test to cover failure cases. + for _, tc := range tcs { + t.Run( + tc.name, func(t *testing.T) { + err := tc.q.Validate() + if tc.expectErr { + assert.ErrorContains(t, err, tc.errMsg) + } else { + assert.NilError(t, err) + } + }, + ) + } } diff --git a/x/leverage/types/msgs.go b/x/leverage/types/msgs.go index e3265db408..ed00bfa897 100644 --- a/x/leverage/types/msgs.go +++ b/x/leverage/types/msgs.go @@ -10,7 +10,7 @@ import ( var _ sdk.Msg = &MsgGovUpdateRegistry{} -// NewMsgUpdateRegistry will creates a new MsgUpdateRegistry instance +// NewMsgUpdateRegistry will create a new MsgUpdateRegistry instance func NewMsgUpdateRegistry(authority, title, description string, updateTokens, addTokens []Token) *MsgGovUpdateRegistry { return &MsgGovUpdateRegistry{ Title: title, @@ -36,6 +36,10 @@ func (msg MsgGovUpdateRegistry) ValidateBasic() error { return err } + if len(msg.AddTokens) == 0 && len(msg.UpdateTokens) == 0 { + return ErrEmptyAddAndUpdateTokens + } + if err := validateRegistryTokenDenoms(msg.AddTokens); err != nil { return err } diff --git a/x/leverage/types/msgs_test.go b/x/leverage/types/msgs_test.go new file mode 100644 index 0000000000..e5feececd1 --- /dev/null +++ b/x/leverage/types/msgs_test.go @@ -0,0 +1,174 @@ +package types_test + +import ( + "testing" + + "github.com/umee-network/umee/v4/x/leverage/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + tassert "github.com/stretchr/testify/assert" + + "gotest.tools/v3/assert" +) + +func TestMsgGovUpdateRegistryValidateBasic(t *testing.T) { + tcs := []struct { + name string + q types.MsgGovUpdateRegistry + err string + }{ + {"no authority", types.MsgGovUpdateRegistry{}, "invalid authority"}, + { + "duplicated add token", types.MsgGovUpdateRegistry{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Title: "Title", + Description: "Description", + AddTokens: []types.Token{ + {BaseDenom: "uumee"}, + {BaseDenom: "uumee"}, + }, + }, "duplicate token", + }, + { + "invalid add token", types.MsgGovUpdateRegistry{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Title: "Title", + Description: "Description", + AddTokens: []types.Token{ + {BaseDenom: "uumee"}, + }, + }, "invalid denom", + }, + { + "duplicated update token", types.MsgGovUpdateRegistry{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Title: "Title", + Description: "Description", + UpdateTokens: []types.Token{ + {BaseDenom: "uumee"}, + {BaseDenom: "uumee"}, + }, + }, "duplicate token", + }, + { + "invalid update token", types.MsgGovUpdateRegistry{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Title: "Title", + Description: "Description", + UpdateTokens: []types.Token{ + {BaseDenom: "uumee"}, + }, + }, "invalid denom", + }, + { + "empty add and update tokens", types.MsgGovUpdateRegistry{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Title: "Title", + Description: "Description", + }, "empty add and update tokens", + }, + { + "valid", types.MsgGovUpdateRegistry{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Title: "Title", + Description: "Description", + AddTokens: []types.Token{ + { + BaseDenom: "uumee", + SymbolDenom: "UMEE", + Exponent: 6, + ReserveFactor: sdk.MustNewDecFromStr("0.2"), + CollateralWeight: sdk.MustNewDecFromStr("0.25"), + LiquidationThreshold: sdk.MustNewDecFromStr("0.5"), + BaseBorrowRate: sdk.MustNewDecFromStr("0.02"), + KinkBorrowRate: sdk.MustNewDecFromStr("0.22"), + MaxBorrowRate: sdk.MustNewDecFromStr("1.52"), + KinkUtilization: sdk.MustNewDecFromStr("0.8"), + LiquidationIncentive: sdk.MustNewDecFromStr("0.1"), + EnableMsgSupply: true, + EnableMsgBorrow: true, + Blacklist: false, + MaxCollateralShare: sdk.MustNewDecFromStr("1"), + MaxSupplyUtilization: sdk.MustNewDecFromStr("0.9"), + MinCollateralLiquidity: sdk.MustNewDecFromStr("0"), + MaxSupply: sdk.NewInt(100_000_000000), + HistoricMedians: 24, + }, + }, + }, "", + }, + } + + for _, tc := range tcs { + t.Run( + tc.name, func(t *testing.T) { + err := tc.q.ValidateBasic() + if tc.err == "" { + assert.NilError(t, err) + } else { + assert.ErrorContains(t, err, tc.err) + } + }, + ) + } +} + +func TestMsgGovUpdateRegistryOtherFunctionality(t *testing.T) { + umee := types.Token{ + BaseDenom: "uumee", + SymbolDenom: "UMEE", + Exponent: 6, + ReserveFactor: sdk.MustNewDecFromStr("0.2"), + CollateralWeight: sdk.MustNewDecFromStr("0.25"), + LiquidationThreshold: sdk.MustNewDecFromStr("0.25"), + BaseBorrowRate: sdk.MustNewDecFromStr("0.02"), + KinkBorrowRate: sdk.MustNewDecFromStr("0.22"), + MaxBorrowRate: sdk.MustNewDecFromStr("1.52"), + KinkUtilization: sdk.MustNewDecFromStr("0.8"), + LiquidationIncentive: sdk.MustNewDecFromStr("0.1"), + EnableMsgSupply: true, + EnableMsgBorrow: true, + Blacklist: false, + MaxCollateralShare: sdk.MustNewDecFromStr("1"), + MaxSupplyUtilization: sdk.MustNewDecFromStr("0.9"), + MinCollateralLiquidity: sdk.MustNewDecFromStr("0"), + MaxSupply: sdk.NewInt(100_000_000000), + HistoricMedians: 24, + } + msg := types.NewMsgUpdateRegistry( + authtypes.NewModuleAddress(govtypes.ModuleName).String(), "title", "description", + []types.Token{umee}, []types.Token{}, + ) + + expResult := `authority: umee10d07y265gmmuvt4z0w9aw880jnsr700jg5w6jp +title: title +description: description +addtokens: [] +updatetokens: + - base_denom: uumee + reserve_factor: "0.200000000000000000" + collateral_weight: "0.250000000000000000" + liquidation_threshold: "0.250000000000000000" + base_borrow_rate: "0.020000000000000000" + kink_borrow_rate: "0.220000000000000000" + max_borrow_rate: "1.520000000000000000" + kink_utilization: "0.800000000000000000" + liquidation_incentive: "0.100000000000000000" + symbol_denom: UMEE + exponent: 6 + enable_msg_supply: true + enable_msg_borrow: true + blacklist: false + max_collateral_share: "1.000000000000000000" + max_supply_utilization: "0.900000000000000000" + min_collateral_liquidity: "0.000000000000000000" + max_supply: "100000000000" + historic_medians: 24 +` + assert.Equal(t, expResult, msg.String()) + tassert.NotNil(t, msg.GetSignBytes(), "sign byte shouldn't be nil") + tassert.NotEmpty(t, msg.GetSigners(), "signers shouldn't be empty") +} diff --git a/x/leverage/types/params.go b/x/leverage/types/params.go index 2cd0f5639a..334f808620 100644 --- a/x/leverage/types/params.go +++ b/x/leverage/types/params.go @@ -18,10 +18,6 @@ var ( KeyDirectLiquidationFee = []byte("DirectLiquidationFee") ) -func NewParams() Params { - return Params{} -} - // ParamSetPairs implements the ParamSet interface and returns all the key/value // pairs pairs of x/leverage module's parameters. func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { @@ -104,6 +100,10 @@ func validateLiquidationThreshold(i interface{}) error { return fmt.Errorf("complete liquidation threshold must be positive: %d", v) } + if v.GT(sdk.OneDec()) { + return fmt.Errorf("complete liquidation threshold cannot exceed 1: %d", v) + } + return nil } diff --git a/x/leverage/types/params_test.go b/x/leverage/types/params_test.go new file mode 100644 index 0000000000..033145513a --- /dev/null +++ b/x/leverage/types/params_test.go @@ -0,0 +1,146 @@ +package types + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "gotest.tools/v3/assert" +) + +func TestParams_Validate(t *testing.T) { + negativeDec := sdk.MustNewDecFromStr("-0.4") + exceededDec := sdk.MustNewDecFromStr("1.4") + + tcs := []struct { + name string + p Params + err string + }{ + {"default params", DefaultParams(), ""}, + { + "negative complete liquidation threshold", + Params{ + CompleteLiquidationThreshold: negativeDec, + }, + "complete liquidation threshold must be positive", + }, + { + "exceeded complete liquidation threshold", + Params{ + CompleteLiquidationThreshold: exceededDec, + }, + "complete liquidation threshold cannot exceed 1", + }, + { + "negative minimum close factor", + Params{ + CompleteLiquidationThreshold: sdk.MustNewDecFromStr("0.4"), + MinimumCloseFactor: negativeDec, + }, + "minimum close factor cannot be negative", + }, + { + "exceeded minimum close factor", + Params{ + CompleteLiquidationThreshold: sdk.MustNewDecFromStr("0.4"), + MinimumCloseFactor: exceededDec, + }, + "minimum close factor cannot exceed 1", + }, + { + "negative oracle reward factor", + Params{ + CompleteLiquidationThreshold: sdk.MustNewDecFromStr("0.4"), + MinimumCloseFactor: sdk.MustNewDecFromStr("0.05"), + OracleRewardFactor: negativeDec, + }, + "oracle reward factor cannot be negative", + }, + { + "exceeded oracle reward factor", + Params{ + CompleteLiquidationThreshold: sdk.MustNewDecFromStr("0.4"), + MinimumCloseFactor: sdk.MustNewDecFromStr("0.05"), + OracleRewardFactor: exceededDec, + }, + "oracle reward factor cannot exceed 1", + }, + { + "negative small liquidation size", + Params{ + CompleteLiquidationThreshold: sdk.MustNewDecFromStr("0.4"), + MinimumCloseFactor: sdk.MustNewDecFromStr("0.05"), + OracleRewardFactor: sdk.MustNewDecFromStr("0.01"), + SmallLiquidationSize: negativeDec, + }, + "small liquidation size cannot be negative", + }, + { + "negative direct liquidation fee", + Params{ + CompleteLiquidationThreshold: sdk.MustNewDecFromStr("0.4"), + MinimumCloseFactor: sdk.MustNewDecFromStr("0.05"), + OracleRewardFactor: sdk.MustNewDecFromStr("0.01"), + SmallLiquidationSize: sdk.MustNewDecFromStr("500.00"), + DirectLiquidationFee: negativeDec, + }, + "direct liquidation fee cannot be negative", + }, + { + "exceeded direct liquidation fee", + Params{ + CompleteLiquidationThreshold: sdk.MustNewDecFromStr("0.4"), + MinimumCloseFactor: sdk.MustNewDecFromStr("0.05"), + OracleRewardFactor: sdk.MustNewDecFromStr("0.01"), + SmallLiquidationSize: sdk.MustNewDecFromStr("500.00"), + DirectLiquidationFee: exceededDec, + }, + "direct liquidation fee must be less than 1", + }, + } + + for _, tc := range tcs { + t.Run( + tc.name, func(t *testing.T) { + err := tc.p.Validate() + if tc.err == "" { + assert.NilError(t, err) + } else { + assert.ErrorContains(t, err, tc.err) + } + }, + ) + } +} + +func TestParams_IfaceValidations(t *testing.T) { + invalidIface := sdkmath.Int{} + expErr := "invalid parameter type" + err := validateLiquidationThreshold(invalidIface) + assert.ErrorContains(t, err, expErr) + err = validateMinimumCloseFactor(invalidIface) + assert.ErrorContains(t, err, expErr) + err = validateOracleRewardFactor(invalidIface) + assert.ErrorContains(t, err, expErr) + err = validateSmallLiquidationSize(invalidIface) + assert.ErrorContains(t, err, expErr) + err = validateDirectLiquidationFee(invalidIface) + assert.ErrorContains(t, err, expErr) +} + +func TestParams_Additional(t *testing.T) { + params := DefaultParams() + expResult := `complete_liquidation_threshold: "0.400000000000000000" +minimum_close_factor: "0.050000000000000000" +oracle_reward_factor: "0.010000000000000000" +small_liquidation_size: "500.000000000000000000" +direct_liquidation_fee: "0.050000000000000000" +` + assert.Equal(t, expResult, params.String()) + + paramSetPairs := params.ParamSetPairs() + assert.Equal(t, 5, len(paramSetPairs)) +} diff --git a/x/leverage/types/tx_test.go b/x/leverage/types/tx_test.go new file mode 100644 index 0000000000..89dbda12d2 --- /dev/null +++ b/x/leverage/types/tx_test.go @@ -0,0 +1,91 @@ +package types_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/umee-network/umee/v4/x/leverage/types" + "gotest.tools/v3/assert" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + denom = "uumee" + uDenom = "u/uumee" +) + +var ( + testAddr, _ = sdk.AccAddressFromBech32("umee1s84d29zk3k20xk9f0hvczkax90l9t94g72n6wm") + uToken = sdk.NewInt64Coin(uDenom, 10) + token = sdk.NewInt64Coin(denom, 10) +) + +func TestTxs(t *testing.T) { + txs := []sdk.Msg{ + types.NewMsgSupply(testAddr, token), + types.NewMsgWithdraw(testAddr, uToken), + types.NewMsgMaxWithdraw(testAddr, denom), + types.NewMsgCollateralize(testAddr, uToken), + types.NewMsgSupplyCollateral(testAddr, token), + types.NewMsgDecollateralize(testAddr, uToken), + types.NewMsgBorrow(testAddr, token), + types.NewMsgMaxBorrow(testAddr, denom), + types.NewMsgRepay(testAddr, token), + types.NewMsgLiquidate(testAddr, testAddr, token, uDenom), + } + + for _, tx := range txs { + err := tx.ValidateBasic() + assert.NilError(t, err, tx.String()) + // check signers + assert.Equal(t, len(tx.GetSigners()), 1) + assert.Equal(t, tx.GetSigners()[0].String(), testAddr.String()) + } +} + +// functions required in msgs.go which are not part of sdk.Msg +type sdkmsg interface { + Route() string + Type() string + GetSignBytes() []byte +} + +func TestRoutes(t *testing.T) { + txs := []sdkmsg{ + types.NewMsgSupply(testAddr, token), + types.NewMsgWithdraw(testAddr, uToken), + types.NewMsgMaxWithdraw(testAddr, denom), + types.NewMsgCollateralize(testAddr, uToken), + types.NewMsgSupplyCollateral(testAddr, token), + types.NewMsgDecollateralize(testAddr, uToken), + types.NewMsgBorrow(testAddr, token), + types.NewMsgMaxBorrow(testAddr, denom), + types.NewMsgRepay(testAddr, token), + types.NewMsgLiquidate(testAddr, testAddr, token, uDenom), + } + + for _, tx := range txs { + assert.Equal(t, + // example: "/umee.leverage.v1.MsgSupply" + // with %T returning "*types.MsgSupply" + addV1ToType(fmt.Sprintf("/umee.%T", tx)), + tx.Route(), + ) + // check for non-empty returns for now + assert.Assert(t, len(tx.GetSignBytes()) != 0) + // exact match required + assert.Equal(t, + // example: "/umee.leverage.v1.MsgSupply" + // with %T returning "*types.MsgSupply" + addV1ToType(fmt.Sprintf("/umee.%T", tx)), + tx.Type(), + ) + } +} + +// addV1ToType replaces "*types" with "leverage.v1" +func addV1ToType(s string) string { + return strings.Replace(s, "*types", "leverage.v1", 1) +} diff --git a/x/oracle/genesis_test.go b/x/oracle/genesis_test.go new file mode 100644 index 0000000000..5dd0243684 --- /dev/null +++ b/x/oracle/genesis_test.go @@ -0,0 +1,228 @@ +package oracle_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/umee-network/umee/v4/x/oracle" + "github.com/umee-network/umee/v4/x/oracle/types" + "gotest.tools/v3/assert" +) + +const ( + umeeAddr = "umee1s84d29zk3k20xk9f0hvczkax90l9t94g72n6wm" + umeevaloperAddr = "umeevaloper1kqh6nt4f48vptvq4j5cgr0nfd2x4z9ulvrtqrh" + denom = "umee" + upperDenom = "UMEE" +) + +var exchangeRate = sdk.MustNewDecFromStr("1.2") + +func (s *IntegrationTestSuite) TestGenesis_InitGenesis() { + keeper, ctx := s.app.OracleKeeper, s.ctx + + tcs := []struct { + name string + g types.GenesisState + expectErr bool + errMsg string + }{ + { + "FeederDelegations.FeederAddress: empty address", + types.GenesisState{ + FeederDelegations: []types.FeederDelegation{ + { + FeederAddress: "", + }, + }, + }, + true, + "empty address string is not allowed", + }, + { + "FeederDelegations.ValidatorAddress: empty address", + types.GenesisState{ + FeederDelegations: []types.FeederDelegation{ + { + FeederAddress: umeeAddr, + ValidatorAddress: "", + }, + }, + }, + true, + "empty address string is not allowed", + }, + { + "valid", + types.GenesisState{ + Params: types.DefaultParams(), + ExchangeRates: types.ExchangeRateTuples{ + types.ExchangeRateTuple{ + Denom: denom, + ExchangeRate: exchangeRate, + }, + }, + HistoricPrices: types.Prices{ + types.Price{ + ExchangeRateTuple: types.ExchangeRateTuple{ + Denom: denom, + ExchangeRate: exchangeRate, + }, + BlockNum: 0, + }, + }, + Medians: types.Prices{ + types.Price{ + ExchangeRateTuple: types.ExchangeRateTuple{ + Denom: denom, + ExchangeRate: exchangeRate, + }, + BlockNum: 0, + }, + }, + MedianDeviations: types.Prices{ + types.Price{ + ExchangeRateTuple: types.ExchangeRateTuple{ + Denom: denom, + ExchangeRate: exchangeRate, + }, + BlockNum: 0, + }, + }, + }, + false, + "", + }, + { + "FeederDelegations.ValidatorAddress: empty address", + types.GenesisState{ + MissCounters: []types.MissCounter{ + { + ValidatorAddress: "", + }, + }, + }, + true, + "empty address string is not allowed", + }, + { + "AggregateExchangeRatePrevotes.Voter: empty address", + types.GenesisState{ + AggregateExchangeRatePrevotes: []types.AggregateExchangeRatePrevote{ + { + Voter: "", + }, + }, + }, + true, + "empty address string is not allowed", + }, + { + "AggregateExchangeRateVotes.Voter: empty address", + types.GenesisState{ + AggregateExchangeRateVotes: []types.AggregateExchangeRateVote{ + { + Voter: "", + }, + }, + }, + true, + "empty address string is not allowed", + }, + } + + for _, tc := range tcs { + s.Run( + tc.name, func() { + if tc.expectErr { + s.Assertions.PanicsWithError(tc.errMsg, func() { oracle.InitGenesis(ctx, keeper, tc.g) }) + } else { + s.Assertions.NotPanics(func() { oracle.InitGenesis(ctx, keeper, tc.g) }) + } + }, + ) + } +} + +func (s *IntegrationTestSuite) TestGenesis_ExportGenesis() { + keeper, ctx := s.app.OracleKeeper, s.ctx + params := types.DefaultParams() + + feederDelegations := []types.FeederDelegation{ + { + FeederAddress: umeeAddr, + ValidatorAddress: umeevaloperAddr, + }, + } + exchangeRateTuples := types.ExchangeRateTuples{ + types.ExchangeRateTuple{ + Denom: upperDenom, + ExchangeRate: exchangeRate, + }, + } + missCounters := []types.MissCounter{ + { + ValidatorAddress: umeevaloperAddr, + }, + } + aggregateExchangeRatePrevotes := []types.AggregateExchangeRatePrevote{ + { + Voter: umeevaloperAddr, + }, + } + aggregateExchangeRateVotes := []types.AggregateExchangeRateVote{ + { + Voter: umeevaloperAddr, + }, + } + historicPrices := []types.Price{ + { + ExchangeRateTuple: types.ExchangeRateTuple{ + Denom: denom, + ExchangeRate: exchangeRate, + }, + BlockNum: 0, + }, + } + medians := []types.Price{ + { + ExchangeRateTuple: types.ExchangeRateTuple{ + Denom: denom, + ExchangeRate: exchangeRate, + }, + BlockNum: 0, + }, + } + medianDeviations := []types.Price{ + { + ExchangeRateTuple: types.ExchangeRateTuple{ + Denom: denom, + ExchangeRate: exchangeRate, + }, + BlockNum: 0, + }, + } + + genesisState := types.GenesisState{ + Params: params, + FeederDelegations: feederDelegations, + ExchangeRates: exchangeRateTuples, + MissCounters: missCounters, + AggregateExchangeRatePrevotes: aggregateExchangeRatePrevotes, + AggregateExchangeRateVotes: aggregateExchangeRateVotes, + Medians: medians, + HistoricPrices: historicPrices, + MedianDeviations: medianDeviations, + } + + oracle.InitGenesis(ctx, keeper, genesisState) + + result := oracle.ExportGenesis(s.ctx, s.app.OracleKeeper) + assert.DeepEqual(s.T(), params, result.Params) + assert.DeepEqual(s.T(), feederDelegations, result.FeederDelegations) + assert.DeepEqual(s.T(), exchangeRateTuples, result.ExchangeRates) + assert.DeepEqual(s.T(), missCounters, result.MissCounters) + assert.DeepEqual(s.T(), aggregateExchangeRatePrevotes, result.AggregateExchangeRatePrevotes) + assert.DeepEqual(s.T(), aggregateExchangeRateVotes, result.AggregateExchangeRateVotes) + assert.DeepEqual(s.T(), medians, result.Medians) + assert.DeepEqual(s.T(), historicPrices, result.HistoricPrices) + assert.DeepEqual(s.T(), medianDeviations, result.MedianDeviations) +} diff --git a/x/oracle/types/params_test.go b/x/oracle/types/params_test.go index 8c12c06b24..6d91c79de8 100644 --- a/x/oracle/types/params_test.go +++ b/x/oracle/types/params_test.go @@ -274,3 +274,8 @@ func TestValidateVotingThreshold(t *testing.T) { } } } + +func TestParams_ParamSetPairs(t *testing.T) { + params := DefaultParams() + assert.Equal(t, 12, len(params.ParamSetPairs())) +} diff --git a/x/uibc/quota/keeper/keeper_test.go b/x/uibc/quota/keeper/keeper_test.go index da54690237..ca3ae13346 100644 --- a/x/uibc/quota/keeper/keeper_test.go +++ b/x/uibc/quota/keeper/keeper_test.go @@ -4,6 +4,10 @@ import ( "fmt" "testing" + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" @@ -17,6 +21,7 @@ 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" ) @@ -86,3 +91,17 @@ func initKeeperTestSuite(t *testing.T) *KeeperTestSuite { return s } + +// creates keeper with all external dependencies (app, leverage etc...) +func initFullKeeper( + t *testing.T, + cdc codec.BinaryCodec, + ics4Wrapper porttypes.ICS4Wrapper, + leverageKeeper uibc.LeverageKeeper, + oracleKeeper uibc.Oracle, +) (sdk.Context, keeper.Keeper) { + storeKey := storetypes.NewMemoryStoreKey("quota") + k := keeper.NewKeeper(cdc, storeKey, ics4Wrapper, leverageKeeper, oracleKeeper) + ctx, _ := tsdk.NewCtxOneStore(t, storeKey) + return ctx, k +} diff --git a/x/uibc/quota/keeper/quota_test.go b/x/uibc/quota/keeper/quota_test.go index 2d96bd3d18..79754e3691 100644 --- a/x/uibc/quota/keeper/quota_test.go +++ b/x/uibc/quota/keeper/quota_test.go @@ -3,6 +3,22 @@ package keeper_test import ( "testing" + "github.com/cosmos/cosmos-sdk/codec" + ctypes "github.com/cosmos/cosmos-sdk/codec/types" + + "github.com/umee-network/umee/v4/x/uibc" + + "github.com/umee-network/umee/v4/x/oracle/types" + + lfixtures "github.com/umee-network/umee/v4/x/leverage/fixtures" + + sdkmath "cosmossdk.io/math" + + ltypes "github.com/umee-network/umee/v4/x/leverage/types" + + "github.com/golang/mock/gomock" + "github.com/umee-network/umee/v4/x/uibc/mocks" + sdk "github.com/cosmos/cosmos-sdk/types" "gotest.tools/v3/assert" ) @@ -25,3 +41,119 @@ func TestResetQuota(t *testing.T) { assert.NilError(t, err) assert.DeepEqual(t, q.Amount, sdk.NewDec(0)) } + +func TestKeeper_CheckAndUpdateQuota(t *testing.T) { + invalidToken := sdk.NewCoin("u/u/umee", sdkmath.NewInt(100)) + umeeUToken := sdk.NewCoin("u/umee", sdkmath.NewInt(100)) + atomToken := sdk.NewCoin("atom", sdkmath.NewInt(1000)) + daiToken := sdk.NewCoin("dai", sdkmath.NewInt(50)) + // gomock initializations + leverageCtrl := gomock.NewController(t) + defer leverageCtrl.Finish() + leverageMock := mocks.NewMockLeverageKeeper(leverageCtrl) + + oracleCtrl := gomock.NewController(t) + defer oracleCtrl.Finish() + oracleMock := mocks.NewMockOracle(oracleCtrl) + + interfaceRegistry := ctypes.NewInterfaceRegistry() + marshaller := codec.NewProtoCodec(interfaceRegistry) + ctx, k := initFullKeeper(t, marshaller, nil, leverageMock, oracleMock) + err := k.ResetAllQuotas(ctx) + assert.NilError(t, err) + + // invalid token, returns error from mock leverage + leverageMock.EXPECT().ExchangeUToken(ctx, invalidToken).Return(sdk.Coin{}, ltypes.ErrNotUToken).AnyTimes() + + err = k.CheckAndUpdateQuota(ctx, invalidToken.Denom, invalidToken.Amount) + assert.ErrorIs(t, err, ltypes.ErrNotUToken) + + // UMEE uToken, exchanges correctly, but returns ErrNotRegisteredToken when trying to get Token's settings + // from leverage mock keeper + leverageMock.EXPECT().ExchangeUToken(ctx, umeeUToken).Return( + sdk.NewCoin("umee", sdkmath.NewInt(100)), + nil, + ).AnyTimes() + leverageMock.EXPECT().GetTokenSettings(ctx, "umee").Return(ltypes.Token{}, ltypes.ErrNotRegisteredToken).AnyTimes() + + err = k.CheckAndUpdateQuota(ctx, umeeUToken.Denom, umeeUToken.Amount) + // returns nil when the error is ErrNotRegisteredToken + assert.NilError(t, err) + + // ATOM, returns token settings correctly from leverage mock keeper, + // then returns an error when trying to get token prices from oracle mock keeper + leverageMock.EXPECT().GetTokenSettings(ctx, "atom").Return( + lfixtures.Token("atom", "ATOM", 6), nil, + ).AnyTimes() + oracleMock.EXPECT().Price(ctx, "ATOM").Return(sdk.Dec{}, types.ErrMalformedLatestAvgPrice) + + err = k.CheckAndUpdateQuota(ctx, atomToken.Denom, atomToken.Amount) + assert.ErrorIs(t, err, types.ErrMalformedLatestAvgPrice) + + // DAI returns token settings and prices from mock leverage and oracle keepers, no errors expected + leverageMock.EXPECT().GetTokenSettings(ctx, "dai").Return( + lfixtures.Token("dai", "DAI", 6), nil, + ).AnyTimes() + oracleMock.EXPECT().Price(ctx, "DAI").Return(sdk.MustNewDecFromStr("0.37"), nil) + + err = k.SetParams(ctx, uibc.DefaultParams()) + assert.NilError(t, err) + + setQuotas := sdk.DecCoins{sdk.NewInt64DecCoin("dai", 10000)} + k.SetTokenOutflows(ctx, setQuotas) + + err = k.CheckAndUpdateQuota(ctx, daiToken.Denom, daiToken.Amount) + assert.NilError(t, err) +} + +func TestKeeper_UndoUpdateQuota(t *testing.T) { + umeeAmount := sdkmath.NewInt(100_000000) + umeePrice := sdk.MustNewDecFromStr("0.37") + umeeQuota := sdkmath.NewInt(10000) + umeeToken := sdk.NewCoin("umee", umeeAmount) + umeeExponent := 6 + // gomock initializations + leverageCtrl := gomock.NewController(t) + defer leverageCtrl.Finish() + leverageMock := mocks.NewMockLeverageKeeper(leverageCtrl) + + oracleCtrl := gomock.NewController(t) + defer oracleCtrl.Finish() + oracleMock := mocks.NewMockOracle(oracleCtrl) + + interfaceRegistry := ctypes.NewInterfaceRegistry() + marshaller := codec.NewProtoCodec(interfaceRegistry) + ctx, k := initFullKeeper(t, marshaller, nil, leverageMock, oracleMock) + err := k.ResetAllQuotas(ctx) + assert.NilError(t, err) + + // UMEE, returns token settings and prices from mock leverage and oracle keepers, no errors expected + leverageMock.EXPECT().GetTokenSettings(ctx, "umee").Return( + lfixtures.Token("umee", "UMEE", uint32(umeeExponent)), nil, + ).AnyTimes() + oracleMock.EXPECT().Price(ctx, "UMEE").Return(umeePrice, nil).AnyTimes() + + err = k.UndoUpdateQuota(ctx, umeeToken.Denom, umeeToken.Amount) + // the result is ignored due to quota reset + assert.NilError(t, err) + + o, err := k.GetTokenOutflows(ctx, umeeToken.Denom) + assert.NilError(t, err) + assert.DeepEqual(t, o.Amount, sdk.ZeroDec()) + + setQuotas := sdk.DecCoins{sdk.NewInt64DecCoin("umee", umeeQuota.Int64())} + k.SetTokenOutflows(ctx, setQuotas) + + err = k.UndoUpdateQuota(ctx, umeeToken.Denom, umeeToken.Amount) + assert.NilError(t, err) + + o, err = k.GetTokenOutflows(ctx, umeeToken.Denom) + assert.NilError(t, err) + + // the expected quota is calculated as follows: + // umee_value = umee_amount * umee_price + // expected_quota = current_quota - umee_value + powerReduction := sdk.MustNewDecFromStr("10").Power(uint64(umeeExponent)) + expectedQuota := sdk.NewDec(umeeQuota.Int64()).Sub(sdk.NewDecFromInt(umeeToken.Amount).Quo(powerReduction).Mul(umeePrice)) + assert.DeepEqual(t, o.Amount, expectedQuota) +}