diff --git a/app/params/weights.go b/app/params/weights.go index e8bc2e14..661c8b76 100644 --- a/app/params/weights.go +++ b/app/params/weights.go @@ -9,4 +9,8 @@ const ( DefaultWeightMsgConvertCoin int = 20 DefaultWeightMsgConvertErc20 int = 20 + + DefaultWeightMsgSwapOrder int = 10 + DefaultWeightMsgAddLiquidity int = 20 + DefaultWeightMsgRemoveLiquidity int = 10 ) diff --git a/x/coinswap/module.go b/x/coinswap/module.go index 17b280e8..ac2e28d0 100644 --- a/x/coinswap/module.go +++ b/x/coinswap/module.go @@ -181,6 +181,7 @@ func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange { // RegisterStoreDecoder registers a decoder for coinswap module's types func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) } diff --git a/x/coinswap/simulation/decoder.go b/x/coinswap/simulation/decoder.go index 554f894e..015f3e83 100644 --- a/x/coinswap/simulation/decoder.go +++ b/x/coinswap/simulation/decoder.go @@ -1,10 +1,41 @@ package simulation import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/Canto-Network/Canto/v7/x/coinswap/types" ) -// DecodeStore unmarshals the KVPair's Value to the corresponding htlc type -func DecodeStore(kvA, kvB kv.Pair) string { - return "" +// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's +// Value to the corresponding farming type. +func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { + return func(kvA, kvB kv.Pair) string { + switch { + case bytes.Equal(kvA.Key[:], []byte(types.KeyPool)): + var pA, pB types.Pool + cdc.MustUnmarshal(kvA.Value, &pA) + cdc.MustUnmarshal(kvB.Value, &pB) + return fmt.Sprintf("%v\n%v", pA, pB) + + case bytes.Equal(kvA.Key[:], []byte(types.KeyNextPoolSequence)): + var seqA, seqB uint64 + seqA = sdk.BigEndianToUint64(kvA.Value) + seqB = sdk.BigEndianToUint64(kvB.Value) + return fmt.Sprintf("%v\n%v", seqA, seqB) + + case bytes.Equal(kvA.Key[:], []byte(types.KeyPoolLptDenom)): + var pA, pB types.Pool + cdc.MustUnmarshal(kvA.Value, &pA) + cdc.MustUnmarshal(kvB.Value, &pB) + return fmt.Sprintf("%v\n%v", pA, pB) + + default: + panic(fmt.Sprintf("invalid coinswap key prefix %X", kvA.Key[:1])) + } + } } diff --git a/x/coinswap/simulation/decoder_test.go b/x/coinswap/simulation/decoder_test.go new file mode 100644 index 00000000..a8161757 --- /dev/null +++ b/x/coinswap/simulation/decoder_test.go @@ -0,0 +1,59 @@ +package simulation_test + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/Canto-Network/Canto/v7/x/coinswap/simulation" + "github.com/Canto-Network/Canto/v7/x/coinswap/types" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/types/kv" +) + +func TestCoinSwapStore(t *testing.T) { + cdc := simapp.MakeTestEncodingConfig() + dec := simulation.NewDecodeStore(cdc.Marshaler) + + pool := types.Pool{ + Id: types.GetPoolId("denom1"), + StandardDenom: "denom2", + CounterpartyDenom: "denom1", + EscrowAddress: types.GetReservePoolAddr("lptDenom").String(), + LptDenom: "lptDenom", + } + + sequence := uint64(1) + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: []byte(types.KeyPool), Value: cdc.Marshaler.MustMarshal(&pool)}, + {Key: []byte(types.KeyPoolLptDenom), Value: cdc.Marshaler.MustMarshal(&pool)}, + {Key: []byte(types.KeyNextPoolSequence), Value: sdk.Uint64ToBigEndian(sequence)}, + {Key: []byte{0x99}, Value: []byte{0x99}}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"Pool", fmt.Sprintf("%v\n%v", pool, pool)}, + {"PoolLptDenom", fmt.Sprintf("%v\n%v", pool, pool)}, + {"NextPoolSequence", fmt.Sprintf("%v\n%v", sequence, sequence)}, + {"other", ""}, + } + for i, tt := range tests { + i, tt := i, tt + t.Run(tt.name, func(t *testing.T) { + switch i { + case len(tests) - 1: + require.Panics(t, func() { dec(kvPairs.Pairs[i], kvPairs.Pairs[i]) }, tt.name) + default: + require.Equal(t, tt.expectedLog, dec(kvPairs.Pairs[i], kvPairs.Pairs[i]), tt.name) + } + }) + } +} diff --git a/x/coinswap/simulation/genesis.go b/x/coinswap/simulation/genesis.go index 98b3a650..f029d36f 100644 --- a/x/coinswap/simulation/genesis.go +++ b/x/coinswap/simulation/genesis.go @@ -1,8 +1,80 @@ package simulation import ( + "encoding/json" + "fmt" + "math/rand" + + "github.com/Canto-Network/Canto/v7/x/coinswap/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" +) + +// simulation parameter constants +const ( + fee = "fee" + poolCreationFee = "pool_creation_fee" + taxRate = "tax_rate" + maxStandardCoinPerPool = "max_standard_coin_per_pool" + maxSwapAmount = "max_swap_amount" ) +func generateRandomFee(r *rand.Rand) sdk.Dec { + return sdk.NewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 10)), 3) +} + +func generateRandomPoolCreationFee(r *rand.Rand) sdk.Coin { + return sdk.NewInt64Coin(sdk.DefaultBondDenom, int64(simtypes.RandIntBetween(r, 0, 1000000))) +} + +func generateRandomTaxRate(r *rand.Rand) sdk.Dec { + return sdk.NewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 10)), 3) +} + +func generateRandomMaxStandardCoinPerPool(r *rand.Rand) sdk.Int { + return sdk.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 0, 10000)), 18) +} + +func generateRandomMaxSwapAmount(r *rand.Rand) sdk.Coins { + return sdk.NewCoins( + sdk.NewCoin(types.UsdcIBCDenom, sdk.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 6)), + sdk.NewCoin(types.UsdtIBCDenom, sdk.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 6)), + sdk.NewCoin(types.EthIBCDenom, sdk.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 16)), + ) +} + // RandomizedGenState generates a random GenesisState for coinswap -func RandomizedGenState(simState *module.SimulationState) {} +func RandomizedGenState(simState *module.SimulationState) { + genesis := types.DefaultGenesisState() + + simState.AppParams.GetOrGenerate( + simState.Cdc, fee, &genesis.Params.Fee, simState.Rand, + func(r *rand.Rand) { genesis.Params.Fee = generateRandomFee(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, poolCreationFee, &genesis.Params.PoolCreationFee, simState.Rand, + func(r *rand.Rand) { genesis.Params.PoolCreationFee = generateRandomPoolCreationFee(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, taxRate, &genesis.Params.TaxRate, simState.Rand, + func(r *rand.Rand) { genesis.Params.TaxRate = generateRandomTaxRate(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, maxStandardCoinPerPool, &genesis.Params.MaxStandardCoinPerPool, simState.Rand, + func(r *rand.Rand) { genesis.Params.MaxStandardCoinPerPool = generateRandomMaxStandardCoinPerPool(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, maxSwapAmount, &genesis.Params.MaxSwapAmount, simState.Rand, + func(r *rand.Rand) { genesis.Params.MaxSwapAmount = generateRandomMaxSwapAmount(r) }, + ) + + bz, _ := json.MarshalIndent(&genesis, "", " ") + fmt.Printf("Selected randomly generated coinswap parameters:\n%s\n", bz) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) + +} diff --git a/x/coinswap/simulation/genesis_test.go b/x/coinswap/simulation/genesis_test.go new file mode 100644 index 00000000..a5c0b8c6 --- /dev/null +++ b/x/coinswap/simulation/genesis_test.go @@ -0,0 +1,80 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/Canto-Network/Canto/v7/x/coinswap/simulation" + "github.com/Canto-Network/Canto/v7/x/coinswap/types" +) + +func TestRandomizedGenState(t *testing.T) { + interfaceRegistry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + + s := rand.NewSource(2) + r := rand.New(s) + + simState := module.SimulationState{ + AppParams: make(simtypes.AppParams), + Cdc: cdc, + Rand: r, + NumBonded: 3, + Accounts: simtypes.RandomAccounts(r, 3), + InitialStake: 1000, + GenState: make(map[string]json.RawMessage), + } + + simulation.RandomizedGenState(&simState) + + var genState types.GenesisState + simState.Cdc.MustUnmarshalJSON(simState.GenState[types.ModuleName], &genState) + + require.Equal(t, sdk.NewDecWithPrec(4, 3), genState.Params.Fee) + require.Equal(t, sdk.NewInt64Coin(sdk.DefaultBondDenom, 163511), genState.Params.PoolCreationFee) + require.Equal(t, sdk.NewDecWithPrec(6, 3), genState.Params.TaxRate) + require.Equal(t, sdk.NewIntWithDecimal(3310, 18), genState.Params.MaxStandardCoinPerPool) + require.Equal(t, sdk.NewCoins( + sdk.NewCoin(types.UsdcIBCDenom, sdk.NewIntWithDecimal(70, 6)), + sdk.NewCoin(types.UsdtIBCDenom, sdk.NewIntWithDecimal(52, 6)), + sdk.NewCoin(types.EthIBCDenom, sdk.NewIntWithDecimal(65, 16)), + ), genState.Params.MaxSwapAmount) + +} + +// TestInvalidGenesisState tests invalid genesis states. +func TestInvalidGenesisState(t *testing.T) { + interfaceRegistry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + + s := rand.NewSource(1) + r := rand.New(s) + + // all these tests will panic + tests := []struct { + simState module.SimulationState + panicMsg string + }{ + { // panic => reason: incomplete initialization of the simState + module.SimulationState{}, "invalid memory address or nil pointer dereference"}, + { // panic => reason: incomplete initialization of the simState + module.SimulationState{ + AppParams: make(simtypes.AppParams), + Cdc: cdc, + Rand: r, + }, "assignment to entry in nil map"}, + } + + for _, tt := range tests { + require.Panicsf(t, func() { simulation.RandomizedGenState(&tt.simState) }, tt.panicMsg) + } +} diff --git a/x/coinswap/simulation/operation_test.go b/x/coinswap/simulation/operation_test.go new file mode 100644 index 00000000..c8d00e02 --- /dev/null +++ b/x/coinswap/simulation/operation_test.go @@ -0,0 +1,126 @@ +package simulation_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/Canto-Network/Canto/v7/app/params" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/teststaking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/Canto-Network/Canto/v7/app" + "github.com/Canto-Network/Canto/v7/x/coinswap/simulation" + "github.com/Canto-Network/Canto/v7/x/coinswap/types" +) + +func TestWeightedOperations(t *testing.T) { + canto, ctx := createTestApp(t, false) + cdc := types.ModuleCdc + appParams := make(simtypes.AppParams) + + weightedOps := simulation.WeightedOperations( + appParams, + cdc, + canto.CoinswapKeeper, + canto.AccountKeeper, + canto.BankKeeper, + ) + + s := rand.NewSource(2) + r := rand.New(s) + accs := getTestingAccounts(t, r, canto, ctx, 10) + + expected := []struct { + weight int + opMsgRoute string + opMsgName string + }{ + {params.DefaultWeightMsgAddLiquidity, types.ModuleName, types.TypeMsgAddLiquidity}, + {params.DefaultWeightMsgSwapOrder, types.ModuleName, types.TypeMsgSwapOrder}, + {params.DefaultWeightMsgRemoveLiquidity, types.ModuleName, types.TypeMsgRemoveLiquidity}, + } + + for i, w := range weightedOps { + opMsg, _, _ := w.Op()(r, canto.BaseApp, ctx, accs, ctx.ChainID()) + require.Equal(t, expected[i].weight, w.Weight()) + require.Equal(t, expected[i].opMsgRoute, opMsg.Route) + require.Equal(t, expected[i].opMsgName, opMsg.Name) + } +} + +func createTestApp(t *testing.T, isCheckTx bool) (*app.Canto, sdk.Context) { + app := app.Setup(isCheckTx, nil) + r := rand.New(rand.NewSource(1)) + + simAccs := simtypes.RandomAccounts(r, 10) + + ctx := app.BaseApp.NewContext(isCheckTx, tmproto.Header{}) + validator := getTestingValidator0(t, app, ctx, simAccs) + consAddr, err := validator.GetConsAddr() + require.NoError(t, err) + ctx = ctx.WithBlockHeader(tmproto.Header{Height: 1, + ChainID: "canto_9001-1", + Time: time.Now().UTC(), + ProposerAddress: consAddr, + }) + return app, ctx +} + +func getTestingAccounts(t *testing.T, r *rand.Rand, app *app.Canto, ctx sdk.Context, n int) []simtypes.Account { + accounts := simtypes.RandomAccounts(r, n) + + initAmt := app.StakingKeeper.TokensFromConsensusPower(ctx, 100_000_000) + initCoins := sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, initAmt), + ) + + // add coins to the accounts + for _, account := range accounts { + acc := app.AccountKeeper.NewAccountWithAddress(ctx, account.Address) + app.AccountKeeper.SetAccount(ctx, acc) + err := fundAccount(app.BankKeeper, ctx, account.Address, initCoins) + require.NoError(t, err) + } + + return accounts +} + +func fundAccount(bk bankkeeper.Keeper, ctx sdk.Context, addr sdk.AccAddress, coins sdk.Coins) error { + if err := bk.MintCoins(ctx, types.ModuleName, coins); err != nil { + return err + } + if err := bk.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addr, coins); err != nil { + return err + } + return nil +} + +func getTestingValidator0(t *testing.T, app *app.Canto, ctx sdk.Context, accounts []simtypes.Account) stakingtypes.Validator { + commission0 := stakingtypes.NewCommission(sdk.ZeroDec(), sdk.OneDec(), sdk.OneDec()) + return getTestingValidator(t, app, ctx, accounts, commission0, 0) +} + +func getTestingValidator(t *testing.T, app *app.Canto, ctx sdk.Context, accounts []simtypes.Account, commission stakingtypes.Commission, n int) stakingtypes.Validator { + account := accounts[n] + valPubKey := account.PubKey + valAddr := sdk.ValAddress(account.PubKey.Address().Bytes()) + validator := teststaking.NewValidator(t, valAddr, valPubKey) + validator, err := validator.SetInitialCommission(commission) + require.NoError(t, err) + + validator.DelegatorShares = sdk.NewDec(100) + validator.Tokens = app.StakingKeeper.TokensFromConsensusPower(ctx, 100) + + app.StakingKeeper.SetValidator(ctx, validator) + app.StakingKeeper.SetValidatorByConsAddr(ctx, validator) + + return validator +} diff --git a/x/coinswap/simulation/operations.go b/x/coinswap/simulation/operations.go index d00e7218..bde5826e 100644 --- a/x/coinswap/simulation/operations.go +++ b/x/coinswap/simulation/operations.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/Canto-Network/Canto/v7/app/params" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/simapp/helpers" @@ -42,21 +43,21 @@ func WeightedOperations( appParams.GetOrGenerate( cdc, OpWeightMsgSwapOrder, &weightSwap, nil, func(_ *rand.Rand) { - weightSwap = 50 + weightSwap = params.DefaultWeightMsgSwapOrder }, ) appParams.GetOrGenerate( cdc, OpWeightMsgAddLiquidity, &weightAdd, nil, func(_ *rand.Rand) { - weightAdd = 100 + weightAdd = params.DefaultWeightMsgAddLiquidity }, ) appParams.GetOrGenerate( cdc, OpWeightMsgRemoveLiquidity, &weightRemove, nil, func(_ *rand.Rand) { - weightRemove = 30 + weightRemove = params.DefaultWeightMsgRemoveLiquidity }, ) @@ -105,6 +106,11 @@ func SimulateMsgAddLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk types.B return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddLiquidity, "tokenDenom should not be standardDenom"), nil, err } + _, err = k.GetMaximumSwapAmount(ctx, maxToken.Denom) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddLiquidity, err.Error()), nil, nil + } + if strings.HasPrefix(maxToken.Denom, types.LptTokenPrefix) { return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddLiquidity, "tokenDenom should not be liquidity token"), nil, err } @@ -212,6 +218,11 @@ func SimulateMsgSwapOrder(k keeper.Keeper, ak types.AccountKeeper, bk types.Bank // sold coin inputCoin = RandomSpendableToken(r, spendable) + _, err = k.GetMaximumSwapAmount(ctx, inputCoin.Denom) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSwapOrder, err.Error()), nil, nil + } + if strings.HasPrefix(inputCoin.Denom, types.LptTokenPrefix) { return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSwapOrder, "inputCoin should not be liquidity token"), nil, err } @@ -240,6 +251,11 @@ func SimulateMsgSwapOrder(k keeper.Keeper, ak types.AccountKeeper, bk types.Bank return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSwapOrder, "total supply is zero"), nil, err } outputCoin = RandomTotalToken(r, coins) + _, err = k.GetMaximumSwapAmount(ctx, inputCoin.Denom) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSwapOrder, err.Error()), nil, nil + } + if strings.HasPrefix(outputCoin.Denom, types.LptTokenPrefix) { return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSwapOrder, "outputCoin should not be liquidity token"), nil, err } diff --git a/x/coinswap/simulation/params.go b/x/coinswap/simulation/params.go index 7b2badc7..457769eb 100644 --- a/x/coinswap/simulation/params.go +++ b/x/coinswap/simulation/params.go @@ -1,10 +1,9 @@ package simulation import ( - "fmt" + "encoding/json" "math/rand" - sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" @@ -18,7 +17,51 @@ func ParamChanges(r *rand.Rand) []simtypes.ParamChange { simulation.NewSimParamChange( types.ModuleName, string(types.KeyFee), func(r *rand.Rand) string { - return fmt.Sprintf("\"%s\"", sdk.NewDecWithPrec(r.Int63n(3), 3)) // 0.1%~0.3% + bz, err := json.Marshal(generateRandomFee(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange( + types.ModuleName, string(types.KeyPoolCreationFee), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomPoolCreationFee(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange( + types.ModuleName, string(types.KeyTaxRate), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomTaxRate(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange( + types.ModuleName, string(types.KeyMaxStandardCoinPerPool), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomMaxStandardCoinPerPool(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange( + types.ModuleName, string(types.KeyMaxSwapAmount), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomMaxSwapAmount(r)) + if err != nil { + panic(err) + } + return string(bz) }, ), } diff --git a/x/coinswap/simulation/params_test.go b/x/coinswap/simulation/params_test.go new file mode 100644 index 00000000..ac49ac9d --- /dev/null +++ b/x/coinswap/simulation/params_test.go @@ -0,0 +1,37 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/Canto-Network/Canto/v7/x/coinswap/simulation" +) + +func TestParamChanges(t *testing.T) { + r := rand.New(rand.NewSource(0)) + + paramChanges := simulation.ParamChanges(r) + require.Len(t, paramChanges, 5) + + expected := []struct { + composedKey string + key string + simValue string + subspace string + }{ + {"coinswap/Fee", "Fee", "\"0.004000000000000000\"", "coinswap"}, + {"coinswap/PoolCreationFee", "PoolCreationFee", `{"denom":"stake","amount":"58514"}`, "coinswap"}, + {"coinswap/TaxRate", "TaxRate", "\"0.003000000000000000\"", "coinswap"}, + {"coinswap/MaxStandardCoinPerPool", "MaxStandardCoinPerPool", "\"2506000000000000000000\"", "coinswap"}, + {"coinswap/MaxSwapAmount", "MaxSwapAmount", `[{"denom":"ibc/17CD484EE7D9723B847D95015FA3EBD1572FD13BC84FB838F55B18A57450F25B","amount":"27000000"},{"denom":"ibc/4F6A2DEFEA52CD8D90966ADCB2BD0593D3993AB0DF7F6AEB3EFD6167D79237B0","amount":"35000000"},{"denom":"ibc/DC186CA7A8C009B43774EBDC825C935CABA9743504CE6037507E6E5CCE12858A","amount":"980000000000000000"}]`, "coinswap"}, + } + + for i, p := range paramChanges { + require.Equal(t, expected[i].composedKey, p.ComposedKey()) + require.Equal(t, expected[i].key, p.Key()) + require.Equal(t, expected[i].simValue, p.SimValue()(r)) + require.Equal(t, expected[i].subspace, p.Subspace()) + } +}