ethergo/chain/gas/cmp_test.go
package gas_test
import (
"math/big"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/core/types"
. "github.com/stretchr/testify/assert"
"github.com/synapsecns/sanguine/core"
"github.com/synapsecns/sanguine/core/testsuite"
"github.com/synapsecns/sanguine/ethergo/chain/gas"
"gotest.tools/assert"
)
// cmpTestCase is a gas comparison test case.
type cmpTestCase struct {
// x and y are the txes we're comparing
x, y *types.Transaction
// base fee is the base fee for the gas block
baseFee *big.Int
// expectedOutput is the expected output as an index
expectedOutput int
// description is hte description
description string
// nilBlock determines wether a nil block should be used for the test
nilBlock bool
}
// getCmpTestCases gets the test cases we use to check against gas price test.
func getCmpTestCases() []*cmpTestCase {
// this will serve as a fuzz test for legacy txes
gofakeit.Seed(time.Now().UnixNano())
// gasPriceTests test gas price v gas price
// we use a random base fee for this test since it shouldn't matter.
var gasPriceTests = []*cmpTestCase{
{
x: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(10),
}),
y: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(11),
}),
baseFee: big.NewInt(int64(gofakeit.Uint64())),
expectedOutput: -1,
description: "gas price x < gas price y",
},
{
x: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(10),
}),
y: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(11),
}),
baseFee: nil,
expectedOutput: -1,
// TODO: we should use ifacemaker to turn block into an interface application wide
// https://github.com/vburenin/ifacemaker and make sure base fee is not called
description: "nil base fee (will panic if EffectiveGasTipCmp() is used which is we want)",
},
{
x: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(11),
}),
y: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(10),
}),
baseFee: big.NewInt(int64(gofakeit.Uint64())),
expectedOutput: 1,
description: "gas price y < gas price x",
},
{
x: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(10),
}),
y: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(10),
}),
baseFee: big.NewInt(int64(gofakeit.Uint64())),
expectedOutput: 0,
description: "gas price y = gas price x",
},
}
// combinedTests test legacy vs dynamic fee
var combinedTests = []*cmpTestCase{
{
x: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(10),
}),
y: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(9),
GasFeeCap: big.NewInt(10),
}),
baseFee: big.NewInt(0),
expectedOutput: 1,
description: "gas tip cap < gas price (legacy) w/ no base fee",
},
{
x: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(10),
}),
y: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(9),
GasFeeCap: big.NewInt(10),
}),
baseFee: big.NewInt(1),
expectedOutput: 0,
description: "gas tip cap < gas price (legacy) w/ base fee making it equal",
},
{
x: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(10),
}),
y: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(9),
GasFeeCap: big.NewInt(11),
}),
baseFee: big.NewInt(2),
expectedOutput: -1,
description: "gas tip cap < gas price (legacy) w/ base fee making less",
},
}
// dynamicTests test dynamicFee vs dynamicFee
var dynamicTests = []*cmpTestCase{
{
x: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(9),
GasFeeCap: big.NewInt(10),
}),
y: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(9),
GasFeeCap: big.NewInt(10),
}),
baseFee: big.NewInt(1),
expectedOutput: 0,
description: "gas tip cap = gas tip cap",
},
{
x: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(2),
GasFeeCap: big.NewInt(9),
}),
y: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(1),
GasFeeCap: big.NewInt(10),
}),
baseFee: big.NewInt(2),
expectedOutput: 1,
description: "gas tip cap x > gas tip y cap and fee cap x < fee cap y w/ base fee pushing it over edge",
},
{
x: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(9),
GasFeeCap: big.NewInt(10),
}),
y: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(9),
GasFeeCap: big.NewInt(10),
}),
nilBlock: true,
expectedOutput: 0,
description: "gas tip cap = gas tip cap and base fee is empty",
},
{
x: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(10),
GasFeeCap: big.NewInt(11),
}),
y: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(9),
GasFeeCap: big.NewInt(12),
}),
nilBlock: true,
expectedOutput: 1,
description: "gas tip cap > gas tip cap and base fee is empty",
},
}
return append(append(gasPriceTests, combinedTests...), dynamicTests...)
}
// TestCompareGas tests the gas comparison method.
func (s GasSuite) TestCompareGas() {
s.T().Parallel()
testCases := getCmpTestCases()
for _, tc := range testCases {
tc := tc // capture range variable
s.T().Run(tc.description, func(t *testing.T) {
t.Parallel()
var gasBlock *types.Block
if !tc.nilBlock {
// test the straight forward txOpts
gasBlock = types.NewBlock(&types.Header{
BaseFee: tc.baseFee,
}, nil, nil, nil, nil)
}
var out int
if gasBlock != nil {
out = compareGas(tc.x, tc.y, gasBlock)
} else {
out = compareGas(tc.x, tc.y, nil)
}
Equal(t, tc.expectedOutput, out)
// test the reverse as a sanity check. Multiplying by engative 1 should be the opposite
// except in cases where
reverseOut := compareGas(tc.y, tc.x, gasBlock)
Equal(t, tc.expectedOutput*-1, reverseOut)
optsOut := compareGas(gas.OptsToComparableTx(s.toTransactOpts(tc.x)), gas.OptsToComparableTx(s.toTransactOpts(tc.y)), gasBlock)
Equal(t, tc.expectedOutput, optsOut)
})
}
}
func compareGas(x *types.Transaction, y *types.Transaction, gasBlock *types.Block) (out int) {
if gasBlock != nil {
out = gas.CompareGas(x, y, gasBlock.BaseFee())
} else {
out = gas.CompareGas(x, y, nil)
}
return out
}
func (s GasSuite) toTransactOpts(rawTx *types.Transaction) *bind.TransactOpts {
switch rawTx.Type() {
case types.LegacyTxType:
return &bind.TransactOpts{
GasPrice: rawTx.GasPrice(),
GasFeeCap: nil,
GasTipCap: nil,
}
case types.DynamicFeeTxType:
return &bind.TransactOpts{
GasFeeCap: rawTx.GasFeeCap(),
GasTipCap: rawTx.GasTipCap(),
}
}
s.T().Errorf("unknown txOpts type %d", rawTx.Type())
return nil
}
// gtTestCase is a test case for testing if a txes gas fee is greater than threshold.
type gtTestCase struct {
// txOpts is the transaction we're testing against
tx *types.Transaction
// threshold is the threshold to pass into the test
threshold *big.Int
// expectedOutput is the expected output of the test case
expectedOutput bool
// description is a description of the test
description string
}
func getGasGtTestCases() []*gtTestCase {
var legacyTxCases = []*gtTestCase{
{
tx: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(500),
}),
threshold: big.NewInt(499),
expectedOutput: true,
description: "legacy txOpts greater then",
},
{
tx: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(499),
}),
threshold: big.NewInt(500),
expectedOutput: false,
description: "legacy txOpts less then",
},
{
tx: types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(500),
}),
threshold: big.NewInt(500),
expectedOutput: false,
description: "legacy txOpts equal",
},
}
var dynamicTxCases = []*gtTestCase{
{
tx: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(499),
GasFeeCap: big.NewInt(500),
}),
threshold: big.NewInt(499),
expectedOutput: true,
description: "legacy txOpts greater then",
},
{
tx: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(498),
GasFeeCap: big.NewInt(499),
}),
threshold: big.NewInt(500),
expectedOutput: false,
description: "legacy txOpts less then",
},
{
tx: types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(499),
GasFeeCap: big.NewInt(500),
}),
threshold: big.NewInt(500),
expectedOutput: false,
description: "legacy txOpts equal",
},
}
return append(legacyTxCases, dynamicTxCases...)
}
// TestGasGT tests the gas greater then method.
func (s GasSuite) TestGasGt() {
s.T().Parallel()
testCases := getGasGtTestCases()
for _, tc := range testCases {
tc := tc // capture range variable
s.T().Run(tc.description, func(t *testing.T) {
t.Parallel()
out := gas.FeeGreaterThan(tc.tx, tc.threshold)
Equal(t, tc.expectedOutput, out)
optOut := gas.FeeGreaterThan(gas.OptsToComparableTx(s.toTransactOpts(tc.tx)), tc.threshold)
Equal(t, tc.expectedOutput, optOut)
})
}
}
// bumpTestCase tests gas price bumping.
type bumpTestCase struct {
// base fee is the base fee for the gas block
baseFee *big.Int
// txOpts to try to bump
txOpts *bind.TransactOpts
// precentIncrease is the percent we should try bumping by
percentIncrease int
// expectedGasPrice. Only checked on a legacy txOpts
expectedGasPrice *big.Int
// expectedGasTipCap is the expected gas tip cap. Only checked on dynamic txes
expectedGasTipCap *big.Int
// expectedGasFeeCap is the expected fee cap
expectedGasFeeCap *big.Int
// description is the description of the test case
description string
}
// checkExpected checks the expected output against the transaction opts.
// this works because *bind.TransactOpts is passed by points.
func (b *bumpTestCase) checkExpected(tb testing.TB) {
tb.Helper()
if gas.IsDynamicTx(b.txOpts) {
Equal(tb, b.txOpts.GasFeeCap.Cmp(b.expectedGasFeeCap), 0)
Equal(tb, b.txOpts.GasTipCap.Cmp(b.expectedGasTipCap), 0)
} else {
Equal(tb, b.txOpts.GasPrice.Cmp(b.expectedGasPrice), 0)
}
}
// getBumpTestCases gets test cases for bumping.
func getBumpTestCases() []*bumpTestCase {
// this will serve as a fuzz test for legacy txes
gofakeit.Seed(time.Now().UnixNano())
testCfg := gas.GetConfig()
var legacyTxTestCases = []*bumpTestCase{
{
txOpts: &bind.TransactOpts{
GasPrice: big.NewInt(100),
},
percentIncrease: 10,
baseFee: big.NewInt(int64(gofakeit.Uint64())),
expectedGasPrice: big.NewInt(110),
description: "bumps gas price by 10%",
},
{
txOpts: &bind.TransactOpts{
GasPrice: core.CopyBigInt(testCfg.MaxPrice),
},
baseFee: big.NewInt(int64(gofakeit.Uint64())),
percentIncrease: 10,
expectedGasPrice: core.CopyBigInt(testCfg.MaxPrice),
description: "gas price does not exceed max price",
},
}
var dynamicTxTestCases = []*bumpTestCase{
{
txOpts: &bind.TransactOpts{
GasTipCap: big.NewInt(10),
GasFeeCap: big.NewInt(100),
},
baseFee: big.NewInt(1),
percentIncrease: 10,
expectedGasTipCap: big.NewInt(11),
expectedGasFeeCap: big.NewInt(110),
description: "bump tip by 10%",
},
{
txOpts: &bind.TransactOpts{
GasTipCap: big.NewInt(10),
GasFeeCap: core.CopyBigInt(testCfg.MaxPrice),
},
baseFee: big.NewInt(1),
percentIncrease: 10,
expectedGasTipCap: big.NewInt(11),
expectedGasFeeCap: core.CopyBigInt(testCfg.MaxPrice),
description: "bump only tip cap (fee cap exceeds max price)",
},
{
txOpts: &bind.TransactOpts{
GasTipCap: big.NewInt(2),
GasFeeCap: core.CopyBigInt(testCfg.MaxPrice),
},
baseFee: big.NewInt(0).Sub(core.CopyBigInt(testCfg.MaxPrice), big.NewInt(2)),
percentIncrease: 10,
expectedGasTipCap: big.NewInt(2),
expectedGasFeeCap: core.CopyBigInt(testCfg.MaxPrice),
description: "bump nothing (fee cap exceeds max price + base fee, but gas tip alone does not exceed fee cap)",
},
{
txOpts: &bind.TransactOpts{
GasTipCap: core.CopyBigInt(testCfg.MaxPrice),
GasFeeCap: core.CopyBigInt(testCfg.MaxPrice),
},
baseFee: big.NewInt(1),
percentIncrease: 10,
expectedGasTipCap: core.CopyBigInt(testCfg.MaxPrice),
expectedGasFeeCap: core.CopyBigInt(testCfg.MaxPrice),
description: "bump nothing (tip cap exceeds max price)",
},
}
return append(legacyTxTestCases, dynamicTxTestCases...)
}
func (s GasSuite) TestBumpGasPrice() {
s.T().Parallel()
testCases := getBumpTestCases()
for _, tc := range testCases {
tc := tc // capture range variable
s.T().Run(tc.description, func(t *testing.T) {
t.Parallel()
gasBlock := types.NewBlock(&types.Header{
BaseFee: tc.baseFee,
}, nil, nil, nil, nil)
gas.BumpGasFees(tc.txOpts, tc.percentIncrease, gasBlock.BaseFee(), gas.GetConfig().MaxPrice)
tc.checkExpected(s.T())
})
}
}
// TestMin makes sure the correct value is returned from the min comparer.
func (s GasSuite) TestMin() {
a := big.NewInt(0)
b := big.NewInt(1)
assert.DeepEqual(s.T(), a, gas.Min(a, b), testsuite.BigIntComparer())
assert.DeepEqual(s.T(), a, gas.Min(b, a), testsuite.BigIntComparer())
}