diff --git a/app/app.go b/app/app.go index 3d488010..de95adfe 100644 --- a/app/app.go +++ b/app/app.go @@ -442,8 +442,8 @@ func NewCanto( AddRoute(distrtypes.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.DistrKeeper)). AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.UpgradeKeeper)). AddRoute(ibcclienttypes.RouterKey, ibcclient.NewClientProposalHandler(app.IBCKeeper.ClientKeeper)). - AddRoute(erc20types.RouterKey, erc20.NewErc20ProposalHandler(&app.Erc20Keeper)). - AddRoute(govshuttletypes.RouterKey, govshuttle.NewgovshuttleProposalHandler(&app.GovshuttleKeeper)) + AddRoute(erc20types.RouterKey, erc20keeper.NewErc20ProposalHandler(&app.Erc20Keeper)). + AddRoute(govshuttletypes.RouterKey, govshuttlekeeper.NewgovshuttleProposalHandler(&app.GovshuttleKeeper)) govKeeper := govkeeper.NewKeeper( appCodec, keys[govtypes.StoreKey], app.GetSubspace(govtypes.ModuleName), @@ -595,12 +595,12 @@ func NewCanto( evm.NewAppModule(app.EvmKeeper, app.AccountKeeper), feemarket.NewAppModule(app.FeeMarketKeeper), // Canto app modules - inflation.NewAppModule(app.InflationKeeper, app.AccountKeeper, app.StakingKeeper), - erc20.NewAppModule(app.Erc20Keeper, app.AccountKeeper), + inflation.NewAppModule(appCodec, app.InflationKeeper, app.AccountKeeper, app.StakingKeeper), + erc20.NewAppModule(appCodec, app.Erc20Keeper, app.AccountKeeper, app.BankKeeper, app.EvmKeeper, app.FeeMarketKeeper), epochs.NewAppModule(appCodec, app.EpochsKeeper), onboarding.NewAppModule(*app.OnboardingKeeper), - govshuttle.NewAppModule(app.GovshuttleKeeper, app.AccountKeeper), - csr.NewAppModule(app.CSRKeeper, app.AccountKeeper), + govshuttle.NewAppModule(appCodec, app.GovshuttleKeeper, app.AccountKeeper), + csr.NewAppModule(appCodec, app.CSRKeeper, app.AccountKeeper), coinswap.NewAppModule(appCodec, app.CoinswapKeeper, app.AccountKeeper, app.BankKeeper), ) @@ -745,9 +745,12 @@ func NewCanto( // canto, ethermint modules evm.NewAppModule(app.EvmKeeper, app.AccountKeeper), epochs.NewAppModule(appCodec, app.EpochsKeeper), - inflation.NewAppModule(app.InflationKeeper, app.AccountKeeper, app.StakingKeeper), + inflation.NewAppModule(appCodec, app.InflationKeeper, app.AccountKeeper, app.StakingKeeper), feemarket.NewAppModule(app.FeeMarketKeeper), coinswap.NewAppModule(appCodec, app.CoinswapKeeper, app.AccountKeeper, app.BankKeeper), + csr.NewAppModule(appCodec, app.CSRKeeper, app.AccountKeeper), + govshuttle.NewAppModule(appCodec, app.GovshuttleKeeper, app.AccountKeeper), + erc20.NewAppModule(appCodec, app.Erc20Keeper, app.AccountKeeper, app.BankKeeper, app.EvmKeeper, app.FeeMarketKeeper), // TODO: Modules that have not yet been implemented for simulation // govshuttle, csr, inflation, erc20 diff --git a/app/params/weights.go b/app/params/weights.go new file mode 100644 index 00000000..661c8b76 --- /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 = 10 + DefaultWeightMsgAddLiquidity int = 20 + DefaultWeightMsgRemoveLiquidity int = 10 +) diff --git a/app/sim_test.go b/app/sim_test.go index bbac1d06..d2000fca 100644 --- a/app/sim_test.go +++ b/app/sim_test.go @@ -204,7 +204,9 @@ func TestAppImportExport(t *testing.T) { }, }, {app.keys[distrtypes.StoreKey], newApp.keys[distrtypes.StoreKey], [][]byte{}}, - {app.keys[paramstypes.StoreKey], newApp.keys[paramstypes.StoreKey], [][]byte{}}, + {app.keys[paramstypes.StoreKey], newApp.keys[paramstypes.StoreKey], [][]byte{ + []byte("evm/EnableExtraEIPs"), []byte("bank/SendEnabled"), + }}, {app.keys[evidencetypes.StoreKey], newApp.keys[evidencetypes.StoreKey], [][]byte{}}, {app.keys[capabilitytypes.StoreKey], newApp.keys[capabilitytypes.StoreKey], [][]byte{}}, {app.keys[feegrant.StoreKey], newApp.keys[feegrant.StoreKey], [][]byte{}}, diff --git a/proto/canto/csr/v1/genesis.proto b/proto/canto/csr/v1/genesis.proto index d9370ecd..a00d7833 100644 --- a/proto/canto/csr/v1/genesis.proto +++ b/proto/canto/csr/v1/genesis.proto @@ -3,6 +3,7 @@ package canto.csr.v1; import "gogoproto/gogo.proto"; import "canto/csr/v1/params.proto"; +import "canto/csr/v1/csr.proto"; option go_package = "github.com/Canto-Network/Canto/v7/x/csr/types"; @@ -10,4 +11,6 @@ option go_package = "github.com/Canto-Network/Canto/v7/x/csr/types"; message GenesisState { // params defines all of the parameters of the module Params params = 1 [ (gogoproto.nullable) = false ]; + repeated CSR csrs = 2 [ (gogoproto.nullable) = false ]; + bytes turnstile_address = 3; } diff --git a/proto/canto/govshuttle/v1/genesis.proto b/proto/canto/govshuttle/v1/genesis.proto index 5bc4b9a8..b6028e23 100644 --- a/proto/canto/govshuttle/v1/genesis.proto +++ b/proto/canto/govshuttle/v1/genesis.proto @@ -10,5 +10,6 @@ option go_package = "github.com/Canto-Network/Canto/v7/x/govshuttle/types"; // GenesisState defines the govshuttle module's genesis state. message GenesisState { Params params = 1 [ (gogoproto.nullable) = false ]; + bytes port_address = 2; // this line is used by starport scaffolding # genesis/proto/state } diff --git a/x/coinswap/module.go b/x/coinswap/module.go index 17b280e8..ac2e28d0 100644 --- a/x/coinswap/module.go +++ b/x/coinswap/module.go @@ -181,6 +181,7 @@ func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange { // RegisterStoreDecoder registers a decoder for coinswap module's types func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) } diff --git a/x/coinswap/simulation/decoder.go b/x/coinswap/simulation/decoder.go index 554f894e..015f3e83 100644 --- a/x/coinswap/simulation/decoder.go +++ b/x/coinswap/simulation/decoder.go @@ -1,10 +1,41 @@ package simulation import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/Canto-Network/Canto/v7/x/coinswap/types" ) -// DecodeStore unmarshals the KVPair's Value to the corresponding htlc type -func DecodeStore(kvA, kvB kv.Pair) string { - return "" +// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's +// Value to the corresponding farming type. +func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { + return func(kvA, kvB kv.Pair) string { + switch { + case bytes.Equal(kvA.Key[:], []byte(types.KeyPool)): + var pA, pB types.Pool + cdc.MustUnmarshal(kvA.Value, &pA) + cdc.MustUnmarshal(kvB.Value, &pB) + return fmt.Sprintf("%v\n%v", pA, pB) + + case bytes.Equal(kvA.Key[:], []byte(types.KeyNextPoolSequence)): + var seqA, seqB uint64 + seqA = sdk.BigEndianToUint64(kvA.Value) + seqB = sdk.BigEndianToUint64(kvB.Value) + return fmt.Sprintf("%v\n%v", seqA, seqB) + + case bytes.Equal(kvA.Key[:], []byte(types.KeyPoolLptDenom)): + var pA, pB types.Pool + cdc.MustUnmarshal(kvA.Value, &pA) + cdc.MustUnmarshal(kvB.Value, &pB) + return fmt.Sprintf("%v\n%v", pA, pB) + + default: + panic(fmt.Sprintf("invalid coinswap key prefix %X", kvA.Key[:1])) + } + } } diff --git a/x/coinswap/simulation/decoder_test.go b/x/coinswap/simulation/decoder_test.go new file mode 100644 index 00000000..a8161757 --- /dev/null +++ b/x/coinswap/simulation/decoder_test.go @@ -0,0 +1,59 @@ +package simulation_test + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/Canto-Network/Canto/v7/x/coinswap/simulation" + "github.com/Canto-Network/Canto/v7/x/coinswap/types" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/types/kv" +) + +func TestCoinSwapStore(t *testing.T) { + cdc := simapp.MakeTestEncodingConfig() + dec := simulation.NewDecodeStore(cdc.Marshaler) + + pool := types.Pool{ + Id: types.GetPoolId("denom1"), + StandardDenom: "denom2", + CounterpartyDenom: "denom1", + EscrowAddress: types.GetReservePoolAddr("lptDenom").String(), + LptDenom: "lptDenom", + } + + sequence := uint64(1) + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: []byte(types.KeyPool), Value: cdc.Marshaler.MustMarshal(&pool)}, + {Key: []byte(types.KeyPoolLptDenom), Value: cdc.Marshaler.MustMarshal(&pool)}, + {Key: []byte(types.KeyNextPoolSequence), Value: sdk.Uint64ToBigEndian(sequence)}, + {Key: []byte{0x99}, Value: []byte{0x99}}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"Pool", fmt.Sprintf("%v\n%v", pool, pool)}, + {"PoolLptDenom", fmt.Sprintf("%v\n%v", pool, pool)}, + {"NextPoolSequence", fmt.Sprintf("%v\n%v", sequence, sequence)}, + {"other", ""}, + } + for i, tt := range tests { + i, tt := i, tt + t.Run(tt.name, func(t *testing.T) { + switch i { + case len(tests) - 1: + require.Panics(t, func() { dec(kvPairs.Pairs[i], kvPairs.Pairs[i]) }, tt.name) + default: + require.Equal(t, tt.expectedLog, dec(kvPairs.Pairs[i], kvPairs.Pairs[i]), tt.name) + } + }) + } +} diff --git a/x/coinswap/simulation/genesis.go b/x/coinswap/simulation/genesis.go index 98b3a650..f029d36f 100644 --- a/x/coinswap/simulation/genesis.go +++ b/x/coinswap/simulation/genesis.go @@ -1,8 +1,80 @@ package simulation import ( + "encoding/json" + "fmt" + "math/rand" + + "github.com/Canto-Network/Canto/v7/x/coinswap/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" +) + +// simulation parameter constants +const ( + fee = "fee" + poolCreationFee = "pool_creation_fee" + taxRate = "tax_rate" + maxStandardCoinPerPool = "max_standard_coin_per_pool" + maxSwapAmount = "max_swap_amount" ) +func generateRandomFee(r *rand.Rand) sdk.Dec { + return sdk.NewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 10)), 3) +} + +func generateRandomPoolCreationFee(r *rand.Rand) sdk.Coin { + return sdk.NewInt64Coin(sdk.DefaultBondDenom, int64(simtypes.RandIntBetween(r, 0, 1000000))) +} + +func generateRandomTaxRate(r *rand.Rand) sdk.Dec { + return sdk.NewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 10)), 3) +} + +func generateRandomMaxStandardCoinPerPool(r *rand.Rand) sdk.Int { + return sdk.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 0, 10000)), 18) +} + +func generateRandomMaxSwapAmount(r *rand.Rand) sdk.Coins { + return sdk.NewCoins( + sdk.NewCoin(types.UsdcIBCDenom, sdk.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 6)), + sdk.NewCoin(types.UsdtIBCDenom, sdk.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 6)), + sdk.NewCoin(types.EthIBCDenom, sdk.NewIntWithDecimal(int64(simtypes.RandIntBetween(r, 1, 100)), 16)), + ) +} + // RandomizedGenState generates a random GenesisState for coinswap -func RandomizedGenState(simState *module.SimulationState) {} +func RandomizedGenState(simState *module.SimulationState) { + genesis := types.DefaultGenesisState() + + simState.AppParams.GetOrGenerate( + simState.Cdc, fee, &genesis.Params.Fee, simState.Rand, + func(r *rand.Rand) { genesis.Params.Fee = generateRandomFee(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, poolCreationFee, &genesis.Params.PoolCreationFee, simState.Rand, + func(r *rand.Rand) { genesis.Params.PoolCreationFee = generateRandomPoolCreationFee(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, taxRate, &genesis.Params.TaxRate, simState.Rand, + func(r *rand.Rand) { genesis.Params.TaxRate = generateRandomTaxRate(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, maxStandardCoinPerPool, &genesis.Params.MaxStandardCoinPerPool, simState.Rand, + func(r *rand.Rand) { genesis.Params.MaxStandardCoinPerPool = generateRandomMaxStandardCoinPerPool(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, maxSwapAmount, &genesis.Params.MaxSwapAmount, simState.Rand, + func(r *rand.Rand) { genesis.Params.MaxSwapAmount = generateRandomMaxSwapAmount(r) }, + ) + + bz, _ := json.MarshalIndent(&genesis, "", " ") + fmt.Printf("Selected randomly generated coinswap parameters:\n%s\n", bz) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) + +} diff --git a/x/coinswap/simulation/genesis_test.go b/x/coinswap/simulation/genesis_test.go new file mode 100644 index 00000000..a5c0b8c6 --- /dev/null +++ b/x/coinswap/simulation/genesis_test.go @@ -0,0 +1,80 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/Canto-Network/Canto/v7/x/coinswap/simulation" + "github.com/Canto-Network/Canto/v7/x/coinswap/types" +) + +func TestRandomizedGenState(t *testing.T) { + interfaceRegistry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + + s := rand.NewSource(2) + r := rand.New(s) + + simState := module.SimulationState{ + AppParams: make(simtypes.AppParams), + Cdc: cdc, + Rand: r, + NumBonded: 3, + Accounts: simtypes.RandomAccounts(r, 3), + InitialStake: 1000, + GenState: make(map[string]json.RawMessage), + } + + simulation.RandomizedGenState(&simState) + + var genState types.GenesisState + simState.Cdc.MustUnmarshalJSON(simState.GenState[types.ModuleName], &genState) + + require.Equal(t, sdk.NewDecWithPrec(4, 3), genState.Params.Fee) + require.Equal(t, sdk.NewInt64Coin(sdk.DefaultBondDenom, 163511), genState.Params.PoolCreationFee) + require.Equal(t, sdk.NewDecWithPrec(6, 3), genState.Params.TaxRate) + require.Equal(t, sdk.NewIntWithDecimal(3310, 18), genState.Params.MaxStandardCoinPerPool) + require.Equal(t, sdk.NewCoins( + sdk.NewCoin(types.UsdcIBCDenom, sdk.NewIntWithDecimal(70, 6)), + sdk.NewCoin(types.UsdtIBCDenom, sdk.NewIntWithDecimal(52, 6)), + sdk.NewCoin(types.EthIBCDenom, sdk.NewIntWithDecimal(65, 16)), + ), genState.Params.MaxSwapAmount) + +} + +// TestInvalidGenesisState tests invalid genesis states. +func TestInvalidGenesisState(t *testing.T) { + interfaceRegistry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + + s := rand.NewSource(1) + r := rand.New(s) + + // all these tests will panic + tests := []struct { + simState module.SimulationState + panicMsg string + }{ + { // panic => reason: incomplete initialization of the simState + module.SimulationState{}, "invalid memory address or nil pointer dereference"}, + { // panic => reason: incomplete initialization of the simState + module.SimulationState{ + AppParams: make(simtypes.AppParams), + Cdc: cdc, + Rand: r, + }, "assignment to entry in nil map"}, + } + + for _, tt := range tests { + require.Panicsf(t, func() { simulation.RandomizedGenState(&tt.simState) }, tt.panicMsg) + } +} diff --git a/x/coinswap/simulation/operation_test.go b/x/coinswap/simulation/operation_test.go new file mode 100644 index 00000000..c8d00e02 --- /dev/null +++ b/x/coinswap/simulation/operation_test.go @@ -0,0 +1,126 @@ +package simulation_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/Canto-Network/Canto/v7/app/params" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/teststaking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/Canto-Network/Canto/v7/app" + "github.com/Canto-Network/Canto/v7/x/coinswap/simulation" + "github.com/Canto-Network/Canto/v7/x/coinswap/types" +) + +func TestWeightedOperations(t *testing.T) { + canto, ctx := createTestApp(t, false) + cdc := types.ModuleCdc + appParams := make(simtypes.AppParams) + + weightedOps := simulation.WeightedOperations( + appParams, + cdc, + canto.CoinswapKeeper, + canto.AccountKeeper, + canto.BankKeeper, + ) + + s := rand.NewSource(2) + r := rand.New(s) + accs := getTestingAccounts(t, r, canto, ctx, 10) + + expected := []struct { + weight int + opMsgRoute string + opMsgName string + }{ + {params.DefaultWeightMsgAddLiquidity, types.ModuleName, types.TypeMsgAddLiquidity}, + {params.DefaultWeightMsgSwapOrder, types.ModuleName, types.TypeMsgSwapOrder}, + {params.DefaultWeightMsgRemoveLiquidity, types.ModuleName, types.TypeMsgRemoveLiquidity}, + } + + for i, w := range weightedOps { + opMsg, _, _ := w.Op()(r, canto.BaseApp, ctx, accs, ctx.ChainID()) + require.Equal(t, expected[i].weight, w.Weight()) + require.Equal(t, expected[i].opMsgRoute, opMsg.Route) + require.Equal(t, expected[i].opMsgName, opMsg.Name) + } +} + +func createTestApp(t *testing.T, isCheckTx bool) (*app.Canto, sdk.Context) { + app := app.Setup(isCheckTx, nil) + r := rand.New(rand.NewSource(1)) + + simAccs := simtypes.RandomAccounts(r, 10) + + ctx := app.BaseApp.NewContext(isCheckTx, tmproto.Header{}) + validator := getTestingValidator0(t, app, ctx, simAccs) + consAddr, err := validator.GetConsAddr() + require.NoError(t, err) + ctx = ctx.WithBlockHeader(tmproto.Header{Height: 1, + ChainID: "canto_9001-1", + Time: time.Now().UTC(), + ProposerAddress: consAddr, + }) + return app, ctx +} + +func getTestingAccounts(t *testing.T, r *rand.Rand, app *app.Canto, ctx sdk.Context, n int) []simtypes.Account { + accounts := simtypes.RandomAccounts(r, n) + + initAmt := app.StakingKeeper.TokensFromConsensusPower(ctx, 100_000_000) + initCoins := sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, initAmt), + ) + + // add coins to the accounts + for _, account := range accounts { + acc := app.AccountKeeper.NewAccountWithAddress(ctx, account.Address) + app.AccountKeeper.SetAccount(ctx, acc) + err := fundAccount(app.BankKeeper, ctx, account.Address, initCoins) + require.NoError(t, err) + } + + return accounts +} + +func fundAccount(bk bankkeeper.Keeper, ctx sdk.Context, addr sdk.AccAddress, coins sdk.Coins) error { + if err := bk.MintCoins(ctx, types.ModuleName, coins); err != nil { + return err + } + if err := bk.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addr, coins); err != nil { + return err + } + return nil +} + +func getTestingValidator0(t *testing.T, app *app.Canto, ctx sdk.Context, accounts []simtypes.Account) stakingtypes.Validator { + commission0 := stakingtypes.NewCommission(sdk.ZeroDec(), sdk.OneDec(), sdk.OneDec()) + return getTestingValidator(t, app, ctx, accounts, commission0, 0) +} + +func getTestingValidator(t *testing.T, app *app.Canto, ctx sdk.Context, accounts []simtypes.Account, commission stakingtypes.Commission, n int) stakingtypes.Validator { + account := accounts[n] + valPubKey := account.PubKey + valAddr := sdk.ValAddress(account.PubKey.Address().Bytes()) + validator := teststaking.NewValidator(t, valAddr, valPubKey) + validator, err := validator.SetInitialCommission(commission) + require.NoError(t, err) + + validator.DelegatorShares = sdk.NewDec(100) + validator.Tokens = app.StakingKeeper.TokensFromConsensusPower(ctx, 100) + + app.StakingKeeper.SetValidator(ctx, validator) + app.StakingKeeper.SetValidatorByConsAddr(ctx, validator) + + return validator +} diff --git a/x/coinswap/simulation/operations.go b/x/coinswap/simulation/operations.go index d00e7218..bde5826e 100644 --- a/x/coinswap/simulation/operations.go +++ b/x/coinswap/simulation/operations.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/Canto-Network/Canto/v7/app/params" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/simapp/helpers" @@ -42,21 +43,21 @@ func WeightedOperations( appParams.GetOrGenerate( cdc, OpWeightMsgSwapOrder, &weightSwap, nil, func(_ *rand.Rand) { - weightSwap = 50 + weightSwap = params.DefaultWeightMsgSwapOrder }, ) appParams.GetOrGenerate( cdc, OpWeightMsgAddLiquidity, &weightAdd, nil, func(_ *rand.Rand) { - weightAdd = 100 + weightAdd = params.DefaultWeightMsgAddLiquidity }, ) appParams.GetOrGenerate( cdc, OpWeightMsgRemoveLiquidity, &weightRemove, nil, func(_ *rand.Rand) { - weightRemove = 30 + weightRemove = params.DefaultWeightMsgRemoveLiquidity }, ) @@ -105,6 +106,11 @@ func SimulateMsgAddLiquidity(k keeper.Keeper, ak types.AccountKeeper, bk types.B return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddLiquidity, "tokenDenom should not be standardDenom"), nil, err } + _, err = k.GetMaximumSwapAmount(ctx, maxToken.Denom) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddLiquidity, err.Error()), nil, nil + } + if strings.HasPrefix(maxToken.Denom, types.LptTokenPrefix) { return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddLiquidity, "tokenDenom should not be liquidity token"), nil, err } @@ -212,6 +218,11 @@ func SimulateMsgSwapOrder(k keeper.Keeper, ak types.AccountKeeper, bk types.Bank // sold coin inputCoin = RandomSpendableToken(r, spendable) + _, err = k.GetMaximumSwapAmount(ctx, inputCoin.Denom) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSwapOrder, err.Error()), nil, nil + } + if strings.HasPrefix(inputCoin.Denom, types.LptTokenPrefix) { return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSwapOrder, "inputCoin should not be liquidity token"), nil, err } @@ -240,6 +251,11 @@ func SimulateMsgSwapOrder(k keeper.Keeper, ak types.AccountKeeper, bk types.Bank return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSwapOrder, "total supply is zero"), nil, err } outputCoin = RandomTotalToken(r, coins) + _, err = k.GetMaximumSwapAmount(ctx, inputCoin.Denom) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSwapOrder, err.Error()), nil, nil + } + if strings.HasPrefix(outputCoin.Denom, types.LptTokenPrefix) { return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSwapOrder, "outputCoin should not be liquidity token"), nil, err } diff --git a/x/coinswap/simulation/params.go b/x/coinswap/simulation/params.go index 7b2badc7..457769eb 100644 --- a/x/coinswap/simulation/params.go +++ b/x/coinswap/simulation/params.go @@ -1,10 +1,9 @@ package simulation import ( - "fmt" + "encoding/json" "math/rand" - sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" @@ -18,7 +17,51 @@ func ParamChanges(r *rand.Rand) []simtypes.ParamChange { simulation.NewSimParamChange( types.ModuleName, string(types.KeyFee), func(r *rand.Rand) string { - return fmt.Sprintf("\"%s\"", sdk.NewDecWithPrec(r.Int63n(3), 3)) // 0.1%~0.3% + bz, err := json.Marshal(generateRandomFee(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange( + types.ModuleName, string(types.KeyPoolCreationFee), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomPoolCreationFee(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange( + types.ModuleName, string(types.KeyTaxRate), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomTaxRate(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange( + types.ModuleName, string(types.KeyMaxStandardCoinPerPool), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomMaxStandardCoinPerPool(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange( + types.ModuleName, string(types.KeyMaxSwapAmount), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomMaxSwapAmount(r)) + if err != nil { + panic(err) + } + return string(bz) }, ), } diff --git a/x/coinswap/simulation/params_test.go b/x/coinswap/simulation/params_test.go new file mode 100644 index 00000000..ac49ac9d --- /dev/null +++ b/x/coinswap/simulation/params_test.go @@ -0,0 +1,37 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/Canto-Network/Canto/v7/x/coinswap/simulation" +) + +func TestParamChanges(t *testing.T) { + r := rand.New(rand.NewSource(0)) + + paramChanges := simulation.ParamChanges(r) + require.Len(t, paramChanges, 5) + + expected := []struct { + composedKey string + key string + simValue string + subspace string + }{ + {"coinswap/Fee", "Fee", "\"0.004000000000000000\"", "coinswap"}, + {"coinswap/PoolCreationFee", "PoolCreationFee", `{"denom":"stake","amount":"58514"}`, "coinswap"}, + {"coinswap/TaxRate", "TaxRate", "\"0.003000000000000000\"", "coinswap"}, + {"coinswap/MaxStandardCoinPerPool", "MaxStandardCoinPerPool", "\"2506000000000000000000\"", "coinswap"}, + {"coinswap/MaxSwapAmount", "MaxSwapAmount", `[{"denom":"ibc/17CD484EE7D9723B847D95015FA3EBD1572FD13BC84FB838F55B18A57450F25B","amount":"27000000"},{"denom":"ibc/4F6A2DEFEA52CD8D90966ADCB2BD0593D3993AB0DF7F6AEB3EFD6167D79237B0","amount":"35000000"},{"denom":"ibc/DC186CA7A8C009B43774EBDC825C935CABA9743504CE6037507E6E5CCE12858A","amount":"980000000000000000"}]`, "coinswap"}, + } + + for i, p := range paramChanges { + require.Equal(t, expected[i].composedKey, p.ComposedKey()) + require.Equal(t, expected[i].key, p.Key()) + require.Equal(t, expected[i].simValue, p.SimValue()(r)) + require.Equal(t, expected[i].subspace, p.Subspace()) + } +} diff --git a/x/csr/genesis.go b/x/csr/genesis.go index 2c4089e9..98dc09c5 100644 --- a/x/csr/genesis.go +++ b/x/csr/genesis.go @@ -5,6 +5,7 @@ import ( "github.com/Canto-Network/Canto/v7/x/csr/types" sdk "github.com/cosmos/cosmos-sdk/types" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + "github.com/ethereum/go-ethereum/common" ) // InitGenesis initializes the capability module's state from a provided genesis @@ -12,6 +13,12 @@ import ( func InitGenesis(ctx sdk.Context, k keeper.Keeper, accountKeeper authkeeper.AccountKeeper, genState types.GenesisState) { // this line is used by starport scaffolding # genesis/module/init k.SetParams(ctx, genState.Params) + for _, csr := range genState.Csrs { + k.SetCSR(ctx, csr) + } + if genState.TurnstileAddress != nil { + k.SetTurnstile(ctx, common.BytesToAddress(genState.TurnstileAddress)) + } // make sure that the csr module account is set on genesis if acc := accountKeeper.GetModuleAccount(ctx, types.ModuleName); acc == nil { // NOTE: shouldn't occur @@ -23,6 +30,20 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, accountKeeper authkeeper.Acco func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { genesis := types.DefaultGenesis() genesis.Params = k.GetParams(ctx) + csrs := k.GetAllCSRs(ctx) + + if len(csrs) == 0 { + genesis.Csrs = []types.CSR{} + } else { + genesis.Csrs = csrs + } + + turnstileAddress, found := k.GetTurnstile(ctx) + if found { + genesis.TurnstileAddress = turnstileAddress.Bytes() + } else { + genesis.TurnstileAddress = nil + } return genesis } diff --git a/x/csr/keeper/csr.go b/x/csr/keeper/csr.go index 7846eb3b..fb5d73e6 100644 --- a/x/csr/keeper/csr.go +++ b/x/csr/keeper/csr.go @@ -25,6 +25,27 @@ func (k Keeper) GetCSR(ctx sdk.Context, nftId uint64) (*types.CSR, bool) { return csr, true } +func (k Keeper) IterateAllCSRs(ctx sdk.Context, cb func(csr types.CSR) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, types.KeyPrefixCSR) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + var csr types.CSR + k.cdc.MustUnmarshal(iter.Value(), &csr) + if cb(csr) { + break + } + } +} + +func (k Keeper) GetAllCSRs(ctx sdk.Context) (csrs []types.CSR) { + k.IterateAllCSRs(ctx, func(csr types.CSR) (stop bool) { + csrs = append(csrs, csr) + return false + }) + return +} + // Returns the NFT ID associated with a smart contract address. If the smart contract address // entered does belong to some NFT, then it will return (id, true), otherwise (0, false). func (k Keeper) GetNFTByContract(ctx sdk.Context, address string) (uint64, bool) { diff --git a/x/csr/keeper/genesis_test.go b/x/csr/keeper/genesis_test.go new file mode 100644 index 00000000..df00586f --- /dev/null +++ b/x/csr/keeper/genesis_test.go @@ -0,0 +1,95 @@ +package keeper_test + +import ( + "time" + + "github.com/Canto-Network/Canto/v7/x/csr" + "github.com/Canto-Network/Canto/v7/x/csr/types" + "github.com/evmos/ethermint/tests" +) + +func (suite *KeeperTestSuite) TestDefaultGenesis() { + genState := types.DefaultGenesis() + + csr.InitGenesis(suite.ctx, suite.app.CSRKeeper, suite.app.AccountKeeper, *genState) + got := csr.ExportGenesis(suite.ctx, suite.app.CSRKeeper) + suite.Require().Equal(genState, got) +} + +func (suite *KeeperTestSuite) TestImportExportGenesisEmpty() { + _, found := suite.app.CSRKeeper.GetTurnstile(suite.ctx) + suite.Require().False(found) + csrs := suite.app.CSRKeeper.GetAllCSRs(suite.ctx) + suite.Require().Empty(csrs) + + genState := csr.ExportGenesis(suite.ctx, suite.app.CSRKeeper) + suite.Require().Nil(genState.TurnstileAddress) + suite.Require().Empty(genState.Csrs) + + // Copy genState to genState2 and init with it + var genState2 types.GenesisState + bz := suite.app.AppCodec().MustMarshalJSON(genState) + suite.app.AppCodec().MustUnmarshalJSON(bz, &genState2) + csr.InitGenesis(suite.ctx, suite.app.CSRKeeper, suite.app.AccountKeeper, genState2) + + _, found = suite.app.CSRKeeper.GetTurnstile(suite.ctx) + suite.Require().False(found) + csrs = suite.app.CSRKeeper.GetAllCSRs(suite.ctx) + suite.Require().Empty(csrs) + genState3 := csr.ExportGenesis(suite.ctx, suite.app.CSRKeeper) + suite.Equal(*genState, genState2) + suite.Equal(genState2, *genState3) + suite.Require().Nil(genState3.TurnstileAddress) + suite.Require().Empty(genState3.Csrs) +} + +func (suite *KeeperTestSuite) TestInitExportGenesis() { + expGenesis := types.DefaultGenesis() + + csr.InitGenesis(suite.ctx, suite.app.CSRKeeper, suite.app.AccountKeeper, *expGenesis) + genState := csr.ExportGenesis(suite.ctx, suite.app.CSRKeeper) + suite.Require().Equal(expGenesis, genState) + + bz := suite.app.AppCodec().MustMarshalJSON(genState) + + var genState2 types.GenesisState + suite.app.AppCodec().MustUnmarshalJSON(bz, &genState2) + csr.InitGenesis(suite.ctx, suite.app.CSRKeeper, suite.app.AccountKeeper, genState2) + genState3 := csr.ExportGenesis(suite.ctx, suite.app.CSRKeeper) + + suite.Require().Equal(*genState, genState2) + suite.Require().Equal(genState2, *genState3) +} + +func (suite *KeeperTestSuite) TestImportExportGenesis() { + t, _ := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z") + suite.ctx = suite.ctx.WithBlockHeight(1).WithBlockTime(t) + + numberCSRs := 10 + csrs := GenerateCSRs(numberCSRs) + for _, csr := range csrs { + suite.app.CSRKeeper.SetCSR(suite.ctx, csr) + } + + turnstileAddress := tests.GenerateAddress() + suite.app.CSRKeeper.SetTurnstile(suite.ctx, turnstileAddress) + + genState := csr.ExportGenesis(suite.ctx, suite.app.CSRKeeper) + bz := suite.app.AppCodec().MustMarshalJSON(genState) + + // Copy genState to genState2 and init with it + var genState2 types.GenesisState + suite.app.AppCodec().MustUnmarshalJSON(bz, &genState2) + csr.InitGenesis(suite.ctx, suite.app.CSRKeeper, suite.app.AccountKeeper, genState2) + exported := csr.ExportGenesis(suite.ctx, suite.app.CSRKeeper) + suite.Equal(*genState, *exported) + + suite.ctx = suite.ctx.WithBlockHeight(1).WithBlockTime(t) + + c := suite.app.CSRKeeper.GetAllCSRs(suite.ctx) + suite.Equal(csrs, c) + + ta, found := suite.app.CSRKeeper.GetTurnstile(suite.ctx) + suite.True(found) + suite.Equal(turnstileAddress, ta) +} diff --git a/x/csr/module.go b/x/csr/module.go index 7845ade4..8744f4ee 100644 --- a/x/csr/module.go +++ b/x/csr/module.go @@ -4,16 +4,15 @@ import ( "context" "encoding/json" "fmt" + "math/rand" // this line is used by starport scaffolding # 1 + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/gorilla/mux" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" - "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/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" @@ -21,6 +20,11 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" abci "github.com/tendermint/tendermint/abci/types" + + "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 ( @@ -34,10 +38,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} } @@ -103,11 +107,12 @@ type AppModule struct { } func NewAppModule( + cdc codec.Codec, keeper keeper.Keeper, acctKeeper authkeeper.AccountKeeper, ) AppModule { return AppModule{ - AppModuleBasic: AppModuleBasic{}, + AppModuleBasic: NewAppModuleBasic(cdc), keeper: keeper, accountKeeper: acctKeeper, } @@ -187,3 +192,32 @@ func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { return []abci.ValidatorUpdate{} } + +// ___________________________________________________________________________ + +// AppModuleSimulation functions + +// GenerateGenesisState creates a randomized GenState of theepochs module. +func (am AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// ProposalContents doesn't return any content functions for governance proposals. +func (AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent { + return []simtypes.WeightedProposalContent{} +} + +// RandomizedParams creates randomizedepochs param changes for the simulator. +func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange { + return simulation.ParamChanges(r) +} + +// RegisterStoreDecoder registers a decoder for supply module's types +func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) +} + +// WeightedOperations returns the all the gov module operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return []simtypes.WeightedOperation{} +} 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..3a87ed9d --- /dev/null +++ b/x/csr/simulation/decoder_test.go @@ -0,0 +1,60 @@ +package simulation_test + +import ( + "fmt" + "testing" + + "github.com/Canto-Network/Canto/v7/x/csr/keeper" + "github.com/evmos/ethermint/tests" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/Canto-Network/Canto/v7/x/csr/simulation" + "github.com/Canto-Network/Canto/v7/x/csr/types" +) + +func TestCsrStore(t *testing.T) { + cdc := simapp.MakeTestEncodingConfig() + dec := simulation.NewDecodeStore(cdc.Marshaler) + + 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.Marshaler.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..d1d3fa6a --- /dev/null +++ b/x/csr/simulation/genesis.go @@ -0,0 +1,48 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + + 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/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) sdk.Dec { + return sdk.NewDecWithPrec(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( + simState.Cdc, enableCsr, &genesis.Params.EnableCsr, simState.Rand, + func(r *rand.Rand) { genesis.Params.EnableCsr = generateRandomBool(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, csrShares, &genesis.Params.CsrShares, simState.Rand, + func(r *rand.Rand) { genesis.Params.CsrShares = generateRandomCsrShares(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/csr/simulation/genesis_test.go b/x/csr/simulation/genesis_test.go new file mode 100644 index 00000000..679451b8 --- /dev/null +++ b/x/csr/simulation/genesis_test.go @@ -0,0 +1,72 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + 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/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: 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, sdk.NewDecWithPrec(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/params.go b/x/csr/simulation/params.go new file mode 100644 index 00000000..fdc6a03b --- /dev/null +++ b/x/csr/simulation/params.go @@ -0,0 +1,34 @@ +package simulation + +import ( + "encoding/json" + "math/rand" + + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/Canto-Network/Canto/v7/x/csr/types" +) + +func ParamChanges(r *rand.Rand) []simtypes.ParamChange { + return []simtypes.ParamChange{ + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyEnableCSR), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomBool(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyCSRShares), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomCsrShares(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + } +} diff --git a/x/csr/simulation/params_test.go b/x/csr/simulation/params_test.go new file mode 100644 index 00000000..568ce3bb --- /dev/null +++ b/x/csr/simulation/params_test.go @@ -0,0 +1,34 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/Canto-Network/Canto/v7/x/csr/simulation" +) + +func TestParamChanges(t *testing.T) { + r := rand.New(rand.NewSource(0)) + + paramChanges := simulation.ParamChanges(r) + require.Len(t, paramChanges, 2) + + expected := []struct { + composedKey string + key string + simValue string + subspace string + }{ + {"csr/EnableCSR", "EnableCSR", "false", "csr"}, + {"csr/CSRShares", "CSRShares", `"0.140000000000000000"`, "csr"}, + } + + for i, p := range paramChanges { + require.Equal(t, expected[i].composedKey, p.ComposedKey()) + require.Equal(t, expected[i].key, p.Key()) + require.Equal(t, expected[i].simValue, p.SimValue()(r)) + require.Equal(t, expected[i].subspace, p.Subspace()) + } +} diff --git a/x/csr/types/genesis.go b/x/csr/types/genesis.go index 521cc943..783f5211 100644 --- a/x/csr/types/genesis.go +++ b/x/csr/types/genesis.go @@ -5,8 +5,14 @@ const DefaultIndex uint64 = 1 // DefaultGenesis returns the default Capability genesis state func DefaultGenesis() *GenesisState { + return NewGenesisState(DefaultParams(), []CSR{}, nil) +} + +func NewGenesisState(params Params, csrs []CSR, turnstileAddress []byte) *GenesisState { return &GenesisState{ - Params: DefaultParams(), + Params: params, + Csrs: csrs, + TurnstileAddress: turnstileAddress, } } diff --git a/x/csr/types/genesis.pb.go b/x/csr/types/genesis.pb.go index 889dc9aa..58d971fa 100644 --- a/x/csr/types/genesis.pb.go +++ b/x/csr/types/genesis.pb.go @@ -26,7 +26,9 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // GenesisState defines the csr module's genesis state. type GenesisState struct { // params defines all of the parameters of the module - Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` + Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` + Csrs []CSR `protobuf:"bytes,2,rep,name=csrs,proto3" json:"csrs"` + TurnstileAddress []byte `protobuf:"bytes,3,opt,name=turnstile_address,json=turnstileAddress,proto3" json:"turnstile_address,omitempty"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -69,6 +71,20 @@ func (m *GenesisState) GetParams() Params { return Params{} } +func (m *GenesisState) GetCsrs() []CSR { + if m != nil { + return m.Csrs + } + return nil +} + +func (m *GenesisState) GetTurnstileAddress() []byte { + if m != nil { + return m.TurnstileAddress + } + return nil +} + func init() { proto.RegisterType((*GenesisState)(nil), "canto.csr.v1.GenesisState") } @@ -76,20 +92,24 @@ func init() { func init() { proto.RegisterFile("canto/csr/v1/genesis.proto", fileDescriptor_4c1065f59845b427) } var fileDescriptor_4c1065f59845b427 = []byte{ - // 200 bytes of a gzipped FileDescriptorProto + // 268 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4a, 0x4e, 0xcc, 0x2b, 0xc9, 0xd7, 0x4f, 0x2e, 0x2e, 0xd2, 0x2f, 0x33, 0xd4, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0xcb, 0xe9, 0x25, 0x17, 0x17, 0xe9, 0x95, 0x19, 0x4a, 0x89, 0xa4, 0xe7, 0xa7, 0xe7, 0x83, 0x25, 0xf4, 0x41, 0x2c, 0x88, 0x1a, 0x29, 0x49, - 0x14, 0xfd, 0x05, 0x89, 0x45, 0x89, 0xb9, 0x50, 0xed, 0x4a, 0x4e, 0x5c, 0x3c, 0xee, 0x10, 0xf3, - 0x82, 0x4b, 0x12, 0x4b, 0x52, 0x85, 0x8c, 0xb8, 0xd8, 0x20, 0xf2, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, - 0xdc, 0x46, 0x22, 0x7a, 0xc8, 0xe6, 0xeb, 0x05, 0x80, 0xe5, 0x9c, 0x58, 0x4e, 0xdc, 0x93, 0x67, - 0x08, 0x82, 0xaa, 0x74, 0x72, 0x3f, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, - 0xe4, 0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, - 0xdd, 0xf4, 0xcc, 0x92, 0x8c, 0xd2, 0x24, 0xbd, 0xe4, 0xfc, 0x5c, 0x7d, 0x67, 0x90, 0x39, 0xba, - 0x7e, 0xa9, 0x25, 0xe5, 0xf9, 0x45, 0xd9, 0x10, 0x9e, 0x7e, 0x99, 0xb9, 0x7e, 0x05, 0xd8, 0x59, - 0x25, 0x95, 0x05, 0xa9, 0xc5, 0x49, 0x6c, 0x60, 0x37, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, - 0x5d, 0xe7, 0x23, 0x60, 0xf0, 0x00, 0x00, 0x00, + 0x14, 0xfd, 0x05, 0x89, 0x45, 0x89, 0xb9, 0x50, 0xed, 0x52, 0x62, 0x28, 0x52, 0x20, 0x53, 0xc0, + 0xe2, 0x4a, 0x73, 0x18, 0xb9, 0x78, 0xdc, 0x21, 0x16, 0x05, 0x97, 0x24, 0x96, 0xa4, 0x0a, 0x19, + 0x71, 0xb1, 0x41, 0x34, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x1b, 0x89, 0xe8, 0x21, 0x5b, 0xac, + 0x17, 0x00, 0x96, 0x73, 0x62, 0x39, 0x71, 0x4f, 0x9e, 0x21, 0x08, 0xaa, 0x52, 0x48, 0x9b, 0x8b, + 0x25, 0xb9, 0xb8, 0xa8, 0x58, 0x82, 0x49, 0x81, 0x59, 0x83, 0xdb, 0x48, 0x10, 0x55, 0x87, 0x73, + 0x70, 0x10, 0x54, 0x39, 0x58, 0x91, 0x90, 0x36, 0x97, 0x60, 0x49, 0x69, 0x51, 0x5e, 0x71, 0x49, + 0x66, 0x4e, 0x6a, 0x7c, 0x62, 0x4a, 0x4a, 0x51, 0x6a, 0x71, 0xb1, 0x04, 0xb3, 0x02, 0xa3, 0x06, + 0x4f, 0x90, 0x00, 0x5c, 0xc2, 0x11, 0x22, 0xee, 0xe4, 0x7e, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, + 0x72, 0x8c, 0x0f, 0x1e, 0xc9, 0x31, 0x4e, 0x78, 0x2c, 0xc7, 0x70, 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, + 0xc7, 0x72, 0x0c, 0x51, 0xba, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, 0xfa, + 0xce, 0x20, 0xfb, 0x74, 0xfd, 0x52, 0x4b, 0xca, 0xf3, 0x8b, 0xb2, 0x21, 0x3c, 0xfd, 0x32, 0x73, + 0xfd, 0x0a, 0xb0, 0x77, 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0x93, 0xd8, 0xc0, 0xde, 0x35, 0x06, 0x04, + 0x00, 0x00, 0xff, 0xff, 0xf7, 0xd8, 0x67, 0x34, 0x63, 0x01, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -112,6 +132,27 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.TurnstileAddress) > 0 { + i -= len(m.TurnstileAddress) + copy(dAtA[i:], m.TurnstileAddress) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.TurnstileAddress))) + i-- + dAtA[i] = 0x1a + } + if len(m.Csrs) > 0 { + for iNdEx := len(m.Csrs) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Csrs[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } { size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -144,6 +185,16 @@ func (m *GenesisState) Size() (n int) { _ = l l = m.Params.Size() n += 1 + l + sovGenesis(uint64(l)) + if len(m.Csrs) > 0 { + for _, e := range m.Csrs { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + l = len(m.TurnstileAddress) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } return n } @@ -215,6 +266,74 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Csrs", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Csrs = append(m.Csrs, CSR{}) + if err := m.Csrs[len(m.Csrs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TurnstileAddress", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.TurnstileAddress = append(m.TurnstileAddress[:0], dAtA[iNdEx:postIndex]...) + if m.TurnstileAddress == nil { + m.TurnstileAddress = []byte{} + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/epochs/module.go b/x/epochs/module.go index 303cb635..f94aefe1 100644 --- a/x/epochs/module.go +++ b/x/epochs/module.go @@ -22,6 +22,7 @@ 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" ) @@ -174,7 +175,9 @@ func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.Val // AppModuleSimulation functions // GenerateGenesisState creates a randomized GenState of theepochs module. -func (AppModule) GenerateGenesisState(simState *module.SimulationState) {} +func (am AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} // ProposalContents doesn't return any content functions for governance proposals. func (AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent { @@ -188,6 +191,7 @@ func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange { // RegisterStoreDecoder registers a decoder for supply module's types func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[types.StoreKey] = 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..d1744ff5 --- /dev/null +++ b/x/epochs/simulation/decoder_test.go @@ -0,0 +1,56 @@ +package simulation_test + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/Canto-Network/Canto/v7/x/epochs/simulation" + "github.com/Canto-Network/Canto/v7/x/epochs/types" +) + +func TestEpochsStore(t *testing.T) { + cdc := simapp.MakeTestEncodingConfig() + dec := simulation.NewDecodeStore(cdc.Marshaler) + + 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.Marshaler.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..ef53b9ee --- /dev/null +++ b/x/epochs/simulation/genesis.go @@ -0,0 +1,45 @@ +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, + }, + } + + genesis.Epochs = epochs + + 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/epochs/simulation/genesis_test.go b/x/epochs/simulation/genesis_test.go new file mode 100644 index 00000000..845f1e0e --- /dev/null +++ b/x/epochs/simulation/genesis_test.go @@ -0,0 +1,92 @@ +package simulation_test + +import ( + "encoding/json" + "fmt" + "math/rand" + "testing" + "time" + + "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: 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, + }, + }, 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 4a1f3c2b..83a2f1ec 100644 --- a/x/erc20/keeper/keeper_test.go +++ b/x/erc20/keeper/keeper_test.go @@ -460,11 +460,29 @@ type MockEVMKeeper struct { mock.Mock } +func (m *MockEVMKeeper) ChainID() *big.Int { + args := m.Called() + return args.Get(0).(*big.Int) +} + +func (m *MockEVMKeeper) GetNonce(ctx sdk.Context, addr common.Address) uint64 { + args := m.Called(mock.Anything, mock.Anything) + return args.Get(0).(uint64) +} + +func (m *MockEVMKeeper) EthereumTx(goCtx context.Context, msg *evmtypes.MsgEthereumTx) (*evmtypes.MsgEthereumTxResponse, error) { + args := m.Called(mock.Anything, mock.Anything) + return args.Get(0).(*evmtypes.MsgEthereumTxResponse), args.Error(1) +} + func (m *MockEVMKeeper) GetParams(ctx sdk.Context) evmtypes.Params { args := m.Called(mock.Anything) return args.Get(0).(evmtypes.Params) } +func (m *MockEVMKeeper) SetParams(ctx sdk.Context, params evmtypes.Params) { +} + func (m *MockEVMKeeper) GetAccountWithoutBalance(ctx sdk.Context, addr common.Address) *statedb.Account { args := m.Called(mock.Anything, mock.Anything) if args.Get(0) == nil { @@ -496,6 +514,11 @@ type MockBankKeeper struct { mock.Mock } +func (b *MockBankKeeper) SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error { + args := b.Called(mock.Anything, mock.Anything, mock.Anything, mock.Anything) + return args.Error(0) +} + func (b *MockBankKeeper) SendCoinsFromModuleToAccount(ctx sdk.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) @@ -506,6 +529,11 @@ func (b *MockBankKeeper) SendCoinsFromAccountToModule(ctx sdk.Context, senderAdd return args.Error(0) } +func (b *MockBankKeeper) SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins { + args := b.Called(mock.Anything, mock.Anything) + return args.Get(0).(sdk.Coins) +} + func (b *MockBankKeeper) MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error { args := b.Called(mock.Anything, mock.Anything, mock.Anything) return args.Error(0) @@ -543,3 +571,9 @@ func (b *MockBankKeeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom args := b.Called(mock.Anything, mock.Anything) return args.Get(0).(sdk.Coin) } + +func (b *MockBankKeeper) GetParams(ctx sdk.Context) banktypes.Params { + return banktypes.DefaultParams() +} + +func (b *MockBankKeeper) SetParams(ctx sdk.Context, params banktypes.Params) {} diff --git a/x/erc20/proposal_handler.go b/x/erc20/keeper/proposal_handler.go similarity index 79% rename from x/erc20/proposal_handler.go rename to x/erc20/keeper/proposal_handler.go index 8e0130ae..adcb1bd9 100644 --- a/x/erc20/proposal_handler.go +++ b/x/erc20/keeper/proposal_handler.go @@ -1,4 +1,4 @@ -package erc20 +package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" @@ -6,12 +6,11 @@ import ( govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/ethereum/go-ethereum/common" - "github.com/Canto-Network/Canto/v7/x/erc20/keeper" "github.com/Canto-Network/Canto/v7/x/erc20/types" ) // NewErc20ProposalHandler creates a governance handler to manage new proposal types. -func NewErc20ProposalHandler(k *keeper.Keeper) govtypes.Handler { +func NewErc20ProposalHandler(k *Keeper) govtypes.Handler { return func(ctx sdk.Context, content govtypes.Content) error { switch c := content.(type) { case *types.RegisterCoinProposal: @@ -27,7 +26,7 @@ func NewErc20ProposalHandler(k *keeper.Keeper) govtypes.Handler { } } -func handleRegisterCoinProposal(ctx sdk.Context, k *keeper.Keeper, p *types.RegisterCoinProposal) error { +func handleRegisterCoinProposal(ctx sdk.Context, k *Keeper, p *types.RegisterCoinProposal) error { pair, err := k.RegisterCoin(ctx, p.Metadata) if err != nil { return err @@ -43,7 +42,7 @@ func handleRegisterCoinProposal(ctx sdk.Context, k *keeper.Keeper, p *types.Regi return nil } -func handleRegisterERC20Proposal(ctx sdk.Context, k *keeper.Keeper, p *types.RegisterERC20Proposal) error { +func handleRegisterERC20Proposal(ctx sdk.Context, k *Keeper, p *types.RegisterERC20Proposal) error { pair, err := k.RegisterERC20(ctx, common.HexToAddress(p.Erc20Address)) if err != nil { return err @@ -59,7 +58,7 @@ func handleRegisterERC20Proposal(ctx sdk.Context, k *keeper.Keeper, p *types.Reg return nil } -func handleToggleConversionProposal(ctx sdk.Context, k *keeper.Keeper, p *types.ToggleTokenConversionProposal) error { +func handleToggleConversionProposal(ctx sdk.Context, k *Keeper, p *types.ToggleTokenConversionProposal) error { pair, err := k.ToggleConversion(ctx, p.Token) if err != nil { return err diff --git a/x/erc20/keeper/test_heleprs.go b/x/erc20/keeper/test_heleprs.go new file mode 100644 index 00000000..0d3222b8 --- /dev/null +++ b/x/erc20/keeper/test_heleprs.go @@ -0,0 +1,120 @@ +package keeper + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + 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) { + c := sdk.WrapSDKContext(ctx) + 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(c, &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(c, 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{}, sdkerrors.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{}, sdkerrors.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 7402f264..db8eaa52 100644 --- a/x/erc20/module.go +++ b/x/erc20/module.go @@ -20,6 +20,7 @@ import ( "github.com/Canto-Network/Canto/v7/x/erc20/client/cli" "github.com/Canto-Network/Canto/v7/x/erc20/keeper" + "github.com/Canto-Network/Canto/v7/x/erc20/simulation" "github.com/Canto-Network/Canto/v7/x/erc20/types" ) @@ -31,7 +32,9 @@ var ( ) // app module Basics object -type AppModuleBasic struct{} +type AppModuleBasic struct { + cdc codec.Codec +} func (AppModuleBasic) Name() string { return types.ModuleName @@ -89,19 +92,29 @@ func (AppModuleBasic) GetQueryCmd() *cobra.Command { type AppModule struct { AppModuleBasic - keeper keeper.Keeper - ak authkeeper.AccountKeeper + keeper keeper.Keeper + accountKeeper authkeeper.AccountKeeper + bankKeeper types.BankKeeper + evmKeeper types.EVMKeeper + feemarketKeeper types.FeeMarketKeeper } // NewAppModule creates a new AppModule Object func NewAppModule( + cdc codec.Codec, k keeper.Keeper, - ak authkeeper.AccountKeeper, + accountKeeper authkeeper.AccountKeeper, + bankKeeper types.BankKeeper, + evmKeeper types.EVMKeeper, + feemarketKeeper types.FeeMarketKeeper, ) AppModule { return AppModule{ - AppModuleBasic: AppModuleBasic{}, - keeper: k, - ak: ak, + AppModuleBasic: AppModuleBasic{cdc: cdc}, + keeper: k, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + evmKeeper: evmKeeper, + feemarketKeeper: feemarketKeeper, } } @@ -153,7 +166,7 @@ func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json. var genesisState types.GenesisState cdc.MustUnmarshalJSON(data, &genesisState) - InitGenesis(ctx, am.keeper, am.ak, genesisState) + InitGenesis(ctx, am.keeper, am.accountKeeper, genesisState) return []abci.ValidatorUpdate{} } @@ -162,20 +175,22 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw return cdc.MustMarshalJSON(gs) } -func (am AppModule) GenerateGenesisState(input *module.SimulationState) { +func (am AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) } func (am AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent { - return []simtypes.WeightedProposalContent{} + return simulation.ProposalContents(am.keeper, am.accountKeeper, am.bankKeeper, am.evmKeeper, am.feemarketKeeper) } func (am AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange { - return []simtypes.ParamChange{} + return simulation.ParamChanges(r) } func (am AppModule) RegisterStoreDecoder(decoderRegistry sdk.StoreDecoderRegistry) { + decoderRegistry[types.StoreKey] = simulation.NewDecodeStore(am.cdc) } func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { - return []simtypes.WeightedOperation{} + return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.bankKeeper, am.keeper) } diff --git a/x/erc20/simulation/decoder.go b/x/erc20/simulation/decoder.go new file mode 100644 index 00000000..ff9d530c --- /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.KeyPrefixTokenPairByERC20): + 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..f2d1bcf1 --- /dev/null +++ b/x/erc20/simulation/decoder_test.go @@ -0,0 +1,52 @@ +package simulation_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/simapp" + "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 := simapp.MakeTestEncodingConfig() + dec := simulation.NewDecodeStore(cdc.Marshaler) + + tokenPair := types.NewTokenPair(testutil.GenerateAddress(), "coin", true, types.OWNER_MODULE) + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: types.KeyPrefixTokenPair, Value: cdc.Marshaler.MustMarshal(&tokenPair)}, + {Key: types.KeyPrefixTokenPairByERC20, Value: cdc.Marshaler.MustMarshal(&tokenPair)}, + {Key: types.KeyPrefixTokenPairByDenom, Value: cdc.Marshaler.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..b1a0dc98 --- /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( + simState.Cdc, enableErc20, &genesis.Params.EnableErc20, simState.Rand, + func(r *rand.Rand) { genesis.Params.EnableErc20 = generateRandomBool(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, 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..749b2aca --- /dev/null +++ b/x/erc20/simulation/genesis_test.go @@ -0,0 +1,73 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + "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: 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..d7a012f4 --- /dev/null +++ b/x/erc20/simulation/operation.go @@ -0,0 +1,181 @@ +package simulation + +import ( + "math/big" + "math/rand" + + "github.com/Canto-Network/Canto/v7/contracts" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + 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" +) + +var ( + Gas = uint64(20000000) + Fees = sdk.Coins{ + { + Denom: sdk.DefaultBondDenom, + Amount: sdk.NewInt(0), + }, + } +) + +// WeightedOperations returns all the operations from the module with their respective weights +func WeightedOperations( + appParams simtypes.AppParams, + cdc codec.JSONCodec, + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simulation.WeightedOperations { + var weightMsgConvertCoinNativeCoin int + appParams.GetOrGenerate(cdc, OpWeightMsgConvertCoin, &weightMsgConvertCoinNativeCoin, nil, func(_ *rand.Rand) { + weightMsgConvertCoinNativeCoin = params.DefaultWeightMsgConvertCoin + }) + + var weightMsgConvertErc20NativeCoin int + appParams.GetOrGenerate(cdc, OpWeightMsgConvertErc20, &weightMsgConvertErc20NativeCoin, nil, func(_ *rand.Rand) { + weightMsgConvertErc20NativeCoin = params.DefaultWeightMsgConvertErc20 + }) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgConvertCoinNativeCoin, + SimulateMsgConvertCoin(ak, bk, k), + ), + simulation.NewWeightedOperation( + weightMsgConvertErc20NativeCoin, + SimulateMsgConvertErc20(ak, bk, k), + ), + } +} + +// SimulateMsgConvertCoin generates a MsgConvertCoin with random values for convertCoinNativeCoin +func SimulateMsgConvertCoin(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) 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 { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgConvertCoin, "no pairs available"), nil, nil + } + + // randomly pick one pair + pair := pairs[r.Intn(len(pairs))] + 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, types.TypeMsgConvertCoin, "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: simapp.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + CoinsSpentInMsg: spendable, + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + } + return types.GenAndDeliverTxWithFees(txCtx, Gas, Fees) + } +} + +// SimulateMsgConvertErc20 generates a MsgConvertErc20 with random values for convertERC20NativeCoin. +func SimulateMsgConvertErc20(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) 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 { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgConvertERC20, "no pairs available"), nil, nil + } + + // randomly pick one pair + pair := pairs[r.Intn(len(pairs))] + + // select random account that has coins baseDenom + var simAccount simtypes.Account + var erc20Balance *big.Int + erc20ABI := contracts.ERC20MinterBurnerDecimalsContract.ABI + 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, types.TypeMsgConvertERC20, "no account has native ERC20"), nil, nil + } + + msg := types.NewMsgConvertERC20(sdk.NewIntFromBigInt(erc20Balance), simAccount.Address, pair.GetERC20Contract(), common.BytesToAddress(simAccount.Address.Bytes())) + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simapp.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + CoinsSpentInMsg: sdk.NewCoins(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + } + return types.GenAndDeliverTxWithFees(txCtx, Gas, Fees) + } +} diff --git a/x/erc20/simulation/operation_test.go b/x/erc20/simulation/operation_test.go new file mode 100644 index 00000000..26ab1f98 --- /dev/null +++ b/x/erc20/simulation/operation_test.go @@ -0,0 +1,125 @@ +package simulation_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/Canto-Network/Canto/v7/app/params" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/teststaking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/Canto-Network/Canto/v7/app" + "github.com/Canto-Network/Canto/v7/x/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.AccountKeeper, + canto.BankKeeper, + canto.Erc20Keeper, + ) + + 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, types.TypeMsgConvertCoin}, + {params.DefaultWeightMsgConvertErc20, types.ModuleName, types.TypeMsgConvertERC20}, + } + + for i, w := range weightedOps { + opMsg, _, _ := w.Op()(r, canto.BaseApp, ctx, accs, ctx.ChainID()) + require.Equal(t, expected[i].weight, w.Weight()) + require.Equal(t, expected[i].opMsgRoute, opMsg.Route) + require.Equal(t, expected[i].opMsgName, opMsg.Name) + } +} + +func createTestApp(t *testing.T, isCheckTx bool) (*app.Canto, sdk.Context) { + app := app.Setup(isCheckTx, nil) + r := rand.New(rand.NewSource(1)) + + simAccs := simtypes.RandomAccounts(r, 10) + + ctx := app.BaseApp.NewContext(isCheckTx, tmproto.Header{}) + validator := getTestingValidator0(t, app, ctx, simAccs) + consAddr, err := validator.GetConsAddr() + require.NoError(t, err) + ctx = ctx.WithBlockHeader(tmproto.Header{Height: 1, + ChainID: "canto_9001-1", + Time: time.Now().UTC(), + ProposerAddress: consAddr, + }) + return app, ctx +} + +func getTestingAccounts(t *testing.T, r *rand.Rand, app *app.Canto, ctx sdk.Context, n int) []simtypes.Account { + accounts := simtypes.RandomAccounts(r, n) + + initAmt := app.StakingKeeper.TokensFromConsensusPower(ctx, 100_000_000) + initCoins := sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, initAmt), + ) + + // add coins to the accounts + for _, account := range accounts { + acc := app.AccountKeeper.NewAccountWithAddress(ctx, account.Address) + app.AccountKeeper.SetAccount(ctx, acc) + err := fundAccount(app.BankKeeper, ctx, account.Address, initCoins) + require.NoError(t, err) + } + + return accounts +} + +func fundAccount(bk bankkeeper.Keeper, ctx sdk.Context, addr sdk.AccAddress, coins sdk.Coins) error { + if err := bk.MintCoins(ctx, types.ModuleName, coins); err != nil { + return err + } + if err := bk.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addr, coins); err != nil { + return err + } + return nil +} + +func getTestingValidator0(t *testing.T, app *app.Canto, ctx sdk.Context, accounts []simtypes.Account) stakingtypes.Validator { + commission0 := stakingtypes.NewCommission(sdk.ZeroDec(), sdk.OneDec(), sdk.OneDec()) + return getTestingValidator(t, app, ctx, accounts, commission0, 0) +} + +func getTestingValidator(t *testing.T, app *app.Canto, ctx sdk.Context, accounts []simtypes.Account, commission stakingtypes.Commission, n int) stakingtypes.Validator { + account := accounts[n] + valPubKey := account.PubKey + valAddr := sdk.ValAddress(account.PubKey.Address().Bytes()) + validator := teststaking.NewValidator(t, valAddr, valPubKey) + validator, err := validator.SetInitialCommission(commission) + require.NoError(t, err) + + validator.DelegatorShares = sdk.NewDec(100) + validator.Tokens = app.StakingKeeper.TokensFromConsensusPower(ctx, 100) + + app.StakingKeeper.SetValidator(ctx, validator) + app.StakingKeeper.SetValidatorByConsAddr(ctx, validator) + + return validator +} diff --git a/x/erc20/simulation/params.go b/x/erc20/simulation/params.go new file mode 100644 index 00000000..9376c13f --- /dev/null +++ b/x/erc20/simulation/params.go @@ -0,0 +1,34 @@ +package simulation + +import ( + "encoding/json" + "math/rand" + + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/Canto-Network/Canto/v7/x/erc20/types" +) + +func ParamChanges(r *rand.Rand) []simtypes.ParamChange { + return []simtypes.ParamChange{ + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyEnableErc20), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomBool(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyEnableEVMHook), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateRandomBool(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + } +} diff --git a/x/erc20/simulation/params_test.go b/x/erc20/simulation/params_test.go new file mode 100644 index 00000000..6e2df4d5 --- /dev/null +++ b/x/erc20/simulation/params_test.go @@ -0,0 +1,34 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/Canto-Network/Canto/v7/x/erc20/simulation" +) + +func TestParamChanges(t *testing.T) { + r := rand.New(rand.NewSource(0)) + + paramChanges := simulation.ParamChanges(r) + require.Len(t, paramChanges, 2) + + expected := []struct { + composedKey string + key string + simValue string + subspace string + }{ + {"erc20/EnableErc20", "EnableErc20", "false", "erc20"}, + {"erc20/EnableEVMHook", "EnableEVMHook", "true", "erc20"}, + } + + for i, p := range paramChanges { + require.Equal(t, expected[i].composedKey, p.ComposedKey()) + require.Equal(t, expected[i].key, p.Key()) + require.Equal(t, expected[i].simValue, p.SimValue()(r)) + require.Equal(t, expected[i].subspace, p.Subspace()) + } +} diff --git a/x/erc20/simulation/proposal.go b/x/erc20/simulation/proposal.go new file mode 100644 index 00000000..c350599e --- /dev/null +++ b/x/erc20/simulation/proposal.go @@ -0,0 +1,228 @@ +package simulation + +import ( + "math/rand" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + 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" + + "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" +) + +// Simulation operation weights constants. +const ( + OpWeightSimulateRegisterCoinProposal = "op_weight_register_coin_proposal" + OpWeightSimulateRegisterERC20Proposal = "op_weight_register_erc20_proposal" + OpWeightSimulateToggleTokenConversionProposal = "op_weight_toggle_token_conversion_proposal" + + erc20Decimals = uint8(18) +) + +// ProposalContents defines the module weighted proposals' contents +func ProposalContents(k keeper.Keeper, ak types.AccountKeeper, bk types.BankKeeper, ek types.EVMKeeper, fk types.FeeMarketKeeper) []simtypes.WeightedProposalContent { + return []simtypes.WeightedProposalContent{ + simulation.NewWeightedProposalContent( + OpWeightSimulateRegisterCoinProposal, + params.DefaultWeightRegisterCoinProposal, + SimulateRegisterCoinProposal(k, bk), + ), + simulation.NewWeightedProposalContent( + OpWeightSimulateRegisterERC20Proposal, + params.DefaultWeightRegisterERC20Proposal, + SimulateRegisterERC20Proposal(k, ak, bk, ek, fk), + ), + simulation.NewWeightedProposalContent( + OpWeightSimulateToggleTokenConversionProposal, + params.DefaultWeightToggleTokenConversionProposal, + SimulateToggleTokenConversionProposal(k, bk, ek, fk), + ), + } +} + +func SimulateRegisterCoinProposal(k keeper.Keeper, bk types.BankKeeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + coinMetadata := types.GenRandomCoinMetadata(r) + if err := bk.MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin(coinMetadata.Base, sdk.NewInt(1)))); 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) + for i := 0; i < randomIteration; i++ { + simAccount, _ := simtypes.RandomAcc(r, accs) + + if err := bk.MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin(coinMetadata.Base, sdk.NewInt(100000000)))); err != nil { + panic(err) + } + if err := bk.SendCoinsFromModuleToAccount(ctx, types.ModuleName, simAccount.Address, sdk.NewCoins(sdk.NewCoin(coinMetadata.Base, sdk.NewInt(100000000)))); err != nil { + panic(err) + } + } + + proposal := types.RegisterCoinProposal{ + Title: simtypes.RandStringOfLength(r, 10), + Description: simtypes.RandStringOfLength(r, 100), + Metadata: coinMetadata, + } + + if err := keeper.NewErc20ProposalHandler(&k)(ctx, &proposal); err != nil { + panic(err) + } + + return nil + } +} + +func SimulateRegisterERC20Proposal(k keeper.Keeper, accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper, evmKeeper types.EVMKeeper, feemarketKeeper types.FeeMarketKeeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + params := k.GetParams(ctx) + params.EnableErc20 = true + k.SetParams(ctx, params) + + evmParams := evmtypes.DefaultParams() + evmParams.EvmDenom = "stake" + evmKeeper.SetParams(ctx, evmParams) + + isNativeErc20 := r.Intn(2) == 1 + // account key + priv, err := ethsecp256k1.GenerateKey() + if err != nil { + panic(err) + } + address := 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) + + coins := sdk.NewCoins(sdk.NewCoin(evmParams.EvmDenom, sdk.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) + } + if isNativeErc20 { + deployer = types.ModuleAddress + contractAddr, err = keeper.DeployERC20Contract(ctx, k, accountKeeper, coinMetadata) + } else { + deployer = address + erc20Name := coinMetadata.Name + erc20Symbol := coinMetadata.Symbol + contractAddr, err = keeper.DeployContract(ctx, evmKeeper, feemarketKeeper, deployer, signer, erc20Name, erc20Symbol, erc20Decimals) + } + + if err != nil { + panic(err) + } + + // mint cosmos coin to random accounts + randomIteration := r.Intn(10) + for i := 0; i < randomIteration; i++ { + simAccount, _ := simtypes.RandomAcc(r, accs) + + mintAmt := sdk.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 { + panic(err) + } + after := k.BalanceOf(ctx, erc20ABI, contractAddr, receiver) + if after.Cmp(before.Add(before, mintAmt.BigInt())) != 0 { + panic("mint failed") + } + } + + if err != nil { + panic(err) + } + + proposal := types.RegisterERC20Proposal{ + Title: simtypes.RandStringOfLength(r, 10), + Description: simtypes.RandStringOfLength(r, 100), + Erc20Address: contractAddr.String(), + } + + if err := keeper.NewErc20ProposalHandler(&k)(ctx, &proposal); err != nil { + panic(err) + } + + return nil + } +} + +func SimulateToggleTokenConversionProposal(k keeper.Keeper, bankKeeper types.BankKeeper, evmKeeper types.EVMKeeper, feemarketKeeper types.FeeMarketKeeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + 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) + } + address := 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, sdk.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, address, signer, erc20Name, erc20Symbol, erc20Decimals) + if err != nil { + panic(err) + } + + _, err = k.RegisterERC20(ctx, contractAddr) + if err != nil { + panic(err) + } + + proposal := types.ToggleTokenConversionProposal{ + Title: simtypes.RandStringOfLength(r, 10), + Description: simtypes.RandStringOfLength(r, 100), + Token: contractAddr.String(), + } + + if err := keeper.NewErc20ProposalHandler(&k)(ctx, &proposal); err != nil { + panic(err) + } + + return nil + } +} diff --git a/x/erc20/simulation/proposal_test.go b/x/erc20/simulation/proposal_test.go new file mode 100644 index 00000000..46f1bbee --- /dev/null +++ b/x/erc20/simulation/proposal_test.go @@ -0,0 +1,48 @@ +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/stretchr/testify/require" +) + +func TestProposalContents(t *testing.T) { + app, ctx := createTestApp(t, false) + + s := rand.NewSource(1) + r := rand.New(s) + + accounts := getTestingAccounts(t, r, app, ctx, 10) + + // execute ProposalContents function + weightedProposalContent := simulation.ProposalContents(app.Erc20Keeper, app.AccountKeeper, app.BankKeeper, app.EvmKeeper, app.FeeMarketKeeper) + require.Len(t, weightedProposalContent, 3) + + w0 := weightedProposalContent[0] + w1 := weightedProposalContent[1] + w2 := weightedProposalContent[2] + + // tests w0 interface: + require.Equal(t, simulation.OpWeightSimulateRegisterCoinProposal, w0.AppParamsKey()) + require.Equal(t, params.DefaultWeightRegisterCoinProposal, w0.DefaultWeight()) + + // tests w1 interface: + require.Equal(t, simulation.OpWeightSimulateRegisterERC20Proposal, w1.AppParamsKey()) + require.Equal(t, params.DefaultWeightRegisterERC20Proposal, w1.DefaultWeight()) + + // tests w2 interface: + require.Equal(t, simulation.OpWeightSimulateToggleTokenConversionProposal, w2.AppParamsKey()) + require.Equal(t, params.DefaultWeightToggleTokenConversionProposal, w2.DefaultWeight()) + + content0 := w0.ContentSimulatorFn()(r, ctx, accounts) + require.Nil(t, content0) + + content1 := w1.ContentSimulatorFn()(r, ctx, accounts) + require.Nil(t, content1) + + content2 := w1.ContentSimulatorFn()(r, ctx, accounts) + require.Nil(t, content2) +} diff --git a/x/erc20/types/interfaces.go b/x/erc20/types/interfaces.go index 497106ab..119da37b 100644 --- a/x/erc20/types/interfaces.go +++ b/x/erc20/types/interfaces.go @@ -1,10 +1,13 @@ package types import ( - context "context" + "context" + "math/big" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/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 +21,14 @@ import ( type AccountKeeper interface { GetModuleAddress(moduleName string) sdk.AccAddress GetSequence(sdk.Context, sdk.AccAddress) (uint64, error) + GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI } // BankKeeper defines the expected interface needed to retrieve account balances. type BankKeeper interface { SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error IsSendEnabledCoin(ctx sdk.Context, coin sdk.Coin) bool @@ -32,12 +37,25 @@ type BankKeeper interface { SetDenomMetaData(ctx sdk.Context, denomMetaData banktypes.Metadata) HasSupply(ctx sdk.Context, denom string) bool GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin + SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + GetParams(ctx sdk.Context) banktypes.Params + SetParams(ctx sdk.Context, params banktypes.Params) } // 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) 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) feemarkettypes.Params + SetParams(ctx sdk.Context, params feemarkettypes.Params) } diff --git a/x/erc20/types/utils.go b/x/erc20/types/utils.go index d3c4e304..27cb15a5 100644 --- a/x/erc20/types/utils.go +++ b/x/erc20/types/utils.go @@ -2,10 +2,17 @@ package types import ( "fmt" + "math/big" + "math/rand" "regexp" "strings" + "github.com/cosmos/cosmos-sdk/simapp/helpers" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/tendermint/tendermint/crypto" ) const ( @@ -81,3 +88,89 @@ func EqualStringSlice(aliasesA, aliasesB []string) bool { return true } + +// DeriveAddress derives an address with the given address length type, module name, and +func DeriveAddress(moduleName, name string) sdk.AccAddress { + return sdk.AccAddress(crypto.AddressHash([]byte(moduleName + name))) +} + +// RandomInt returns a random integer in the half-open interval [min, max). +func RandomInt(r *rand.Rand, min, max sdk.Int) sdk.Int { + return min.Add(sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.Sub(min).BigInt()))) +} + +// RandomDec returns a random decimal in the half-open interval [min, max). +func RandomDec(r *rand.Rand, min, max sdk.Dec) sdk.Dec { + return min.Add(sdk.NewDecFromBigIntWithPrec(new(big.Int).Rand(r, max.Sub(min).BigInt()), sdk.Precision)) +} + +// GenAndDeliverTx generates a transactions and delivers it. +func GenAndDeliverTx(txCtx simulation.OperationInput, fees sdk.Coins, gas uint64) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + tx, err := helpers.GenTx( + txCtx.TxGen, + []sdk.Msg{txCtx.Msg}, + fees, + gas, + txCtx.Context.ChainID(), + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + txCtx.SimAccount.PrivKey, + ) + + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate mock tx"), nil, err + } + + _, _, err = txCtx.App.Deliver(txCtx.TxGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to deliver tx"), nil, err + } + + return simtypes.NewOperationMsg(txCtx.Msg, true, "", txCtx.Cdc), nil, nil +} + +// GenAndDeliverTxWithFees generates a transaction with given fee and delivers it. +func GenAndDeliverTxWithFees(txCtx simulation.OperationInput, gas uint64, fees sdk.Coins) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + spendable := txCtx.Bankkeeper.SpendableCoins(txCtx.Context, account.GetAddress()) + + var err error + + _, hasNeg := spendable.SafeSub(txCtx.CoinsSpentInMsg) + if hasNeg { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "message doesn't leave room for fees"), nil, err + } + + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate fees"), nil, err + } + return GenAndDeliverTx(txCtx, fees, gas) +} + +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/genesis.go b/x/govshuttle/genesis.go index cfd60638..9c41dabb 100644 --- a/x/govshuttle/genesis.go +++ b/x/govshuttle/genesis.go @@ -5,6 +5,7 @@ import ( "github.com/Canto-Network/Canto/v7/x/govshuttle/types" sdk "github.com/cosmos/cosmos-sdk/types" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + "github.com/ethereum/go-ethereum/common" ) // InitGenesis initializes the capability module's state from a provided genesis @@ -12,16 +13,24 @@ import ( func InitGenesis(ctx sdk.Context, k keeper.Keeper, accountKeeper authkeeper.AccountKeeper, genState types.GenesisState) { // this line is used by starport scaffolding # genesis/module/init k.SetParams(ctx, genState.Params) - + if genState.PortAddress != nil { + k.SetPort(ctx, common.BytesToAddress(genState.PortAddress)) + } if acc := accountKeeper.GetModuleAccount(ctx, types.ModuleName); acc == nil { panic("the govshuttle module account has not been set") } + } // ExportGenesis returns the capability module's exported genesis. func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { - genesis := types.DefaultGenesis() - genesis.Params = k.GetParams(ctx) + portAddress, found := k.GetPort(ctx) + var genesis *types.GenesisState + if found { + genesis = types.NewGenesisState(k.GetParams(ctx), portAddress.Bytes()) + } else { + genesis = types.NewGenesisState(k.GetParams(ctx), nil) + } // this line is used by starport scaffolding # genesis/module/export diff --git a/x/govshuttle/genesis_test.go b/x/govshuttle/genesis_test.go deleted file mode 100644 index 13baacd8..00000000 --- a/x/govshuttle/genesis_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package govshuttle_test - -import ( - "testing" - //keepertest "github.com/Canto-Network/Canto/v2/testutil/keeper" - //"github.com/Canto-Network/Canto/v2/testutil/nullify" - // "github.com/Canto-Network/Canto/v2/x/govshuttle" - // "github.com/Canto-Network/Canto/v2/x/govshuttle/types" - // "github.com/stretchr/testify/require" -) - -func TestGenesis(t *testing.T) { - // genesisState := types.GenesisState{ - // Params: types.DefaultParams(), - - // // this line is used by starport scaffolding # genesis/test/state - // } - - // k, ctx := keepertest.govshuttleKeeper(t) - // govshuttle.InitGenesis(ctx, *k, genesisState) - // got := govshuttle.ExportGenesis(ctx, *k) - // require.NotNil(t, got) - - // nullify.Fill(&genesisState) - // nullify.Fill(got) - - // this line is used by starport scaffolding # genesis/test/assert -} diff --git a/x/govshuttle/keeper/genesis_test.go b/x/govshuttle/keeper/genesis_test.go new file mode 100644 index 00000000..9ddbca94 --- /dev/null +++ b/x/govshuttle/keeper/genesis_test.go @@ -0,0 +1,82 @@ +package keeper_test + +import ( + "time" + + "github.com/evmos/ethermint/tests" + + "github.com/Canto-Network/Canto/v7/x/govshuttle" + "github.com/Canto-Network/Canto/v7/x/govshuttle/types" +) + +func (suite *KeeperTestSuite) TestDefaultGenesis() { + genState := types.DefaultGenesis() + + govshuttle.InitGenesis(suite.ctx, suite.app.GovshuttleKeeper, suite.app.AccountKeeper, *genState) + got := govshuttle.ExportGenesis(suite.ctx, suite.app.GovshuttleKeeper) + suite.Require().Equal(genState, got) +} + +func (suite *KeeperTestSuite) TestImportExportGenesisEmpty() { + _, found := suite.app.GovshuttleKeeper.GetPort(suite.ctx) + suite.Require().False(found) + genState := govshuttle.ExportGenesis(suite.ctx, suite.app.GovshuttleKeeper) + suite.Require().Nil(genState.PortAddress) + + // Copy genState to genState2 and init with it + var genState2 types.GenesisState + bz := suite.app.AppCodec().MustMarshalJSON(genState) + suite.app.AppCodec().MustUnmarshalJSON(bz, &genState2) + govshuttle.InitGenesis(suite.ctx, suite.app.GovshuttleKeeper, suite.app.AccountKeeper, genState2) + + _, found = suite.app.GovshuttleKeeper.GetPort(suite.ctx) + suite.Require().False(found) + + genState3 := govshuttle.ExportGenesis(suite.ctx, suite.app.GovshuttleKeeper) + suite.Equal(*genState, genState2) + suite.Equal(genState2, *genState3) + suite.Require().Nil(genState.PortAddress) +} + +func (suite *KeeperTestSuite) TestInitExportGenesis() { + portAddress := tests.GenerateAddress() + expGenesis := types.NewGenesisState(types.DefaultParams(), portAddress.Bytes()) + + govshuttle.InitGenesis(suite.ctx, suite.app.GovshuttleKeeper, suite.app.AccountKeeper, *expGenesis) + genState := govshuttle.ExportGenesis(suite.ctx, suite.app.GovshuttleKeeper) + suite.Require().Equal(expGenesis, genState) + + bz := suite.app.AppCodec().MustMarshalJSON(genState) + + var genState2 types.GenesisState + suite.app.AppCodec().MustUnmarshalJSON(bz, &genState2) + govshuttle.InitGenesis(suite.ctx, suite.app.GovshuttleKeeper, suite.app.AccountKeeper, genState2) + genState3 := govshuttle.ExportGenesis(suite.ctx, suite.app.GovshuttleKeeper) + + suite.Require().Equal(*genState, genState2) + suite.Require().Equal(genState2, *genState3) +} + +func (suite *KeeperTestSuite) TestImportExportGenesis() { + t, _ := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z") + suite.ctx = suite.ctx.WithBlockHeight(1).WithBlockTime(t) + + portAddress := tests.GenerateAddress() + suite.app.GovshuttleKeeper.SetPort(suite.ctx, portAddress) + + genState := govshuttle.ExportGenesis(suite.ctx, suite.app.GovshuttleKeeper) + bz := suite.app.AppCodec().MustMarshalJSON(genState) + + // Copy genState to genState2 and init with it + var genState2 types.GenesisState + suite.app.AppCodec().MustUnmarshalJSON(bz, &genState2) + govshuttle.InitGenesis(suite.ctx, suite.app.GovshuttleKeeper, suite.app.AccountKeeper, genState2) + exported := govshuttle.ExportGenesis(suite.ctx, suite.app.GovshuttleKeeper) + suite.Equal(*genState, *exported) + + suite.ctx = suite.ctx.WithBlockHeight(1).WithBlockTime(t) + + p, found := suite.app.GovshuttleKeeper.GetPort(suite.ctx) + suite.True(found) + suite.Equal(portAddress, p) +} diff --git a/x/govshuttle/keeper/keeper.go b/x/govshuttle/keeper/keeper.go index d89f423b..bc3c777d 100644 --- a/x/govshuttle/keeper/keeper.go +++ b/x/govshuttle/keeper/keeper.go @@ -7,10 +7,11 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/tendermint/tendermint/libs/log" - "github.com/Canto-Network/Canto/v7/x/govshuttle/types" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + + "github.com/Canto-Network/Canto/v7/x/govshuttle/types" ) type ( diff --git a/x/govshuttle/keeper/keeper_test.go b/x/govshuttle/keeper/keeper_test.go index 73096158..db821b18 100644 --- a/x/govshuttle/keeper/keeper_test.go +++ b/x/govshuttle/keeper/keeper_test.go @@ -4,8 +4,14 @@ import ( "encoding/json" "math/big" "testing" + "time" + "github.com/evmos/ethermint/crypto/ethsecp256k1" "github.com/stretchr/testify/suite" + "github.com/tendermint/tendermint/crypto/tmhash" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmversion "github.com/tendermint/tendermint/proto/tendermint/version" + "github.com/tendermint/tendermint/version" "github.com/Canto-Network/Canto/v7/app" "github.com/cosmos/cosmos-sdk/crypto/keyring" @@ -53,6 +59,42 @@ func (suite *KeeperTestSuite) DoSetupTest(t require.TestingT) { //init app suite.app = app.Setup(checkTx, feemarketGenesis) + + priv, err := ethsecp256k1.GenerateKey() + require.NoError(t, err) + + suite.address = common.BytesToAddress(priv.PubKey().Address().Bytes()) + + // consensus key + privCons, err := ethsecp256k1.GenerateKey() + require.NoError(t, err) + consAddress := sdk.ConsAddress(privCons.PubKey().Address()) + + // setup context + suite.ctx = suite.app.BaseApp.NewContext(checkTx, tmproto.Header{ + Height: 1, + ChainID: "canto_9001-1", + Time: time.Now().UTC(), + ProposerAddress: consAddress.Bytes(), + + Version: tmversion.Consensus{ + Block: version.BlockProtocol, + }, + LastBlockId: tmproto.BlockID{ + Hash: tmhash.Sum([]byte("block_id")), + PartSetHeader: tmproto.PartSetHeader{ + Total: 11, + Hash: tmhash.Sum([]byte("partset_header")), + }, + }, + AppHash: tmhash.Sum([]byte("app")), + DataHash: tmhash.Sum([]byte("data")), + EvidenceHash: tmhash.Sum([]byte("evidence")), + ValidatorsHash: tmhash.Sum([]byte("validators")), + NextValidatorsHash: tmhash.Sum([]byte("next_validators")), + ConsensusHash: tmhash.Sum([]byte("consensus")), + LastResultsHash: tmhash.Sum([]byte("last_result")), + }) } func (suite *KeeperTestSuite) SetupTest() { diff --git a/x/govshuttle/proposal_handler.go b/x/govshuttle/keeper/proposal_handler.go similarity index 76% rename from x/govshuttle/proposal_handler.go rename to x/govshuttle/keeper/proposal_handler.go index 62eaefee..9c1d35ca 100644 --- a/x/govshuttle/proposal_handler.go +++ b/x/govshuttle/keeper/proposal_handler.go @@ -1,7 +1,6 @@ -package govshuttle +package keeper import ( - "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" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -9,7 +8,7 @@ import ( ) // Return governance handler to process Compound Proposal -func NewgovshuttleProposalHandler(k *keeper.Keeper) govtypes.Handler { +func NewgovshuttleProposalHandler(k *Keeper) govtypes.Handler { return func(ctx sdk.Context, content govtypes.Content) error { switch c := content.(type) { case *types.LendingMarketProposal: @@ -23,7 +22,7 @@ func NewgovshuttleProposalHandler(k *keeper.Keeper) govtypes.Handler { } } -func handleLendingMarketProposal(ctx sdk.Context, k *keeper.Keeper, p *types.LendingMarketProposal) error { +func handleLendingMarketProposal(ctx sdk.Context, k *Keeper, p *types.LendingMarketProposal) error { err := p.ValidateBasic() if err != nil { return err @@ -37,7 +36,7 @@ func handleLendingMarketProposal(ctx sdk.Context, k *keeper.Keeper, p *types.Len } // governance proposal handler -func handleTreasuryProposal(ctx sdk.Context, k *keeper.Keeper, tp *types.TreasuryProposal) error { +func handleTreasuryProposal(ctx sdk.Context, k *Keeper, tp *types.TreasuryProposal) error { err := tp.ValidateBasic() if err != nil { return err diff --git a/x/govshuttle/module.go b/x/govshuttle/module.go index 66d460c8..9cd9a2b9 100644 --- a/x/govshuttle/module.go +++ b/x/govshuttle/module.go @@ -34,10 +34,10 @@ var ( // AppModuleBasic implements the AppModuleBasic interface for the capability 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} } @@ -105,11 +105,12 @@ type AppModule struct { } func NewAppModule( + cdc codec.Codec, k keeper.Keeper, ak authkeeper.AccountKeeper, ) AppModule { return AppModule{ - AppModuleBasic: AppModuleBasic{}, + AppModuleBasic: AppModuleBasic{cdc: cdc}, keeper: k, accountkeeper: ak, } diff --git a/x/govshuttle/module_simulation.go b/x/govshuttle/module_simulation.go index 0fa01a22..29779a80 100644 --- a/x/govshuttle/module_simulation.go +++ b/x/govshuttle/module_simulation.go @@ -3,23 +3,21 @@ 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" "github.com/cosmos/cosmos-sdk/baseapp" simappparams "github.com/cosmos/cosmos-sdk/simapp/params" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/Canto-Network/Canto/v7/x/govshuttle/simulation" + "github.com/Canto-Network/Canto/v7/x/govshuttle/types" ) // avoid unused import issue var ( //_ = sample.AccAddress - _ = govshuttlesimulation.FindAccount + //_ = govshuttlesimulation.FindAccount _ = simappparams.StakePerAccount - _ = simulation.MsgEntryKind _ = baseapp.Paramspace ) @@ -29,30 +27,23 @@ const ( // GenerateGenesisState creates a randomized GenState of the module func (AppModule) GenerateGenesisState(simState *module.SimulationState) { - accs := make([]string, len(simState.Accounts)) - for i, acc := range simState.Accounts { - accs[i] = acc.Address.String() - } - govshuttleGenesis := types.GenesisState{ - Params: types.DefaultParams(), - // this line is used by starport scaffolding # simapp/module/genesisState - } - simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&govshuttleGenesis) + simulation.RandomizedGenState(simState) } // ProposalContents doesn't return any content functions for governance proposals -func (AppModule) ProposalContents(_ module.SimulationState) []simtypes.WeightedProposalContent { - return nil +func (am AppModule) ProposalContents(_ module.SimulationState) []simtypes.WeightedProposalContent { + return simulation.ProposalContents(am.keeper) } // RandomizedParams creates randomized param changes for the simulator func (am AppModule) RandomizedParams(_ *rand.Rand) []simtypes.ParamChange { - return []simtypes.ParamChange{} } // RegisterStoreDecoder registers a decoder -func (am AppModule) RegisterStoreDecoder(_ sdk.StoreDecoderRegistry) {} +func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.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..89fb820c --- /dev/null +++ b/x/govshuttle/simulation/decoder_test.go @@ -0,0 +1,48 @@ +package simulation_test + +import ( + "fmt" + "testing" + + "github.com/evmos/ethermint/tests" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/simapp" + "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 := simapp.MakeTestEncodingConfig() + dec := simulation.NewDecodeStore(cdc.Marshaler) + + 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..e09d2e2b --- /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 erc20 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..a3b2fcdd --- /dev/null +++ b/x/govshuttle/simulation/genesis_test.go @@ -0,0 +1,70 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + "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: 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/proposal.go b/x/govshuttle/simulation/proposal.go new file mode 100644 index 00000000..7806c74a --- /dev/null +++ b/x/govshuttle/simulation/proposal.go @@ -0,0 +1,86 @@ +package simulation + +import ( + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "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" +) + +// Simulation operation weights constants. +const ( + OpWeightSimulateLendingMarketProposal = "op_weight_lending_market_proposal" + OpWeightSimulateTreasuryProposal = "op_weight_treasury_proposal" +) + +// ProposalContents defines the module weighted proposals' contents +func ProposalContents(k keeper.Keeper) []simtypes.WeightedProposalContent { + return []simtypes.WeightedProposalContent{ + simulation.NewWeightedProposalContent( + OpWeightSimulateLendingMarketProposal, + params.DefaultWeightLendingMarketProposal, + SimulateLendingMarketProposal(k), + ), + simulation.NewWeightedProposalContent( + OpWeightSimulateTreasuryProposal, + params.DefaultWeightRegisterERC20Proposal, + SimulateTreasuryProposal(k), + ), + } +} + +func SimulateLendingMarketProposal(k keeper.Keeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + + treasuryProposalMetadata := types.TreasuryProposalMetadata{ + PropID: 1, + Recipient: accs[0].Address.String(), + Amount: uint64(1000000000000000000), + 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"} + + if err := keeper.NewgovshuttleProposalHandler(&k)(ctx, lendingMarketProposal); err != nil { + panic(err) + } + + return nil + } +} + +func SimulateTreasuryProposal(k keeper.Keeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + + treasuryProposalMetadata := types.TreasuryProposalMetadata{ + PropID: 1, + Recipient: accs[0].Address.String(), + Amount: uint64(1000000000000000000), + Denom: "canto", + } + + proposal := types.TreasuryProposal{ + Title: simtypes.RandStringOfLength(r, 10), + Description: simtypes.RandStringOfLength(r, 100), + Metadata: &treasuryProposalMetadata, + } + + if err := keeper.NewgovshuttleProposalHandler(&k)(ctx, &proposal); err != nil { + panic(err) + } + + return nil + } +} diff --git a/x/govshuttle/simulation/proposal_test.go b/x/govshuttle/simulation/proposal_test.go new file mode 100644 index 00000000..c6257d6f --- /dev/null +++ b/x/govshuttle/simulation/proposal_test.go @@ -0,0 +1,119 @@ +package simulation_test + +import ( + "math/rand" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/teststaking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/Canto-Network/Canto/v7/app" + "github.com/Canto-Network/Canto/v7/app/params" + "github.com/Canto-Network/Canto/v7/x/govshuttle/simulation" + "github.com/Canto-Network/Canto/v7/x/govshuttle/types" +) + +func TestProposalContents(t *testing.T) { + app, ctx := createTestApp(t, false) + + s := rand.NewSource(1) + r := rand.New(s) + + accounts := getTestingAccounts(t, r, app, ctx, 10) + + // execute ProposalContents function + weightedProposalContent := simulation.ProposalContents(app.GovshuttleKeeper) + require.Len(t, weightedProposalContent, 2) + + w0 := weightedProposalContent[0] + w1 := weightedProposalContent[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()) + + content0 := w0.ContentSimulatorFn()(r, ctx, accounts) + require.Nil(t, content0) + + content1 := w1.ContentSimulatorFn()(r, ctx, accounts) + require.Nil(t, content1) +} + +func createTestApp(t *testing.T, isCheckTx bool) (*app.Canto, sdk.Context) { + app := app.Setup(isCheckTx, nil) + r := rand.New(rand.NewSource(1)) + + simAccs := simtypes.RandomAccounts(r, 10) + + ctx := app.BaseApp.NewContext(isCheckTx, tmproto.Header{}) + validator := getTestingValidator0(t, app, ctx, simAccs) + consAddr, err := validator.GetConsAddr() + require.NoError(t, err) + ctx = ctx.WithBlockHeader(tmproto.Header{Height: 1, + ChainID: "canto_9001-1", + Time: time.Now().UTC(), + ProposerAddress: consAddr, + }) + return app, ctx +} + +func getTestingAccounts(t *testing.T, r *rand.Rand, app *app.Canto, ctx sdk.Context, n int) []simtypes.Account { + accounts := simtypes.RandomAccounts(r, n) + + initAmt := app.StakingKeeper.TokensFromConsensusPower(ctx, 100_000_000) + initCoins := sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, initAmt), + ) + + // add coins to the accounts + for _, account := range accounts { + acc := app.AccountKeeper.NewAccountWithAddress(ctx, account.Address) + app.AccountKeeper.SetAccount(ctx, acc) + err := fundAccount(app.BankKeeper, ctx, account.Address, initCoins) + require.NoError(t, err) + } + + return accounts +} + +func fundAccount(bk bankkeeper.Keeper, ctx sdk.Context, addr sdk.AccAddress, coins sdk.Coins) error { + if err := bk.MintCoins(ctx, types.ModuleName, coins); err != nil { + return err + } + if err := bk.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addr, coins); err != nil { + return err + } + return nil +} + +func getTestingValidator0(t *testing.T, app *app.Canto, ctx sdk.Context, accounts []simtypes.Account) stakingtypes.Validator { + commission0 := stakingtypes.NewCommission(sdk.ZeroDec(), sdk.OneDec(), sdk.OneDec()) + return getTestingValidator(t, app, ctx, accounts, commission0, 0) +} + +func getTestingValidator(t *testing.T, app *app.Canto, ctx sdk.Context, accounts []simtypes.Account, commission stakingtypes.Commission, n int) stakingtypes.Validator { + account := accounts[n] + valPubKey := account.PubKey + valAddr := sdk.ValAddress(account.PubKey.Address().Bytes()) + validator := teststaking.NewValidator(t, valAddr, valPubKey) + validator, err := validator.SetInitialCommission(commission) + require.NoError(t, err) + + validator.DelegatorShares = sdk.NewDec(100) + validator.Tokens = app.StakingKeeper.TokensFromConsensusPower(ctx, 100) + + app.StakingKeeper.SetValidator(ctx, validator) + app.StakingKeeper.SetValidatorByConsAddr(ctx, validator) + + return validator +} diff --git a/x/govshuttle/types/genesis.go b/x/govshuttle/types/genesis.go index 8df94bae..aa6e9e46 100644 --- a/x/govshuttle/types/genesis.go +++ b/x/govshuttle/types/genesis.go @@ -1,17 +1,18 @@ package types -import ( -// this line is used by starport scaffolding # genesis/types/import -) - // DefaultIndex is the default capability global index const DefaultIndex uint64 = 1 // DefaultGenesis returns the default Capability genesis state func DefaultGenesis() *GenesisState { + return NewGenesisState(DefaultParams(), nil) +} + +func NewGenesisState(params Params, portAddress []byte) *GenesisState { return &GenesisState{ - // this line is used by starport scaffolding # genesis/types/default - Params: DefaultParams(), + Params: params, + PortAddress: portAddress, + // this line is used by starport scaffolding # genesis/types/init } } diff --git a/x/govshuttle/types/genesis.pb.go b/x/govshuttle/types/genesis.pb.go index a53f33c7..45929555 100644 --- a/x/govshuttle/types/genesis.pb.go +++ b/x/govshuttle/types/genesis.pb.go @@ -25,7 +25,8 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // GenesisState defines the govshuttle module's genesis state. type GenesisState struct { - Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` + Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` + PortAddress []byte `protobuf:"bytes,2,opt,name=port_address,json=portAddress,proto3" json:"port_address,omitempty"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -68,6 +69,13 @@ func (m *GenesisState) GetParams() Params { return Params{} } +func (m *GenesisState) GetPortAddress() []byte { + if m != nil { + return m.PortAddress + } + return nil +} + func init() { proto.RegisterType((*GenesisState)(nil), "canto.govshuttle.v1.GenesisState") } @@ -75,20 +83,22 @@ func init() { func init() { proto.RegisterFile("canto/govshuttle/v1/genesis.proto", fileDescriptor_356493b1fc5972f0) } var fileDescriptor_356493b1fc5972f0 = []byte{ - // 208 bytes of a gzipped FileDescriptorProto + // 237 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4c, 0x4e, 0xcc, 0x2b, 0xc9, 0xd7, 0x4f, 0xcf, 0x2f, 0x2b, 0xce, 0x28, 0x2d, 0x29, 0xc9, 0x49, 0xd5, 0x2f, 0x33, 0xd4, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x06, 0x2b, 0xd1, 0x43, 0x28, 0xd1, 0x2b, 0x33, 0x94, 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0xcb, 0xeb, - 0x83, 0x58, 0x10, 0xa5, 0x52, 0x2a, 0x58, 0x4d, 0x43, 0x68, 0x04, 0xab, 0x52, 0xf2, 0xe4, 0xe2, + 0x83, 0x58, 0x10, 0xa5, 0x52, 0x2a, 0x58, 0x4d, 0x43, 0x68, 0x04, 0xab, 0x52, 0xca, 0xe1, 0xe2, 0x71, 0x87, 0xd8, 0x10, 0x5c, 0x92, 0x58, 0x92, 0x2a, 0x64, 0xc9, 0xc5, 0x56, 0x90, 0x58, 0x94, 0x98, 0x5b, 0x2c, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x6d, 0x24, 0xad, 0x87, 0xc5, 0x46, 0xbd, 0x00, - 0xb0, 0x12, 0x27, 0x96, 0x13, 0xf7, 0xe4, 0x19, 0x82, 0xa0, 0x1a, 0x9c, 0xfc, 0x4e, 0x3c, 0x92, - 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, - 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0xca, 0x24, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, - 0x39, 0x3f, 0x57, 0xdf, 0x19, 0x64, 0x9c, 0xae, 0x5f, 0x6a, 0x49, 0x79, 0x7e, 0x51, 0x36, 0x84, - 0xa7, 0x5f, 0x66, 0xae, 0x5f, 0x81, 0xec, 0xd0, 0x92, 0xca, 0x82, 0xd4, 0xe2, 0x24, 0x36, 0xb0, - 0x0b, 0x8d, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xba, 0xa8, 0x69, 0x0f, 0x17, 0x01, 0x00, 0x00, + 0xb0, 0x12, 0x27, 0x96, 0x13, 0xf7, 0xe4, 0x19, 0x82, 0xa0, 0x1a, 0x84, 0x14, 0xb9, 0x78, 0x0a, + 0xf2, 0x8b, 0x4a, 0xe2, 0x13, 0x53, 0x52, 0x8a, 0x52, 0x8b, 0x8b, 0x25, 0x98, 0x14, 0x18, 0x35, + 0x78, 0x82, 0xb8, 0x41, 0x62, 0x8e, 0x10, 0x21, 0x27, 0xbf, 0x13, 0x8f, 0xe4, 0x18, 0x2f, 0x3c, + 0x92, 0x63, 0x7c, 0xf0, 0x48, 0x8e, 0x71, 0xc2, 0x63, 0x39, 0x86, 0x0b, 0x8f, 0xe5, 0x18, 0x6e, + 0x3c, 0x96, 0x63, 0x88, 0x32, 0x49, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, + 0x77, 0x06, 0xd9, 0xa8, 0xeb, 0x97, 0x5a, 0x52, 0x9e, 0x5f, 0x94, 0x0d, 0xe1, 0xe9, 0x97, 0x99, + 0xeb, 0x57, 0x20, 0xfb, 0xa5, 0xa4, 0xb2, 0x20, 0xb5, 0x38, 0x89, 0x0d, 0xec, 0x09, 0x63, 0x40, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xb7, 0x01, 0xeb, 0xfd, 0x3a, 0x01, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -111,6 +121,13 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.PortAddress) > 0 { + i -= len(m.PortAddress) + copy(dAtA[i:], m.PortAddress) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.PortAddress))) + i-- + dAtA[i] = 0x12 + } { size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -143,6 +160,10 @@ func (m *GenesisState) Size() (n int) { _ = l l = m.Params.Size() n += 1 + l + sovGenesis(uint64(l)) + l = len(m.PortAddress) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } return n } @@ -214,6 +235,40 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PortAddress", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PortAddress = append(m.PortAddress[:0], dAtA[iNdEx:postIndex]...) + if m.PortAddress == nil { + m.PortAddress = []byte{} + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/inflation/module.go b/x/inflation/module.go index 7d8cb58d..73361f30 100644 --- a/x/inflation/module.go +++ b/x/inflation/module.go @@ -21,6 +21,7 @@ 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" ) @@ -32,7 +33,9 @@ var ( ) // app module Basics object -type AppModuleBasic struct{} +type AppModuleBasic struct { + cdc codec.Codec +} // Name returns the inflation module's name. func (AppModuleBasic) Name() string { @@ -100,12 +103,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: AppModuleBasic{cdc: cdc}, keeper: k, ak: ak, sk: sk, @@ -185,6 +189,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw // GenerateGenesisState creates a randomized GenState of the inflation module. func (am AppModule) GenerateGenesisState(input *module.SimulationState) { + simulation.RandomizedGenState(input) } // ProposalContents doesn't return any content functions for governance proposals. @@ -194,11 +199,12 @@ func (am AppModule) ProposalContents(simState module.SimulationState) []simtypes // RandomizedParams creates randomized inflation param changes for the simulator. func (am AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange { - return []simtypes.ParamChange{} + return simulation.ParamChanges(r) } // RegisterStoreDecoder registers a decoder for inflation module's types. func (am AppModule) RegisterStoreDecoder(decoderRegistry sdk.StoreDecoderRegistry) { + decoderRegistry[types.StoreKey] = 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..f8823059 --- /dev/null +++ b/x/inflation/simulation/decoder.go @@ -0,0 +1,53 @@ +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/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 sdk.Dec + 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..f0c60fd9 --- /dev/null +++ b/x/inflation/simulation/decoder_test.go @@ -0,0 +1,62 @@ +package simulation_test + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/simapp" + "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 := simapp.MakeTestEncodingConfig() + dec := simulation.NewDecodeStore(cdc.Marshaler) + + period := uint64(1) + epochMintProvision := sdk.NewDec(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..1c029a46 --- /dev/null +++ b/x/inflation/simulation/genesis.go @@ -0,0 +1,126 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + + 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: sdk.NewDec(int64(simtypes.RandIntBetween(r, 0, 10000000))), + R: sdk.NewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 100)), 2), + C: sdk.ZeroDec(), + BondingTarget: sdk.NewDecWithPrec(int64(simtypes.RandIntBetween(r, 1, 100)), 2), + MaxVariance: sdk.ZeroDec(), + } +} + +func generateInflationDistribution(r *rand.Rand) types.InflationDistribution { + + stakingRewards := sdk.NewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 100)), 2) + communityPool := sdk.NewDec(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( + simState.Cdc, mintDenom, &genesis.Params.MintDenom, simState.Rand, + func(r *rand.Rand) { genesis.Params.MintDenom = generateMintDenom(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, exponentialCalculation, &genesis.Params.ExponentialCalculation, simState.Rand, + func(r *rand.Rand) { genesis.Params.ExponentialCalculation = generateExponentialCalculation(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, inflationDistribution, &genesis.Params.InflationDistribution, simState.Rand, + func(r *rand.Rand) { genesis.Params.InflationDistribution = generateInflationDistribution(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, enableInflation, &genesis.Params.EnableInflation, simState.Rand, + func(r *rand.Rand) { genesis.Params.EnableInflation = generateEnableInflation(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, period, &genesis.Period, simState.Rand, + func(r *rand.Rand) { genesis.Period = generatePeriod(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, epochIdentifier, &genesis.EpochIdentifier, simState.Rand, + func(r *rand.Rand) { genesis.EpochIdentifier = generateEpochIdentifier(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, epochsPerPeriod, &genesis.EpochsPerPeriod, simState.Rand, + func(r *rand.Rand) { genesis.EpochsPerPeriod = generateEpochsPerPeriod(r) }, + ) + + simState.AppParams.GetOrGenerate( + simState.Cdc, 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..5060c44b --- /dev/null +++ b/x/inflation/simulation/genesis_test.go @@ -0,0 +1,88 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + 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/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: 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: sdk.NewDec(2712964), + R: sdk.NewDecWithPrec(11, 2), + C: sdk.ZeroDec(), + BondingTarget: sdk.NewDecWithPrec(94, 2), + MaxVariance: sdk.ZeroDec(), + }, genState.Params.ExponentialCalculation) + require.Equal(t, types.InflationDistribution{ + StakingRewards: sdk.NewDecWithPrec(1, 1), + CommunityPool: sdk.NewDecWithPrec(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/params.go b/x/inflation/simulation/params.go new file mode 100644 index 00000000..5fb263df --- /dev/null +++ b/x/inflation/simulation/params.go @@ -0,0 +1,52 @@ +package simulation + +import ( + "encoding/json" + "math/rand" + + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/Canto-Network/Canto/v7/x/inflation/types" +) + +func ParamChanges(r *rand.Rand) []simtypes.ParamChange { + return []simtypes.ParamChange{ + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyMintDenom), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateMintDenom(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyExponentialCalculation), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateExponentialCalculation(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyInflationDistribution), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateInflationDistribution(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyEnableInflation), + func(r *rand.Rand) string { + bz, err := json.Marshal(generateEnableInflation(r)) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + } +} diff --git a/x/inflation/simulation/params_test.go b/x/inflation/simulation/params_test.go new file mode 100644 index 00000000..2061452e --- /dev/null +++ b/x/inflation/simulation/params_test.go @@ -0,0 +1,36 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/Canto-Network/Canto/v7/x/inflation/simulation" +) + +func TestParamChanges(t *testing.T) { + r := rand.New(rand.NewSource(0)) + + paramChanges := simulation.ParamChanges(r) + require.Len(t, paramChanges, 4) + + expected := []struct { + composedKey string + key string + simValue string + subspace string + }{ + {"inflation/ParamStoreKeyMintDenom", "ParamStoreKeyMintDenom", "\"stake\"", "inflation"}, + {"inflation/ParamStoreKeyExponentialCalculation", "ParamStoreKeyExponentialCalculation", `{"a":"9793274.000000000000000000","r":"0.140000000000000000","c":"0.000000000000000000","bonding_target":"0.950000000000000000","max_variance":"0.000000000000000000"}`, "inflation"}, + {"inflation/ParamStoreKeyInflationDistribution", "ParamStoreKeyInflationDistribution", `{"staking_rewards":"0.060000000000000000","community_pool":"0.940000000000000000"}`, "inflation"}, + {"inflation/ParamStoreKeyEnableInflation", "ParamStoreKeyEnableInflation", "true", "inflation"}, + } + + for i, p := range paramChanges { + require.Equal(t, expected[i].composedKey, p.ComposedKey()) + require.Equal(t, expected[i].key, p.Key()) + require.Equal(t, expected[i].simValue, p.SimValue()(r)) + require.Equal(t, expected[i].subspace, p.Subspace()) + } +}