Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(zetaclient)!: Add support for EIP-1559 gas fees #2634

Merged
merged 15 commits into from
Aug 6, 2024
Merged
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

* [2578](https://github.com/zeta-chain/node/pull/2578) - Add Gateway address in protocol contract list
* [2634](https://github.com/zeta-chain/node/pull/2634) - add support for EIP-1559 gas fees

## v19.0.0

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 {
swift1337 marked this conversation as resolved.
Show resolved Hide resolved
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")

Check warning on line 42 in zetaclient/chains/evm/signer/gas.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/evm/signer/gas.go#L39-L42

Added lines #L39 - L42 were not covered by tests
case g.PriorityFee == nil:
return errors.New("priority fee per unit is nil")
swift1337 marked this conversation as resolved.
Show resolved Hide resolved
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(),
)

Check warning on line 50 in zetaclient/chains/evm/signer/gas.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/evm/signer/gas.go#L45-L50

Added lines #L45 - L50 were not covered by tests
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
Loading