Skip to content

Commit

Permalink
Burn EVM transaction fees
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristianBorst committed Apr 18, 2024
1 parent 6e467ea commit b4a9241
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 32 deletions.
2 changes: 2 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
)

Expand Down
3 changes: 3 additions & 0 deletions tests/e2e/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
38 changes: 33 additions & 5 deletions x/evm/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -194,23 +195,29 @@ 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,
},
{
"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,
},
{
"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,
},
Expand All @@ -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,
},
Expand All @@ -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
Expand Down Expand Up @@ -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")

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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"
Expand All @@ -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")
Expand Down
1 change: 1 addition & 0 deletions x/evm/keeper/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
16 changes: 8 additions & 8 deletions x/evm/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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,
},
}
Expand Down
37 changes: 25 additions & 12 deletions x/evm/keeper/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -470,28 +470,32 @@ 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:
// negative refund errors
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
Expand All @@ -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) {
Expand Down
10 changes: 5 additions & 5 deletions x/evm/keeper/state_transition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -342,7 +342,7 @@ func (suite *KeeperTestSuite) TestGasToRefund() {
}
})
}
suite.mintFeeCollector = false
suite.mintFeeBurner = false
}

func (suite *KeeperTestSuite) TestRefundGas() {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -415,15 +415,15 @@ 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 {
suite.Require().Error(err)
}
})
}
suite.mintFeeCollector = false
suite.mintFeeBurner = false
}

func (suite *KeeperTestSuite) TestResetGasMeterAndConsumeGas() {
Expand Down
19 changes: 17 additions & 2 deletions x/evm/keeper/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions x/evm/types/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package types

import (
"github.com/ethereum/go-ethereum/common"

authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)

const (
Expand All @@ -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
Expand Down

0 comments on commit b4a9241

Please sign in to comment.