From ff6ed2b8eac4796ae2484c9ada9851735a8e9075 Mon Sep 17 00:00:00 2001 From: corey Date: Fri, 6 Feb 2026 12:14:14 +0800 Subject: [PATCH 1/8] feat(token-price-oracle): support fixed gas fee cap and tip cap configuration --- token-price-oracle/client/l2_client.go | 32 ++++++++++++++++++- token-price-oracle/client/sign.go | 44 ++++++++++++++++---------- token-price-oracle/config/config.go | 8 +++++ token-price-oracle/flags/flags.go | 19 +++++++++++ 4 files changed, 86 insertions(+), 17 deletions(-) diff --git a/token-price-oracle/client/l2_client.go b/token-price-oracle/client/l2_client.go index a69ea044c..ea716d546 100644 --- a/token-price-oracle/client/l2_client.go +++ b/token-price-oracle/client/l2_client.go @@ -22,6 +22,8 @@ type L2Client struct { opts *bind.TransactOpts signer *Signer externalSign bool + gasFeeCap *big.Int // Fixed gas fee cap (nil means use dynamic) + gasTipCap *big.Int // Fixed gas tip cap (nil means use dynamic) } // NewL2Client creates new L2 client @@ -50,6 +52,16 @@ func NewL2Client(rpcURL string, cfg *config.Config) (*L2Client, error) { externalSign: cfg.ExternalSign, } + // Set fixed gas fee if configured (0 means use dynamic) + if cfg.GasFeeCap > 0 { + l2Client.gasFeeCap = big.NewInt(int64(cfg.GasFeeCap)) + log.Info("Using fixed gas fee cap", "gasFeeCap", cfg.GasFeeCap) + } + if cfg.GasTipCap > 0 { + l2Client.gasTipCap = big.NewInt(int64(cfg.GasTipCap)) + log.Info("Using fixed gas tip cap", "gasTipCap", cfg.GasTipCap) + } + if cfg.ExternalSign { // External sign mode rsaPriv, err := externalsign.ParseRsaPrivateKey(cfg.ExternalSignRsaPriv) @@ -129,7 +141,7 @@ func (c *L2Client) GetClient() *ethclient.Client { // Returns a new instance to prevent concurrent modification func (c *L2Client) GetOpts() *bind.TransactOpts { // Return a copy to prevent shared state issues - return &bind.TransactOpts{ + opts := &bind.TransactOpts{ From: c.opts.From, Nonce: c.opts.Nonce, Signer: c.opts.Signer, @@ -141,6 +153,14 @@ func (c *L2Client) GetOpts() *bind.TransactOpts { Context: c.opts.Context, NoSend: c.opts.NoSend, } + // Override with fixed gas fee if configured + if c.gasFeeCap != nil { + opts.GasFeeCap = c.gasFeeCap + } + if c.gasTipCap != nil { + opts.GasTipCap = c.gasTipCap + } + return opts } // GetBalance returns account balance @@ -167,3 +187,13 @@ func (c *L2Client) GetSigner() *Signer { func (c *L2Client) GetChainID() *big.Int { return c.chainID } + +// GetFixedGasFeeCap returns the fixed gas fee cap (nil if not configured) +func (c *L2Client) GetFixedGasFeeCap() *big.Int { + return c.gasFeeCap +} + +// GetFixedGasTipCap returns the fixed gas tip cap (nil if not configured) +func (c *L2Client) GetFixedGasTipCap() *big.Int { + return c.gasTipCap +} diff --git a/token-price-oracle/client/sign.go b/token-price-oracle/client/sign.go index 340e32140..cd3d79095 100644 --- a/token-price-oracle/client/sign.go +++ b/token-price-oracle/client/sign.go @@ -96,26 +96,38 @@ func (s *Signer) CreateAndSignTx( return nil, fmt.Errorf("failed to get nonce: %w", err) } - // Get gas tip cap - tip, err := client.GetClient().SuggestGasTipCap(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get gas tip cap: %w", err) - } - - // Get base fee from latest block - head, err := client.GetClient().HeaderByNumber(ctx, nil) - if err != nil { - return nil, fmt.Errorf("failed to get block header: %w", err) + // Get gas tip cap (use fixed if configured, otherwise dynamic) + var tip *big.Int + if fixedTip := client.GetFixedGasTipCap(); fixedTip != nil { + tip = fixedTip + log.Debug("Using fixed gas tip cap", "tip", tip) + } else { + tip, err = client.GetClient().SuggestGasTipCap(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get gas tip cap: %w", err) + } } + // Get gas fee cap (use fixed if configured, otherwise dynamic) var gasFeeCap *big.Int - if head.BaseFee != nil { - gasFeeCap = new(big.Int).Add( - tip, - new(big.Int).Mul(head.BaseFee, big.NewInt(2)), - ) + if fixedFeeCap := client.GetFixedGasFeeCap(); fixedFeeCap != nil { + gasFeeCap = fixedFeeCap + log.Debug("Using fixed gas fee cap", "gasFeeCap", gasFeeCap) } else { - gasFeeCap = new(big.Int).Set(tip) + // Get base fee from latest block + head, err := client.GetClient().HeaderByNumber(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to get block header: %w", err) + } + + if head.BaseFee != nil { + gasFeeCap = new(big.Int).Add( + tip, + new(big.Int).Mul(head.BaseFee, big.NewInt(2)), + ) + } else { + gasFeeCap = new(big.Int).Set(tip) + } } // Estimate gas diff --git a/token-price-oracle/config/config.go b/token-price-oracle/config/config.go index c66b69e9b..b55b806ea 100644 --- a/token-price-oracle/config/config.go +++ b/token-price-oracle/config/config.go @@ -85,6 +85,10 @@ type Config struct { LogFileMaxSize int LogFileMaxAge int LogCompress bool + + // Gas fee (optional - if set, use fixed values instead of dynamic) + GasFeeCap uint64 // Fixed gas fee cap in wei (0 means use dynamic) + GasTipCap uint64 // Fixed gas tip cap in wei (0 means use dynamic) } // LoadConfig loads configuration from cli.Context @@ -110,6 +114,10 @@ func LoadConfig(ctx *cli.Context) (*Config, error) { LogFileMaxSize: ctx.Int(flags.LogFileMaxSizeFlag.Name), LogFileMaxAge: ctx.Int(flags.LogFileMaxAgeFlag.Name), LogCompress: ctx.Bool(flags.LogCompressFlag.Name), + + // Gas fee (0 means use dynamic) + GasFeeCap: ctx.Uint64(flags.GasFeeCapFlag.Name), + GasTipCap: ctx.Uint64(flags.GasTipCapFlag.Name), } // Parse token registry address (optional) diff --git a/token-price-oracle/flags/flags.go b/token-price-oracle/flags/flags.go index 785783e15..17caea4c4 100644 --- a/token-price-oracle/flags/flags.go +++ b/token-price-oracle/flags/flags.go @@ -176,6 +176,21 @@ var ( Usage: "The RSA private key for external sign", EnvVar: prefixEnvVar("EXTERNAL_SIGN_RSA_PRIV"), } + + // Gas fee flags (optional - if set, use fixed values instead of dynamic) + GasFeeCapFlag = cli.Uint64Flag{ + Name: "gas-fee-cap", + Usage: "Fixed gas fee cap in wei (if set, overrides dynamic gas price)", + Value: 0, + EnvVar: prefixEnvVar("GAS_FEE_CAP"), + } + + GasTipCapFlag = cli.Uint64Flag{ + Name: "gas-tip-cap", + Usage: "Fixed gas tip cap in wei (if set, overrides dynamic gas tip)", + Value: 0, + EnvVar: prefixEnvVar("GAS_TIP_CAP"), + } ) var requiredFlags = []cli.Flag{ @@ -210,6 +225,10 @@ var optionalFlags = []cli.Flag{ ExternalSignChainFlag, ExternalSignUrlFlag, ExternalSignRsaPrivFlag, + + // Gas fee + GasFeeCapFlag, + GasTipCapFlag, } // Flags contains the list of configuration options available to the binary. From 230c67b8903a7c1db27669ede3cce4b4c9c4808f Mon Sep 17 00:00:00 2001 From: corey Date: Fri, 6 Feb 2026 17:00:37 +0800 Subject: [PATCH 2/8] fix(token-price-oracle): use SetUint64 to avoid int64 overflow in gas cap conversion --- token-price-oracle/client/l2_client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token-price-oracle/client/l2_client.go b/token-price-oracle/client/l2_client.go index ea716d546..8402a7cb2 100644 --- a/token-price-oracle/client/l2_client.go +++ b/token-price-oracle/client/l2_client.go @@ -54,11 +54,11 @@ func NewL2Client(rpcURL string, cfg *config.Config) (*L2Client, error) { // Set fixed gas fee if configured (0 means use dynamic) if cfg.GasFeeCap > 0 { - l2Client.gasFeeCap = big.NewInt(int64(cfg.GasFeeCap)) + l2Client.gasFeeCap = new(big.Int).SetUint64(cfg.GasFeeCap) log.Info("Using fixed gas fee cap", "gasFeeCap", cfg.GasFeeCap) } if cfg.GasTipCap > 0 { - l2Client.gasTipCap = big.NewInt(int64(cfg.GasTipCap)) + l2Client.gasTipCap = new(big.Int).SetUint64(cfg.GasTipCap) log.Info("Using fixed gas tip cap", "gasTipCap", cfg.GasTipCap) } From ffd24b5f65d864b305fdcb03925386242cd1a994 Mon Sep 17 00:00:00 2001 From: corey Date: Fri, 6 Feb 2026 17:52:20 +0800 Subject: [PATCH 3/8] refactor(token-price-oracle): change gas fee/tip cap to max limit (use min of dynamic and configured value) --- token-price-oracle/client/l2_client.go | 14 +++---- token-price-oracle/client/sign.go | 53 ++++++++++++++------------ token-price-oracle/config/config.go | 18 ++++++--- token-price-oracle/flags/flags.go | 8 ++-- 4 files changed, 50 insertions(+), 43 deletions(-) diff --git a/token-price-oracle/client/l2_client.go b/token-price-oracle/client/l2_client.go index 8402a7cb2..15cda8ced 100644 --- a/token-price-oracle/client/l2_client.go +++ b/token-price-oracle/client/l2_client.go @@ -52,14 +52,14 @@ func NewL2Client(rpcURL string, cfg *config.Config) (*L2Client, error) { externalSign: cfg.ExternalSign, } - // Set fixed gas fee if configured (0 means use dynamic) - if cfg.GasFeeCap > 0 { - l2Client.gasFeeCap = new(big.Int).SetUint64(cfg.GasFeeCap) - log.Info("Using fixed gas fee cap", "gasFeeCap", cfg.GasFeeCap) + // Set gas fee caps if configured (used as max cap, not fixed value) + if cfg.GasFeeCap != nil { + l2Client.gasFeeCap = new(big.Int).SetUint64(*cfg.GasFeeCap) + log.Info("Using gas fee cap limit", "maxGasFeeCap", *cfg.GasFeeCap) } - if cfg.GasTipCap > 0 { - l2Client.gasTipCap = new(big.Int).SetUint64(cfg.GasTipCap) - log.Info("Using fixed gas tip cap", "gasTipCap", cfg.GasTipCap) + if cfg.GasTipCap != nil { + l2Client.gasTipCap = new(big.Int).SetUint64(*cfg.GasTipCap) + log.Info("Using gas tip cap limit", "maxGasTipCap", *cfg.GasTipCap) } if cfg.ExternalSign { diff --git a/token-price-oracle/client/sign.go b/token-price-oracle/client/sign.go index cd3d79095..7399cc260 100644 --- a/token-price-oracle/client/sign.go +++ b/token-price-oracle/client/sign.go @@ -96,37 +96,40 @@ func (s *Signer) CreateAndSignTx( return nil, fmt.Errorf("failed to get nonce: %w", err) } - // Get gas tip cap (use fixed if configured, otherwise dynamic) - var tip *big.Int - if fixedTip := client.GetFixedGasTipCap(); fixedTip != nil { - tip = fixedTip - log.Debug("Using fixed gas tip cap", "tip", tip) - } else { - tip, err = client.GetClient().SuggestGasTipCap(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get gas tip cap: %w", err) + // Get gas tip cap (dynamic, then apply cap if configured) + tip, err := client.GetClient().SuggestGasTipCap(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get gas tip cap: %w", err) + } + if maxTip := client.GetFixedGasTipCap(); maxTip != nil { + if tip.Cmp(maxTip) > 0 { + log.Debug("Applying gas tip cap limit", "dynamic", tip, "cap", maxTip) + tip = maxTip } } - // Get gas fee cap (use fixed if configured, otherwise dynamic) + // Get base fee from latest block + head, err := client.GetClient().HeaderByNumber(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to get block header: %w", err) + } + + // Calculate dynamic gas fee cap var gasFeeCap *big.Int - if fixedFeeCap := client.GetFixedGasFeeCap(); fixedFeeCap != nil { - gasFeeCap = fixedFeeCap - log.Debug("Using fixed gas fee cap", "gasFeeCap", gasFeeCap) + if head.BaseFee != nil { + gasFeeCap = new(big.Int).Add( + tip, + new(big.Int).Mul(head.BaseFee, big.NewInt(2)), + ) } else { - // Get base fee from latest block - head, err := client.GetClient().HeaderByNumber(ctx, nil) - if err != nil { - return nil, fmt.Errorf("failed to get block header: %w", err) - } + gasFeeCap = new(big.Int).Set(tip) + } - if head.BaseFee != nil { - gasFeeCap = new(big.Int).Add( - tip, - new(big.Int).Mul(head.BaseFee, big.NewInt(2)), - ) - } else { - gasFeeCap = new(big.Int).Set(tip) + // Apply gas fee cap limit if configured + if maxFeeCap := client.GetFixedGasFeeCap(); maxFeeCap != nil { + if gasFeeCap.Cmp(maxFeeCap) > 0 { + log.Debug("Applying gas fee cap limit", "dynamic", gasFeeCap, "cap", maxFeeCap) + gasFeeCap = maxFeeCap } } diff --git a/token-price-oracle/config/config.go b/token-price-oracle/config/config.go index b55b806ea..a97a54ec3 100644 --- a/token-price-oracle/config/config.go +++ b/token-price-oracle/config/config.go @@ -86,9 +86,9 @@ type Config struct { LogFileMaxAge int LogCompress bool - // Gas fee (optional - if set, use fixed values instead of dynamic) - GasFeeCap uint64 // Fixed gas fee cap in wei (0 means use dynamic) - GasTipCap uint64 // Fixed gas tip cap in wei (0 means use dynamic) + // Gas fee caps (optional - if set, use as max cap) + GasFeeCap *uint64 // Max gas fee cap in wei (nil means no cap) + GasTipCap *uint64 // Max gas tip cap in wei (nil means no cap) } // LoadConfig loads configuration from cli.Context @@ -114,10 +114,16 @@ func LoadConfig(ctx *cli.Context) (*Config, error) { LogFileMaxSize: ctx.Int(flags.LogFileMaxSizeFlag.Name), LogFileMaxAge: ctx.Int(flags.LogFileMaxAgeFlag.Name), LogCompress: ctx.Bool(flags.LogCompressFlag.Name), + } - // Gas fee (0 means use dynamic) - GasFeeCap: ctx.Uint64(flags.GasFeeCapFlag.Name), - GasTipCap: ctx.Uint64(flags.GasTipCapFlag.Name), + // Gas fee caps (only set if flag is explicitly provided) + if ctx.IsSet(flags.GasFeeCapFlag.Name) { + v := ctx.Uint64(flags.GasFeeCapFlag.Name) + cfg.GasFeeCap = &v + } + if ctx.IsSet(flags.GasTipCapFlag.Name) { + v := ctx.Uint64(flags.GasTipCapFlag.Name) + cfg.GasTipCap = &v } // Parse token registry address (optional) diff --git a/token-price-oracle/flags/flags.go b/token-price-oracle/flags/flags.go index 17caea4c4..1692806b7 100644 --- a/token-price-oracle/flags/flags.go +++ b/token-price-oracle/flags/flags.go @@ -177,18 +177,16 @@ var ( EnvVar: prefixEnvVar("EXTERNAL_SIGN_RSA_PRIV"), } - // Gas fee flags (optional - if set, use fixed values instead of dynamic) + // Gas fee flags (optional - if set, use as max cap instead of dynamic) GasFeeCapFlag = cli.Uint64Flag{ Name: "gas-fee-cap", - Usage: "Fixed gas fee cap in wei (if set, overrides dynamic gas price)", - Value: 0, + Usage: "Max gas fee cap in wei (if set, actual fee = min(dynamic, this value))", EnvVar: prefixEnvVar("GAS_FEE_CAP"), } GasTipCapFlag = cli.Uint64Flag{ Name: "gas-tip-cap", - Usage: "Fixed gas tip cap in wei (if set, overrides dynamic gas tip)", - Value: 0, + Usage: "Max gas tip cap in wei (if set, actual tip = min(dynamic, this value))", EnvVar: prefixEnvVar("GAS_TIP_CAP"), } ) From 39381648ce5c4d33294a2da1b8157452730bffdb Mon Sep 17 00:00:00 2001 From: corey Date: Fri, 6 Feb 2026 18:05:40 +0800 Subject: [PATCH 4/8] fix --- token-price-oracle/client/l2_client.go | 34 ++++++-------- token-price-oracle/client/sign.go | 4 +- token-price-oracle/updater/tx_manager.go | 58 ++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/token-price-oracle/client/l2_client.go b/token-price-oracle/client/l2_client.go index 15cda8ced..1568b068c 100644 --- a/token-price-oracle/client/l2_client.go +++ b/token-price-oracle/client/l2_client.go @@ -22,8 +22,8 @@ type L2Client struct { opts *bind.TransactOpts signer *Signer externalSign bool - gasFeeCap *big.Int // Fixed gas fee cap (nil means use dynamic) - gasTipCap *big.Int // Fixed gas tip cap (nil means use dynamic) + gasFeeCap *big.Int // Max gas fee cap (nil means no cap) + gasTipCap *big.Int // Max gas tip cap (nil means no cap) } // NewL2Client creates new L2 client @@ -139,28 +139,20 @@ func (c *L2Client) GetClient() *ethclient.Client { // GetOpts returns a copy of transaction options // Returns a new instance to prevent concurrent modification +// Note: Gas caps are applied by TxManager.applyGasCaps(), not here func (c *L2Client) GetOpts() *bind.TransactOpts { - // Return a copy to prevent shared state issues - opts := &bind.TransactOpts{ - From: c.opts.From, - Nonce: c.opts.Nonce, - Signer: c.opts.Signer, - Value: c.opts.Value, - GasPrice: c.opts.GasPrice, + return &bind.TransactOpts{ + From: c.opts.From, + Nonce: c.opts.Nonce, + Signer: c.opts.Signer, + Value: c.opts.Value, + GasPrice: c.opts.GasPrice, GasFeeCap: c.opts.GasFeeCap, GasTipCap: c.opts.GasTipCap, GasLimit: c.opts.GasLimit, Context: c.opts.Context, NoSend: c.opts.NoSend, } - // Override with fixed gas fee if configured - if c.gasFeeCap != nil { - opts.GasFeeCap = c.gasFeeCap - } - if c.gasTipCap != nil { - opts.GasTipCap = c.gasTipCap - } - return opts } // GetBalance returns account balance @@ -188,12 +180,12 @@ func (c *L2Client) GetChainID() *big.Int { return c.chainID } -// GetFixedGasFeeCap returns the fixed gas fee cap (nil if not configured) -func (c *L2Client) GetFixedGasFeeCap() *big.Int { +// GetMaxGasFeeCap returns the max gas fee cap (nil if not configured) +func (c *L2Client) GetMaxGasFeeCap() *big.Int { return c.gasFeeCap } -// GetFixedGasTipCap returns the fixed gas tip cap (nil if not configured) -func (c *L2Client) GetFixedGasTipCap() *big.Int { +// GetMaxGasTipCap returns the max gas tip cap (nil if not configured) +func (c *L2Client) GetMaxGasTipCap() *big.Int { return c.gasTipCap } diff --git a/token-price-oracle/client/sign.go b/token-price-oracle/client/sign.go index 7399cc260..10e928178 100644 --- a/token-price-oracle/client/sign.go +++ b/token-price-oracle/client/sign.go @@ -101,7 +101,7 @@ func (s *Signer) CreateAndSignTx( if err != nil { return nil, fmt.Errorf("failed to get gas tip cap: %w", err) } - if maxTip := client.GetFixedGasTipCap(); maxTip != nil { + if maxTip := client.GetMaxGasTipCap(); maxTip != nil { if tip.Cmp(maxTip) > 0 { log.Debug("Applying gas tip cap limit", "dynamic", tip, "cap", maxTip) tip = maxTip @@ -126,7 +126,7 @@ func (s *Signer) CreateAndSignTx( } // Apply gas fee cap limit if configured - if maxFeeCap := client.GetFixedGasFeeCap(); maxFeeCap != nil { + if maxFeeCap := client.GetMaxGasFeeCap(); maxFeeCap != nil { if gasFeeCap.Cmp(maxFeeCap) > 0 { log.Debug("Applying gas fee cap limit", "dynamic", gasFeeCap, "cap", maxFeeCap) gasFeeCap = maxFeeCap diff --git a/token-price-oracle/updater/tx_manager.go b/token-price-oracle/updater/tx_manager.go index ebed49395..d723e2965 100644 --- a/token-price-oracle/updater/tx_manager.go +++ b/token-price-oracle/updater/tx_manager.go @@ -3,6 +3,7 @@ package updater import ( "context" "fmt" + "math/big" "sync" "time" @@ -72,6 +73,11 @@ func (m *TxManager) sendWithLocalSign(ctx context.Context, txFunc func(*bind.Tra auth := m.l2Client.GetOpts() auth.Context = ctx + // Apply gas caps if configured (same logic as external sign) + if err := m.applyGasCaps(ctx, auth); err != nil { + return nil, fmt.Errorf("failed to apply gas caps: %w", err) + } + // First, estimate gas with GasLimit = 0 auth.GasLimit = 0 auth.NoSend = true @@ -193,6 +199,58 @@ func (m *TxManager) sendWithExternalSign(ctx context.Context, txFunc func(*bind. return receipt, nil } +// applyGasCaps applies configured gas caps as upper limits to dynamic gas prices +// This ensures consistent behavior between local sign and external sign +func (m *TxManager) applyGasCaps(ctx context.Context, auth *bind.TransactOpts) error { + maxTipCap := m.l2Client.GetMaxGasTipCap() + maxFeeCap := m.l2Client.GetMaxGasFeeCap() + + // If no caps configured, let bind package handle gas pricing dynamically + if maxTipCap == nil && maxFeeCap == nil { + return nil + } + + // Get dynamic gas tip cap + tip, err := m.l2Client.GetClient().SuggestGasTipCap(ctx) + if err != nil { + return fmt.Errorf("failed to get gas tip cap: %w", err) + } + + // Apply tip cap limit if configured + if maxTipCap != nil && tip.Cmp(maxTipCap) > 0 { + log.Debug("Applying gas tip cap limit", "dynamic", tip, "cap", maxTipCap) + tip = new(big.Int).Set(maxTipCap) + } + auth.GasTipCap = tip + + // Get base fee from latest block + head, err := m.l2Client.GetClient().HeaderByNumber(ctx, nil) + if err != nil { + return fmt.Errorf("failed to get block header: %w", err) + } + + // Calculate dynamic gas fee cap + var gasFeeCap *big.Int + if head.BaseFee != nil { + gasFeeCap = new(big.Int).Add( + tip, + new(big.Int).Mul(head.BaseFee, big.NewInt(2)), + ) + } else { + gasFeeCap = new(big.Int).Set(tip) + } + + // Apply fee cap limit if configured + if maxFeeCap != nil && gasFeeCap.Cmp(maxFeeCap) > 0 { + log.Debug("Applying gas fee cap limit", "dynamic", gasFeeCap, "cap", maxFeeCap) + gasFeeCap = new(big.Int).Set(maxFeeCap) + } + auth.GasFeeCap = gasFeeCap + + log.Debug("Gas caps applied", "tipCap", auth.GasTipCap, "feeCap", auth.GasFeeCap) + return nil +} + // waitForReceipt waits for a transaction receipt with timeout and custom polling interval func (m *TxManager) waitForReceipt(ctx context.Context, txHash common.Hash, timeout, pollInterval time.Duration) (*types.Receipt, error) { deadline := time.Now().Add(timeout) From 6796e5735d0806f7b91571d1f55f656eb93819f9 Mon Sep 17 00:00:00 2001 From: corey Date: Tue, 10 Feb 2026 10:41:20 +0800 Subject: [PATCH 5/8] fix --- token-price-oracle/client/sign.go | 10 ++++++++-- token-price-oracle/config/config.go | 5 +++++ token-price-oracle/updater/tx_manager.go | 6 ++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/token-price-oracle/client/sign.go b/token-price-oracle/client/sign.go index 10e928178..47df3b5c0 100644 --- a/token-price-oracle/client/sign.go +++ b/token-price-oracle/client/sign.go @@ -104,7 +104,7 @@ func (s *Signer) CreateAndSignTx( if maxTip := client.GetMaxGasTipCap(); maxTip != nil { if tip.Cmp(maxTip) > 0 { log.Debug("Applying gas tip cap limit", "dynamic", tip, "cap", maxTip) - tip = maxTip + tip = new(big.Int).Set(maxTip) } } @@ -129,10 +129,16 @@ func (s *Signer) CreateAndSignTx( if maxFeeCap := client.GetMaxGasFeeCap(); maxFeeCap != nil { if gasFeeCap.Cmp(maxFeeCap) > 0 { log.Debug("Applying gas fee cap limit", "dynamic", gasFeeCap, "cap", maxFeeCap) - gasFeeCap = maxFeeCap + gasFeeCap = new(big.Int).Set(maxFeeCap) } } + // Ensure gasTipCap <= gasFeeCap (EIP-1559 invariant) + if tip.Cmp(gasFeeCap) > 0 { + log.Debug("Clamping tip to gasFeeCap", "tip", tip, "gasFeeCap", gasFeeCap) + tip = new(big.Int).Set(gasFeeCap) + } + // Estimate gas gas, err := client.GetClient().EstimateGas(ctx, ethereum.CallMsg{ From: from, diff --git a/token-price-oracle/config/config.go b/token-price-oracle/config/config.go index a97a54ec3..ef0923326 100644 --- a/token-price-oracle/config/config.go +++ b/token-price-oracle/config/config.go @@ -126,6 +126,11 @@ func LoadConfig(ctx *cli.Context) (*Config, error) { cfg.GasTipCap = &v } + // Validate GasFeeCap >= GasTipCap when both are set (EIP-1559 invariant) + if cfg.GasFeeCap != nil && cfg.GasTipCap != nil && *cfg.GasFeeCap < *cfg.GasTipCap { + return nil, fmt.Errorf("--gas-fee-cap (%d) must be >= --gas-tip-cap (%d)", *cfg.GasFeeCap, *cfg.GasTipCap) + } + // Parse token registry address (optional) cfg.L2TokenRegistryAddr = predeploys.L2TokenRegistryAddr diff --git a/token-price-oracle/updater/tx_manager.go b/token-price-oracle/updater/tx_manager.go index d723e2965..eaa28a1e7 100644 --- a/token-price-oracle/updater/tx_manager.go +++ b/token-price-oracle/updater/tx_manager.go @@ -247,6 +247,12 @@ func (m *TxManager) applyGasCaps(ctx context.Context, auth *bind.TransactOpts) e } auth.GasFeeCap = gasFeeCap + // Ensure gasTipCap <= gasFeeCap (EIP-1559 invariant) + if auth.GasTipCap.Cmp(auth.GasFeeCap) > 0 { + log.Debug("Clamping tip to gasFeeCap", "tip", auth.GasTipCap, "gasFeeCap", auth.GasFeeCap) + auth.GasTipCap = new(big.Int).Set(auth.GasFeeCap) + } + log.Debug("Gas caps applied", "tipCap", auth.GasTipCap, "feeCap", auth.GasFeeCap) return nil } From 25cff003ca47d058940a42ff880a237df28c86a5 Mon Sep 17 00:00:00 2001 From: corey Date: Tue, 10 Feb 2026 14:34:58 +0800 Subject: [PATCH 6/8] refactor(token-price-oracle): extract common gas caps calculation logic - Add client/gas.go with CalculateGasCaps and CalculateGasCapsAlways - Simplify tx_manager.go applyGasCaps to use shared function - Simplify sign.go CreateAndSignTx to use shared function - Ensures consistent gas cap strategy and EIP-1559 invariant handling --- token-price-oracle/client/gas.go | 93 ++++++++++++++++++++++++ token-price-oracle/client/sign.go | 55 +++----------- token-price-oracle/updater/tx_manager.go | 55 ++------------ 3 files changed, 109 insertions(+), 94 deletions(-) create mode 100644 token-price-oracle/client/gas.go diff --git a/token-price-oracle/client/gas.go b/token-price-oracle/client/gas.go new file mode 100644 index 000000000..9ded06457 --- /dev/null +++ b/token-price-oracle/client/gas.go @@ -0,0 +1,93 @@ +package client + +import ( + "context" + "fmt" + "math/big" + + "github.com/morph-l2/go-ethereum/log" +) + +// GasCaps holds the calculated gas tip cap and gas fee cap +type GasCaps struct { + TipCap *big.Int + FeeCap *big.Int +} + +// CalculateGasCaps calculates dynamic gas caps with optional max limits. +// It fetches the suggested tip and base fee from the network, applies configured +// max limits, and ensures the EIP-1559 invariant (tipCap <= feeCap) is maintained. +// +// If no max caps are configured, returns (nil, nil) to indicate caller should use default behavior. +// Use CalculateGasCapsAlways if you always need gas cap values. +func CalculateGasCaps(ctx context.Context, client *L2Client) (*GasCaps, error) { + maxTipCap := client.GetMaxGasTipCap() + maxFeeCap := client.GetMaxGasFeeCap() + + // If no caps configured, return nil to indicate default behavior + if maxTipCap == nil && maxFeeCap == nil { + return nil, nil + } + + return doCalculateGasCaps(ctx, client, maxTipCap, maxFeeCap) +} + +// CalculateGasCapsAlways calculates dynamic gas caps, always returning values. +// Use this when you need gas cap values regardless of whether max caps are configured. +func CalculateGasCapsAlways(ctx context.Context, client *L2Client) (*GasCaps, error) { + maxTipCap := client.GetMaxGasTipCap() + maxFeeCap := client.GetMaxGasFeeCap() + return doCalculateGasCaps(ctx, client, maxTipCap, maxFeeCap) +} + +func doCalculateGasCaps(ctx context.Context, client *L2Client, maxTipCap, maxFeeCap *big.Int) (*GasCaps, error) { + + // Get dynamic gas tip cap from network + tip, err := client.GetClient().SuggestGasTipCap(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get suggested gas tip cap: %w", err) + } + + // Apply tip cap limit if configured + if maxTipCap != nil && tip.Cmp(maxTipCap) > 0 { + log.Debug("Applying gas tip cap limit", "dynamic", tip, "max", maxTipCap) + tip = new(big.Int).Set(maxTipCap) + } + + // Get base fee from latest block + head, err := client.GetClient().HeaderByNumber(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to get block header: %w", err) + } + + // Calculate dynamic gas fee cap: tip + baseFee * 2 + var feeCap *big.Int + if head.BaseFee != nil { + feeCap = new(big.Int).Add( + tip, + new(big.Int).Mul(head.BaseFee, big.NewInt(2)), + ) + } else { + feeCap = new(big.Int).Set(tip) + } + + // Apply fee cap limit if configured + if maxFeeCap != nil && feeCap.Cmp(maxFeeCap) > 0 { + log.Debug("Applying gas fee cap limit", "dynamic", feeCap, "max", maxFeeCap) + feeCap = new(big.Int).Set(maxFeeCap) + } + + // Ensure tipCap <= feeCap (EIP-1559 invariant) + if tip.Cmp(feeCap) > 0 { + log.Debug("Clamping tip to feeCap for EIP-1559 invariant", "tip", tip, "feeCap", feeCap) + tip = new(big.Int).Set(feeCap) + } + + log.Debug("Gas caps calculated", "tipCap", tip, "feeCap", feeCap) + + return &GasCaps{ + TipCap: tip, + FeeCap: feeCap, + }, nil +} + diff --git a/token-price-oracle/client/sign.go b/token-price-oracle/client/sign.go index 47df3b5c0..657cef32f 100644 --- a/token-price-oracle/client/sign.go +++ b/token-price-oracle/client/sign.go @@ -96,55 +96,18 @@ func (s *Signer) CreateAndSignTx( return nil, fmt.Errorf("failed to get nonce: %w", err) } - // Get gas tip cap (dynamic, then apply cap if configured) - tip, err := client.GetClient().SuggestGasTipCap(ctx) + // Calculate gas caps (dynamic values with optional max limits) + caps, err := CalculateGasCapsAlways(ctx, client) if err != nil { - return nil, fmt.Errorf("failed to get gas tip cap: %w", err) - } - if maxTip := client.GetMaxGasTipCap(); maxTip != nil { - if tip.Cmp(maxTip) > 0 { - log.Debug("Applying gas tip cap limit", "dynamic", tip, "cap", maxTip) - tip = new(big.Int).Set(maxTip) - } - } - - // Get base fee from latest block - head, err := client.GetClient().HeaderByNumber(ctx, nil) - if err != nil { - return nil, fmt.Errorf("failed to get block header: %w", err) - } - - // Calculate dynamic gas fee cap - var gasFeeCap *big.Int - if head.BaseFee != nil { - gasFeeCap = new(big.Int).Add( - tip, - new(big.Int).Mul(head.BaseFee, big.NewInt(2)), - ) - } else { - gasFeeCap = new(big.Int).Set(tip) - } - - // Apply gas fee cap limit if configured - if maxFeeCap := client.GetMaxGasFeeCap(); maxFeeCap != nil { - if gasFeeCap.Cmp(maxFeeCap) > 0 { - log.Debug("Applying gas fee cap limit", "dynamic", gasFeeCap, "cap", maxFeeCap) - gasFeeCap = new(big.Int).Set(maxFeeCap) - } - } - - // Ensure gasTipCap <= gasFeeCap (EIP-1559 invariant) - if tip.Cmp(gasFeeCap) > 0 { - log.Debug("Clamping tip to gasFeeCap", "tip", tip, "gasFeeCap", gasFeeCap) - tip = new(big.Int).Set(gasFeeCap) + return nil, fmt.Errorf("failed to calculate gas caps: %w", err) } // Estimate gas gas, err := client.GetClient().EstimateGas(ctx, ethereum.CallMsg{ From: from, To: &to, - GasFeeCap: gasFeeCap, - GasTipCap: tip, + GasFeeCap: caps.FeeCap, + GasTipCap: caps.TipCap, Data: callData, }) if err != nil { @@ -158,8 +121,8 @@ func (s *Signer) CreateAndSignTx( tx := types.NewTx(&types.DynamicFeeTx{ ChainID: s.chainID, Nonce: nonce, - GasTipCap: tip, - GasFeeCap: gasFeeCap, + GasTipCap: caps.TipCap, + GasFeeCap: caps.FeeCap, Gas: gas, To: &to, Data: callData, @@ -170,8 +133,8 @@ func (s *Signer) CreateAndSignTx( "to", to.Hex(), "nonce", nonce, "gas", gas, - "gasFeeCap", gasFeeCap, - "gasTipCap", tip) + "gasFeeCap", caps.FeeCap, + "gasTipCap", caps.TipCap) // Sign transaction return s.Sign(tx) diff --git a/token-price-oracle/updater/tx_manager.go b/token-price-oracle/updater/tx_manager.go index eaa28a1e7..81838f52c 100644 --- a/token-price-oracle/updater/tx_manager.go +++ b/token-price-oracle/updater/tx_manager.go @@ -3,7 +3,6 @@ package updater import ( "context" "fmt" - "math/big" "sync" "time" @@ -202,58 +201,18 @@ func (m *TxManager) sendWithExternalSign(ctx context.Context, txFunc func(*bind. // applyGasCaps applies configured gas caps as upper limits to dynamic gas prices // This ensures consistent behavior between local sign and external sign func (m *TxManager) applyGasCaps(ctx context.Context, auth *bind.TransactOpts) error { - maxTipCap := m.l2Client.GetMaxGasTipCap() - maxFeeCap := m.l2Client.GetMaxGasFeeCap() - - // If no caps configured, let bind package handle gas pricing dynamically - if maxTipCap == nil && maxFeeCap == nil { - return nil - } - - // Get dynamic gas tip cap - tip, err := m.l2Client.GetClient().SuggestGasTipCap(ctx) - if err != nil { - return fmt.Errorf("failed to get gas tip cap: %w", err) - } - - // Apply tip cap limit if configured - if maxTipCap != nil && tip.Cmp(maxTipCap) > 0 { - log.Debug("Applying gas tip cap limit", "dynamic", tip, "cap", maxTipCap) - tip = new(big.Int).Set(maxTipCap) - } - auth.GasTipCap = tip - - // Get base fee from latest block - head, err := m.l2Client.GetClient().HeaderByNumber(ctx, nil) + caps, err := client.CalculateGasCaps(ctx, m.l2Client) if err != nil { - return fmt.Errorf("failed to get block header: %w", err) - } - - // Calculate dynamic gas fee cap - var gasFeeCap *big.Int - if head.BaseFee != nil { - gasFeeCap = new(big.Int).Add( - tip, - new(big.Int).Mul(head.BaseFee, big.NewInt(2)), - ) - } else { - gasFeeCap = new(big.Int).Set(tip) + return err } - // Apply fee cap limit if configured - if maxFeeCap != nil && gasFeeCap.Cmp(maxFeeCap) > 0 { - log.Debug("Applying gas fee cap limit", "dynamic", gasFeeCap, "cap", maxFeeCap) - gasFeeCap = new(big.Int).Set(maxFeeCap) - } - auth.GasFeeCap = gasFeeCap - - // Ensure gasTipCap <= gasFeeCap (EIP-1559 invariant) - if auth.GasTipCap.Cmp(auth.GasFeeCap) > 0 { - log.Debug("Clamping tip to gasFeeCap", "tip", auth.GasTipCap, "gasFeeCap", auth.GasFeeCap) - auth.GasTipCap = new(big.Int).Set(auth.GasFeeCap) + // If no caps configured, let bind package handle gas pricing dynamically + if caps == nil { + return nil } - log.Debug("Gas caps applied", "tipCap", auth.GasTipCap, "feeCap", auth.GasFeeCap) + auth.GasTipCap = caps.TipCap + auth.GasFeeCap = caps.FeeCap return nil } From 93b273a71b3ea8bd76a905680e24dd2bb6e930e7 Mon Sep 17 00:00:00 2001 From: corey Date: Tue, 10 Feb 2026 14:49:04 +0800 Subject: [PATCH 7/8] clean --- token-price-oracle/client/gas.go | 14 -------------- token-price-oracle/client/sign.go | 3 +-- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/token-price-oracle/client/gas.go b/token-price-oracle/client/gas.go index 9ded06457..ff1fc9551 100644 --- a/token-price-oracle/client/gas.go +++ b/token-price-oracle/client/gas.go @@ -24,19 +24,6 @@ func CalculateGasCaps(ctx context.Context, client *L2Client) (*GasCaps, error) { maxTipCap := client.GetMaxGasTipCap() maxFeeCap := client.GetMaxGasFeeCap() - // If no caps configured, return nil to indicate default behavior - if maxTipCap == nil && maxFeeCap == nil { - return nil, nil - } - - return doCalculateGasCaps(ctx, client, maxTipCap, maxFeeCap) -} - -// CalculateGasCapsAlways calculates dynamic gas caps, always returning values. -// Use this when you need gas cap values regardless of whether max caps are configured. -func CalculateGasCapsAlways(ctx context.Context, client *L2Client) (*GasCaps, error) { - maxTipCap := client.GetMaxGasTipCap() - maxFeeCap := client.GetMaxGasFeeCap() return doCalculateGasCaps(ctx, client, maxTipCap, maxFeeCap) } @@ -90,4 +77,3 @@ func doCalculateGasCaps(ctx context.Context, client *L2Client, maxTipCap, maxFee FeeCap: feeCap, }, nil } - diff --git a/token-price-oracle/client/sign.go b/token-price-oracle/client/sign.go index 657cef32f..392879a4f 100644 --- a/token-price-oracle/client/sign.go +++ b/token-price-oracle/client/sign.go @@ -97,7 +97,7 @@ func (s *Signer) CreateAndSignTx( } // Calculate gas caps (dynamic values with optional max limits) - caps, err := CalculateGasCapsAlways(ctx, client) + caps, err := CalculateGasCaps(ctx, client) if err != nil { return nil, fmt.Errorf("failed to calculate gas caps: %w", err) } @@ -139,4 +139,3 @@ func (s *Signer) CreateAndSignTx( // Sign transaction return s.Sign(tx) } - From 6fc109e1665fae5690ccb6fdd63f0423519f551f Mon Sep 17 00:00:00 2001 From: corey Date: Tue, 10 Feb 2026 15:50:57 +0800 Subject: [PATCH 8/8] clean --- token-price-oracle/client/gas.go | 3 --- token-price-oracle/updater/tx_manager.go | 5 ----- 2 files changed, 8 deletions(-) diff --git a/token-price-oracle/client/gas.go b/token-price-oracle/client/gas.go index ff1fc9551..2d1c4892d 100644 --- a/token-price-oracle/client/gas.go +++ b/token-price-oracle/client/gas.go @@ -17,9 +17,6 @@ type GasCaps struct { // CalculateGasCaps calculates dynamic gas caps with optional max limits. // It fetches the suggested tip and base fee from the network, applies configured // max limits, and ensures the EIP-1559 invariant (tipCap <= feeCap) is maintained. -// -// If no max caps are configured, returns (nil, nil) to indicate caller should use default behavior. -// Use CalculateGasCapsAlways if you always need gas cap values. func CalculateGasCaps(ctx context.Context, client *L2Client) (*GasCaps, error) { maxTipCap := client.GetMaxGasTipCap() maxFeeCap := client.GetMaxGasFeeCap() diff --git a/token-price-oracle/updater/tx_manager.go b/token-price-oracle/updater/tx_manager.go index 81838f52c..0ae0c8417 100644 --- a/token-price-oracle/updater/tx_manager.go +++ b/token-price-oracle/updater/tx_manager.go @@ -206,11 +206,6 @@ func (m *TxManager) applyGasCaps(ctx context.Context, auth *bind.TransactOpts) e return err } - // If no caps configured, let bind package handle gas pricing dynamically - if caps == nil { - return nil - } - auth.GasTipCap = caps.TipCap auth.GasFeeCap = caps.FeeCap return nil