package keeper_test import ( _ "embed" "encoding/json" "math" "math/big" "testing" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" sdkmath "cosmossdk.io/math" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" feemarkettypes "github.com/cerc-io/laconicd/x/feemarket/types" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" tmjson "github.com/tendermint/tendermint/libs/json" "github.com/cerc-io/laconicd/app" "github.com/cerc-io/laconicd/crypto/ethsecp256k1" "github.com/cerc-io/laconicd/encoding" "github.com/cerc-io/laconicd/server/config" "github.com/cerc-io/laconicd/tests" ethermint "github.com/cerc-io/laconicd/types" "github.com/cerc-io/laconicd/x/evm/statedb" "github.com/cerc-io/laconicd/x/evm/types" evmtypes "github.com/cerc-io/laconicd/x/evm/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/ethereum/go-ethereum/params" abci "github.com/tendermint/tendermint/abci/types" "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" ) var testTokens = sdkmath.NewIntWithDecimal(1000, 18) type KeeperTestSuite struct { suite.Suite ctx sdk.Context app *app.EthermintApp queryClient types.QueryClient address common.Address consAddress sdk.ConsAddress // for generate test tx clientCtx client.Context ethSigner ethtypes.Signer appCodec codec.Codec signer keyring.Signer enableFeemarket bool enableLondonHF bool mintFeeCollector bool denom string } var s *KeeperTestSuite func TestKeeperTestSuite(t *testing.T) { s = new(KeeperTestSuite) s.enableFeemarket = false s.enableLondonHF = true suite.Run(t, s) // Run Ginkgo integration tests RegisterFailHandler(Fail) RunSpecs(t, "Keeper Suite") } func (suite *KeeperTestSuite) SetupTest() { checkTx := false suite.app = app.Setup(checkTx, nil) suite.SetupApp(checkTx) } // SetupApp setup test environment, it uses`require.TestingT` to support both `testing.T` and `testing.B`. func (suite *KeeperTestSuite) SetupApp(checkTx bool) { t := suite.T() // account key, use a constant account to keep unit test deterministic. ecdsaPriv, err := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") require.NoError(t, err) priv := ðsecp256k1.PrivKey{ Key: crypto.FromECDSA(ecdsaPriv), } suite.address = common.BytesToAddress(priv.PubKey().Address().Bytes()) suite.signer = tests.NewSigner(priv) // consensus key priv, err = ethsecp256k1.GenerateKey() require.NoError(t, err) suite.consAddress = sdk.ConsAddress(priv.PubKey().Address()) suite.app = app.Setup(checkTx, func(app *app.EthermintApp, genesis simapp.GenesisState) simapp.GenesisState { feemarketGenesis := feemarkettypes.DefaultGenesisState() if suite.enableFeemarket { feemarketGenesis.Params.EnableHeight = 1 feemarketGenesis.Params.NoBaseFee = false } else { feemarketGenesis.Params.NoBaseFee = true } genesis[feemarkettypes.ModuleName] = app.AppCodec().MustMarshalJSON(feemarketGenesis) if !suite.enableLondonHF { evmGenesis := types.DefaultGenesisState() maxInt := sdkmath.NewInt(math.MaxInt64) evmGenesis.Params.ChainConfig.LondonBlock = &maxInt evmGenesis.Params.ChainConfig.ArrowGlacierBlock = &maxInt evmGenesis.Params.ChainConfig.GrayGlacierBlock = &maxInt evmGenesis.Params.ChainConfig.MergeNetsplitBlock = &maxInt evmGenesis.Params.ChainConfig.ShanghaiBlock = &maxInt evmGenesis.Params.ChainConfig.CancunBlock = &maxInt genesis[types.ModuleName] = app.AppCodec().MustMarshalJSON(evmGenesis) } return genesis }) if suite.mintFeeCollector { // mint some coin to fee collector coins := sdk.NewCoins(sdk.NewCoin(types.DefaultEVMDenom, sdkmath.NewInt(int64(params.TxGas)-1))) genesisState := app.NewTestGenesisState(suite.app.AppCodec()) balances := []banktypes.Balance{ { Address: suite.app.AccountKeeper.GetModuleAddress(authtypes.FeeCollectorName).String(), Coins: coins, }, } var bankGenesis banktypes.GenesisState suite.app.AppCodec().MustUnmarshalJSON(genesisState[banktypes.ModuleName], &bankGenesis) // Update balances and total supply bankGenesis.Balances = append(bankGenesis.Balances, balances...) bankGenesis.Supply = bankGenesis.Supply.Add(coins...) genesisState[banktypes.ModuleName] = suite.app.AppCodec().MustMarshalJSON(&bankGenesis) // we marshal the genesisState of all module to a byte array stateBytes, err := tmjson.MarshalIndent(genesisState, "", " ") require.NoError(t, err) // Initialize the chain suite.app.InitChain( abci.RequestInitChain{ ChainId: "ethermint_9000-1", Validators: []abci.ValidatorUpdate{}, ConsensusParams: app.DefaultConsensusParams, AppStateBytes: stateBytes, }, ) } suite.ctx = suite.app.BaseApp.NewContext(checkTx, tmproto.Header{ Height: 1, ChainID: "ethermint_9000-1", Time: time.Now().UTC(), ProposerAddress: suite.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")), }) queryHelper := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry()) types.RegisterQueryServer(queryHelper, suite.app.EvmKeeper) suite.queryClient = types.NewQueryClient(queryHelper) acc := ðermint.EthAccount{ BaseAccount: authtypes.NewBaseAccount(sdk.AccAddress(suite.address.Bytes()), nil, 0, 0), CodeHash: common.BytesToHash(crypto.Keccak256(nil)).String(), } suite.app.AccountKeeper.SetAccount(suite.ctx, acc) valAddr := sdk.ValAddress(suite.address.Bytes()) validator, err := stakingtypes.NewValidator(valAddr, priv.PubKey(), stakingtypes.Description{}) require.NoError(t, err) err = suite.app.StakingKeeper.SetValidatorByConsAddr(suite.ctx, validator) require.NoError(t, err) err = suite.app.StakingKeeper.SetValidatorByConsAddr(suite.ctx, validator) require.NoError(t, err) suite.app.StakingKeeper.SetValidator(suite.ctx, validator) encodingConfig := encoding.MakeConfig(app.ModuleBasics) suite.clientCtx = client.Context{}.WithTxConfig(encodingConfig.TxConfig) suite.ethSigner = ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID()) suite.appCodec = encodingConfig.Codec suite.denom = evmtypes.DefaultEVMDenom } func (suite *KeeperTestSuite) EvmDenom() string { ctx := sdk.WrapSDKContext(suite.ctx) rsp, _ := suite.queryClient.Params(ctx, &types.QueryParamsRequest{}) return rsp.Params.EvmDenom } // Commit and begin new block func (suite *KeeperTestSuite) Commit() { _ = suite.app.Commit() header := suite.ctx.BlockHeader() header.Height += 1 suite.app.BeginBlock(abci.RequestBeginBlock{ Header: header, }) // update ctx suite.ctx = suite.app.BaseApp.NewContext(false, header) queryHelper := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry()) types.RegisterQueryServer(queryHelper, suite.app.EvmKeeper) suite.queryClient = types.NewQueryClient(queryHelper) } func (suite *KeeperTestSuite) StateDB() *statedb.StateDB { return statedb.New(suite.ctx, suite.app.EvmKeeper, statedb.NewEmptyTxConfig(common.BytesToHash(suite.ctx.HeaderHash().Bytes()))) } // DeployTestContract deploy a test erc20 contract and returns the contract address func (suite *KeeperTestSuite) DeployTestContract(t require.TestingT, owner common.Address, supply *big.Int) common.Address { ctx := sdk.WrapSDKContext(suite.ctx) chainID := suite.app.EvmKeeper.ChainID() ctorArgs, err := types.ERC20Contract.ABI.Pack("", owner, supply) require.NoError(t, err) nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) data := append(types.ERC20Contract.Bin, ctorArgs...) args, err := json.Marshal(&types.TransactionArgs{ From: &suite.address, Data: (*hexutil.Bytes)(&data), }) require.NoError(t, err) res, err := suite.queryClient.EstimateGas(ctx, &types.EthCallRequest{ Args: args, GasCap: uint64(config.DefaultGasCap), ProposerAddress: suite.ctx.BlockHeader().ProposerAddress, }) require.NoError(t, err) var erc20DeployTx *types.MsgEthereumTx if suite.enableFeemarket { erc20DeployTx = types.NewTxContract( chainID, nonce, nil, // amount res.Gas, // gasLimit nil, // gasPrice suite.app.FeeMarketKeeper.GetBaseFee(suite.ctx), big.NewInt(1), data, // input ðtypes.AccessList{}, // accesses ) } else { erc20DeployTx = types.NewTxContract( chainID, nonce, nil, // amount res.Gas, // gasLimit nil, // gasPrice nil, nil, data, // input nil, // accesses ) } erc20DeployTx.From = suite.address.Hex() err = erc20DeployTx.Sign(ethtypes.LatestSignerForChainID(chainID), suite.signer) require.NoError(t, err) rsp, err := suite.app.EvmKeeper.EthereumTx(ctx, erc20DeployTx) require.NoError(t, err) require.Empty(t, rsp.VmError) return crypto.CreateAddress(suite.address, nonce) } func (suite *KeeperTestSuite) TransferERC20Token(t require.TestingT, contractAddr, from, to common.Address, amount *big.Int) *types.MsgEthereumTx { ctx := sdk.WrapSDKContext(suite.ctx) chainID := suite.app.EvmKeeper.ChainID() transferData, err := types.ERC20Contract.ABI.Pack("transfer", to, amount) require.NoError(t, err) args, err := json.Marshal(&types.TransactionArgs{To: &contractAddr, From: &from, Data: (*hexutil.Bytes)(&transferData)}) require.NoError(t, err) res, err := suite.queryClient.EstimateGas(ctx, &types.EthCallRequest{ Args: args, GasCap: 25_000_000, ProposerAddress: suite.ctx.BlockHeader().ProposerAddress, }) require.NoError(t, err) nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) var ercTransferTx *types.MsgEthereumTx if suite.enableFeemarket { ercTransferTx = types.NewTx( chainID, nonce, &contractAddr, nil, res.Gas, nil, suite.app.FeeMarketKeeper.GetBaseFee(suite.ctx), big.NewInt(1), transferData, ðtypes.AccessList{}, // accesses ) } else { ercTransferTx = types.NewTx( chainID, nonce, &contractAddr, nil, res.Gas, nil, nil, nil, transferData, nil, ) } ercTransferTx.From = suite.address.Hex() err = ercTransferTx.Sign(ethtypes.LatestSignerForChainID(chainID), suite.signer) require.NoError(t, err) rsp, err := suite.app.EvmKeeper.EthereumTx(ctx, ercTransferTx) require.NoError(t, err) require.Empty(t, rsp.VmError) return ercTransferTx } // DeployTestMessageCall deploy a test erc20 contract and returns the contract address func (suite *KeeperTestSuite) DeployTestMessageCall(t require.TestingT) common.Address { ctx := sdk.WrapSDKContext(suite.ctx) chainID := suite.app.EvmKeeper.ChainID() data := types.TestMessageCall.Bin args, err := json.Marshal(&types.TransactionArgs{ From: &suite.address, Data: (*hexutil.Bytes)(&data), }) require.NoError(t, err) res, err := suite.queryClient.EstimateGas(ctx, &types.EthCallRequest{ Args: args, GasCap: uint64(config.DefaultGasCap), ProposerAddress: suite.ctx.BlockHeader().ProposerAddress, }) require.NoError(t, err) nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) var erc20DeployTx *types.MsgEthereumTx if suite.enableFeemarket { erc20DeployTx = types.NewTxContract( chainID, nonce, nil, // amount res.Gas, // gasLimit nil, // gasPrice suite.app.FeeMarketKeeper.GetBaseFee(suite.ctx), big.NewInt(1), data, // input ðtypes.AccessList{}, // accesses ) } else { erc20DeployTx = types.NewTxContract( chainID, nonce, nil, // amount res.Gas, // gasLimit nil, // gasPrice nil, nil, data, // input nil, // accesses ) } erc20DeployTx.From = suite.address.Hex() err = erc20DeployTx.Sign(ethtypes.LatestSignerForChainID(chainID), suite.signer) require.NoError(t, err) rsp, err := suite.app.EvmKeeper.EthereumTx(ctx, erc20DeployTx) require.NoError(t, err) require.Empty(t, rsp.VmError) return crypto.CreateAddress(suite.address, nonce) } func (suite *KeeperTestSuite) TestBaseFee() { testCases := []struct { name string enableLondonHF bool enableFeemarket bool expectBaseFee *big.Int }{ {"not enable london HF, not enable feemarket", false, false, nil}, {"enable london HF, not enable feemarket", true, false, big.NewInt(0)}, {"enable london HF, enable feemarket", true, true, big.NewInt(1000000000)}, {"not enable london HF, enable feemarket", false, true, nil}, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.enableFeemarket = tc.enableFeemarket suite.enableLondonHF = tc.enableLondonHF suite.SetupTest() suite.app.EvmKeeper.BeginBlock(suite.ctx, abci.RequestBeginBlock{}) params := suite.app.EvmKeeper.GetParams(suite.ctx) ethCfg := params.ChainConfig.EthereumConfig(suite.app.EvmKeeper.ChainID()) baseFee := suite.app.EvmKeeper.GetBaseFee(suite.ctx, ethCfg) suite.Require().Equal(tc.expectBaseFee, baseFee) }) } suite.enableFeemarket = false suite.enableLondonHF = true } func (suite *KeeperTestSuite) TestGetAccountStorage() { testCases := []struct { name string malleate func() expRes []int }{ { "Only one account that's not a contract (no storage)", func() {}, []int{0}, }, { "Two accounts - one contract (with storage), one wallet", func() { supply := big.NewInt(100) suite.DeployTestContract(suite.T(), suite.address, supply) }, []int{2, 0}, }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() tc.malleate() i := 0 suite.app.AccountKeeper.IterateAccounts(suite.ctx, func(account authtypes.AccountI) bool { ethAccount, ok := account.(ethermint.EthAccountI) if !ok { // ignore non EthAccounts return false } addr := ethAccount.EthAddress() storage := suite.app.EvmKeeper.GetAccountStorage(suite.ctx, addr) suite.Require().Equal(tc.expRes[i], len(storage)) i++ return false }) }) } } func (suite *KeeperTestSuite) TestGetAccountOrEmpty() { empty := statedb.Account{ Balance: new(big.Int), CodeHash: types.EmptyCodeHash, } supply := big.NewInt(100) contractAddr := suite.DeployTestContract(suite.T(), suite.address, supply) testCases := []struct { name string addr common.Address expEmpty bool }{ { "unexisting account - get empty", common.Address{}, true, }, { "existing contract account", contractAddr, false, }, } for _, tc := range testCases { suite.Run(tc.name, func() { res := suite.app.EvmKeeper.GetAccountOrEmpty(suite.ctx, tc.addr) if tc.expEmpty { suite.Require().Equal(empty, res) } else { suite.Require().NotEqual(empty, res) } }) } }