diff --git a/app/app.go b/app/app.go index 1b248bea..2b86fd0c 100644 --- a/app/app.go +++ b/app/app.go @@ -758,12 +758,12 @@ func NewCanto( feemarket.NewAppModule(app.FeeMarketKeeper, feeMarketSs), // Canto app modules - inflation.NewAppModule(app.InflationKeeper, app.AccountKeeper, *app.StakingKeeper), - erc20.NewAppModule(app.Erc20Keeper, app.AccountKeeper, app.AccountKeeper.AddressCodec()), + inflation.NewAppModule(appCodec, app.InflationKeeper, app.AccountKeeper, *app.StakingKeeper), + erc20.NewAppModule(appCodec, app.Erc20Keeper, app.AccountKeeper, app.BankKeeper, app.EvmKeeper, app.FeeMarketKeeper, app.AccountKeeper.AddressCodec()), epochs.NewAppModule(appCodec, app.EpochsKeeper), onboarding.NewAppModule(*app.OnboardingKeeper), - govshuttle.NewAppModule(app.GovshuttleKeeper, app.AccountKeeper, app.AccountKeeper.AddressCodec()), - csr.NewAppModule(app.CSRKeeper, app.AccountKeeper), + govshuttle.NewAppModule(appCodec, app.GovshuttleKeeper, app.AccountKeeper, app.AccountKeeper.AddressCodec()), + csr.NewAppModule(appCodec, app.CSRKeeper, app.AccountKeeper), coinswap.NewAppModule(appCodec, app.CoinswapKeeper, app.AccountKeeper, app.BankKeeper), ) diff --git a/app/params/weights.go b/app/params/weights.go new file mode 100644 index 00000000..8e1ce11a --- /dev/null +++ b/app/params/weights.go @@ -0,0 +1,16 @@ +package params + +const ( + DefaultWeightRegisterCoinProposal int = 5 + DefaultWeightRegisterERC20Proposal int = 5 + DefaultWeightToggleTokenConversionProposal int = 5 + DefaultWeightLendingMarketProposal int = 5 + DefaultWeightTreasuryProposal int = 5 + + DefaultWeightMsgConvertCoin int = 20 + DefaultWeightMsgConvertErc20 int = 20 + + DefaultWeightMsgSwapOrder int = 50 + DefaultWeightMsgAddLiquidity int = 10 + DefaultWeightMsgRemoveLiquidity int = 10 +) diff --git a/x/coinswap/module.go b/x/coinswap/module.go index 5b0731df..59916e5c 100644 --- a/x/coinswap/module.go +++ b/x/coinswap/module.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "math/rand" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" @@ -40,6 +39,11 @@ type AppModuleBasic struct { cdc codec.Codec } +// NewAppModuleBasic return a new AppModuleBasic +func NewAppModuleBasic(cdc codec.Codec) AppModuleBasic { + return AppModuleBasic{cdc: cdc} +} + // Name returns the coinswap module's name. func (AppModuleBasic) Name() string { return types.ModuleName } @@ -100,7 +104,7 @@ type AppModule struct { // NewAppModule creates a new AppModule object func NewAppModule(cdc codec.Codec, keeper keeper.Keeper, accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper) AppModule { return AppModule{ - AppModuleBasic: AppModuleBasic{cdc: cdc}, + AppModuleBasic: NewAppModuleBasic(cdc), keeper: keeper, accountKeeper: accountKeeper, bankKeeper: bankKeeper, @@ -157,13 +161,14 @@ func (AppModule) ProposalContents(simState module.SimulationState) []simtypes.We return nil } -// RandomizedParams creates randomized coinswap param changes for the simulator. -func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.LegacyParamChange { - return simulation.ParamChanges(r) +// ProposalMsgs returns msgs used for governance proposals for simulations. +func (AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { + return simulation.ProposalMsgs() } // RegisterStoreDecoder registers a decoder for coinswap module's types func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) } // WeightedOperations returns the all the coinswap module operations with their respective weights. 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..c8c44140 --- /dev/null +++ b/x/coinswap/simulation/decoder_test.go @@ -0,0 +1,61 @@ +package simulation_test + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + module "github.com/Canto-Network/Canto/v7/x/coinswap" + "github.com/Canto-Network/Canto/v7/x/coinswap/simulation" + "github.com/Canto-Network/Canto/v7/x/coinswap/types" + "github.com/cosmos/cosmos-sdk/types/kv" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" +) + +func TestCoinSwapStore(t *testing.T) { + encodingConfig := moduletestutil.MakeTestEncodingConfig(module.AppModuleBasic{}) + cdc := encodingConfig.Codec + dec := simulation.NewDecodeStore(cdc) + + 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.MustMarshal(&pool)}, + {Key: []byte(types.KeyPoolLptDenom), Value: cdc.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..9bd2ef67 100644 --- a/x/coinswap/simulation/genesis.go +++ b/x/coinswap/simulation/genesis.go @@ -1,8 +1,81 @@ package simulation import ( + "encoding/json" + "fmt" + "math/rand" + + sdkmath "cosmossdk.io/math" + "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) sdkmath.LegacyDec { + return sdkmath.LegacyNewDecWithPrec(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) sdkmath.LegacyDec { + return sdkmath.LegacyNewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 10)), 3) +} + +func generateRandomMaxStandardCoinPerPool(r *rand.Rand) sdkmath.Int { + return sdkmath.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 0, 10000)), 18) +} + +func generateRandomMaxSwapAmount(r *rand.Rand) sdk.Coins { + return sdk.NewCoins( + sdk.NewCoin(types.UsdcIBCDenom, sdkmath.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 6)), + sdk.NewCoin(types.UsdtIBCDenom, sdkmath.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 6)), + sdk.NewCoin(types.EthIBCDenom, sdkmath.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( + fee, &genesis.Params.Fee, simState.Rand, + func(r *rand.Rand) { genesis.Params.Fee = generateRandomFee(r) }, + ) + + simState.AppParams.GetOrGenerate( + poolCreationFee, &genesis.Params.PoolCreationFee, simState.Rand, + func(r *rand.Rand) { genesis.Params.PoolCreationFee = generateRandomPoolCreationFee(r) }, + ) + + simState.AppParams.GetOrGenerate( + taxRate, &genesis.Params.TaxRate, simState.Rand, + func(r *rand.Rand) { genesis.Params.TaxRate = generateRandomTaxRate(r) }, + ) + + simState.AppParams.GetOrGenerate( + maxStandardCoinPerPool, &genesis.Params.MaxStandardCoinPerPool, simState.Rand, + func(r *rand.Rand) { genesis.Params.MaxStandardCoinPerPool = generateRandomMaxStandardCoinPerPool(r) }, + ) + + simState.AppParams.GetOrGenerate( + 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..f2e86669 --- /dev/null +++ b/x/coinswap/simulation/genesis_test.go @@ -0,0 +1,81 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + "cosmossdk.io/math" + 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: math.NewInt(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, math.LegacyNewDecWithPrec(4, 3), genState.Params.Fee) + require.Equal(t, sdk.NewInt64Coin(sdk.DefaultBondDenom, 163511), genState.Params.PoolCreationFee) + require.Equal(t, math.LegacyNewDecWithPrec(6, 3), genState.Params.TaxRate) + require.Equal(t, math.NewIntWithDecimal(3310, 18), genState.Params.MaxStandardCoinPerPool) + require.Equal(t, sdk.NewCoins( + sdk.NewCoin(types.UsdcIBCDenom, math.NewIntWithDecimal(70, 6)), + sdk.NewCoin(types.UsdtIBCDenom, math.NewIntWithDecimal(52, 6)), + sdk.NewCoin(types.EthIBCDenom, math.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/operations.go b/x/coinswap/simulation/operation.go similarity index 53% rename from x/coinswap/simulation/operations.go rename to x/coinswap/simulation/operation.go index 6ad47484..ab1240e2 100644 --- a/x/coinswap/simulation/operations.go +++ b/x/coinswap/simulation/operation.go @@ -1,15 +1,14 @@ package simulation import ( - "fmt" + "errors" "math/rand" "strings" "time" - errorsmod "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" - "github.com/Canto-Network/Canto/v7/x/coinswap/keeper" - "github.com/Canto-Network/Canto/v7/x/coinswap/types" + + "github.com/Canto-Network/Canto/v7/app/params" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" @@ -17,13 +16,9 @@ import ( moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" -) -// coinswap message types -var ( - TypeMsgAddLiquidity = sdk.MsgTypeURL(&types.MsgAddLiquidity{}) - TypeMsgRemoveLiquidity = sdk.MsgTypeURL(&types.MsgRemoveLiquidity{}) - TypeMsgSwapOrder = sdk.MsgTypeURL(&types.MsgSwapOrder{}) + "github.com/Canto-Network/Canto/v7/x/coinswap/keeper" + "github.com/Canto-Network/Canto/v7/x/coinswap/types" ) // Simulation operation weights constants @@ -33,6 +28,12 @@ const ( OpWeightMsgRemoveLiquidity = "op_weight_msg_remove_liquidity" ) +var ( + TypeMsgSwapOrder = sdk.MsgTypeURL(&types.MsgSwapOrder{}) + TypeMsgAddLiquidity = sdk.MsgTypeURL(&types.MsgAddLiquidity{}) + TypeMsgRemoveLiquidity = sdk.MsgTypeURL(&types.MsgRemoveLiquidity{}) +) + func WeightedOperations( appParams simtypes.AppParams, cdc codec.JSONCodec, @@ -49,21 +50,21 @@ func WeightedOperations( appParams.GetOrGenerate( OpWeightMsgSwapOrder, &weightSwap, nil, func(_ *rand.Rand) { - weightSwap = 50 + weightSwap = params.DefaultWeightMsgSwapOrder }, ) appParams.GetOrGenerate( OpWeightMsgAddLiquidity, &weightAdd, nil, func(_ *rand.Rand) { - weightAdd = 100 + weightAdd = params.DefaultWeightMsgAddLiquidity }, ) appParams.GetOrGenerate( OpWeightMsgRemoveLiquidity, &weightRemove, nil, func(_ *rand.Rand) { - weightRemove = 30 + weightRemove = params.DefaultWeightMsgRemoveLiquidity }, ) @@ -92,6 +93,10 @@ func SimulateMsgAddLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk types.B opMsg simtypes.OperationMsg, fOps []simtypes.FutureOperation, err error, ) { simAccount, _ := simtypes.RandomAcc(r, accs) + err = FundAccount(r, ctx, k, bk, simAccount.Address) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "unable to fund account"), nil, err + } account := ak.GetAccount(ctx, simAccount.Address) var ( @@ -99,57 +104,107 @@ func SimulateMsgAddLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk types.B minLiquidity sdkmath.Int ) - standardDenom, err := k.GetStandardDenom(ctx) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "failed to get standardDenom"), nil, nil - } - + standardDenom, _ := k.GetStandardDenom(ctx) spendable := bk.SpendableCoins(ctx, account.GetAddress()) - exactStandardAmt := simtypes.RandomAmount(r, spendable.AmountOf(standardDenom)) - if !exactStandardAmt.IsPositive() { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "standardAmount should be positive"), nil, nil + exactStandardAmt, err := simtypes.RandPositiveInt(r, spendable.AmountOf(standardDenom)) + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgAddLiquidity, + "standardAmount should be positive", + ), nil, nil + } + params := k.GetParams(ctx) + if exactStandardAmt.GTE(params.MaxStandardCoinPerPool) { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgAddLiquidity, + "standardAmount should be less than MaxStandardCoinPerPool", + ), nil, nil + } + + maxToken, err = randToken(r, spendable) + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgAddLiquidity, + "insufficient funds", + ), nil, nil } - maxToken = RandomSpendableToken(r, spendable) if maxToken.Denom == standardDenom { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "tokenDenom should not be standardDenom"), nil, err + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgAddLiquidity, + "tokenDenom should not be standardDenom", + ), nil, nil } if strings.HasPrefix(maxToken.Denom, types.LptTokenPrefix) { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "tokenDenom should not be liquidity token"), nil, err + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgAddLiquidity, + "tokenDenom should not be liquidity token", + ), nil, nil } if !maxToken.Amount.IsPositive() { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "maxToken must is positive"), nil, err - } - - poolId := types.GetPoolId(maxToken.Denom) - pool, has := k.GetPool(ctx, poolId) - if has { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "pool not found"), nil, err - } - - reservePool, err := k.GetPoolBalances(ctx, pool.EscrowAddress) - - if err != nil { - minLiquidity = exactStandardAmt - } else { - standardReserveAmt := reservePool.AmountOf(standardDenom) - liquidity := bk.GetSupply(ctx, pool.LptDenom).Amount - minLiquidity = liquidity.Mul(exactStandardAmt).Quo(standardReserveAmt) - - if !maxToken.Amount.Sub(reservePool.AmountOf(maxToken.GetDenom()).Mul(exactStandardAmt).Quo(standardReserveAmt)).IsPositive() { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "insufficient funds"), nil, err + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgAddLiquidity, + "maxToken must is positive", + ), nil, err + } + + // check maxToken is registered in MaxSwapAmount + found := func(denom string) bool { + MaxSwapAmount := params.MaxSwapAmount + for _, coin := range MaxSwapAmount { + if coin.Denom == denom { + return true + } } + return false + }(maxToken.Denom) - params := k.GetParams(ctx) - poolCreationFee := params.PoolCreationFee + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgAddLiquidity, + "maxToken is not registered in MaxSwapAmount", + ), nil, err + } + poolID := types.GetPoolId(maxToken.Denom) + pool, has := k.GetPool(ctx, poolID) + if !has { + poolCreationFee := k.GetParams(ctx).PoolCreationFee spendTotal := poolCreationFee.Amount if strings.EqualFold(poolCreationFee.Denom, standardDenom) { spendTotal = spendTotal.Add(exactStandardAmt) } if spendable.AmountOf(poolCreationFee.Denom).LT(spendTotal) { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgAddLiquidity, + "insufficient funds", + ), nil, err + } + minLiquidity = exactStandardAmt + } else { + balances, err := k.GetPoolBalances(ctx, pool.EscrowAddress) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "pool address not found"), nil, err + } + + standardReserveAmt := balances.AmountOf(standardDenom) + if !standardReserveAmt.IsPositive() { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "standardReserveAmt should be positive"), nil, err + } + liquidity := bk.GetSupply(ctx, pool.LptDenom).Amount + minLiquidity = liquidity.Mul(exactStandardAmt).Quo(standardReserveAmt) + + if !maxToken.Amount.Sub(balances.AmountOf(maxToken.Denom).Mul(exactStandardAmt).Quo(standardReserveAmt)).IsPositive() { return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "insufficient funds"), nil, err } } @@ -164,18 +219,20 @@ func SimulateMsgAddLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk types.B ) var fees sdk.Coins - coinsTemp, hasNeg := spendable.SafeSub(sdk.NewCoin(standardDenom, exactStandardAmt)) - coinsTemp, hasNeg = coinsTemp.SafeSub(maxToken) - + coinsTemp, hasNeg := spendable.SafeSub( + sdk.NewCoins(sdk.NewCoin(standardDenom, exactStandardAmt), maxToken)...) if !hasNeg { fees, err = simtypes.RandomFees(r, ctx, coinsTemp) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate fees"), nil, nil + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgAddLiquidity, + "unable to generate fees", + ), nil, nil } } txGen := moduletestutil.MakeTestEncodingConfig().TxConfig - tx, err := simtestutil.GenSignedMockTx( r, txGen, @@ -187,13 +244,16 @@ func SimulateMsgAddLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk types.B []uint64{account.GetSequence()}, simAccount.PrivKey, ) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate mock tx"), nil, err + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgAddLiquidity, + "unable to generate mock tx", + ), nil, err } - if _, _, err := app.SimTxFinalizeBlock(txGen.TxEncoder(), tx); err != nil { - return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to deliver tx"), nil, err + if _, _, err := app.SimDeliver(txGen.TxEncoder(), tx); err != nil { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgAddLiquidity, "unable to deliver tx"), nil, err } return simtypes.NewOperationMsg(msg, true, ""), nil, nil @@ -214,96 +274,108 @@ func SimulateMsgSwapOrder(k keeper.Keeper, ak types.AccountKeeper, bk types.Bank ) simAccount, _ := simtypes.RandomAcc(r, accs) - account := ak.GetAccount(ctx, simAccount.Address) - spendable := bk.SpendableCoins(ctx, account.GetAddress()) - standardDenom, err := k.GetStandardDenom(ctx) + err = FundAccount(r, ctx, k, bk, simAccount.Address) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "failed to get standardDenom"), nil, err + return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "unable to fund account"), nil, err } + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + standardDenom, _ := k.GetStandardDenom(ctx) if spendable.IsZero() { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "spendable is zero"), nil, err - } - - // sold coin - inputCoin = RandomSpendableToken(r, spendable) - - if strings.HasPrefix(inputCoin.Denom, types.LptTokenPrefix) { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "inputCoin should not be liquidity token"), nil, err + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgSwapOrder, + "spendable is zero", + ), nil, err } - if !inputCoin.Amount.IsPositive() { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "inputCoin must is positive"), nil, err + pools := k.GetAllPools(ctx) + if len(pools) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgSwapOrder, + "no pool found", + ), nil, nil } - poolId := types.GetPoolId(inputCoin.Denom) - pool, has := k.GetPool(ctx, poolId) - if !has { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "inputCoin should exist in the pool"), nil, nil - } - - if _, err := k.GetPoolBalancesByLptDenom(ctx, pool.LptDenom); err != nil { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "inputCoin should exist in the pool"), nil, nil - } + pool := pools[r.Intn(len(pools))] - // bought coin - var coins sdk.Coins - bk.IterateTotalSupply(ctx, func(coin sdk.Coin) bool { - coins = append(coins, coin) - return false - }) - if coins.IsZero() { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "total supply is zero"), nil, err - } - outputCoin = RandomTotalToken(r, coins) - if strings.HasPrefix(outputCoin.Denom, types.LptTokenPrefix) { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "outputCoin should not be liquidity token"), nil, err - } - - if !outputCoin.Amount.IsPositive() { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "outputCoin must is positive"), nil, err + reservePool, err := k.GetPoolBalancesByLptDenom(ctx, pool.LptDenom) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgRemoveLiquidity, "inputCoin should exist in the pool"), nil, nil } - poolId = types.GetPoolId(outputCoin.Denom) - pool, has = k.GetPool(ctx, poolId) - if !has { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "inputCoin should exist in the pool"), nil, nil - } + standardReserveAmt := reservePool.AmountOf(standardDenom) + tokenReserveAmt := reservePool.AmountOf(pool.CounterpartyDenom) - if _, err := k.GetPoolBalancesByLptDenom(ctx, pool.LptDenom); err != nil { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "inputCoin should exist in the pool"), nil, nil + if !standardReserveAmt.IsPositive() || !tokenReserveAmt.IsPositive() { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgSwapOrder, + "reserve pool should be positive", + ), nil, nil } - if outputCoin.Denom == inputCoin.Denom { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "outputCoin denom and inputcoin denom should not be the same"), nil, nil + // sold coin + tokenToStandard := randBoolean(r) + swapLimit := k.GetParams(ctx).MaxSwapAmount.AmountOf(pool.CounterpartyDenom) + if tokenToStandard { + inputCoin = sdk.NewCoin(pool.CounterpartyDenom, simtypes.RandomAmount(r, swapLimit)) + outputCoin = sdk.NewCoin(standardDenom, simtypes.RandomAmount(r, swapLimit)) + } else { + inputCoin = sdk.NewCoin(standardDenom, simtypes.RandomAmount(r, swapLimit)) + outputCoin = sdk.NewCoin(pool.CounterpartyDenom, simtypes.RandomAmount(r, swapLimit)) } - isDoubleSwap := (outputCoin.Denom != standardDenom) && (inputCoin.Denom != standardDenom) isBuyOrder = randBoolean(r) - if isBuyOrder && isDoubleSwap { - inputCoin, outputCoin, err = doubleSwapBill(inputCoin, outputCoin, ctx, k) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, err.Error()), nil, nil - } - } else if isBuyOrder && !isDoubleSwap { + if isBuyOrder { inputCoin, outputCoin, err = singleSwapBill(inputCoin, outputCoin, ctx, k) if err != nil { return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, err.Error()), nil, nil } - } else if !isBuyOrder && isDoubleSwap { - inputCoin, outputCoin, err = doubleSwapSellOrder(inputCoin, outputCoin, ctx, k) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, err.Error()), nil, nil + if tokenToStandard && inputCoin.Amount.GTE(swapLimit) { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgSwapOrder, + "inputCoin amount should be less than swapLimit", + ), nil, nil + } + if inputCoin.Amount.GTE(spendable.AmountOf(inputCoin.Denom)) { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgSwapOrder, + "insufficient funds", + ), nil, nil } - } else if !isBuyOrder && !isDoubleSwap { + } else { inputCoin, outputCoin, err = singleSwapSellOrder(inputCoin, outputCoin, ctx, k) if err != nil { return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, err.Error()), nil, nil } + if !tokenToStandard && outputCoin.Amount.GTE(swapLimit) { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgSwapOrder, + "outputCoin amount should be less than swapLimit", + ), nil, nil + } + if inputCoin.Amount.GTE(spendable.AmountOf(inputCoin.Denom)) { + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgSwapOrder, + "insufficient funds", + ), nil, nil + } } + if !outputCoin.Amount.IsPositive() { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "outputCoin must is positive"), nil, err + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgSwapOrder, + "outputCoin must is positive", + ), nil, err } deadline := randDeadline(r) @@ -321,11 +393,16 @@ func SimulateMsgSwapOrder(k keeper.Keeper, ak types.AccountKeeper, bk types.Bank ) var fees sdk.Coins - coinsTemp, hasNeg := spendable.SafeSub(sdk.NewCoin(inputCoin.Denom, inputCoin.Amount)) + coinsTemp, hasNeg := spendable.SafeSub( + sdk.NewCoins(sdk.NewCoin(inputCoin.Denom, inputCoin.Amount))...) if !hasNeg { fees, err = simtypes.RandomFees(r, ctx, coinsTemp) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate fees"), nil, nil + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgSwapOrder, + "unable to generate fees", + ), nil, nil } } @@ -341,13 +418,16 @@ func SimulateMsgSwapOrder(k keeper.Keeper, ak types.AccountKeeper, bk types.Bank []uint64{account.GetSequence()}, simAccount.PrivKey, ) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate mock tx"), nil, err + return simtypes.NoOpMsg( + types.ModuleName, + TypeMsgSwapOrder, + "unable to generate mock tx", + ), nil, err } - if _, _, err := app.SimTxFinalizeBlock(txGen.TxEncoder(), tx); err != nil { - return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to deliver tx"), nil, err + if _, _, err := app.SimDeliver(txGen.TxEncoder(), tx); err != nil { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "unable to deliver tx"), nil, err } return simtypes.NewOperationMsg(msg, true, ""), nil, nil @@ -361,13 +441,32 @@ func SimulateMsgRemoveLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk type ) ( opMsg simtypes.OperationMsg, fOps []simtypes.FutureOperation, err error, ) { - simAccount, _ := simtypes.RandomAcc(r, accs) - account := ak.GetAccount(ctx, simAccount.Address) - standardDenom, err := k.GetStandardDenom(ctx) + + pools := k.GetAllPools(ctx) + if len(pools) == 0 { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgRemoveLiquidity, "no pool found"), nil, nil + } + + pool := pools[r.Intn(len(pools))] + + simAccount, err := func(accs []simtypes.Account) (simtypes.Account, error) { + for _, acc := range accs { + coins := bk.GetAllBalances(ctx, acc.Address) + for _, coin := range coins { + if coin.Denom == pool.LptDenom { + return acc, nil + } + } + } + return simtypes.Account{}, errors.New("no account has LptCoin") + }(accs) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "failed to get standardDenom"), nil, err + return simtypes.NoOpMsg(types.ModuleName, TypeMsgRemoveLiquidity, err.Error()), nil, nil } + account := ak.GetAccount(ctx, simAccount.Address) + standardDenom, _ := k.GetStandardDenom(ctx) + var ( minToken sdkmath.Int minStandardAmt sdkmath.Int @@ -376,30 +475,19 @@ func SimulateMsgRemoveLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk type spendable := bk.SpendableCoins(ctx, account.GetAddress()) if spendable.IsZero() { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "spendable is zero"), nil, err - } - - token := RandomSpendableToken(r, spendable) - - if token.Denom == standardDenom { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgRemoveLiquidity, "tokenDenom should not be standardDenom"), nil, err - } - - pool, has := k.GetPoolByLptDenom(ctx, token.Denom) - if !has { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "inputCoin should exist in the pool"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, TypeMsgRemoveLiquidity, "spendable is zero"), nil, err } reservePool, err := k.GetPoolBalancesByLptDenom(ctx, pool.LptDenom) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, TypeMsgSwapOrder, "inputCoin should exist in the pool"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, TypeMsgRemoveLiquidity, "inputCoin should exist in the pool"), nil, nil } standardReserveAmt := reservePool.AmountOf(standardDenom) tokenReserveAmt := reservePool.AmountOf(pool.CounterpartyDenom) - withdrawLiquidity = sdk.NewCoin(token.GetDenom(), simtypes.RandomAmount(r, token.Amount)) - liquidityReserve := bk.GetSupply(ctx, token.Denom).Amount + withdrawLiquidity = sdk.NewCoin(pool.LptDenom, simtypes.RandomAmount(r, spendable.AmountOf(pool.LptDenom))) + liquidityReserve := bk.GetSupply(ctx, pool.LptDenom).Amount if !withdrawLiquidity.IsValid() || !withdrawLiquidity.IsPositive() { return simtypes.NoOpMsg(types.ModuleName, TypeMsgRemoveLiquidity, "invalid withdrawLiquidity"), nil, nil @@ -428,12 +516,11 @@ func SimulateMsgRemoveLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk type ) var fees sdk.Coins - coinsTemp, hasNeg := spendable.SafeSub(sdk.NewCoin(pool.CounterpartyDenom, minToken)) - coinsTemp, hasNeg = coinsTemp.SafeSub(sdk.NewCoin(standardDenom, minStandardAmt)) + coinsTemp, hasNeg := spendable.SafeSub(sdk.NewCoins(sdk.NewCoin(pool.CounterpartyDenom, minToken), sdk.NewCoin(standardDenom, minStandardAmt))...) if !hasNeg { fees, err = simtypes.RandomFees(r, ctx, coinsTemp) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate fees"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, TypeMsgRemoveLiquidity, "unable to generate fees"), nil, nil } } @@ -452,11 +539,11 @@ func SimulateMsgRemoveLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk type ) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate mock tx"), nil, err + return simtypes.NoOpMsg(types.ModuleName, TypeMsgRemoveLiquidity, "unable to generate mock tx"), nil, err } - if _, _, err := app.SimTxFinalizeBlock(txGen.TxEncoder(), tx); err != nil { - return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to deliver tx"), nil, nil + if _, _, err := app.SimDeliver(txGen.TxEncoder(), tx); err != nil { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgRemoveLiquidity, "unable to deliver tx"), nil, nil } return simtypes.NewOperationMsg(msg, true, ""), nil, nil @@ -464,6 +551,37 @@ func SimulateMsgRemoveLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk type } } +func FundAccount(r *rand.Rand, ctx sdk.Context, k keeper.Keeper, bk types.BankKeeper, account sdk.AccAddress) error { + params := k.GetParams(ctx) + MaxSwapAmount := params.MaxSwapAmount + + for _, coin := range MaxSwapAmount { + denom := coin.Denom + randomAmount := simtypes.RandomAmount(r, sdkmath.NewInt(100000000000000)) + err := bk.MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin(denom, randomAmount))) + if err != nil { + return errors.New("unable to mint coins") + } + err = bk.SendCoinsFromModuleToAccount(ctx, types.ModuleName, account, sdk.NewCoins(sdk.NewCoin(denom, randomAmount))) + if err != nil { + return errors.New("unable to send coins") + } + } + return nil +} + +func randToken(r *rand.Rand, spendableCoin sdk.Coins) (sdk.Coin, error) { + if len(spendableCoin) == 0 { + return sdk.Coin{}, errors.New("insufficient funds") + } + token := spendableCoin[r.Intn(len(spendableCoin))] + randAmt, err := simtypes.RandPositiveInt(r, token.Amount.QuoRaw(4)) + if err != nil { + return sdk.Coin{}, errors.New("insufficient funds") + } + return sdk.NewCoin(token.Denom, randAmt), nil +} + func RandomSpendableToken(r *rand.Rand, spendableCoin sdk.Coins) sdk.Coin { token := spendableCoin[r.Intn(len(spendableCoin))] return sdk.NewCoin(token.Denom, simtypes.RandomAmount(r, token.Amount.QuoRaw(2))) @@ -483,37 +601,6 @@ func randBoolean(r *rand.Rand) bool { return r.Int()%2 == 0 } -// Double swap bill -func doubleSwapBill(inputCoin, outputCoin sdk.Coin, ctx sdk.Context, k keeper.Keeper) (sdk.Coin, sdk.Coin, error) { - standardDenom, err := k.GetStandardDenom(ctx) - if err != nil { - return sdk.Coin{}, sdk.Coin{}, err - } - - param := k.GetParams(ctx) - - // generate sold standard Coin - lptDenom, _ := k.GetLptDenomFromDenoms(ctx, outputCoin.Denom, standardDenom) - reservePool, _ := k.GetPoolBalancesByLptDenom(ctx, lptDenom) - outputReserve := reservePool.AmountOf(outputCoin.Denom) - inputReserve := reservePool.AmountOf(standardDenom) - if outputCoin.Amount.GTE(outputReserve) { - return sdk.Coin{}, sdk.Coin{}, errorsmod.Wrap(types.ErrConstraintNotMet, fmt.Sprintf("insufficient amount of %s, user expected: %s, actual: %s", outputCoin.Denom, outputCoin.Amount, outputReserve)) - } - soldStandardAmount := keeper.GetOutputPrice(outputCoin.Amount, inputReserve, outputReserve, param.Fee) - soldStandardCoin := sdk.NewCoin(standardDenom, soldStandardAmount) - - // generate input coin - lptDenom2, _ := k.GetLptDenomFromDenoms(ctx, soldStandardCoin.Denom, inputCoin.Denom) - reservePool2, _ := k.GetPoolBalancesByLptDenom(ctx, lptDenom2) - outputReserve2 := reservePool2.AmountOf(soldStandardCoin.Denom) - inputReserve2 := reservePool2.AmountOf(inputCoin.Denom) - soldTokenAmt := keeper.GetOutputPrice(soldStandardCoin.Amount, inputReserve2, outputReserve2, param.Fee) - inputCoin = sdk.NewCoin(inputCoin.Denom, soldTokenAmt) - - return inputCoin, outputCoin, nil -} - // A single swap bill func singleSwapBill(inputCoin, outputCoin sdk.Coin, ctx sdk.Context, k keeper.Keeper) (sdk.Coin, sdk.Coin, error) { param := k.GetParams(ctx) @@ -523,33 +610,11 @@ func singleSwapBill(inputCoin, outputCoin sdk.Coin, ctx sdk.Context, k keeper.Ke outputReserve := reservePool.AmountOf(outputCoin.Denom) inputReserve := reservePool.AmountOf(inputCoin.Denom) soldTokenAmt := keeper.GetOutputPrice(outputCoin.Amount, inputReserve, outputReserve, param.Fee) - inputCoin = sdk.NewCoin(inputCoin.Denom, soldTokenAmt) - - return inputCoin, outputCoin, nil -} -// Double swap sell orders -func doubleSwapSellOrder(inputCoin, outputCoin sdk.Coin, ctx sdk.Context, k keeper.Keeper) (sdk.Coin, sdk.Coin, error) { - standardDenom, err := k.GetStandardDenom(ctx) - if err != nil { - return sdk.Coin{}, sdk.Coin{}, err + if soldTokenAmt.IsNegative() { + return sdk.Coin{}, sdk.Coin{}, errors.New("wrong token price calcualtion") } - - param := k.GetParams(ctx) - - lptDenom, _ := k.GetLptDenomFromDenoms(ctx, inputCoin.Denom, standardDenom) - reservePool, _ := k.GetPoolBalancesByLptDenom(ctx, lptDenom) - inputReserve := reservePool.AmountOf(inputCoin.Denom) - outputReserve := reservePool.AmountOf(standardDenom) - standardAmount := keeper.GetInputPrice(inputCoin.Amount, inputReserve, outputReserve, param.Fee) - standardCoin := sdk.NewCoin(standardDenom, standardAmount) - - lptDenom2, _ := k.GetLptDenomFromDenoms(ctx, standardCoin.Denom, outputCoin.Denom) - reservePool2, _ := k.GetPoolBalancesByLptDenom(ctx, lptDenom2) - inputReserve2 := reservePool2.AmountOf(standardCoin.Denom) - outputReserve2 := reservePool2.AmountOf(outputCoin.Denom) - boughtTokenAmt := keeper.GetInputPrice(standardCoin.Amount, inputReserve2, outputReserve2, param.Fee) - outputCoin = sdk.NewCoin(outputCoin.Denom, boughtTokenAmt) + inputCoin = sdk.NewCoin(inputCoin.Denom, soldTokenAmt) return inputCoin, outputCoin, nil } diff --git a/x/coinswap/simulation/operation_test.go b/x/coinswap/simulation/operation_test.go new file mode 100644 index 00000000..3df06f99 --- /dev/null +++ b/x/coinswap/simulation/operation_test.go @@ -0,0 +1,127 @@ +package simulation_test + +import ( + "math/rand" + "testing" + "time" + + sdkmath "cosmossdk.io/math" + "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/testutil" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + cmtproto "github.com/cometbft/cometbft/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, sdk.MsgTypeURL(&types.MsgAddLiquidity{})}, + {params.DefaultWeightMsgSwapOrder, types.ModuleName, sdk.MsgTypeURL(&types.MsgSwapOrder{})}, + {params.DefaultWeightMsgRemoveLiquidity, types.ModuleName, sdk.MsgTypeURL(&types.MsgRemoveLiquidity{})}, + } + + 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) + validator := getTestingValidator0(t, app, ctx, simAccs) + consAddr, err := validator.GetConsAddr() + require.NoError(t, err) + ctx = ctx.WithBlockHeader(cmtproto.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(sdkmath.LegacyZeroDec(), sdkmath.LegacyOneDec(), sdkmath.LegacyOneDec()) + 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 := testutil.NewValidator(t, valAddr, valPubKey) + validator, err := validator.SetInitialCommission(commission) + require.NoError(t, err) + + validator.DelegatorShares = sdkmath.LegacyNewDec(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/params.go b/x/coinswap/simulation/params.go deleted file mode 100644 index f5e8f815..00000000 --- a/x/coinswap/simulation/params.go +++ /dev/null @@ -1,25 +0,0 @@ -package simulation - -import ( - "fmt" - "math/rand" - - sdkmath "cosmossdk.io/math" - simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "github.com/cosmos/cosmos-sdk/x/simulation" - - "github.com/Canto-Network/Canto/v7/x/coinswap/types" -) - -// ParamChanges defines the parameters that can be modified by param change proposals -// on the simulation -func ParamChanges(r *rand.Rand) []simtypes.LegacyParamChange { - return []simtypes.LegacyParamChange{ - simulation.NewSimLegacyParamChange( - types.ModuleName, string(types.KeyFee), - func(r *rand.Rand) string { - return fmt.Sprintf("\"%s\"", sdkmath.LegacyNewDecWithPrec(r.Int63n(3), 3)) // 0.1%~0.3% - }, - ), - } -} diff --git a/x/coinswap/simulation/proposal.go b/x/coinswap/simulation/proposal.go new file mode 100644 index 00000000..08ca66f7 --- /dev/null +++ b/x/coinswap/simulation/proposal.go @@ -0,0 +1,52 @@ +package simulation + +import ( + "math/rand" + + math "cosmossdk.io/math" + "github.com/Canto-Network/Canto/v7/x/coinswap/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +// Simulation operation weights constants +const ( + DefaultWeightMsgUpdateParams int = 100 + + OpWeightMsgUpdateParams = "op_weight_msg_update_params" +) + +// ProposalMsgs defines the module weighted proposals' contents +func ProposalMsgs() []simtypes.WeightedProposalMsg { + return []simtypes.WeightedProposalMsg{ + simulation.NewWeightedProposalMsg( + OpWeightMsgUpdateParams, + DefaultWeightMsgUpdateParams, + SimulateMsgUpdateParams, + ), + } +} + +// SimulateMsgUpdateParams returns a random MsgUpdateParams +func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { + // use the default gov module account address as authority + var authority sdk.AccAddress = address.Module("gov") + + params := types.DefaultParams() + params.Fee = math.LegacyNewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 10)), 3) + params.PoolCreationFee = sdk.NewInt64Coin(sdk.DefaultBondDenom, int64(simtypes.RandIntBetween(r, 0, 1000000))) + params.TaxRate = math.LegacyNewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 10)), 3) + params.MaxStandardCoinPerPool = math.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 0, 1000000)), 18) + params.MaxSwapAmount = sdk.NewCoins( + sdk.NewCoin(types.UsdcIBCDenom, math.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 6)), + sdk.NewCoin(types.UsdtIBCDenom, math.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 6)), + sdk.NewCoin(types.EthIBCDenom, math.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 16)), + ) + + return &types.MsgUpdateParams{ + Authority: authority.String(), + Params: params, + } +} diff --git a/x/coinswap/simulation/proposal_test.go b/x/coinswap/simulation/proposal_test.go new file mode 100644 index 00000000..d0bde3a2 --- /dev/null +++ b/x/coinswap/simulation/proposal_test.go @@ -0,0 +1,48 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + "cosmossdk.io/math" + "github.com/Canto-Network/Canto/v7/x/coinswap/simulation" + "github.com/Canto-Network/Canto/v7/x/coinswap/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/stretchr/testify/require" +) + +func TestProposalMsgs(t *testing.T) { + // initialize parameters + s := rand.NewSource(1) + r := rand.New(s) + + ctx := sdk.NewContext(nil, cmtproto.Header{}, true, nil) + accounts := simtypes.RandomAccounts(r, 3) + + // execute ProposalMsgs function + weightedProposalMsgs := simulation.ProposalMsgs() + require.Equal(t, 1, len(weightedProposalMsgs)) + + w0 := weightedProposalMsgs[0] + + // tests w0 interface: + require.Equal(t, simulation.OpWeightMsgUpdateParams, w0.AppParamsKey()) + require.Equal(t, simulation.DefaultWeightMsgUpdateParams, w0.DefaultWeight()) + + msg := w0.MsgSimulatorFn()(r, ctx, accounts) + msgUpdateParams, ok := msg.(*types.MsgUpdateParams) + require.True(t, ok) + require.Equal(t, sdk.AccAddress(address.Module("gov")).String(), msgUpdateParams.Authority) + require.Equal(t, math.LegacyNewDecWithPrec(0, 3), msgUpdateParams.Params.Fee) + require.Equal(t, sdk.NewInt64Coin(sdk.DefaultBondDenom, 240456), msgUpdateParams.Params.PoolCreationFee) + require.Equal(t, math.LegacyNewDecWithPrec(0, 3), msgUpdateParams.Params.TaxRate) + require.Equal(t, math.NewIntWithDecimal(410694, 18), msgUpdateParams.Params.MaxStandardCoinPerPool) + require.Equal(t, sdk.NewCoins( + sdk.NewCoin(types.UsdcIBCDenom, math.NewIntWithDecimal(89, 6)), + sdk.NewCoin(types.UsdtIBCDenom, math.NewIntWithDecimal(22, 6)), + sdk.NewCoin(types.EthIBCDenom, math.NewIntWithDecimal(12, 16)), + ), msgUpdateParams.Params.MaxSwapAmount) +} diff --git a/x/coinswap/types/msgs.go b/x/coinswap/types/msgs.go index cddda09c..ffc2e8d5 100644 --- a/x/coinswap/types/msgs.go +++ b/x/coinswap/types/msgs.go @@ -23,6 +23,10 @@ const ( LptTokenFormat = "lpt-%d" ) +/* --------------------------------------------------------------------------- */ +// MsgSwapOrder +/* --------------------------------------------------------------------------- */ + // MsgSwapOrder - struct for swapping a coin // Input and Output can either be exact or calculated. // An exact coin has the senders desired buy or sell amount. diff --git a/x/csr/module.go b/x/csr/module.go index 97fd9678..985d4ca4 100644 --- a/x/csr/module.go +++ b/x/csr/module.go @@ -7,6 +7,7 @@ import ( // this line is used by starport scaffolding # 1 + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" @@ -21,14 +22,16 @@ import ( "github.com/Canto-Network/Canto/v7/x/csr/client/cli" "github.com/Canto-Network/Canto/v7/x/csr/keeper" + "github.com/Canto-Network/Canto/v7/x/csr/simulation" "github.com/Canto-Network/Canto/v7/x/csr/types" ) var ( - _ module.AppModuleBasic = AppModuleBasic{} - _ module.AppModuleBasic = AppModule{} - _ module.HasServices = AppModule{} - _ module.HasABCIGenesis = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + _ module.AppModuleBasic = AppModule{} + _ module.HasServices = AppModule{} + _ module.HasABCIGenesis = AppModule{} + _ module.AppModuleSimulation = AppModule{} _ appmodule.AppModule = AppModule{} _ appmodule.HasBeginBlocker = AppModule{} @@ -40,10 +43,10 @@ var ( // AppModuleBasic implements the AppModuleBasic interface for the csr module. type AppModuleBasic struct { - cdc codec.BinaryCodec + cdc codec.Codec } -func NewAppModuleBasic(cdc codec.BinaryCodec) AppModuleBasic { +func NewAppModuleBasic(cdc codec.Codec) AppModuleBasic { return AppModuleBasic{cdc: cdc} } @@ -104,12 +107,19 @@ type AppModule struct { keeper keeper.Keeper } +func (am AppModule) GenerateGenesisState(input *module.SimulationState) {} + +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return []simtypes.WeightedOperation{} +} + func NewAppModule( + cdc codec.Codec, keeper keeper.Keeper, acctKeeper authkeeper.AccountKeeper, ) AppModule { return AppModule{ - AppModuleBasic: AppModuleBasic{}, + AppModuleBasic: NewAppModuleBasic(cdc), keeper: keeper, accountKeeper: acctKeeper, } @@ -175,3 +185,12 @@ func (am AppModule) BeginBlock(ctx context.Context) error { } return nil } + +// ProposalMsgs returns msgs used for governance proposals for simulations. +func (AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { + return simulation.ProposalMsgs() +} + +func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { + sdr[types.ModuleName] = simulation.NewDecodeStore(am.cdc) +} diff --git a/x/csr/simulation/decoder.go b/x/csr/simulation/decoder.go new file mode 100644 index 00000000..59fbbeee --- /dev/null +++ b/x/csr/simulation/decoder.go @@ -0,0 +1,42 @@ +package simulation + +import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/kv" + "github.com/ethereum/go-ethereum/common" + + "github.com/Canto-Network/Canto/v7/x/csr/keeper" + "github.com/Canto-Network/Canto/v7/x/csr/types" +) + +// 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[:1], types.KeyPrefixCSR): + var cA, cB types.CSR + cdc.MustUnmarshal(kvA.Value, &cA) + cdc.MustUnmarshal(kvB.Value, &cB) + return fmt.Sprintf("%v\n%v", cA, cB) + + case bytes.Equal(kvA.Key[:1], types.KeyPrefixContract): + var nftA, nftB uint64 + nftA = keeper.BytesToUInt64(kvA.Value) + nftB = keeper.BytesToUInt64(kvB.Value) + return fmt.Sprintf("%v\n%v", nftA, nftB) + + case bytes.Equal(kvA.Key[:1], types.KeyPrefixAddrs): + var tsA, tsB common.Address + tsA = common.BytesToAddress(kvA.Value) + tsB = common.BytesToAddress(kvB.Value) + return fmt.Sprintf("%v\n%v", tsA, tsB) + + default: + panic(fmt.Sprintf("invalid csr key prefix %X", kvA.Key[:1])) + } + } +} diff --git a/x/csr/simulation/decoder_test.go b/x/csr/simulation/decoder_test.go new file mode 100644 index 00000000..679bdab1 --- /dev/null +++ b/x/csr/simulation/decoder_test.go @@ -0,0 +1,61 @@ +package simulation_test + +import ( + "fmt" + "testing" + + "github.com/Canto-Network/Canto/v7/x/csr/keeper" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/evmos/ethermint/tests" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/Canto-Network/Canto/v7/x/csr" + "github.com/Canto-Network/Canto/v7/x/csr/simulation" + "github.com/Canto-Network/Canto/v7/x/csr/types" +) + +func TestCsrStore(t *testing.T) { + cdc := moduletestutil.MakeTestEncodingConfig(csr.AppModuleBasic{}).Codec + dec := simulation.NewDecodeStore(cdc) + + csr := types.CSR{ + Id: 1, + Contracts: []string{tests.GenerateAddress().Hex()}, + } + + nftId := uint64(1) + + turnstile := tests.GenerateAddress() + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: types.KeyPrefixCSR, Value: cdc.MustMarshal(&csr)}, + {Key: types.KeyPrefixContract, Value: keeper.UInt64ToBytes(nftId)}, + {Key: types.KeyPrefixAddrs, Value: turnstile.Bytes()}, + {Key: []byte{0x99}, Value: []byte{0x99}}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"CSR", fmt.Sprintf("%v\n%v", csr, csr)}, + {"NFTId", fmt.Sprintf("%v\n%v", nftId, nftId)}, + {"Turnstile", fmt.Sprintf("%v\n%v", turnstile, turnstile)}, + {"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/csr/simulation/genesis.go b/x/csr/simulation/genesis.go new file mode 100644 index 00000000..3a81c1b4 --- /dev/null +++ b/x/csr/simulation/genesis.go @@ -0,0 +1,48 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/Canto-Network/Canto/v7/x/csr/types" +) + +// DONTCOVER + +// simulation parameter constants +const ( + enableCsr = "enable_csr" + csrShares = "csr_shares" +) + +func generateRandomBool(r *rand.Rand) bool { + return r.Int63()%2 == 0 +} + +func generateRandomCsrShares(r *rand.Rand) sdkmath.LegacyDec { + return sdkmath.LegacyNewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 100)), 2) +} + +// RandomizedGenState generates a random GenesisState for CSR. +func RandomizedGenState(simState *module.SimulationState) { + genesis := types.DefaultGenesis() + + simState.AppParams.GetOrGenerate( + enableCsr, &genesis.Params.EnableCsr, simState.Rand, + func(r *rand.Rand) { genesis.Params.EnableCsr = generateRandomBool(r) }, + ) + + simState.AppParams.GetOrGenerate( + csrShares, &genesis.Params.CsrShares, simState.Rand, + func(r *rand.Rand) { genesis.Params.CsrShares = generateRandomCsrShares(r) }, + ) + + bz, _ := json.MarshalIndent(&genesis, "", " ") + fmt.Printf("Selected randomly generated csr parameters:\n%s\n", bz) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) +} diff --git a/x/csr/simulation/genesis_test.go b/x/csr/simulation/genesis_test.go new file mode 100644 index 00000000..423c62e5 --- /dev/null +++ b/x/csr/simulation/genesis_test.go @@ -0,0 +1,72 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + sdkmath "cosmossdk.io/math" + "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/csr/simulation" + "github.com/Canto-Network/Canto/v7/x/csr/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: sdkmath.NewInt(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, true, genState.Params.EnableCsr) + require.Equal(t, sdkmath.LegacyNewDecWithPrec(11, 2), genState.Params.CsrShares) +} + +// 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/csr/simulation/proposals.go b/x/csr/simulation/proposals.go new file mode 100644 index 00000000..36b2f118 --- /dev/null +++ b/x/csr/simulation/proposals.go @@ -0,0 +1,44 @@ +package simulation + +import ( + "math/rand" + + "github.com/Canto-Network/Canto/v7/x/csr/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +// Simulation operation weights constants +const ( + DefaultWeightMsgUpdateParams int = 50 + + OpWeightMsgUpdateParams = "op_weight_msg_update_params" +) + +// ProposalMsgs defines the module weighted proposals' contents +func ProposalMsgs() []simtypes.WeightedProposalMsg { + return []simtypes.WeightedProposalMsg{ + simulation.NewWeightedProposalMsg( + OpWeightMsgUpdateParams, + DefaultWeightMsgUpdateParams, + SimulateMsgUpdateParams, + ), + } +} + +// SimulateMsgUpdateParams returns a random MsgUpdateParams +func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { + // use the default gov module account address as authority + var authority sdk.AccAddress = address.Module("gov") + + params := types.DefaultParams() + params.CsrShares = generateRandomCsrShares(r) + params.EnableCsr = generateRandomBool(r) + + return &types.MsgUpdateParams{ + Authority: authority.String(), + Params: params, + } +} diff --git a/x/csr/simulation/proposals_test.go b/x/csr/simulation/proposals_test.go new file mode 100644 index 00000000..43952a5b --- /dev/null +++ b/x/csr/simulation/proposals_test.go @@ -0,0 +1,42 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + "cosmossdk.io/math" + "github.com/Canto-Network/Canto/v7/x/csr/simulation" + "github.com/Canto-Network/Canto/v7/x/csr/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/stretchr/testify/require" +) + +func TestProposalMsgs(t *testing.T) { + // initialize parameters + s := rand.NewSource(1) + r := rand.New(s) + + ctx := sdk.NewContext(nil, cmtproto.Header{}, true, nil) + accounts := simtypes.RandomAccounts(r, 3) + + // execute ProposalMsgs function + weightedProposalMsgs := simulation.ProposalMsgs() + require.Equal(t, 1, len(weightedProposalMsgs)) + + w0 := weightedProposalMsgs[0] + + // tests w0 interface: + require.Equal(t, simulation.OpWeightMsgUpdateParams, w0.AppParamsKey()) + require.Equal(t, simulation.DefaultWeightMsgUpdateParams, w0.DefaultWeight()) + + msg := w0.MsgSimulatorFn()(r, ctx, accounts) + msgUpdateParams, ok := msg.(*types.MsgUpdateParams) + require.True(t, ok) + + require.Equal(t, sdk.AccAddress(address.Module("gov")).String(), msgUpdateParams.Authority) + require.Equal(t, math.LegacyNewDecWithPrec(40, 2), msgUpdateParams.Params.CsrShares) //nolint:staticcheck // we're testing deprecated code here + require.Equal(t, true, msgUpdateParams.Params.EnableCsr) +} diff --git a/x/epochs/module.go b/x/epochs/module.go index 7eff2e93..c283da5d 100644 --- a/x/epochs/module.go +++ b/x/epochs/module.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "math/rand" "github.com/cosmos/cosmos-sdk/baseapp" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -23,14 +22,16 @@ import ( "github.com/Canto-Network/Canto/v7/x/epochs/client/cli" "github.com/Canto-Network/Canto/v7/x/epochs/keeper" + "github.com/Canto-Network/Canto/v7/x/epochs/simulation" "github.com/Canto-Network/Canto/v7/x/epochs/types" ) var ( - _ module.AppModuleBasic = AppModuleBasic{} - _ module.AppModuleBasic = AppModule{} - _ module.HasServices = AppModule{} - _ module.HasABCIGenesis = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + _ module.AppModuleBasic = AppModule{} + _ module.HasServices = AppModule{} + _ module.HasABCIGenesis = AppModule{} + _ module.AppModuleSimulation = AppModule{} _ appmodule.AppModule = AppModule{} _ appmodule.HasBeginBlocker = AppModule{} @@ -171,13 +172,9 @@ func (AppModule) ProposalContents(simState module.SimulationState) []simtypes.We return []simtypes.WeightedProposalContent{} } -// RandomizedParams creates randomizedepochs param changes for the simulator. -func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.LegacyParamChange { - return []simtypes.LegacyParamChange{} -} - -// RegisterStoreDecoder registers a decoder for supply module's types +// RegisterStoreDecoder registers a decoder for epoch module's types func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { + sdr[types.ModuleName] = simulation.NewDecodeStore(am.cdc) } // WeightedOperations returns the all the gov module operations with their respective weights. diff --git a/x/epochs/simulation/decoder.go b/x/epochs/simulation/decoder.go new file mode 100644 index 00000000..970e94de --- /dev/null +++ b/x/epochs/simulation/decoder.go @@ -0,0 +1,28 @@ +package simulation + +import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/Canto-Network/Canto/v7/x/epochs/types" +) + +// 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[:1], types.KeyPrefixEpoch): + var eA, eB types.EpochInfo + cdc.MustUnmarshal(kvA.Value, &eA) + cdc.MustUnmarshal(kvA.Value, &eB) + return fmt.Sprintf("%v\n%v", eA, eB) + + default: + panic(fmt.Sprintf("invalid epochs key prefix %X", kvA.Key[:1])) + } + } +} diff --git a/x/epochs/simulation/decoder_test.go b/x/epochs/simulation/decoder_test.go new file mode 100644 index 00000000..319f0af3 --- /dev/null +++ b/x/epochs/simulation/decoder_test.go @@ -0,0 +1,57 @@ +package simulation_test + +import ( + "fmt" + "testing" + "time" + + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/Canto-Network/Canto/v7/x/epochs" + "github.com/Canto-Network/Canto/v7/x/epochs/simulation" + "github.com/Canto-Network/Canto/v7/x/epochs/types" +) + +func TestEpochsStore(t *testing.T) { + cdc := moduletestutil.MakeTestEncodingConfig(epochs.AppModuleBasic{}).Codec + dec := simulation.NewDecodeStore(cdc) + + epoch := types.EpochInfo{ + Identifier: types.DayEpochID, + StartTime: time.Time{}, + Duration: time.Hour * 24, + CurrentEpoch: 0, + CurrentEpochStartHeight: 0, + CurrentEpochStartTime: time.Time{}, + EpochCountingStarted: false, + } + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: types.KeyPrefixEpoch, Value: cdc.MustMarshal(&epoch)}, + {Key: []byte{0x99}, Value: []byte{0x99}}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"Epoch", fmt.Sprintf("%v\n%v", epoch, epoch)}, + {"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/epochs/simulation/genesis.go b/x/epochs/simulation/genesis.go new file mode 100644 index 00000000..5a55cbe4 --- /dev/null +++ b/x/epochs/simulation/genesis.go @@ -0,0 +1,54 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/Canto-Network/Canto/v7/x/epochs/types" +) + +// DONTCOVER + +// RandomizedGenState generates a random GenesisState for epochs. +func RandomizedGenState(simState *module.SimulationState) { + genesis := types.DefaultGenesisState() + + epochs := []types.EpochInfo{ + { + Identifier: types.WeekEpochID, + StartTime: simState.GenTimestamp, + Duration: time.Hour * 24 * 7, + CurrentEpoch: 0, + CurrentEpochStartHeight: 0, + CurrentEpochStartTime: simState.GenTimestamp, + EpochCountingStarted: false, + }, + { + Identifier: types.DayEpochID, + StartTime: simState.GenTimestamp, + Duration: time.Hour * 24, + CurrentEpoch: 0, + CurrentEpochStartHeight: 0, + CurrentEpochStartTime: simState.GenTimestamp, + EpochCountingStarted: false, + }, + { + Identifier: types.HourEpochID, + StartTime: simState.GenTimestamp, + Duration: time.Hour * 1, + CurrentEpoch: 0, + CurrentEpochStartHeight: 0, + CurrentEpochStartTime: simState.GenTimestamp, + EpochCountingStarted: false, + }, + } + + genesis.Epochs = epochs + + bz, _ := json.MarshalIndent(&genesis, "", " ") + fmt.Printf("Selected randomly generated epochs parameters:\n%s\n", bz) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) +} diff --git a/x/epochs/simulation/genesis_test.go b/x/epochs/simulation/genesis_test.go new file mode 100644 index 00000000..c02af2ce --- /dev/null +++ b/x/epochs/simulation/genesis_test.go @@ -0,0 +1,102 @@ +package simulation_test + +import ( + "encoding/json" + "fmt" + "math/rand" + "testing" + "time" + + sdkmath "cosmossdk.io/math" + "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/epochs/simulation" + "github.com/Canto-Network/Canto/v7/x/epochs/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: sdkmath.NewInt(1000), + GenState: make(map[string]json.RawMessage), + } + + simulation.RandomizedGenState(&simState) + + var genState types.GenesisState + simState.Cdc.MustUnmarshalJSON(simState.GenState[types.ModuleName], &genState) + fmt.Println(genState.Epochs) + require.Equal(t, []types.EpochInfo{ + { + Identifier: "week", + StartTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + Duration: 604800000000000, + CurrentEpoch: 0, + CurrentEpochStartTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + EpochCountingStarted: false, + CurrentEpochStartHeight: 0, + }, + { + Identifier: "day", + StartTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + Duration: 86400000000000, + CurrentEpoch: 0, + CurrentEpochStartTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + EpochCountingStarted: false, + CurrentEpochStartHeight: 0, + }, + { + Identifier: "hour", + StartTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + Duration: 3600000000000, + CurrentEpoch: 0, + CurrentEpochStartTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + EpochCountingStarted: false, + CurrentEpochStartHeight: 0, + }, + }, genState.Epochs) + +} + +// 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/erc20/keeper/keeper_test.go b/x/erc20/keeper/keeper_test.go index 01b19723..b6d6a170 100644 --- a/x/erc20/keeper/keeper_test.go +++ b/x/erc20/keeper/keeper_test.go @@ -478,6 +478,26 @@ type MockEVMKeeper struct { mock.Mock } +func (m *MockEVMKeeper) SetParams(ctx sdk.Context, params evmtypes.Params) error { + //TODO implement me + panic("implement me") +} + +func (m *MockEVMKeeper) ChainID() *big.Int { + //TODO implement me + panic("implement me") +} + +func (m *MockEVMKeeper) GetNonce(ctx sdk.Context, addr common.Address) uint64 { + //TODO implement me + panic("implement me") +} + +func (m *MockEVMKeeper) EthereumTx(goCtx context.Context, msg *evmtypes.MsgEthereumTx) (*evmtypes.MsgEthereumTxResponse, error) { + //TODO implement me + panic("implement me") +} + func (m *MockEVMKeeper) GetParams(ctx sdk.Context) evmtypes.Params { args := m.Called(mock.Anything) return args.Get(0).(evmtypes.Params) @@ -514,6 +534,26 @@ type MockBankKeeper struct { mock.Mock } +func (b *MockBankKeeper) SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt sdk.Coins) error { + //TODO implement me + panic("implement me") +} + +func (b *MockBankKeeper) SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins { + //TODO implement me + panic("implement me") +} + +func (b *MockBankKeeper) GetParams(ctx context.Context) banktypes.Params { + //TODO implement me + panic("implement me") +} + +func (b *MockBankKeeper) SetParams(ctx context.Context, params banktypes.Params) error { + //TODO implement me + panic("implement me") +} + func (b *MockBankKeeper) SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error { args := b.Called(mock.Anything, mock.Anything, mock.Anything, mock.Anything) return args.Error(0) diff --git a/x/erc20/keeper/test_heleprs.go b/x/erc20/keeper/test_heleprs.go new file mode 100644 index 00000000..0a5c33b6 --- /dev/null +++ b/x/erc20/keeper/test_heleprs.go @@ -0,0 +1,119 @@ +package keeper + +import ( + "encoding/json" + "fmt" + "math/big" + + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/evmos/ethermint/server/config" + evm "github.com/evmos/ethermint/x/evm/types" + feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" + + "github.com/Canto-Network/Canto/v7/contracts" + "github.com/Canto-Network/Canto/v7/x/erc20/types" +) + +func DeployContract(ctx sdk.Context, + evmKeeper types.EVMKeeper, feemarketKeeper types.FeeMarketKeeper, + address common.Address, signer keyring.Signer, + name, symbol string, decimals uint8) (common.Address, error) { + chainID := evmKeeper.ChainID() + + ctorArgs, err := contracts.ERC20MinterBurnerDecimalsContract.ABI.Pack("", name, symbol, decimals) + if err != nil { + return common.Address{}, err + } + + data := append(contracts.ERC20MinterBurnerDecimalsContract.Bin, ctorArgs...) + args, err := json.Marshal(&evm.TransactionArgs{ + From: &address, + Data: (*hexutil.Bytes)(&data), + }) + if err != nil { + return common.Address{}, err + } + + res, err := evmKeeper.EstimateGas(ctx, &evm.EthCallRequest{ + Args: args, + GasCap: uint64(config.DefaultGasCap), + }) + if err != nil { + return common.Address{}, err + } + + nonce := evmKeeper.GetNonce(ctx, address) + erc20DeployTx := evm.NewTxContract( + chainID, + nonce, + nil, // amount + res.Gas, // gasLimit + nil, // gasPrice + feemarkettypes.DefaultParams().BaseFee.BigInt(), + big.NewInt(1), + data, // input + ðtypes.AccessList{}, // accesses + ) + + erc20DeployTx.From = address.Hex() + err = erc20DeployTx.Sign(ethtypes.LatestSignerForChainID(chainID), signer) + if err != nil { + return common.Address{}, err + } + + rsp, err := evmKeeper.EthereumTx(ctx, erc20DeployTx) + if err != nil { + return common.Address{}, err + } + + if rsp.VmError != "" { + return common.Address{}, fmt.Errorf("failed to deploy contract: %s", rsp.VmError) + } + return crypto.CreateAddress(address, nonce), nil +} + +func DeployERC20Contract( + ctx sdk.Context, + k Keeper, + ak types.AccountKeeper, + coinMetadata banktypes.Metadata, +) (common.Address, error) { + decimals := uint8(0) + if len(coinMetadata.DenomUnits) > 0 { + decimalsIdx := len(coinMetadata.DenomUnits) - 1 + decimals = uint8(coinMetadata.DenomUnits[decimalsIdx].Exponent) + } + ctorArgs, err := contracts.ERC20MinterBurnerDecimalsContract.ABI.Pack( + "", + coinMetadata.Name, + coinMetadata.Symbol, + decimals, + ) + if err != nil { + return common.Address{}, errorsmod.Wrapf(types.ErrABIPack, "coin metadata is invalid %s: %s", coinMetadata.Name, err.Error()) + } + + data := make([]byte, len(contracts.ERC20MinterBurnerDecimalsContract.Bin)+len(ctorArgs)) + copy(data[:len(contracts.ERC20MinterBurnerDecimalsContract.Bin)], contracts.ERC20MinterBurnerDecimalsContract.Bin) + copy(data[len(contracts.ERC20MinterBurnerDecimalsContract.Bin):], ctorArgs) + + nonce, err := ak.GetSequence(ctx, types.ModuleAddress.Bytes()) + if err != nil { + return common.Address{}, err + } + + contractAddr := crypto.CreateAddress(types.ModuleAddress, nonce) + _, err = k.CallEVMWithData(ctx, types.ModuleAddress, nil, data, true) + if err != nil { + return common.Address{}, errorsmod.Wrapf(err, "failed to deploy contract for %s", coinMetadata.Name) + } + + return contractAddr, nil +} diff --git a/x/erc20/module.go b/x/erc20/module.go index d29a534e..0305b256 100644 --- a/x/erc20/module.go +++ b/x/erc20/module.go @@ -4,13 +4,13 @@ import ( "context" "encoding/json" "fmt" - "math/rand" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" "cosmossdk.io/core/address" "cosmossdk.io/core/appmodule" + "github.com/Canto-Network/Canto/v7/x/erc20/simulation" abci "github.com/cometbft/cometbft/abci/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" @@ -27,17 +27,23 @@ import ( // type check to ensure the interface is properly implemented var ( - _ module.AppModuleBasic = AppModuleBasic{} - _ module.AppModuleBasic = AppModule{} - _ module.HasServices = AppModule{} - _ module.HasABCIGenesis = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + _ module.AppModuleBasic = AppModule{} + _ module.HasServices = AppModule{} + _ module.HasABCIGenesis = AppModule{} + _ module.AppModuleSimulation = AppModule{} _ appmodule.AppModule = AppModule{} ) // app module Basics object type AppModuleBasic struct { - ac address.Codec + ac address.Codec + cdc codec.Codec +} + +func NewAppModuleBasic(ac address.Codec, cdc codec.Codec) AppModuleBasic { + return AppModuleBasic{ac: ac, cdc: cdc} } func (AppModuleBasic) Name() string { @@ -93,19 +99,30 @@ func (AppModuleBasic) GetQueryCmd() *cobra.Command { type AppModule struct { AppModuleBasic keeper keeper.Keeper - ak authkeeper.AccountKeeper + // TODO: Add keepers that your application module simulation requires + ak authkeeper.AccountKeeper + bk types.BankKeeper + ek types.EVMKeeper + fk types.FeeMarketKeeper } // NewAppModule creates a new AppModule Object func NewAppModule( + cdc codec.Codec, k keeper.Keeper, ak authkeeper.AccountKeeper, + bk types.BankKeeper, + ek types.EVMKeeper, + fk types.FeeMarketKeeper, ac address.Codec, ) AppModule { return AppModule{ - AppModuleBasic: AppModuleBasic{ac: ac}, + AppModuleBasic: AppModuleBasic{ac: ac, cdc: cdc}, keeper: k, ak: ak, + bk: bk, + ek: ek, + fk: fk, } } @@ -136,6 +153,11 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { } } +// ProposalMsgs returns msgs used for governance proposals for simulations. +func (am AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { + return simulation.ProposalMsgs(am.keeper, am.ak, am.bk, am.ek, am.fk) +} + func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { var genesisState types.GenesisState @@ -156,13 +178,12 @@ func (am AppModule) ProposalContents(simState module.SimulationState) []simtypes return []simtypes.WeightedProposalContent{} } -func (am AppModule) RandomizedParams(r *rand.Rand) []simtypes.LegacyParamChange { - return []simtypes.LegacyParamChange{} -} - func (am AppModule) RegisterStoreDecoder(decoderRegistry simtypes.StoreDecoderRegistry) { + decoderRegistry[types.ModuleName] = simulation.NewDecodeStore(am.cdc) } func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { - return []simtypes.WeightedOperation{} + return simulation.WeightedOperations( + simState.AppParams, simState.Cdc, am.keeper, am.ak, am.bk, am.ek, am.fk, + ) } diff --git a/x/erc20/simulation/decoder.go b/x/erc20/simulation/decoder.go new file mode 100644 index 00000000..c650a1d5 --- /dev/null +++ b/x/erc20/simulation/decoder.go @@ -0,0 +1,40 @@ +package simulation + +import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/Canto-Network/Canto/v7/x/erc20/types" +) + +// 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[:1], types.KeyPrefixTokenPair): + var tpA, tpB types.TokenPair + cdc.MustUnmarshal(kvA.Value, &tpA) + cdc.MustUnmarshal(kvB.Value, &tpB) + return fmt.Sprintf("%v\n%v", tpA, tpB) + + case bytes.Equal(kvA.Key[:1], types.KeyPrefixTokenPairByERC20Address): + var tpA, tpB types.TokenPair + cdc.MustUnmarshal(kvA.Value, &tpA) + cdc.MustUnmarshal(kvB.Value, &tpB) + return fmt.Sprintf("%v\n%v", tpA, tpB) + + case bytes.Equal(kvA.Key[:1], types.KeyPrefixTokenPairByDenom): + var tpA, tpB types.TokenPair + cdc.MustUnmarshal(kvA.Value, &tpA) + cdc.MustUnmarshal(kvB.Value, &tpB) + return fmt.Sprintf("%v\n%v", tpA, tpB) + + default: + panic(fmt.Sprintf("invalid erc20 key prefix %X", kvA.Key[:1])) + } + } +} diff --git a/x/erc20/simulation/decoder_test.go b/x/erc20/simulation/decoder_test.go new file mode 100644 index 00000000..583f2394 --- /dev/null +++ b/x/erc20/simulation/decoder_test.go @@ -0,0 +1,53 @@ +package simulation_test + +import ( + "fmt" + "testing" + + "github.com/Canto-Network/Canto/v7/x/erc20" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/types/kv" + testutil "github.com/evmos/ethermint/tests" + + "github.com/Canto-Network/Canto/v7/x/erc20/simulation" + "github.com/Canto-Network/Canto/v7/x/erc20/types" +) + +func TestERC20Store(t *testing.T) { + cdc := moduletestutil.MakeTestEncodingConfig(erc20.AppModuleBasic{}).Codec + dec := simulation.NewDecodeStore(cdc) + + tokenPair := types.NewTokenPair(testutil.GenerateAddress(), "coin", true, types.OWNER_MODULE) + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: types.KeyPrefixTokenPair, Value: cdc.MustMarshal(&tokenPair)}, + {Key: types.KeyPrefixTokenPairByERC20Address, Value: cdc.MustMarshal(&tokenPair)}, + {Key: types.KeyPrefixTokenPairByDenom, Value: cdc.MustMarshal(&tokenPair)}, + {Key: []byte{0x99}, Value: []byte{0x99}}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"TokenPair", fmt.Sprintf("%v\n%v", tokenPair, tokenPair)}, + {"TokenPairByERC20", fmt.Sprintf("%v\n%v", tokenPair, tokenPair)}, + {"TokenPairByDenom", fmt.Sprintf("%v\n%v", tokenPair, tokenPair)}, + {"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/erc20/simulation/genesis.go b/x/erc20/simulation/genesis.go new file mode 100644 index 00000000..db72b090 --- /dev/null +++ b/x/erc20/simulation/genesis.go @@ -0,0 +1,41 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/Canto-Network/Canto/v7/x/erc20/types" +) + +// DONTCOVER + +// simulation parameter constants +const ( + enableErc20 = "enable_erc20" + enableEVMHook = "enable_evm_hook" +) + +func generateRandomBool(r *rand.Rand) bool { + return r.Int63()%2 == 0 +} + +func RandomizedGenState(simState *module.SimulationState) { + genesis := types.DefaultGenesisState() + + simState.AppParams.GetOrGenerate( + enableErc20, &genesis.Params.EnableErc20, simState.Rand, + func(r *rand.Rand) { genesis.Params.EnableErc20 = generateRandomBool(r) }, + ) + + simState.AppParams.GetOrGenerate( + enableEVMHook, &genesis.Params.EnableEVMHook, simState.Rand, + func(r *rand.Rand) { genesis.Params.EnableEVMHook = generateRandomBool(r) }, + ) + + bz, _ := json.MarshalIndent(&genesis, "", " ") + fmt.Printf("Selected randomly generated erc20 parameters:\n%s\n", bz) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) +} diff --git a/x/erc20/simulation/genesis_test.go b/x/erc20/simulation/genesis_test.go new file mode 100644 index 00000000..ca0e9204 --- /dev/null +++ b/x/erc20/simulation/genesis_test.go @@ -0,0 +1,74 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + sdkmath "cosmossdk.io/math" + "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/erc20/simulation" + "github.com/Canto-Network/Canto/v7/x/erc20/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: sdkmath.NewInt(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, []types.TokenPair{}, genState.TokenPairs) + require.Equal(t, true, genState.Params.EnableErc20) + require.Equal(t, true, genState.Params.EnableEVMHook) + +} + +// 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/erc20/simulation/operation.go b/x/erc20/simulation/operation.go new file mode 100644 index 00000000..9dda50ec --- /dev/null +++ b/x/erc20/simulation/operation.go @@ -0,0 +1,207 @@ +package simulation + +import ( + "math/big" + "math/rand" + + sdkmath "cosmossdk.io/math" + "github.com/Canto-Network/Canto/v7/contracts" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/ethereum/go-ethereum/common" + "github.com/evmos/ethermint/crypto/ethsecp256k1" + + "github.com/Canto-Network/Canto/v7/app/params" + "github.com/Canto-Network/Canto/v7/x/erc20/keeper" + "github.com/Canto-Network/Canto/v7/x/erc20/types" +) + +// Simulation operation weights constants. +const ( + OpWeightMsgConvertCoin = "op_weight_msg_convert_coin" + OpWeightMsgConvertErc20 = "op_weight_msg_convert_erc20" +) + +// WeightedOperations returns all the operations from the module with their respective weights +func WeightedOperations( + appParams simtypes.AppParams, + cdc codec.JSONCodec, + k keeper.Keeper, + ak types.AccountKeeper, + bk types.BankKeeper, + ek types.EVMKeeper, + fk types.FeeMarketKeeper, +) simulation.WeightedOperations { + var weightMsgConvertCoinNativeCoin int + appParams.GetOrGenerate(OpWeightMsgConvertCoin, &weightMsgConvertCoinNativeCoin, nil, func(_ *rand.Rand) { + weightMsgConvertCoinNativeCoin = params.DefaultWeightMsgConvertCoin + }) + + var weightMsgConvertErc20NativeCoin int + appParams.GetOrGenerate(OpWeightMsgConvertErc20, &weightMsgConvertErc20NativeCoin, nil, func(_ *rand.Rand) { + weightMsgConvertErc20NativeCoin = params.DefaultWeightMsgConvertErc20 + }) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgConvertCoinNativeCoin, + SimulateMsgConvertCoin(k, ak, bk), + ), + simulation.NewWeightedOperation( + weightMsgConvertErc20NativeCoin, + SimulateMsgConvertErc20(k, ak, bk, ek, fk), + ), + } +} + +// SimulateMsgConvertCoin generates a MsgConvertCoin with random values for convertCoinNativeCoin +func SimulateMsgConvertCoin(k keeper.Keeper, ak types.AccountKeeper, bk types.BankKeeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + + pairs := k.GetTokenPairs(ctx) + + if len(pairs) == 0 { + _, err := SimulateRegisterCoin(r, ctx, accs, k, bk) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(&types.MsgConvertCoin{}), "no pairs available"), nil, nil + } + pairs = k.GetTokenPairs(ctx) + } + + // randomly pick one pair + pair := pairs[r.Intn(len(pairs))] + if !pair.Enabled { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(&types.MsgConvertCoin{}), "token pair is not enabled"), nil, nil + } + baseDenom := pair.GetDenom() + + // select random account that has coins baseDenom + var simAccount simtypes.Account + var spendable sdk.Coins + skip := true + + for i := 0; i < len(accs); i++ { + simAccount, _ = simtypes.RandomAcc(r, accs) + spendable = bk.SpendableCoins(ctx, simAccount.Address) + if spendable.AmountOf(baseDenom).IsPositive() { + skip = false + break + } + } + + if skip { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(&types.MsgConvertCoin{}), "no account has coins"), nil, nil + } + + priv, _ := ethsecp256k1.GenerateKey() + address := common.BytesToAddress(priv.PubKey().Address().Bytes()) + + msg := types.NewMsgConvertCoin( + sdk.NewCoin(baseDenom, spendable.AmountOf(baseDenom)), + address, + simAccount.Address, + ) + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + CoinsSpentInMsg: spendable, + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + } + op, fOps, err := simulation.GenAndDeliverTxWithRandFees(txCtx) + return op, fOps, err + } +} + +// SimulateMsgConvertErc20 generates a MsgConvertErc20 with random values for convertERC20NativeCoin. +func SimulateMsgConvertErc20(k keeper.Keeper, ak types.AccountKeeper, bk types.BankKeeper, ek types.EVMKeeper, fk types.FeeMarketKeeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + pairs := k.GetTokenPairs(ctx) + + if len(pairs) == 0 { + _, err := SimulateRegisterERC20(r, ctx, accs, k, ak, bk, ek, fk) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(&types.MsgConvertERC20{}), "no pairs available"), nil, nil + } + pairs = k.GetTokenPairs(ctx) + } + + // randomly pick one pair + pair := pairs[r.Intn(len(pairs))] + if !pair.Enabled { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(&types.MsgConvertERC20{}), "token pair is not enabled"), nil, nil + } + + erc20ABI := contracts.ERC20MinterBurnerDecimalsContract.ABI + deployer := types.ModuleAddress + contractAddr := pair.GetERC20Contract() + randomIteration := r.Intn(10) + for i := 0; i < randomIteration; i++ { + simAccount, _ := simtypes.RandomAcc(r, accs) + + mintAmt := sdkmath.NewInt(1000000000) + receiver := common.BytesToAddress(simAccount.Address.Bytes()) + before := k.BalanceOf(ctx, erc20ABI, contractAddr, receiver) + _, err := k.CallEVM(ctx, erc20ABI, deployer, contractAddr, true, "mint", receiver, mintAmt.BigInt()) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(&types.MsgConvertERC20{}), "no account has native ERC20"), nil, nil + } + after := k.BalanceOf(ctx, erc20ABI, contractAddr, receiver) + if after.Cmp(before.Add(before, mintAmt.BigInt())) != 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(&types.MsgConvertERC20{}), "no account has native ERC20"), nil, nil + } + } + + // select random account that has coins baseDenom + var simAccount simtypes.Account + var erc20Balance *big.Int + skip := true + + for i := 0; i < len(accs); i++ { + simAccount, _ = simtypes.RandomAcc(r, accs) + erc20Balance = k.BalanceOf(ctx, erc20ABI, pair.GetERC20Contract(), common.BytesToAddress(simAccount.Address.Bytes())) + if erc20Balance.Cmp(big.NewInt(0)) > 0 { + skip = false + break + } + } + + if skip { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(&types.MsgConvertERC20{}), "no account has native ERC20"), nil, nil + } + + msg := types.NewMsgConvertERC20(sdkmath.NewIntFromBigInt(erc20Balance), simAccount.Address, pair.GetERC20Contract(), common.BytesToAddress(simAccount.Address.Bytes())) + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + CoinsSpentInMsg: sdk.NewCoins(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + } + + op, fOps, err := simulation.GenAndDeliverTxWithRandFees(txCtx) + return op, fOps, err + } +} diff --git a/x/erc20/simulation/operation_test.go b/x/erc20/simulation/operation_test.go new file mode 100644 index 00000000..e8d183f1 --- /dev/null +++ b/x/erc20/simulation/operation_test.go @@ -0,0 +1,138 @@ +package simulation_test + +import ( + "math/rand" + "testing" + "time" + + sdkmath "cosmossdk.io/math" + "github.com/Canto-Network/Canto/v7/app/params" + abci "github.com/cometbft/cometbft/abci/types" + "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/testutil" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "github.com/Canto-Network/Canto/v7/app" + "github.com/Canto-Network/Canto/v7/x/erc20/simulation" + "github.com/Canto-Network/Canto/v7/x/erc20/types" +) + +func TestWeightedOperations(t *testing.T) { + canto, ctx := createTestApp(t, false) + cdc := types.ModuleCdc + appParams := make(simtypes.AppParams) + + weightedOps := simulation.WeightedOperations( + appParams, + cdc, + canto.Erc20Keeper, + canto.AccountKeeper, + canto.BankKeeper, + canto.EvmKeeper, + canto.FeeMarketKeeper, + ) + + s := rand.NewSource(2) + r := rand.New(s) + accs := getTestingAccounts(t, r, canto, ctx, 10) + + expected := []struct { + weight int + opMsgRoute string + opMsgName string + }{ + {params.DefaultWeightMsgConvertCoin, types.ModuleName, sdk.MsgTypeURL(&types.MsgConvertCoin{})}, + {params.DefaultWeightMsgConvertErc20, types.ModuleName, sdk.MsgTypeURL(&types.MsgConvertERC20{})}, + } + + for i, w := range weightedOps { + opMsg, _, err := w.Op()(r, canto.BaseApp, ctx, accs, ctx.ChainID()) + require.NoError(t, err) + 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) + validator := getTestingValidator0(t, app, ctx, simAccs) + consAddr, err := validator.GetConsAddr() + require.NoError(t, err) + ctx = ctx.WithBlockHeader(cmtproto.Header{Height: 1, + ChainID: "canto_9001-1", + Time: time.Now().UTC(), + ProposerAddress: consAddr, + }) + ctx = ctx.WithChainID("canto_9001-1") + app.FinalizeBlock( + &abci.RequestFinalizeBlock{ + Height: 1, + 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(sdkmath.LegacyZeroDec(), sdkmath.LegacyOneDec(), sdkmath.LegacyOneDec()) + 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 := testutil.NewValidator(t, valAddr, valPubKey) + validator, err := validator.SetInitialCommission(commission) + require.NoError(t, err) + + validator.DelegatorShares = sdkmath.LegacyNewDec(100) + validator.Tokens = app.StakingKeeper.TokensFromConsensusPower(ctx, 100) + + app.StakingKeeper.SetValidator(ctx, validator) + app.StakingKeeper.SetValidatorByConsAddr(ctx, validator) + + return validator +} diff --git a/x/erc20/simulation/proposal.go b/x/erc20/simulation/proposal.go new file mode 100644 index 00000000..ed59b76d --- /dev/null +++ b/x/erc20/simulation/proposal.go @@ -0,0 +1,260 @@ +package simulation + +import ( + "math/rand" + "strings" + + sdkmath "cosmossdk.io/math" + "github.com/Canto-Network/Canto/v7/app/params" + "github.com/Canto-Network/Canto/v7/contracts" + "github.com/Canto-Network/Canto/v7/x/erc20/keeper" + "github.com/Canto-Network/Canto/v7/x/erc20/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/ethereum/go-ethereum/common" + "github.com/evmos/ethermint/crypto/ethsecp256k1" + "github.com/evmos/ethermint/tests" + evmtypes "github.com/evmos/ethermint/x/evm/types" +) + +// Simulation operation weights constants +const ( + DefaultWeightMsgUpdateParams int = 100 + + OpWeightMsgUpdateParams = "op_weight_msg_update_params" + OpWeightSimulateRegisterCoinProposal = "op_weight_register_coin_proposal" + OpWeightSimulateRegisterERC20Proposal = "op_weight_register_erc20_proposal" + OpWeightSimulateToggleTokenConversionProposal = "op_weight_toggle_token_conversion_proposal" + + erc20Decimals = uint8(18) +) + +// ProposalMsgs defines the module weighted proposals' contents +func ProposalMsgs(k keeper.Keeper, ak types.AccountKeeper, bk types.BankKeeper, ek types.EVMKeeper, fk types.FeeMarketKeeper) []simtypes.WeightedProposalMsg { + return []simtypes.WeightedProposalMsg{ + simulation.NewWeightedProposalMsg( + OpWeightMsgUpdateParams, + DefaultWeightMsgUpdateParams, + SimulateMsgUpdateParams, + ), + simulation.NewWeightedProposalMsg( + OpWeightSimulateRegisterCoinProposal, + params.DefaultWeightRegisterCoinProposal, + SimulateMsgRegisterCoin(k, bk), + ), + simulation.NewWeightedProposalMsg( + OpWeightSimulateRegisterERC20Proposal, + params.DefaultWeightRegisterERC20Proposal, + SimulateMsgRegisterERC20(k, ak, bk, ek, fk), + ), + simulation.NewWeightedProposalMsg( + OpWeightSimulateToggleTokenConversionProposal, + params.DefaultWeightToggleTokenConversionProposal, + SimulateMsgToggleTokenConversion(k, bk, ek, fk), + ), + } +} + +// SimulateMsgUpdateParams returns a random MsgUpdateParams +func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { + // use the default gov module account address as authority + var authority sdk.AccAddress = address.Module("gov") + + params := types.DefaultParams() + + params.EnableErc20 = generateRandomBool(r) + params.EnableEVMHook = generateRandomBool(r) + + return &types.MsgUpdateParams{ + Authority: authority.String(), + Params: params, + } +} + +func SimulateRegisterCoin(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, k keeper.Keeper, bk types.BankKeeper) (sdk.Msg, error) { + coinMetadata := types.GenRandomCoinMetadata(r) + if err := bk.MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin(coinMetadata.Base, sdkmath.NewInt(1000000000000)))); err != nil { + panic(err) + } + bankparams := bk.GetParams(ctx) + bankparams.DefaultSendEnabled = true + bk.SetParams(ctx, bankparams) + + params := k.GetParams(ctx) + params.EnableErc20 = true + k.SetParams(ctx, params) + + // mint cosmos coin to random accounts + randomIteration := r.Intn(10) + mintAmt := sdkmath.NewInt(100000000) + for i := 0; i < randomIteration; i++ { + simAccount, _ := simtypes.RandomAcc(r, accs) + + if err := bk.MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin(coinMetadata.Base, mintAmt))); err != nil { + return &types.MsgRegisterCoin{}, err + } + if err := bk.SendCoinsFromModuleToAccount(ctx, types.ModuleName, simAccount.Address, sdk.NewCoins(sdk.NewCoin(coinMetadata.Base, sdkmath.NewInt(1000000000)))); err != nil { + return &types.MsgRegisterCoin{}, err + } + + } + + // use the default gov module account address as authority + var authority sdk.AccAddress = address.Module("gov") + + msg := &types.MsgRegisterCoin{ + Authority: authority.String(), + Title: simtypes.RandStringOfLength(r, 10), + Description: simtypes.RandStringOfLength(r, 100), + Metadata: coinMetadata, + } + + if _, err := k.RegisterCoinProposal(ctx, msg); err != nil { + return &types.MsgRegisterCoin{}, err + } + + return msg, nil +} + +func SimulateMsgRegisterCoin(k keeper.Keeper, bk types.BankKeeper) simtypes.MsgSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) sdk.Msg { + msg, err := SimulateRegisterCoin(r, ctx, accs, k, bk) + if err != nil { + panic(err) + } + return msg + } +} + +func SimulateRegisterERC20(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, k keeper.Keeper, ak types.AccountKeeper, bk types.BankKeeper, ek types.EVMKeeper, fk types.FeeMarketKeeper) (sdk.Msg, error) { + params := k.GetParams(ctx) + params.EnableErc20 = true + k.SetParams(ctx, params) + + evmParams := evmtypes.DefaultParams() + evmParams.EvmDenom = "stake" + ek.SetParams(ctx, evmParams) + + // account key + priv, err := ethsecp256k1.GenerateKey() + if err != nil { + panic(err) + } + addr := common.BytesToAddress(priv.PubKey().Address().Bytes()) + signer := tests.NewSigner(priv) + + erc20ABI := contracts.ERC20MinterBurnerDecimalsContract.ABI + + var deployer common.Address + var contractAddr common.Address + coinMetadata := types.GenRandomCoinMetadata(r) + + deployer = addr + erc20Name := coinMetadata.Name + erc20Symbol := coinMetadata.Symbol + contractAddr, err = keeper.DeployContract(ctx, ek, fk, deployer, signer, erc20Name, erc20Symbol, erc20Decimals) + + // mint cosmos coin to random accounts + randomIteration := r.Intn(10) + for i := 0; i < randomIteration; i++ { + simAccount, _ := simtypes.RandomAcc(r, accs) + + mintAmt := sdkmath.NewInt(100000000) + receiver := common.BytesToAddress(simAccount.Address.Bytes()) + before := k.BalanceOf(ctx, erc20ABI, contractAddr, receiver) + _, err = k.CallEVM(ctx, erc20ABI, deployer, contractAddr, true, "mint", receiver, mintAmt.BigInt()) + if err != nil { + return &types.MsgRegisterERC20{}, err + } + after := k.BalanceOf(ctx, erc20ABI, contractAddr, receiver) + if after.Cmp(before.Add(before, mintAmt.BigInt())) != 0 { + return &types.MsgRegisterERC20{}, err + } + } + + // use the default gov module account address as authority + var authority sdk.AccAddress = address.Module("gov") + + msg := &types.MsgRegisterERC20{ + Authority: authority.String(), + Title: simtypes.RandStringOfLength(r, 10), + Description: simtypes.RandStringOfLength(r, 100), + Erc20Address: contractAddr.String(), + } + + if _, err := k.RegisterERC20Proposal(ctx, msg); err != nil { + return &types.MsgRegisterERC20{}, err + } + + return msg, nil +} + +func SimulateMsgRegisterERC20(k keeper.Keeper, accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper, evmKeeper types.EVMKeeper, feemarketKeeper types.FeeMarketKeeper) simtypes.MsgSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) sdk.Msg { // use the default gov module account address as authority + msg, err := SimulateRegisterERC20(r, ctx, accs, k, accountKeeper, bankKeeper, evmKeeper, feemarketKeeper) + if err != nil { + panic(err) + } + return msg + } +} + +func SimulateMsgToggleTokenConversion(k keeper.Keeper, bankKeeper types.BankKeeper, evmKeeper types.EVMKeeper, feemarketKeeper types.FeeMarketKeeper) simtypes.MsgSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) sdk.Msg { + params := k.GetParams(ctx) + params.EnableErc20 = true + k.SetParams(ctx, params) + + evmParams := evmtypes.DefaultParams() + evmParams.EvmDenom = "stake" + evmKeeper.SetParams(ctx, evmParams) + + // account key + priv, err := ethsecp256k1.GenerateKey() + if err != nil { + panic(err) + } + addr := common.BytesToAddress(priv.PubKey().Address().Bytes()) + signer := tests.NewSigner(priv) + + erc20Name := simtypes.RandStringOfLength(r, 10) + erc20Symbol := strings.ToUpper(simtypes.RandStringOfLength(r, 4)) + + coins := sdk.NewCoins(sdk.NewCoin(evmParams.EvmDenom, sdkmath.NewInt(10000000000000000))) + if err = bankKeeper.MintCoins(ctx, types.ModuleName, coins); err != nil { + panic(err) + } + + if err = bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, authtypes.FeeCollectorName, coins); err != nil { + panic(err) + } + + contractAddr, err := keeper.DeployContract(ctx, evmKeeper, feemarketKeeper, addr, signer, erc20Name, erc20Symbol, erc20Decimals) + if err != nil { + panic(err) + } + + _, err = k.RegisterERC20(ctx, contractAddr) + if err != nil { + panic(err) + } + + var authority sdk.AccAddress = address.Module("gov") + + msg := &types.MsgToggleTokenConversion{ + Authority: authority.String(), + Title: simtypes.RandStringOfLength(r, 10), + Description: simtypes.RandStringOfLength(r, 100), + Token: contractAddr.String(), + } + + if _, err := k.ToggleTokenConversionProposal(ctx, msg); err != nil { + panic(err) + } + + return msg + } +} diff --git a/x/erc20/simulation/proposal_test.go b/x/erc20/simulation/proposal_test.go new file mode 100644 index 00000000..b6eafe00 --- /dev/null +++ b/x/erc20/simulation/proposal_test.go @@ -0,0 +1,83 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + "github.com/Canto-Network/Canto/v7/app/params" + "github.com/Canto-Network/Canto/v7/x/erc20/simulation" + "github.com/Canto-Network/Canto/v7/x/erc20/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/stretchr/testify/require" +) + +func TestProposalMsgs(t *testing.T) { + app, ctx := createTestApp(t, false) + + // initialize parameters + s := rand.NewSource(2) + r := rand.New(s) + + accounts := getTestingAccounts(t, r, app, ctx, 10) + + // execute ProposalMsgs function + weightedProposalMsgs := simulation.ProposalMsgs(app.Erc20Keeper, app.AccountKeeper, app.BankKeeper, app.EvmKeeper, app.FeeMarketKeeper) + require.Equal(t, 4, len(weightedProposalMsgs)) + + w0 := weightedProposalMsgs[0] + w1 := weightedProposalMsgs[1] + w2 := weightedProposalMsgs[2] + w3 := weightedProposalMsgs[3] + + // tests w0 interface: + require.Equal(t, simulation.OpWeightMsgUpdateParams, w0.AppParamsKey()) + require.Equal(t, simulation.DefaultWeightMsgUpdateParams, w0.DefaultWeight()) + + // tests w1 interface: + require.Equal(t, simulation.OpWeightSimulateRegisterCoinProposal, w1.AppParamsKey()) + require.Equal(t, params.DefaultWeightRegisterCoinProposal, w1.DefaultWeight()) + + // tests w2 interface: + require.Equal(t, simulation.OpWeightSimulateRegisterERC20Proposal, w2.AppParamsKey()) + require.Equal(t, params.DefaultWeightRegisterERC20Proposal, w2.DefaultWeight()) + + // tests w3 interface: + require.Equal(t, simulation.OpWeightSimulateToggleTokenConversionProposal, w3.AppParamsKey()) + require.Equal(t, params.DefaultWeightToggleTokenConversionProposal, w3.DefaultWeight()) + + msg := w0.MsgSimulatorFn()(r, ctx, accounts) + msgUpdateParams, ok := msg.(*types.MsgUpdateParams) + require.True(t, ok) + + require.Equal(t, sdk.AccAddress(address.Module("gov")).String(), msgUpdateParams.Authority) + require.Equal(t, true, msgUpdateParams.Params.EnableErc20) + require.Equal(t, true, msgUpdateParams.Params.EnableEVMHook) + + msg = w1.MsgSimulatorFn()(r, ctx, accounts) + msgRegisterCoin, ok := msg.(*types.MsgRegisterCoin) + require.True(t, ok) + + require.Equal(t, sdk.AccAddress(address.Module("gov")).String(), msgRegisterCoin.Authority) + require.NotNil(t, msgRegisterCoin.Title) + require.NotNil(t, msgRegisterCoin.Description) + require.NotNil(t, msgRegisterCoin.Metadata) + + msg = w2.MsgSimulatorFn()(r, ctx, accounts) + msgRegisterERC20, ok := msg.(*types.MsgRegisterERC20) + require.True(t, ok) + + require.Equal(t, sdk.AccAddress(address.Module("gov")).String(), msgRegisterERC20.Authority) + require.NotNil(t, msgRegisterERC20.Title) + require.NotNil(t, msgRegisterERC20.Description) + require.NotNil(t, msgRegisterERC20.Erc20Address) + + msg = w3.MsgSimulatorFn()(r, ctx, accounts) + msgToggleTokenConversion, ok := msg.(*types.MsgToggleTokenConversion) + require.True(t, ok) + + require.Equal(t, sdk.AccAddress(address.Module("gov")).String(), msgToggleTokenConversion.Authority) + require.NotNil(t, msgToggleTokenConversion.Title) + require.NotNil(t, msgToggleTokenConversion.Description) + require.NotNil(t, msgToggleTokenConversion.Token) +} diff --git a/x/erc20/types/interfaces.go b/x/erc20/types/interfaces.go index c1f20de9..c5de19de 100644 --- a/x/erc20/types/interfaces.go +++ b/x/erc20/types/interfaces.go @@ -2,9 +2,11 @@ package types import ( "context" + "math/big" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -18,12 +20,14 @@ import ( type AccountKeeper interface { GetModuleAddress(moduleName string) sdk.AccAddress GetSequence(context.Context, sdk.AccAddress) (uint64, error) + GetAccount(ctx context.Context, addr sdk.AccAddress) sdk.AccountI } // BankKeeper defines the expected interface needed to retrieve account balances. type BankKeeper interface { SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt sdk.Coins) error MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error BurnCoins(ctx context.Context, moduleName string, amt sdk.Coins) error IsSendEnabledCoin(ctx context.Context, coin sdk.Coin) bool @@ -32,12 +36,25 @@ type BankKeeper interface { SetDenomMetaData(ctx context.Context, denomMetaData banktypes.Metadata) HasSupply(ctx context.Context, denom string) bool GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin + SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins + GetParams(ctx context.Context) banktypes.Params + SetParams(ctx context.Context, params banktypes.Params) error } // EVMKeeper defines the expected EVM keeper interface used on erc20 type EVMKeeper interface { GetParams(ctx sdk.Context) evmtypes.Params + SetParams(ctx sdk.Context, params evmtypes.Params) error GetAccountWithoutBalance(ctx sdk.Context, addr common.Address) *statedb.Account EstimateGas(c context.Context, req *evmtypes.EthCallRequest) (*evmtypes.EstimateGasResponse, error) ApplyMessage(ctx sdk.Context, msg core.Message, tracer vm.EVMLogger, commit bool) (*evmtypes.MsgEthereumTxResponse, error) + ChainID() *big.Int + GetNonce(ctx sdk.Context, addr common.Address) uint64 + EthereumTx(goCtx context.Context, msg *evmtypes.MsgEthereumTx) (*evmtypes.MsgEthereumTxResponse, error) +} + +type FeeMarketKeeper interface { + GetBaseFee(ctx sdk.Context) *big.Int + GetParams(ctx sdk.Context) (params feemarkettypes.Params) + SetParams(ctx sdk.Context, params feemarkettypes.Params) error } diff --git a/x/erc20/types/utils.go b/x/erc20/types/utils.go index d3c4e304..d5d59d97 100644 --- a/x/erc20/types/utils.go +++ b/x/erc20/types/utils.go @@ -2,9 +2,11 @@ package types import ( "fmt" + "math/rand" "regexp" "strings" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) @@ -81,3 +83,30 @@ func EqualStringSlice(aliasesA, aliasesB []string) bool { return true } + +func GenRandomCoinMetadata(r *rand.Rand) banktypes.Metadata { + randDescription := simtypes.RandStringOfLength(r, 10) + randTokenBase := "a" + simtypes.RandStringOfLength(r, 4) + randSymbol := strings.ToUpper(simtypes.RandStringOfLength(r, 4)) + + validMetadata := banktypes.Metadata{ + Description: randDescription, + Base: randTokenBase, + // NOTE: Denom units MUST be increasing + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: randTokenBase, + Exponent: 0, + }, + { + Denom: randTokenBase[1:], + Exponent: uint32(18), + }, + }, + Name: randTokenBase, + Symbol: randSymbol, + Display: randTokenBase, + } + + return validMetadata +} diff --git a/x/govshuttle/module.go b/x/govshuttle/module.go index 39544f20..a66a84a9 100644 --- a/x/govshuttle/module.go +++ b/x/govshuttle/module.go @@ -6,6 +6,7 @@ import ( // this line is used by starport scaffolding # 1 + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" @@ -13,16 +14,16 @@ import ( "cosmossdk.io/core/address" "cosmossdk.io/core/appmodule" + "github.com/Canto-Network/Canto/v7/x/govshuttle/client/cli" + "github.com/Canto-Network/Canto/v7/x/govshuttle/keeper" + "github.com/Canto-Network/Canto/v7/x/govshuttle/simulation" + "github.com/Canto-Network/Canto/v7/x/govshuttle/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" - - "github.com/Canto-Network/Canto/v7/x/govshuttle/client/cli" - "github.com/Canto-Network/Canto/v7/x/govshuttle/keeper" - "github.com/Canto-Network/Canto/v7/x/govshuttle/types" ) var ( @@ -41,7 +42,12 @@ var ( // AppModule implements the sdk.AppModuleBasic interface. type AppModuleBasic struct { - ac address.Codec + ac address.Codec + cdc codec.Codec +} + +func NewAppModuleBasic(ac address.Codec, cdc codec.Codec) AppModuleBasic { + return AppModuleBasic{ac: ac, cdc: cdc} } // Name returns the capability module's name. @@ -104,12 +110,13 @@ type AppModule struct { } func NewAppModule( + cdc codec.Codec, k keeper.Keeper, ak authkeeper.AccountKeeper, ac address.Codec, ) AppModule { return AppModule{ - AppModuleBasic: AppModuleBasic{ac: ac}, + AppModuleBasic: AppModuleBasic{ac: ac, cdc: cdc}, keeper: k, accountkeeper: ak, } @@ -156,3 +163,8 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw // ConsensusVersion implements ConsensusVersion. func (AppModule) ConsensusVersion() uint64 { return 2 } + +// ProposalMsgs returns msgs used for governance proposals for simulations. +func (am AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { + return simulation.ProposalMsgs(am.keeper) +} diff --git a/x/govshuttle/module_simulation.go b/x/govshuttle/module_simulation.go index 4ceed0de..80d8dc8a 100644 --- a/x/govshuttle/module_simulation.go +++ b/x/govshuttle/module_simulation.go @@ -1,8 +1,6 @@ package govshuttle import ( - "math/rand" - //"github.com/Canto-Network/Canto/v2/testutil/sample" govshuttlesimulation "github.com/Canto-Network/Canto/v7/x/govshuttle/simulation" "github.com/Canto-Network/Canto/v7/x/govshuttle/types" @@ -44,14 +42,10 @@ func (AppModule) ProposalContents(_ module.SimulationState) []simtypes.WeightedP return nil } -// RandomizedParams creates randomized param changes for the simulator -func (am AppModule) RandomizedParams(_ *rand.Rand) []simtypes.LegacyParamChange { - - return []simtypes.LegacyParamChange{} -} - // RegisterStoreDecoder registers a decoder -func (am AppModule) RegisterStoreDecoder(_ simtypes.StoreDecoderRegistry) {} +func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { + sdr[types.ModuleName] = govshuttlesimulation.NewDecodeStore(am.cdc) +} // WeightedOperations returns the all the gov module operations with their respective weights. func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { diff --git a/x/govshuttle/simulation/decoder.go b/x/govshuttle/simulation/decoder.go new file mode 100644 index 00000000..99a179d2 --- /dev/null +++ b/x/govshuttle/simulation/decoder.go @@ -0,0 +1,29 @@ +package simulation + +import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/kv" + "github.com/ethereum/go-ethereum/common" + + "github.com/Canto-Network/Canto/v7/x/govshuttle/types" +) + +// 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[:4], types.PortKey): + var paA, paB common.Address + paA = common.BytesToAddress(kvA.Value) + paB = common.BytesToAddress(kvB.Value) + return fmt.Sprintf("%v\n%v", paA, paB) + + default: + panic(fmt.Sprintf("invalid govshuttle key prefix %X", kvA.Key[:1])) + } + } +} diff --git a/x/govshuttle/simulation/decoder_test.go b/x/govshuttle/simulation/decoder_test.go new file mode 100644 index 00000000..253db186 --- /dev/null +++ b/x/govshuttle/simulation/decoder_test.go @@ -0,0 +1,49 @@ +package simulation_test + +import ( + "fmt" + "testing" + + "github.com/Canto-Network/Canto/v7/x/govshuttle" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/evmos/ethermint/tests" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/Canto-Network/Canto/v7/x/govshuttle/simulation" + "github.com/Canto-Network/Canto/v7/x/govshuttle/types" +) + +func TestGovShuttleStore(t *testing.T) { + cdc := moduletestutil.MakeTestEncodingConfig(govshuttle.AppModuleBasic{}).Codec + dec := simulation.NewDecodeStore(cdc) + + portAddress := tests.GenerateAddress() + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: types.PortKey, Value: portAddress.Bytes()}, + {Key: []byte{0x99}, Value: []byte{0x99}}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"PortAddress", fmt.Sprintf("%v\n%v", portAddress, portAddress)}, + {"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/govshuttle/simulation/genesis.go b/x/govshuttle/simulation/genesis.go new file mode 100644 index 00000000..8fc016eb --- /dev/null +++ b/x/govshuttle/simulation/genesis.go @@ -0,0 +1,20 @@ +package simulation + +import ( + "encoding/json" + "fmt" + + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/Canto-Network/Canto/v7/x/govshuttle/types" +) + +// DONTCOVER + +func RandomizedGenState(simState *module.SimulationState) { + genesis := types.DefaultGenesis() + + bz, _ := json.MarshalIndent(&genesis, "", " ") + fmt.Printf("Selected randomly generated govshuttle parameters:\n%s\n", bz) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) +} diff --git a/x/govshuttle/simulation/genesis_test.go b/x/govshuttle/simulation/genesis_test.go new file mode 100644 index 00000000..2a333634 --- /dev/null +++ b/x/govshuttle/simulation/genesis_test.go @@ -0,0 +1,71 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + sdkmath "cosmossdk.io/math" + "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/govshuttle/simulation" + "github.com/Canto-Network/Canto/v7/x/govshuttle/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: sdkmath.NewInt(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, types.Params{}, genState.Params) +} + +// 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/govshuttle/simulation/proposals.go b/x/govshuttle/simulation/proposals.go new file mode 100644 index 00000000..36ddd124 --- /dev/null +++ b/x/govshuttle/simulation/proposals.go @@ -0,0 +1,97 @@ +package simulation + +import ( + "math/rand" + + "github.com/Canto-Network/Canto/v7/app/params" + "github.com/Canto-Network/Canto/v7/x/govshuttle/keeper" + "github.com/Canto-Network/Canto/v7/x/govshuttle/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +// Simulation operation weights constants +const ( + OpWeightSimulateLendingMarketProposal = "op_weight_lending_market_proposal" + OpWeightSimulateTreasuryProposal = "op_weight_treasury_proposal" +) + +// ProposalMsgs defines the module weighted proposals' contents +func ProposalMsgs(k keeper.Keeper) []simtypes.WeightedProposalMsg { + return []simtypes.WeightedProposalMsg{ + simulation.NewWeightedProposalMsg( + OpWeightSimulateLendingMarketProposal, + params.DefaultWeightLendingMarketProposal, + SimulateMsgLendingMarket(k), + ), + simulation.NewWeightedProposalMsg( + OpWeightSimulateTreasuryProposal, + params.DefaultWeightRegisterERC20Proposal, + SimulateMsgTreasury(k), + ), + } +} + +func SimulateMsgLendingMarket(k keeper.Keeper) simtypes.MsgSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) sdk.Msg { + treasuryProposalMetadata := types.TreasuryProposalMetadata{ + PropID: 1, + Recipient: accs[r.Intn(len(accs))].Address.String(), + Amount: uint64(simtypes.RandIntBetween(r, 0, 10000)), + Denom: "canto", + } + + treasuryProposal := types.TreasuryProposal{ + Title: simtypes.RandStringOfLength(r, 10), + Description: simtypes.RandStringOfLength(r, 100), + Metadata: &treasuryProposalMetadata, + } + + lendingMarketProposal := treasuryProposal.FromTreasuryToLendingMarket() + lendingMarketProposal.Metadata.Calldatas = []string{"callData1"} + + var authority sdk.AccAddress = address.Module("gov") + + msg := &types.MsgLendingMarketProposal{ + Authority: authority.String(), + Title: lendingMarketProposal.Title, + Description: lendingMarketProposal.Description, + Metadata: lendingMarketProposal.Metadata, + } + + if _, err := k.LendingMarketProposal(ctx, msg); err != nil { + panic(err) + } + + return msg + } +} + +func SimulateMsgTreasury(k keeper.Keeper) simtypes.MsgSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) sdk.Msg { + + treasuryProposalMetadata := types.TreasuryProposalMetadata{ + PropID: 1, + Recipient: accs[r.Intn(len(accs))].Address.String(), + Amount: uint64(simtypes.RandIntBetween(r, 0, 10000)), + Denom: "canto", + } + + var authority sdk.AccAddress = address.Module("gov") + + msg := &types.MsgTreasuryProposal{ + Authority: authority.String(), + Title: simtypes.RandStringOfLength(r, 10), + Description: simtypes.RandStringOfLength(r, 100), + Metadata: &treasuryProposalMetadata, + } + + if _, err := k.TreasuryProposal(ctx, msg); err != nil { + panic(err) + } + + return msg + } +} diff --git a/x/govshuttle/simulation/proposals_test.go b/x/govshuttle/simulation/proposals_test.go new file mode 100644 index 00000000..88b01e7b --- /dev/null +++ b/x/govshuttle/simulation/proposals_test.go @@ -0,0 +1,100 @@ +package simulation_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/Canto-Network/Canto/v7/app/params" + "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" + "github.com/Canto-Network/Canto/v7/app" + "github.com/Canto-Network/Canto/v7/x/govshuttle/simulation" + "github.com/Canto-Network/Canto/v7/x/govshuttle/types" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/staking/testutil" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +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) + validator := getTestingValidator0(t, app, ctx, simAccs) + consAddr, err := validator.GetConsAddr() + require.NoError(t, err) + ctx = ctx.WithBlockHeader(cmtproto.Header{Height: 1, + ChainID: "canto_9001-1", + Time: time.Now().UTC(), + ProposerAddress: consAddr, + }) + ctx = ctx.WithChainID("canto_9001-1") + return app, ctx +} + +func getTestingValidator0(t *testing.T, app *app.Canto, ctx sdk.Context, accounts []simtypes.Account) stakingtypes.Validator { + commission0 := stakingtypes.NewCommission(sdkmath.LegacyZeroDec(), sdkmath.LegacyOneDec(), sdkmath.LegacyOneDec()) + 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 := testutil.NewValidator(t, valAddr, valPubKey) + validator, err := validator.SetInitialCommission(commission) + require.NoError(t, err) + + validator.DelegatorShares = sdkmath.LegacyNewDec(100) + validator.Tokens = app.StakingKeeper.TokensFromConsensusPower(ctx, 100) + + app.StakingKeeper.SetValidator(ctx, validator) + app.StakingKeeper.SetValidatorByConsAddr(ctx, validator) + + return validator +} + +func TestProposalMsgs(t *testing.T) { + app, ctx := createTestApp(t, false) + + // initialize parameters + s := rand.NewSource(1) + r := rand.New(s) + + accounts := simtypes.RandomAccounts(r, 3) + + // execute ProposalMsgs function + weightedProposalMsgs := simulation.ProposalMsgs(app.GovshuttleKeeper) + require.Equal(t, 2, len(weightedProposalMsgs)) + + w0 := weightedProposalMsgs[0] + w1 := weightedProposalMsgs[1] + + // tests w0 interface + require.Equal(t, simulation.OpWeightSimulateLendingMarketProposal, w0.AppParamsKey()) + require.Equal(t, params.DefaultWeightLendingMarketProposal, w0.DefaultWeight()) + + // tests w1 interface + require.Equal(t, simulation.OpWeightSimulateTreasuryProposal, w1.AppParamsKey()) + require.Equal(t, params.DefaultWeightTreasuryProposal, w1.DefaultWeight()) + + msg := w0.MsgSimulatorFn()(r, ctx, accounts) + MsgLendingMarket, ok := msg.(*types.MsgLendingMarketProposal) + require.True(t, ok) + require.Equal(t, sdk.AccAddress(address.Module("gov")).String(), MsgLendingMarket.Authority) + + msg = w1.MsgSimulatorFn()(r, ctx, accounts) + MsgTreasury, ok := msg.(*types.MsgTreasuryProposal) + require.True(t, ok) + require.Equal(t, sdk.AccAddress(address.Module("gov")).String(), MsgTreasury.Authority) + +} diff --git a/x/inflation/module.go b/x/inflation/module.go index 48b7ef7a..cc8443e9 100644 --- a/x/inflation/module.go +++ b/x/inflation/module.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "math/rand" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" @@ -23,21 +22,29 @@ import ( "github.com/Canto-Network/Canto/v7/x/inflation/client/cli" "github.com/Canto-Network/Canto/v7/x/inflation/keeper" + "github.com/Canto-Network/Canto/v7/x/inflation/simulation" "github.com/Canto-Network/Canto/v7/x/inflation/types" ) // type check to ensure the interface is properly implemented var ( - _ module.AppModuleBasic = AppModuleBasic{} - _ module.AppModuleBasic = AppModule{} - _ module.HasServices = AppModule{} - _ module.HasABCIGenesis = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + _ module.AppModuleBasic = AppModule{} + _ module.HasServices = AppModule{} + _ module.HasABCIGenesis = AppModule{} + _ module.AppModuleSimulation = AppModule{} _ appmodule.AppModule = AppModule{} ) // app module Basics object -type AppModuleBasic struct{} +type AppModuleBasic struct { + cdc codec.Codec +} + +func NewAppModuleBasic(cdc codec.Codec) AppModuleBasic { + return AppModuleBasic{cdc: cdc} +} // Name returns the inflation module's name. func (AppModuleBasic) Name() string { @@ -105,12 +112,13 @@ type AppModule struct { // NewAppModule creates a new AppModule Object func NewAppModule( + cdc codec.Codec, k keeper.Keeper, ak authkeeper.AccountKeeper, sk stakingkeeper.Keeper, ) AppModule { return AppModule{ - AppModuleBasic: AppModuleBasic{}, + AppModuleBasic: NewAppModuleBasic(cdc), keeper: k, ak: ak, sk: sk, @@ -179,13 +187,14 @@ func (am AppModule) ProposalContents(simState module.SimulationState) []simtypes return []simtypes.WeightedProposalContent{} } -// RandomizedParams creates randomized inflation param changes for the simulator. -func (am AppModule) RandomizedParams(r *rand.Rand) []simtypes.LegacyParamChange { - return []simtypes.LegacyParamChange{} +// ProposalMsgs returns msgs used for governance proposals for simulations. +func (AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { + return simulation.ProposalMsgs() } // RegisterStoreDecoder registers a decoder for inflation module's types. func (am AppModule) RegisterStoreDecoder(decoderRegistry simtypes.StoreDecoderRegistry) { + decoderRegistry[types.ModuleName] = simulation.NewDecodeStore(am.cdc) } // WeightedOperations doesn't return any inflation module operation. diff --git a/x/inflation/simulation/decoder.go b/x/inflation/simulation/decoder.go new file mode 100644 index 00000000..0b54a1c8 --- /dev/null +++ b/x/inflation/simulation/decoder.go @@ -0,0 +1,54 @@ +package simulation + +import ( + "bytes" + "fmt" + + sdkmath "cosmossdk.io/math" + "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/inflation/types" +) + +// 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[:1], types.KeyPrefixPeriod): + var pA, pB uint64 + pA = sdk.BigEndianToUint64(kvA.Value) + pB = sdk.BigEndianToUint64(kvB.Value) + return fmt.Sprintf("%v\n%v", pA, pB) + + case bytes.Equal(kvA.Key[:1], types.KeyPrefixEpochMintProvision): + var empA, empB sdkmath.LegacyDec + empA.Unmarshal(kvA.Value) + empB.Unmarshal(kvB.Value) + return fmt.Sprintf("%v\n%v", empA, empB) + + case bytes.Equal(kvA.Key[:1], types.KeyPrefixEpochIdentifier): + var eiA, eiB string + eiA = string(kvA.Value) + eiB = string(kvB.Value) + return fmt.Sprintf("%v\n%v", eiA, eiB) + + case bytes.Equal(kvA.Key[:1], types.KeyPrefixEpochsPerPeriod): + var eppA, eppB uint64 + eppA = sdk.BigEndianToUint64(kvA.Value) + eppB = sdk.BigEndianToUint64(kvB.Value) + return fmt.Sprintf("%v\n%v", eppA, eppB) + + case bytes.Equal(kvA.Key[:1], types.KeyPrefixSkippedEpochs): + var seA, seB uint64 + seA = sdk.BigEndianToUint64(kvA.Value) + seB = sdk.BigEndianToUint64(kvB.Value) + return fmt.Sprintf("%v\n%v", seA, seB) + + default: + panic(fmt.Sprintf("invalid farming key prefix %X", kvA.Key[:1])) + } + } +} diff --git a/x/inflation/simulation/decoder_test.go b/x/inflation/simulation/decoder_test.go new file mode 100644 index 00000000..684862c1 --- /dev/null +++ b/x/inflation/simulation/decoder_test.go @@ -0,0 +1,64 @@ +package simulation_test + +import ( + "fmt" + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/Canto-Network/Canto/v7/x/inflation" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/Canto-Network/Canto/v7/x/inflation/simulation" + "github.com/Canto-Network/Canto/v7/x/inflation/types" +) + +func TestInflationStore(t *testing.T) { + cdc := moduletestutil.MakeTestEncodingConfig(inflation.AppModuleBasic{}).Codec + dec := simulation.NewDecodeStore(cdc) + + period := uint64(1) + epochMintProvision := sdkmath.LegacyNewDec(2) + epochIdentifier := "epochIdentifier" + epochPerPeriod := uint64(3) + skippedEpoch := uint64(4) + + marshaled, _ := epochMintProvision.Marshal() + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: types.KeyPrefixPeriod, Value: sdk.Uint64ToBigEndian(period)}, + {Key: types.KeyPrefixEpochMintProvision, Value: marshaled}, + {Key: types.KeyPrefixEpochIdentifier, Value: []byte(epochIdentifier)}, + {Key: types.KeyPrefixEpochsPerPeriod, Value: sdk.Uint64ToBigEndian(epochPerPeriod)}, + {Key: types.KeyPrefixSkippedEpochs, Value: sdk.Uint64ToBigEndian(skippedEpoch)}, + {Key: []byte{0x99}, Value: []byte{0x99}}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"Period", fmt.Sprintf("%v\n%v", period, period)}, + {"EpochMintProvision", fmt.Sprintf("%v\n%v", epochMintProvision, epochMintProvision)}, + {"EpochIdentifier", fmt.Sprintf("%v\n%v", epochIdentifier, epochIdentifier)}, + {"EpochsPerPeriod", fmt.Sprintf("%v\n%v", epochPerPeriod, epochPerPeriod)}, + {"SkippedEpochs", fmt.Sprintf("%v\n%v", skippedEpoch, skippedEpoch)}, + {"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/inflation/simulation/genesis.go b/x/inflation/simulation/genesis.go new file mode 100644 index 00000000..f90fd085 --- /dev/null +++ b/x/inflation/simulation/genesis.go @@ -0,0 +1,127 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/Canto-Network/Canto/v7/x/inflation/types" +) + +// DONTCOVER + +// simulation parameter constants +const ( + mintDenom = "mint_denom" + exponentialCalculation = "exponential_calculation" + inflationDistribution = "inflation_distribution" + enableInflation = "enable_inflation" + period = "period" + epochIdentifier = "epoch_identifier" + epochsPerPeriod = "epochs_per_period" + skippedEpochs = "skipped_epochs" +) + +func generateRandomBool(r *rand.Rand) bool { + return r.Int63()%2 == 0 +} + +func generateMintDenom(r *rand.Rand) string { + return sdk.DefaultBondDenom +} + +func generateExponentialCalculation(r *rand.Rand) types.ExponentialCalculation { + return types.ExponentialCalculation{ + A: sdkmath.LegacyNewDec(int64(simtypes.RandIntBetween(r, 0, 10000000))), + R: sdkmath.LegacyNewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 100)), 2), + C: sdkmath.LegacyZeroDec(), + BondingTarget: sdkmath.LegacyNewDecWithPrec(int64(simtypes.RandIntBetween(r, 1, 100)), 2), + MaxVariance: sdkmath.LegacyZeroDec(), + } +} + +func generateInflationDistribution(r *rand.Rand) types.InflationDistribution { + + stakingRewards := sdkmath.LegacyNewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 100)), 2) + communityPool := sdkmath.LegacyNewDec(1).Sub(stakingRewards) + + return types.InflationDistribution{ + StakingRewards: stakingRewards, + CommunityPool: communityPool, + } +} + +func generateEnableInflation(r *rand.Rand) bool { + return generateRandomBool(r) +} + +func generatePeriod(r *rand.Rand) uint64 { + return uint64(simtypes.RandIntBetween(r, 0, 10000000)) +} + +func generateEpochIdentifier(r *rand.Rand) string { + return "day" +} + +func generateEpochsPerPeriod(r *rand.Rand) int64 { + return int64(simtypes.RandIntBetween(r, 0, 10000000)) +} + +func generateSkippedEpochs(r *rand.Rand) uint64 { + return uint64(simtypes.RandIntBetween(r, 0, 10000000)) +} + +// RandomizedGenState generates a random GenesisState for inflation. + +func RandomizedGenState(simState *module.SimulationState) { + genesis := types.DefaultGenesisState() + + simState.AppParams.GetOrGenerate( + mintDenom, &genesis.Params.MintDenom, simState.Rand, + func(r *rand.Rand) { genesis.Params.MintDenom = generateMintDenom(r) }, + ) + + simState.AppParams.GetOrGenerate( + exponentialCalculation, &genesis.Params.ExponentialCalculation, simState.Rand, + func(r *rand.Rand) { genesis.Params.ExponentialCalculation = generateExponentialCalculation(r) }, + ) + + simState.AppParams.GetOrGenerate( + inflationDistribution, &genesis.Params.InflationDistribution, simState.Rand, + func(r *rand.Rand) { genesis.Params.InflationDistribution = generateInflationDistribution(r) }, + ) + + simState.AppParams.GetOrGenerate( + enableInflation, &genesis.Params.EnableInflation, simState.Rand, + func(r *rand.Rand) { genesis.Params.EnableInflation = generateEnableInflation(r) }, + ) + + simState.AppParams.GetOrGenerate( + period, &genesis.Period, simState.Rand, + func(r *rand.Rand) { genesis.Period = generatePeriod(r) }, + ) + + simState.AppParams.GetOrGenerate( + epochIdentifier, &genesis.EpochIdentifier, simState.Rand, + func(r *rand.Rand) { genesis.EpochIdentifier = generateEpochIdentifier(r) }, + ) + + simState.AppParams.GetOrGenerate( + epochsPerPeriod, &genesis.EpochsPerPeriod, simState.Rand, + func(r *rand.Rand) { genesis.EpochsPerPeriod = generateEpochsPerPeriod(r) }, + ) + + simState.AppParams.GetOrGenerate( + skippedEpochs, &genesis.SkippedEpochs, simState.Rand, + func(r *rand.Rand) { genesis.SkippedEpochs = generateSkippedEpochs(r) }, + ) + + bz, _ := json.MarshalIndent(&genesis, "", " ") + fmt.Printf("Selected randomly generated inflation parameters:\n%s\n", bz) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) +} diff --git a/x/inflation/simulation/genesis_test.go b/x/inflation/simulation/genesis_test.go new file mode 100644 index 00000000..96394285 --- /dev/null +++ b/x/inflation/simulation/genesis_test.go @@ -0,0 +1,88 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + sdkmath "cosmossdk.io/math" + "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/inflation/simulation" + "github.com/Canto-Network/Canto/v7/x/inflation/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: sdkmath.NewInt(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, "stake", genState.Params.MintDenom) + require.Equal(t, types.ExponentialCalculation{ + A: sdkmath.LegacyNewDec(2712964), + R: sdkmath.LegacyNewDecWithPrec(11, 2), + C: sdkmath.LegacyZeroDec(), + BondingTarget: sdkmath.LegacyNewDecWithPrec(94, 2), + MaxVariance: sdkmath.LegacyZeroDec(), + }, genState.Params.ExponentialCalculation) + require.Equal(t, types.InflationDistribution{ + StakingRewards: sdkmath.LegacyNewDecWithPrec(1, 1), + CommunityPool: sdkmath.LegacyNewDecWithPrec(9, 1), + }, genState.Params.InflationDistribution) + require.Equal(t, false, genState.Params.EnableInflation) + require.Equal(t, uint64(1654145), genState.Period) + require.Equal(t, "day", genState.EpochIdentifier) + require.Equal(t, int64(6634432), genState.EpochsPerPeriod) + require.Equal(t, uint64(5142676), genState.SkippedEpochs) + +} + +// 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/inflation/simulation/proposals.go b/x/inflation/simulation/proposals.go new file mode 100644 index 00000000..a9fd7b3e --- /dev/null +++ b/x/inflation/simulation/proposals.go @@ -0,0 +1,47 @@ +package simulation + +import ( + "math/rand" + + "github.com/Canto-Network/Canto/v7/x/inflation/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +// Simulation operation weights constants +const ( + DefaultWeightMsgUpdateParams int = 100 + + OpWeightMsgUpdateParams = "op_weight_msg_update_params" +) + +// ProposalMsgs defines the module weighted proposals' contents +func ProposalMsgs() []simtypes.WeightedProposalMsg { + return []simtypes.WeightedProposalMsg{ + simulation.NewWeightedProposalMsg( + OpWeightMsgUpdateParams, + DefaultWeightMsgUpdateParams, + SimulateMsgUpdateParams, + ), + } +} + +// SimulateMsgUpdateParams returns a random MsgUpdateParams +func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { + // use the default gov module account address as authority + var authority sdk.AccAddress = address.Module("gov") + + params := types.DefaultParams() + + params.MintDenom = generateMintDenom(r) + params.ExponentialCalculation = generateExponentialCalculation(r) + params.InflationDistribution = generateInflationDistribution(r) + params.EnableInflation = generateRandomBool(r) + + return &types.MsgUpdateParams{ + Authority: authority.String(), + Params: params, + } +} diff --git a/x/inflation/simulation/proposals_test.go b/x/inflation/simulation/proposals_test.go new file mode 100644 index 00000000..ae1b03ce --- /dev/null +++ b/x/inflation/simulation/proposals_test.go @@ -0,0 +1,53 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/Canto-Network/Canto/v7/x/inflation/simulation" + "github.com/Canto-Network/Canto/v7/x/inflation/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/stretchr/testify/require" +) + +func TestProposalMsgs(t *testing.T) { + // initialize parameters + s := rand.NewSource(1) + r := rand.New(s) + + ctx := sdk.NewContext(nil, cmtproto.Header{}, true, nil) + accounts := simtypes.RandomAccounts(r, 3) + + // execute ProposalMsgs function + weightedProposalMsgs := simulation.ProposalMsgs() + require.Equal(t, 1, len(weightedProposalMsgs)) + + w0 := weightedProposalMsgs[0] + + // tests w0 interface: + require.Equal(t, simulation.OpWeightMsgUpdateParams, w0.AppParamsKey()) + require.Equal(t, simulation.DefaultWeightMsgUpdateParams, w0.DefaultWeight()) + + msg := w0.MsgSimulatorFn()(r, ctx, accounts) + msgUpdateParams, ok := msg.(*types.MsgUpdateParams) + require.True(t, ok) + + require.Equal(t, sdk.AccAddress(address.Module("gov")).String(), msgUpdateParams.Authority) + require.Equal(t, sdk.DefaultBondDenom, msgUpdateParams.Params.MintDenom) //nolint:staticcheck // we're testing deprecated code here + require.Equal(t, types.ExponentialCalculation{ + A: sdkmath.LegacyNewDec(6122540), + R: sdkmath.LegacyNewDecWithPrec(56, 2), + C: sdkmath.LegacyZeroDec(), + BondingTarget: sdkmath.LegacyNewDecWithPrec(7, 2), + MaxVariance: sdkmath.LegacyZeroDec(), + }, msgUpdateParams.Params.ExponentialCalculation) + require.Equal(t, types.InflationDistribution{ + StakingRewards: sdkmath.LegacyNewDecWithPrec(94, 2), + CommunityPool: sdkmath.LegacyNewDecWithPrec(6, 2), + }, msgUpdateParams.Params.InflationDistribution) + require.Equal(t, false, msgUpdateParams.Params.EnableInflation) +} diff --git a/x/onboarding/module.go b/x/onboarding/module.go index 97cbe2eb..05a511be 100644 --- a/x/onboarding/module.go +++ b/x/onboarding/module.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "math/rand" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -146,10 +145,6 @@ func (AppModule) ProposalContents(_ module.SimulationState) []simtypes.WeightedP return []simtypes.WeightedProposalContent{} } -func (AppModule) RandomizedParams(_ *rand.Rand) []simtypes.LegacyParamChange { - return []simtypes.LegacyParamChange{} -} - func (AppModule) RegisterStoreDecoder(_ simtypes.StoreDecoderRegistry) { }