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 @@ -46,6 +46,7 @@
* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envelop parsing
* [2560](https://github.com/zeta-chain/node/pull/2560) - add support for Solana SOL token withdraw
* [2533](https://github.com/zeta-chain/node/pull/2533) - parse memo from both OP_RETURN and inscription
* [2634](https://github.com/zeta-chain/node/pull/2634) - add support for EIP-1559 gas fees
swift1337 marked this conversation as resolved.
Show resolved Hide resolved

### Refactor

Expand Down
114 changes: 114 additions & 0 deletions zetaclient/chains/evm/signer/gas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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.
Price *big.Int

// PriorityFee a fee paid directly to validators.
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")
case g.PriorityFee == nil:
return errors.New("priority fee per unit is nil")

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/evm/signer/gas.go#L38-L43

Added lines #L38 - L43 were not covered by tests
swift1337 marked this conversation as resolved.
Show resolved Hide resolved
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
}

// makeGasFromCCTX creates Gas struct based from CCTX and priorityFee.
func makeGasFromCCTX(cctx *types.CrossChainTx, logger zerolog.Logger) (Gas, error) {
swift1337 marked this conversation as resolved.
Show resolved Hide resolved
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
}
134 changes: 134 additions & 0 deletions zetaclient/chains/evm/signer/gas_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package signer

import (
"math/big"
"testing"

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

func Test_makeGasFromCCTX(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 := makeGasFromCCTX(tt.cctx, logger)
if tt.errorContains != "" {
assert.ErrorContains(t, err, tt.errorContains)
return
}

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

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