Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions relayer/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,95 @@
require.True(t, expectedSubnets.Equals(cfg.GetTrackedSubnets()))
}

// TestMaxFeePerGasValidation tests the MaxFeePerGas validation logic
func TestMaxFeePerGasValidation(t *testing.T) {
testCases := []struct {
name string
maxFeePerGas uint64
maxBaseFee uint64
maxPriorityFeePerGas uint64
expectError bool
errorContains string
}{
{
name: "MaxFeePerGas not set - should pass",
maxFeePerGas: 0,
maxBaseFee: 50000000000,
maxPriorityFeePerGas: 2500000000,
expectError: false,
},
{
name: "Valid MaxFeePerGas - should pass",
maxFeePerGas: 60000000000,
maxBaseFee: 50000000000,
maxPriorityFeePerGas: 2500000000,
expectError: false,
},
{
name: "MaxFeePerGas equals sum - should pass",
maxFeePerGas: 52500000000,
maxBaseFee: 50000000000,
maxPriorityFeePerGas: 2500000000,
expectError: false,
},
{
name: "MaxFeePerGas less than sum - should fail",
maxFeePerGas: 50000000000,
maxBaseFee: 50000000000,
maxPriorityFeePerGas: 2500000000,
expectError: true,
errorContains: "max-fee-per-gas (50000000000) must be at least max-base-fee (50000000000) + max-priority-fee-per-gas (2500000000) = 52500000000",

Check failure on line 783 in relayer/config/config_test.go

View workflow job for this annotation

GitHub Actions / golangci

The line is 155 characters long, which exceeds the maximum of 120 characters. (lll)
},
{
name: "MaxFeePerGas less than priority fee - should fail",
maxFeePerGas: 1000000000,
maxBaseFee: 0, // Not set
maxPriorityFeePerGas: 2500000000,
expectError: true,
errorContains: "max-fee-per-gas (1000000000) must be at least max-priority-fee-per-gas (2500000000)",
},
{
name: "MaxFeePerGas with only priority fee set - should pass",
maxFeePerGas: 5000000000,
maxBaseFee: 0, // Not set
maxPriorityFeePerGas: 2500000000,
expectError: false,
},
{
name: "Edge case: MaxFeePerGas equals priority fee - should pass",
maxFeePerGas: 2500000000,
maxBaseFee: 0, // Not set
maxPriorityFeePerGas: 2500000000,
expectError: false,
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
destBlockchain := &DestinationBlockchain{
SubnetID: "2TGBXcnwx5PqiXWiqxAKUaNSqDguXNh1mxnp82jui68hxJSZAx",
BlockchainID: "S4mMqUXe7vHsGiRAma6bv3CKnyaLssyAxmQ2KvFpX1KEvfFCD",
VM: "evm",
RPCEndpoint: basecfg.APIConfig{
BaseURL: "https://subnets.avax.network/mysubnet/rpc",
},
AccountPrivateKeys: []string{"56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027"},
MaxFeePerGas: tt.maxFeePerGas,
MaxBaseFee: tt.maxBaseFee,
MaxPriorityFeePerGas: tt.maxPriorityFeePerGas,
}

err := destBlockchain.Validate()
if tt.expectError {
require.Error(t, err)
require.Contains(t, err.Error(), tt.errorContains)
} else {
require.NoError(t, err)
}
})
}
}

