Skip to content

Commit

Permalink
feat(zetaclient)!: Add support for EIP-1559 gas fees (#2634)
Browse files Browse the repository at this point in the history
* Add Gas struct

* Add EIP-1559 fees

* Update changelog

* Add test cases for legacy vs dynamicFee txs

* Fix typo; Add E2E coverage

* Address PR comments

* Address PR comments

* Use gasFeeCap formula

* Revert "Use gasFeeCap formula"

This reverts commit 2260925.

* Address PR comments

* Fix e2e upgrade tests
  • Loading branch information
swift1337 authored and gartnera committed Aug 15, 2024
1 parent 5527bf3 commit 0098138
Show file tree
Hide file tree
Showing 10 changed files with 690 additions and 289 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing
* [2533](https://github.com/zeta-chain/node/pull/2533) - parse memo from both OP_RETURN and inscription
* [2597](https://github.com/zeta-chain/node/pull/2597) - Add generic rpc metrics to zetaclient
* [2634](https://github.com/zeta-chain/node/pull/2634) - add support for EIP-1559 gas fees


## v19.0.1
Expand Down
19 changes: 19 additions & 0 deletions e2e/e2etests/test_eth_withdraw.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package e2etests
import (
"math/big"

ethcommon "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/require"

"github.com/zeta-chain/zetacore/e2e/runner"
Expand Down Expand Up @@ -37,5 +39,22 @@ func TestEtherWithdraw(r *runner.E2ERunner, args []string) {

utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined)

// Previous binary doesn't take EIP-1559 into account, so this will fail.
// Thus, we need to skip this check for upgrade tests
if !r.IsRunningUpgrade() {
withdrawalReceipt := mustFetchEthReceipt(r, cctx)
require.Equal(r, uint8(ethtypes.DynamicFeeTxType), withdrawalReceipt.Type, "receipt type mismatch")
}

r.Logger.Info("TestEtherWithdraw completed")
}

func mustFetchEthReceipt(r *runner.E2ERunner, cctx *crosschaintypes.CrossChainTx) *ethtypes.Receipt {
hash := cctx.GetCurrentOutboundParam().Hash
require.NotEmpty(r, hash, "outbound hash is empty")

receipt, err := r.EVMClient.TransactionReceipt(r.Ctx, ethcommon.HexToHash(hash))
require.NoError(r, err)

return receipt
}
12 changes: 12 additions & 0 deletions e2e/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ import (

type E2ERunnerOption func(*E2ERunner)

// Important ENV
const (
EnvKeyLocalnetMode = "LOCALNET_MODE"

LocalnetModeUpgrade = "upgrade"
)

func WithZetaTxServer(txServer *txserver.ZetaTxServer) E2ERunnerOption {
return func(r *E2ERunner) {
r.ZetaTxServer = txServer
Expand Down Expand Up @@ -322,6 +329,11 @@ func (r *E2ERunner) PrintContractAddresses() {
r.Logger.Print("TestDappEVM: %s", r.EvmTestDAppAddr.Hex())
}

// IsRunningUpgrade returns true if the test is running an upgrade test suite.
func (r *E2ERunner) IsRunningUpgrade() bool {
return os.Getenv(EnvKeyLocalnetMode) == LocalnetModeUpgrade
}

// Errorf logs an error message. Mimics the behavior of testing.T.Errorf
func (r *E2ERunner) Errorf(format string, args ...any) {
r.Logger.Error(format, args...)
Expand Down
2 changes: 1 addition & 1 deletion zetaclient/chains/evm/observer/observer_gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func (ob *Observer) supportsPriorityFee(ctx context.Context) (bool, error) {
defer ob.Mu().Unlock()

ob.priorityFeeConfig.checked = true
ob.priorityFeeConfig.checked = isSupported
ob.priorityFeeConfig.supported = isSupported

return isSupported, nil
}
Expand Down
120 changes: 120 additions & 0 deletions zetaclient/chains/evm/signer/gas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package signer

import (
"fmt"
"math/big"

"github.com/pkg/errors"
"github.com/rs/zerolog"

"github.com/zeta-chain/zetacore/x/crosschain/types"
)

const (
minGasLimit = 100_000
maxGasLimit = 1_000_000
)

// Gas represents gas parameters for EVM transactions.
//
// This is pretty interesting because all EVM chains now support EIP-1559, but some chains do it in a specific way
// https://eips.ethereum.org/EIPS/eip-1559
// https://www.blocknative.com/blog/eip-1559-fees
// https://github.com/bnb-chain/BEPs/blob/master/BEPs/BEP226.md (tl;dr: baseFee is always zero)
//
// However, this doesn't affect tx creation nor broadcasting
type Gas struct {
Limit uint64

// This is a "total" gasPrice per 1 unit of gas.
// GasPrice for pre EIP-1559 transactions or maxFeePerGas for EIP-1559.
Price *big.Int

// PriorityFee a fee paid directly to validators for EIP-1559.
PriorityFee *big.Int
}

func (g Gas) validate() error {
switch {
case g.Limit == 0:
return errors.New("gas limit is zero")
case g.Price == nil:
return errors.New("max fee per unit is nil")
case g.PriorityFee == nil:
return errors.New("priority fee per unit is nil")
case g.Price.Cmp(g.PriorityFee) == -1:
return fmt.Errorf(
"max fee per unit (%d) is less than priority fee per unit (%d)",
g.Price.Int64(),
g.PriorityFee.Int64(),
)
default:
return nil
}
}

// isLegacy determines whether the gas is meant for LegacyTx{} (pre EIP-1559)
// or DynamicFeeTx{} (post EIP-1559).
//
// Returns true if priority fee is <= 0.
func (g Gas) isLegacy() bool {
return g.PriorityFee.Sign() < 1
}

func gasFromCCTX(cctx *types.CrossChainTx, logger zerolog.Logger) (Gas, error) {
var (
params = cctx.GetCurrentOutboundParam()
limit = params.GasLimit
)

switch {
case limit < minGasLimit:
limit = minGasLimit
logger.Warn().
Uint64("cctx.initial_gas_limit", params.GasLimit).
Uint64("cctx.gas_limit", limit).
Msgf("Gas limit is too low. Setting to the minimum (%d)", minGasLimit)
case limit > maxGasLimit:
limit = maxGasLimit
logger.Warn().
Uint64("cctx.initial_gas_limit", params.GasLimit).
Uint64("cctx.gas_limit", limit).
Msgf("Gas limit is too high; Setting to the maximum (%d)", maxGasLimit)
}

gasPrice, err := bigIntFromString(params.GasPrice)
if err != nil {
return Gas{}, errors.Wrap(err, "unable to parse gasPrice")
}

priorityFee, err := bigIntFromString(params.GasPriorityFee)
switch {
case err != nil:
return Gas{}, errors.Wrap(err, "unable to parse priorityFee")
case gasPrice.Cmp(priorityFee) == -1:
return Gas{}, fmt.Errorf("gasPrice (%d) is less than priorityFee (%d)", gasPrice.Int64(), priorityFee.Int64())
}

return Gas{
Limit: limit,
Price: gasPrice,
PriorityFee: priorityFee,
}, nil
}

func bigIntFromString(s string) (*big.Int, error) {
if s == "" || s == "0" {
return big.NewInt(0), nil
}

v, ok := new(big.Int).SetString(s, 10)
if !ok {
return nil, fmt.Errorf("unable to parse %q as big.Int", s)
}

if v.Sign() == -1 {
return nil, fmt.Errorf("big.Int is negative: %d", v.Int64())
}

return v, nil
}
144 changes: 144 additions & 0 deletions zetaclient/chains/evm/signer/gas_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package signer

import (
"math/big"
"testing"

"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/zeta-chain/zetacore/x/crosschain/types"
)

func TestGasFromCCTX(t *testing.T) {
logger := zerolog.New(zerolog.NewTestWriter(t))

makeCCTX := func(gasLimit uint64, price, priorityFee string) *types.CrossChainTx {
cctx := getCCTX(t)
cctx.GetOutboundParams()[0].GasLimit = gasLimit
cctx.GetOutboundParams()[0].GasPrice = price
cctx.GetOutboundParams()[0].GasPriorityFee = priorityFee

return cctx
}

for _, tt := range []struct {
name string
cctx *types.CrossChainTx
errorContains string
assert func(t *testing.T, g Gas)
}{
{
name: "legacy: gas is too low",
cctx: makeCCTX(minGasLimit-200, gwei(2).String(), ""),
assert: func(t *testing.T, g Gas) {
assert.True(t, g.isLegacy())
assertGasEquals(t, Gas{
Limit: minGasLimit,
PriorityFee: gwei(0),
Price: gwei(2),
}, g)
},
},
{
name: "london: gas is too low",
cctx: makeCCTX(minGasLimit-200, gwei(2).String(), gwei(1).String()),
assert: func(t *testing.T, g Gas) {
assert.False(t, g.isLegacy())
assertGasEquals(t, Gas{
Limit: minGasLimit,
Price: gwei(2),
PriorityFee: gwei(1),
}, g)
},
},
{
name: "pre London gas logic",
cctx: makeCCTX(minGasLimit+100, gwei(3).String(), ""),
assert: func(t *testing.T, g Gas) {
assert.True(t, g.isLegacy())
assertGasEquals(t, Gas{
Limit: 100_100,
Price: gwei(3),
PriorityFee: gwei(0),
}, g)
},
},
{
name: "post London gas logic",
cctx: makeCCTX(minGasLimit+200, gwei(4).String(), gwei(1).String()),
assert: func(t *testing.T, g Gas) {
assert.False(t, g.isLegacy())
assertGasEquals(t, Gas{
Limit: 100_200,
Price: gwei(4),
PriorityFee: gwei(1),
}, g)
},
},
{
name: "gas is too high, force to the ceiling",
cctx: makeCCTX(maxGasLimit+200, gwei(4).String(), gwei(1).String()),
assert: func(t *testing.T, g Gas) {
assert.False(t, g.isLegacy())
assertGasEquals(t, Gas{
Limit: maxGasLimit,
Price: gwei(4),
PriorityFee: gwei(1),
}, g)
},
},
{
name: "priority fee is invalid",
cctx: makeCCTX(123_000, gwei(4).String(), "oopsie"),
errorContains: "unable to parse priorityFee",
},
{
name: "priority fee is negative",
cctx: makeCCTX(123_000, gwei(4).String(), "-1"),
errorContains: "unable to parse priorityFee: big.Int is negative",
},
{
name: "gasPrice is less than priorityFee",
cctx: makeCCTX(123_000, gwei(4).String(), gwei(5).String()),
errorContains: "gasPrice (4000000000) is less than priorityFee (5000000000)",
},
{
name: "gasPrice is invalid",
cctx: makeCCTX(123_000, "hello", gwei(5).String()),
errorContains: "unable to parse gasPrice",
},
} {
t.Run(tt.name, func(t *testing.T) {
g, err := gasFromCCTX(tt.cctx, logger)
if tt.errorContains != "" {
assert.ErrorContains(t, err, tt.errorContains)
return
}

assert.NoError(t, err)
assert.NoError(t, g.validate())
tt.assert(t, g)
})
}

t.Run("empty priority fee", func(t *testing.T) {
gas := Gas{
Limit: 123_000,
Price: gwei(4),
PriorityFee: nil,
}

assert.Error(t, gas.validate())
})
}

func assertGasEquals(t *testing.T, expected, actual Gas) {
assert.Equal(t, int64(expected.Limit), int64(actual.Limit), "gas limit")
assert.Equal(t, expected.Price.Int64(), actual.Price.Int64(), "max fee per unit")
assert.Equal(t, expected.PriorityFee.Int64(), actual.PriorityFee.Int64(), "priority fee per unit")
}

func gwei(i int64) *big.Int {
const g = 1_000_000_000
return big.NewInt(i * g)
}
Loading

0 comments on commit 0098138

Please sign in to comment.