-
Notifications
You must be signed in to change notification settings - Fork 108
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(zetaclient)!: Add support for EIP-1559 gas fees (#2634)
* 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
Showing
10 changed files
with
690 additions
and
289 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.