func TestConfigSanitization(t *testing.T) {
testCases := []struct {
name string
Expand Down
16 changes: 16 additions & 0 deletions relayer/config/destination_blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type DestinationBlockchain struct {
BlockGasLimit uint64 `mapstructure:"block-gas-limit" json:"block-gas-limit"`
MaxBaseFee uint64 `mapstructure:"max-base-fee" json:"max-base-fee"`
MaxPriorityFeePerGas uint64 `mapstructure:"max-priority-fee-per-gas" json:"max-priority-fee-per-gas"`
MaxFeePerGas uint64 `mapstructure:"max-fee-per-gas" json:"max-fee-per-gas"`

TxInclusionTimeoutSeconds uint64 `mapstructure:"tx-inclusion-timeout-seconds" json:"tx-inclusion-timeout-seconds"`

Expand Down Expand Up @@ -140,6 +141,21 @@ func (s *DestinationBlockchain) Validate() error {
s.TxInclusionTimeoutSeconds = defaultTxInclusionTimeoutSeconds
}

// Validate MaxFeePerGas if configured
if s.MaxFeePerGas > 0 {
// Ensure MaxFeePerGas is at least as large as MaxBaseFee + MaxPriorityFeePerGas
// This prevents configuration that would make transactions impossible to send
if s.MaxBaseFee > 0 && s.MaxFeePerGas < (s.MaxBaseFee+s.MaxPriorityFeePerGas) {
return fmt.Errorf("max-fee-per-gas (%d) must be at least max-base-fee (%d) + max-priority-fee-per-gas (%d) = %d",
s.MaxFeePerGas, s.MaxBaseFee, s.MaxPriorityFeePerGas, s.MaxBaseFee+s.MaxPriorityFeePerGas)
}
// Even if MaxBaseFee is not set, ensure MaxFeePerGas is at least the priority fee
if s.MaxFeePerGas < s.MaxPriorityFeePerGas {
return fmt.Errorf("max-fee-per-gas (%d) must be at least max-priority-fee-per-gas (%d)",
s.MaxFeePerGas, s.MaxPriorityFeePerGas)
}
}

return nil
}

Expand Down
26 changes: 25 additions & 1 deletion vms/evm/destination_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type destinationClient struct {
blockGasLimit uint64
maxBaseFee *big.Int
maxPriorityFeePerGas *big.Int
maxFeePerGas *big.Int
logger logging.Logger
txInclusionTimeout time.Duration
}
Expand Down Expand Up @@ -212,6 +213,7 @@ func NewDestinationClient(
blockGasLimit: destinationBlockchain.BlockGasLimit,
maxBaseFee: new(big.Int).SetUint64(destinationBlockchain.MaxBaseFee),
maxPriorityFeePerGas: new(big.Int).SetUint64(destinationBlockchain.MaxPriorityFeePerGas),
maxFeePerGas: new(big.Int).SetUint64(destinationBlockchain.MaxFeePerGas),
txInclusionTimeout: time.Duration(destinationBlockchain.TxInclusionTimeoutSeconds) * time.Second,
}

Expand All @@ -223,7 +225,8 @@ func NewDestinationClient(
// maximum base is calculated as the current base fee multiplied by the default base fee factor.
// The maximum priority fee per gas is set the minimum of the suggested gas tip cap and the configured
// maximum priority fee per gas. The max fee per gas is set to the sum of the max base fee and the
// max priority fee per gas.
// max priority fee per gas. If maxFeePerGas is configured, the total gas fee cap will be capped
// at that value, with priority fee adjusted accordingly to fit within the limit.
func (c *destinationClient) SendTx(
signedMessage *avalancheWarp.Message,
deliverers set.Set[common.Address],
Expand Down Expand Up @@ -270,6 +273,27 @@ func (c *destinationClient) SendTx(
to := common.HexToAddress(toAddress)
gasFeeCap := new(big.Int).Add(maxBaseFee, gasTipCap)

// If maxFeePerGas is configured and is greater than 0, cap the total gas fee per gas unit
if c.maxFeePerGas != nil && c.maxFeePerGas.Cmp(big.NewInt(0)) > 0 && gasFeeCap.Cmp(c.maxFeePerGas) > 0 {
c.logger.Warn(
"Calculated gas fee cap exceeds configured maximum fee per gas, capping to max",
zap.String("calculatedGasFeeCap", gasFeeCap.String()),
zap.String("maxFeePerGas", c.maxFeePerGas.String()),
)
gasFeeCap = new(big.Int).Set(c.maxFeePerGas)

// Ensure gasTipCap doesn't exceed the capped gasFeeCap
// gasFeeCap = maxBaseFee + gasTipCap, so gasTipCap = gasFeeCap - maxBaseFee
adjustedTipCap := new(big.Int).Sub(gasFeeCap, maxBaseFee)
if adjustedTipCap.Cmp(big.NewInt(0)) < 0 {
// If maxBaseFee is higher than maxFeePerGas, set gasTipCap to 0
gasTipCap = big.NewInt(0)
} else if adjustedTipCap.Cmp(gasTipCap) < 0 {
// Adjust gasTipCap to fit within the capped gasFeeCap
gasTipCap = adjustedTipCap
}
}

resultChan := make(chan txResult)

messageData := txData{
Expand Down
147 changes: 147 additions & 0 deletions vms/evm/destination_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
evmChainID: big.NewInt(5),
maxBaseFee: test.maxBaseFee,
maxPriorityFeePerGas: big.NewInt(0),
maxFeePerGas: big.NewInt(0), // Initialize to prevent nil panic
blockGasLimit: 0,
txInclusionTimeout: 30 * time.Second,
}
Expand Down Expand Up @@ -155,3 +156,149 @@
})
}
}

// TestMaxFeePerGasCalculation tests the gas fee calculation and capping logic
func TestMaxFeePerGasCalculation(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

testCases := []struct {
name string
maxBaseFee uint64
maxPriorityFeePerGas uint64
maxFeePerGas uint64
currentBaseFee uint64
suggestedTip uint64
expectedGasFeeCap uint64
expectedGasTipCap uint64
}{
{
name: "No MaxFeePerGas - normal behavior",
maxBaseFee: 50000000000,
maxPriorityFeePerGas: 2500000000,
maxFeePerGas: 0, // Not set
currentBaseFee: 25000000000,
suggestedTip: 2000000000,
expectedGasFeeCap: 52000000000, // maxBaseFee + suggestedTip
expectedGasTipCap: 2000000000,
},
{
name: "MaxFeePerGas higher than calculated - no capping",
maxBaseFee: 50000000000,
maxPriorityFeePerGas: 2500000000,
maxFeePerGas: 60000000000,
currentBaseFee: 25000000000,
suggestedTip: 2000000000,
expectedGasFeeCap: 52000000000, // maxBaseFee + suggestedTip
expectedGasTipCap: 2000000000,
},
{
name: "MaxFeePerGas lower than calculated - should cap",
maxBaseFee: 50000000000,
maxPriorityFeePerGas: 2500000000,
maxFeePerGas: 51000000000,
currentBaseFee: 25000000000,
suggestedTip: 2000000000,
expectedGasFeeCap: 51000000000, // Capped to maxFeePerGas
expectedGasTipCap: 1000000000, // Adjusted: 51B - 50B = 1B
},
{
name: "MaxFeePerGas much lower - tip becomes zero",
maxBaseFee: 50000000000,
maxPriorityFeePerGas: 2500000000,
maxFeePerGas: 40000000000,
currentBaseFee: 25000000000,
suggestedTip: 2000000000,
expectedGasFeeCap: 40000000000, // Capped to maxFeePerGas
expectedGasTipCap: 0, // Adjusted: 40B - 50B = -10B, so 0
},
{
name: "Dynamic base fee calculation when maxBaseFee is 0",
maxBaseFee: 0, // Will use currentBaseFee * 3
maxPriorityFeePerGas: 2500000000,
maxFeePerGas: 100000000000,
currentBaseFee: 25000000000,
suggestedTip: 2000000000,
expectedGasFeeCap: 77000000000, // (25B * 3) + 2B = 77B
expectedGasTipCap: 2000000000,
},
{
name: "Dynamic base fee with MaxFeePerGas capping",
maxBaseFee: 0, // Will use currentBaseFee * 3
maxPriorityFeePerGas: 2500000000,
maxFeePerGas: 70000000000,
currentBaseFee: 25000000000,
suggestedTip: 2000000000,
expectedGasFeeCap: 70000000000, // Capped
expectedGasTipCap: 0, // Adjusted: 70B - 75B = -5B, so 0
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
mockClient := mock_ethclient.NewMockClient(ctrl)

// Setup base fee estimation if maxBaseFee is 0
if tt.maxBaseFee == 0 {
mockClient.EXPECT().
EstimateBaseFee(gomock.Any()).
Return(big.NewInt(int64(tt.currentBaseFee)), nil).
Times(1)
}

// Setup suggested gas tip cap
mockClient.EXPECT().
SuggestGasTipCap(gomock.Any()).
Return(big.NewInt(int64(tt.suggestedTip)), nil).
Times(1)

// Create destination client with test configuration
destClient := &destinationClient{
client: mockClient,
maxBaseFee: big.NewInt(int64(tt.maxBaseFee)),
maxPriorityFeePerGas: big.NewInt(int64(tt.maxPriorityFeePerGas)),
maxFeePerGas: big.NewInt(int64(tt.maxFeePerGas)),
evmChainID: big.NewInt(1),
logger: logging.NoLog{},
}

// Test just the gas fee calculation logic by replicating the SendTx logic
var maxBaseFee *big.Int
if destClient.maxBaseFee.Cmp(big.NewInt(0)) > 0 {
maxBaseFee = destClient.maxBaseFee
} else {
baseFee, err := destClient.client.EstimateBaseFee(nil)
require.NoError(t, err)
maxBaseFee = new(big.Int).Mul(baseFee, big.NewInt(defaultBaseFeeFactor))
}

gasTipCap, err := destClient.client.SuggestGasTipCap(nil)
require.NoError(t, err)

Check failure on line 277 in vms/evm/destination_client_test.go

View workflow job for this annotation

GitHub Actions / golangci

File is not properly formatted (goimports)
if gasTipCap.Cmp(destClient.maxPriorityFeePerGas) > 0 {
gasTipCap = destClient.maxPriorityFeePerGas
}

gasFeeCap := new(big.Int).Add(maxBaseFee, gasTipCap)

// Apply MaxFeePerGas capping logic (replicated from SendTx)
if destClient.maxFeePerGas != nil && destClient.maxFeePerGas.Cmp(big.NewInt(0)) > 0 && gasFeeCap.Cmp(destClient.maxFeePerGas) > 0 {

Check failure on line 285 in vms/evm/destination_client_test.go

View workflow job for this annotation

GitHub Actions / golangci

The line is 134 characters long, which exceeds the maximum of 120 characters. (lll)
gasFeeCap = new(big.Int).Set(destClient.maxFeePerGas)

adjustedTipCap := new(big.Int).Sub(gasFeeCap, maxBaseFee)
if adjustedTipCap.Cmp(big.NewInt(0)) < 0 {
gasTipCap = big.NewInt(0)
} else if adjustedTipCap.Cmp(gasTipCap) < 0 {
gasTipCap = adjustedTipCap
}
}

// Verify the calculated values match expectations
require.Equal(t, big.NewInt(int64(tt.expectedGasFeeCap)), gasFeeCap,
"Expected gasFeeCap %d, got %s", tt.expectedGasFeeCap, gasFeeCap.String())

require.Equal(t, big.NewInt(int64(tt.expectedGasTipCap)), gasTipCap,
"Expected gasTipCap %d, got %s", tt.expectedGasTipCap, gasTipCap.String())
})
}
}
Loading