diff --git a/app/app.go b/app/app.go index ec29bfbf..efa3e379 100644 --- a/app/app.go +++ b/app/app.go @@ -167,11 +167,13 @@ var ( govtypes.ModuleName: {authtypes.Burner}, ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, evmtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, // used for secure addition and subtraction of balance using module account + evmtypes.FeeBurner: {authtypes.Burner}, // Escrows and burns EVM gas fees } // module accounts that are allowed to receive tokens allowedReceivingModAcc = map[string]bool{ distrtypes.ModuleName: true, + evmtypes.FeeBurner: true, } ) diff --git a/tests/e2e/integration_test.go b/tests/e2e/integration_test.go index 3b36acc9..bcf675e9 100644 --- a/tests/e2e/integration_test.go +++ b/tests/e2e/integration_test.go @@ -827,6 +827,9 @@ func (s *IntegrationTestSuite) TestBatchETHTransactions() { syncCtx := s.network.Validators[0].ClientCtx.WithBroadcastMode(flags.BroadcastBlock) txResponse, err := syncCtx.BroadcastTx(txBytes) + if err != nil || txResponse.Code != 0 { + fmt.Println("failed transaction: ", txResponse, " err: ", err) + } s.Require().NoError(err) s.Require().Equal(uint32(0), txResponse.Code) diff --git a/x/evm/handler_test.go b/x/evm/handler_test.go index 733b1708..a81f10a1 100644 --- a/x/evm/handler_test.go +++ b/x/evm/handler_test.go @@ -61,7 +61,7 @@ type EvmTestSuite struct { dynamicTxFee bool } -/// DoSetupTest setup test environment, it uses`require.TestingT` to support both `testing.T` and `testing.B`. +// / DoSetupTest setup test environment, it uses`require.TestingT` to support both `testing.T` and `testing.B`. func (suite *EvmTestSuite) DoSetupTest(t require.TestingT) { checkTx := false @@ -184,6 +184,7 @@ func TestEvmTestSuite(t *testing.T) { func (suite *EvmTestSuite) TestHandleMsgEthereumTx() { var tx *types.MsgEthereumTx + var gasLimit, gasPrice uint64 testCases := []struct { msg string @@ -194,7 +195,9 @@ func (suite *EvmTestSuite) TestHandleMsgEthereumTx() { "passed", func() { to := common.BytesToAddress(suite.to) - tx = types.NewTx(suite.chainID, 0, &to, big.NewInt(100), 10_000_000, big.NewInt(10000), nil, nil, nil, nil) + gasLimit = 10_000_000 + gasPrice = 10000 + tx = types.NewTx(suite.chainID, 0, &to, big.NewInt(100), gasLimit, big.NewInt(int64(gasPrice)), nil, nil, nil, nil) suite.SignTx(tx) }, true, @@ -202,7 +205,9 @@ func (suite *EvmTestSuite) TestHandleMsgEthereumTx() { { "insufficient balance", func() { - tx = types.NewTxContract(suite.chainID, 0, big.NewInt(100), 0, big.NewInt(10000), nil, nil, nil, nil) + gasLimit = 0 + gasPrice = 10000 + tx = types.NewTxContract(suite.chainID, 0, big.NewInt(100), gasLimit, big.NewInt(int64(gasPrice)), nil, nil, nil, nil) suite.SignTx(tx) }, false, @@ -210,7 +215,9 @@ func (suite *EvmTestSuite) TestHandleMsgEthereumTx() { { "tx encoding failed", func() { - tx = types.NewTxContract(suite.chainID, 0, big.NewInt(100), 0, big.NewInt(10000), nil, nil, nil, nil) + gasLimit = 0 + gasPrice = 10000 + tx = types.NewTxContract(suite.chainID, 0, big.NewInt(100), gasLimit, big.NewInt(int64(gasPrice)), nil, nil, nil, nil) }, false, }, @@ -224,7 +231,9 @@ func (suite *EvmTestSuite) TestHandleMsgEthereumTx() { { "VerifySig failed", func() { - tx = types.NewTxContract(suite.chainID, 0, big.NewInt(100), 0, big.NewInt(10000), nil, nil, nil, nil) + gasLimit = 0 + gasPrice = 10000 + tx = types.NewTxContract(suite.chainID, 0, big.NewInt(100), gasLimit, big.NewInt(int64(gasPrice)), nil, nil, nil, nil) }, false, }, @@ -235,6 +244,9 @@ func (suite *EvmTestSuite) TestHandleMsgEthereumTx() { suite.SetupTest() // reset //nolint tc.malleate() + gasFee := big.NewInt(0).Mul(big.NewInt(int64(gasLimit)), big.NewInt(int64(gasPrice))) + suite.FundModuleAccount(types.FeeBurner, gasFee) + res, err := suite.handler(suite.ctx, tx) //nolint @@ -271,11 +283,14 @@ func (suite *EvmTestSuite) TestHandlerLogs() { gasLimit := uint64(100000) gasPrice := big.NewInt(1000000) + gasFee := big.NewInt(0).Mul(gasPrice, big.NewInt(int64(gasLimit))) bytecode := common.FromHex("0x6080604052348015600f57600080fd5b5060117f775a94827b8fd9b519d36cd827093c664f93347070a554f65e4a6f56cd73889860405160405180910390a2603580604b6000396000f3fe6080604052600080fdfea165627a7a723058206cab665f0f557620554bb45adf266708d2bd349b8a4314bdff205ee8440e3c240029") tx := types.NewTx(suite.chainID, 1, nil, big.NewInt(0), gasLimit, gasPrice, nil, nil, bytecode, nil) suite.SignTx(tx) + suite.FundModuleAccount(types.FeeBurner, gasFee) + result, err := suite.handler(suite.ctx, tx) suite.Require().NoError(err, "failed to handle eth tx msg") @@ -288,6 +303,12 @@ func (suite *EvmTestSuite) TestHandlerLogs() { suite.Require().Equal(len(txResponse.Logs[0].Topics), 2) } +func (suite *EvmTestSuite) FundModuleAccount(mod string, amount *big.Int) { + coins := sdk.NewCoins(sdk.NewCoin(types.DefaultEVMDenom, sdk.NewIntFromBigInt(amount))) + suite.Require().NoError(suite.app.BankKeeper.MintCoins(suite.ctx, types.ModuleName, coins)) + suite.Require().NoError(suite.app.BankKeeper.SendCoinsFromModuleToModule(suite.ctx, types.ModuleName, types.FeeBurner, coins)) +} + func (suite *EvmTestSuite) TestDeployAndCallContract() { // Test contract: //http://remix.ethereum.org/#optimize=false&evmVersion=istanbul&version=soljson-v0.5.15+commit.6a57276f.js @@ -346,6 +367,9 @@ func (suite *EvmTestSuite) TestDeployAndCallContract() { // Deploy contract - Owner.sol gasLimit := uint64(100000000) gasPrice := big.NewInt(10000) + gasFee := big.NewInt(0).Mul(big.NewInt(int64(gasLimit)), gasPrice) + + suite.FundModuleAccount(types.FeeBurner, gasFee) bytecode := common.FromHex("0x608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a73560405160405180910390a36102c4806100dc6000396000f3fe608060405234801561001057600080fd5b5060043610610053576000357c010000000000000000000000000000000000000000000000000000000090048063893d20e814610058578063a6f9dae1146100a2575b600080fd5b6100606100e6565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100e4600480360360208110156100b857600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919050505061010f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146101d1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260138152602001807f43616c6c6572206973206e6f74206f776e65720000000000000000000000000081525060200191505060405180910390fd5b8073ffffffffffffffffffffffffffffffffffffffff166000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a73560405160405180910390a3806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505056fea265627a7a72315820f397f2733a89198bc7fed0764083694c5b828791f39ebcbc9e414bccef14b48064736f6c63430005100032") tx := types.NewTx(suite.chainID, 1, nil, big.NewInt(0), gasLimit, gasPrice, nil, nil, bytecode, nil) @@ -363,6 +387,9 @@ func (suite *EvmTestSuite) TestDeployAndCallContract() { // store - changeOwner gasLimit = uint64(100000000000) gasPrice = big.NewInt(100) + gasFee = big.NewInt(0).Mul(big.NewInt(int64(gasLimit)), gasPrice) + suite.FundModuleAccount(types.FeeBurner, gasFee) + receiver := crypto.CreateAddress(suite.from, 1) storeAddr := "0xa6f9dae10000000000000000000000006a82e4a67715c8412a9114fbd2cbaefbc8181424" @@ -381,6 +408,7 @@ func (suite *EvmTestSuite) TestDeployAndCallContract() { bytecode = common.FromHex("0x893d20e8") tx = types.NewTx(suite.chainID, 2, &receiver, big.NewInt(0), gasLimit, gasPrice, nil, nil, bytecode, nil) suite.SignTx(tx) + suite.FundModuleAccount(types.FeeBurner, gasFee) _, err = suite.handler(suite.ctx, tx) suite.Require().NoError(err, "failed to handle eth tx msg") diff --git a/x/evm/keeper/abci.go b/x/evm/keeper/abci.go index e619b626..b53ded8d 100644 --- a/x/evm/keeper/abci.go +++ b/x/evm/keeper/abci.go @@ -17,6 +17,7 @@ func (k *Keeper) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { // KVStore. The EVM end block logic doesn't update the validator set, thus it returns // an empty slice. func (k *Keeper) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.ValidatorUpdate { + k.BurnConsumedGas(ctx) // Gas costs are handled within msg handler so costs should be ignored infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) diff --git a/x/evm/keeper/keeper_test.go b/x/evm/keeper/keeper_test.go index c88bb050..c391e58f 100644 --- a/x/evm/keeper/keeper_test.go +++ b/x/evm/keeper/keeper_test.go @@ -67,10 +67,10 @@ type KeeperTestSuite struct { appCodec codec.Codec signer keyring.Signer - enableFeemarket bool - enableLondonHF bool - mintFeeCollector bool - denom string + enableFeemarket bool + enableLondonHF bool + mintFeeBurner bool + denom string } var s *KeeperTestSuite @@ -92,7 +92,7 @@ func (suite *KeeperTestSuite) SetupTest() { suite.SetupApp(checkTx) } -/// SetupApp setup test environment, it uses`require.TestingT` to support both `testing.T` and `testing.B`. +// / 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. @@ -130,13 +130,13 @@ func (suite *KeeperTestSuite) SetupApp(checkTx bool) { return genesis }) - if suite.mintFeeCollector { - // mint some coin to fee collector + if suite.mintFeeBurner { + // mint some coin to fee burner escrow account coins := sdk.NewCoins(sdk.NewCoin(types.DefaultEVMDenom, sdk.NewInt(int64(params.TxGas)-1))) genesisState := app.ModuleBasics.DefaultGenesis(suite.app.AppCodec()) balances := []banktypes.Balance{ { - Address: suite.app.AccountKeeper.GetModuleAddress(authtypes.FeeCollectorName).String(), + Address: types.FeeBurnerAccount.String(), Coins: coins, }, } diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go index ec79aefc..524e8110 100644 --- a/x/evm/keeper/state_transition.go +++ b/x/evm/keeper/state_transition.go @@ -8,7 +8,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ethermint "github.com/evmos/ethermint/types" @@ -280,9 +279,10 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*t } } - // refund gas in order to match the Ethereum gas consumption instead of the default SDK one. - if err = k.RefundGas(ctx, msg, msg.Gas()-res.GasUsed, cfg.Params.EvmDenom); err != nil { - return nil, sdkerrors.Wrapf(err, "failed to refund gas leftover gas to sender %s", msg.From()) + // refund gas in order to match the Ethereum gas consumption instead of the default SDK one, then burn + // the collected amount + if err = k.RefundExcessGas(ctx, msg, msg.Gas()-res.GasUsed, cfg.Params.EvmDenom); err != nil { + return nil, sdkerrors.Wrapf(err, "failed to refund excess gas to sender %s or burn ", msg.From()) } if len(receipt.Logs) > 0 { @@ -470,13 +470,15 @@ func (k *Keeper) GetEthIntrinsicGas(ctx sdk.Context, msg core.Message, cfg *para return core.IntrinsicGas(msg.Data(), msg.AccessList(), isContractCreation, homestead, istanbul) } -// RefundGas transfers the leftover gas to the sender of the message, caped to half of the total gas -// consumed in the transaction. Additionally, the function sets the total gas consumed to the value +// RefundExcessGas transfers the leftover gas to the sender of the message +// Additionally, the function sets the total gas consumed to the value // returned by the EVM execution, thus ignoring the previous intrinsic gas consumed during in the // AnteHandler. -func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64, denom string) error { +// The remaining gas will be burnt in the EndBlocker +func (k *Keeper) RefundExcessGas(ctx sdk.Context, msg core.Message, leftoverGas uint64, denom string) error { // Return EVM tokens for remaining gas, exchanged at the original rate. remaining := new(big.Int).Mul(new(big.Int).SetUint64(leftoverGas), msg.GasPrice()) + gasAccountBalance := k.bankKeeper.GetBalance(ctx, types.FeeBurnerAccount, denom) switch remaining.Sign() { case -1: @@ -484,14 +486,16 @@ func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64 return sdkerrors.Wrapf(types.ErrInvalidRefund, "refunded amount value cannot be negative %d", remaining.Int64()) case 1: // positive amount refund - refundedCoins := sdk.Coins{sdk.NewCoin(denom, sdk.NewIntFromBigInt(remaining))} + refundCoin := sdk.NewCoin(denom, sdk.NewIntFromBigInt(remaining)) + if gasAccountBalance.IsLT(refundCoin) { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "fee burner account has insufficient funds (%s) to refund %s", gasAccountBalance.String(), refundCoin.String()) + } - // refund to sender from the fee collector module account, which is the escrow account in charge of collecting tx fees + // refund to sender from the fee burner account, which is the escrow account in charge of collecting and burning EVM tx fees - err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, authtypes.FeeCollectorName, msg.From().Bytes(), refundedCoins) + err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.FeeBurner, msg.From().Bytes(), sdk.NewCoins(refundCoin)) if err != nil { - err = sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "fee collector account failed to refund fees: %s", err.Error()) - return sdkerrors.Wrapf(err, "failed to refund %d leftover gas (%s)", leftoverGas, refundedCoins.String()) + return sdkerrors.Wrapf(err, "failed to refund %d leftover gas (%s)", leftoverGas, refundCoin.String()) } default: // no refund, consume gas and update the tx gas meter @@ -500,6 +504,15 @@ func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64 return nil } +func (k *Keeper) BurnConsumedGas(ctx sdk.Context) error { + denom := k.GetParams(ctx).EvmDenom + gasAccountBalance := k.bankKeeper.GetBalance(ctx, types.FeeBurnerAccount, denom) + if err := k.bankKeeper.BurnCoins(ctx, types.FeeBurner, sdk.NewCoins(gasAccountBalance)); err != nil { + return sdkerrors.Wrap(err, "failed to burn FeeBurner account balance") + } + return nil +} + // ResetGasMeterAndConsumeGas reset first the gas meter consumed value to zero and set it back to the new value // 'gasUsed' func (k *Keeper) ResetGasMeterAndConsumeGas(ctx sdk.Context, gasUsed uint64) { diff --git a/x/evm/keeper/state_transition_test.go b/x/evm/keeper/state_transition_test.go index 914db7d9..854df756 100644 --- a/x/evm/keeper/state_transition_test.go +++ b/x/evm/keeper/state_transition_test.go @@ -326,7 +326,7 @@ func (suite *KeeperTestSuite) TestGasToRefund() { for _, tc := range testCases { suite.Run(fmt.Sprintf("Case %s", tc.name), func() { - suite.mintFeeCollector = true + suite.mintFeeBurner = true suite.SetupTest() // reset vmdb := suite.StateDB() vmdb.AddRefund(10) @@ -342,7 +342,7 @@ func (suite *KeeperTestSuite) TestGasToRefund() { } }) } - suite.mintFeeCollector = false + suite.mintFeeBurner = false } func (suite *KeeperTestSuite) TestRefundGas() { @@ -385,7 +385,7 @@ func (suite *KeeperTestSuite) TestRefundGas() { for _, tc := range testCases { suite.Run(fmt.Sprintf("Case %s", tc.name), func() { - suite.mintFeeCollector = true + suite.mintFeeBurner = true suite.SetupTest() // reset keeperParams := suite.app.EvmKeeper.GetParams(suite.ctx) @@ -415,7 +415,7 @@ func (suite *KeeperTestSuite) TestRefundGas() { refund := keeper.GasToRefund(vmdb.GetRefund(), gasUsed, tc.refundQuotient) suite.Require().Equal(tc.expGasRefund, refund) - err = suite.app.EvmKeeper.RefundGas(suite.ctx, m, refund, "aphoton") + err = suite.app.EvmKeeper.RefundExcessGas(suite.ctx, m, refund, "aphoton") if tc.noError { suite.Require().NoError(err) } else { @@ -423,7 +423,7 @@ func (suite *KeeperTestSuite) TestRefundGas() { } }) } - suite.mintFeeCollector = false + suite.mintFeeBurner = false } func (suite *KeeperTestSuite) TestResetGasMeterAndConsumeGas() { diff --git a/x/evm/keeper/utils.go b/x/evm/keeper/utils.go index d497382c..e3569947 100644 --- a/x/evm/keeper/utils.go +++ b/x/evm/keeper/utils.go @@ -74,16 +74,31 @@ func (k Keeper) DeductTxCostsFromUserBalance( fees := sdk.Coins{sdk.NewCoin(denom, sdk.NewIntFromBigInt(feeAmt))} // deduct the full gas cost from the user balance - if err := authante.DeductFees(k.bankKeeper, ctx, signerAcc, fees); err != nil { + if err := k.EscrowGasFee(ctx, signerAcc.GetAddress(), fees); err != nil { return nil, sdkerrors.Wrapf( err, - "failed to deduct full gas cost %s from the user %s balance", + "failed to escrow full gas cost %s from user %s 's balance", fees, msgEthTx.From, ) } return fees, nil } +// EscrowGasFee collects the `fees` from `acc` and custodies the coins in the EVM FeeBurner account +// Note: `fees` should be the maximum gas fee paid by the user, excess will be refunded later +func (k Keeper) EscrowGasFee(ctx sdk.Context, acc sdk.AccAddress, fees sdk.Coins) error { + if !fees.IsValid() { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees) + } + + err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, acc, evmtypes.FeeBurner, fees) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, err.Error()) + } + + return nil +} + // CheckSenderBalance validates that the tx cost value is positive and that the // sender has enough funds to pay for the fees and value of the transaction. func CheckSenderBalance( diff --git a/x/evm/types/key.go b/x/evm/types/key.go index ade14eb2..812b3348 100644 --- a/x/evm/types/key.go +++ b/x/evm/types/key.go @@ -2,6 +2,8 @@ package types import ( "github.com/ethereum/go-ethereum/common" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) const ( @@ -19,8 +21,12 @@ const ( // RouterKey uses module name for routing RouterKey = ModuleName + + FeeBurner = "evm_fee_burner" ) +var FeeBurnerAccount = authtypes.NewModuleAddress(FeeBurner) + // prefix bytes for the EVM persistent store const ( prefixCode = iota + 1