Skip to content
Open
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
76 changes: 76 additions & 0 deletions token-price-oracle/client/gas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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.
func CalculateGasCaps(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
}
34 changes: 28 additions & 6 deletions token-price-oracle/client/l2_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type L2Client struct {
opts *bind.TransactOpts
signer *Signer
externalSign bool
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
Expand Down Expand Up @@ -50,6 +52,16 @@ func NewL2Client(rpcURL string, cfg *config.Config) (*L2Client, error) {
externalSign: cfg.ExternalSign,
}

// 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 != nil {
l2Client.gasTipCap = new(big.Int).SetUint64(*cfg.GasTipCap)
log.Info("Using gas tip cap limit", "maxGasTipCap", *cfg.GasTipCap)
}

if cfg.ExternalSign {
// External sign mode
rsaPriv, err := externalsign.ParseRsaPrivateKey(cfg.ExternalSignRsaPriv)
Expand Down Expand Up @@ -127,14 +139,14 @@ 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
return &bind.TransactOpts{
From: c.opts.From,
Nonce: c.opts.Nonce,
Signer: c.opts.Signer,
Value: c.opts.Value,
GasPrice: c.opts.GasPrice,
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,
Expand Down Expand Up @@ -167,3 +179,13 @@ func (c *L2Client) GetSigner() *Signer {
func (c *L2Client) GetChainID() *big.Int {
return c.chainID
}

// GetMaxGasFeeCap returns the max gas fee cap (nil if not configured)
func (c *L2Client) GetMaxGasFeeCap() *big.Int {
return c.gasFeeCap
}

// GetMaxGasTipCap returns the max gas tip cap (nil if not configured)
func (c *L2Client) GetMaxGasTipCap() *big.Int {
return c.gasTipCap
}
35 changes: 9 additions & 26 deletions token-price-oracle/client/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,34 +96,18 @@ func (s *Signer) CreateAndSignTx(
return nil, fmt.Errorf("failed to get nonce: %w", err)
}

// Get gas tip cap
tip, err := client.GetClient().SuggestGasTipCap(ctx)
// Calculate gas caps (dynamic values with optional max limits)
caps, err := CalculateGasCaps(ctx, client)
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)
}

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 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 {
Expand All @@ -137,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,
Expand All @@ -149,10 +133,9 @@ 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)
}

19 changes: 19 additions & 0 deletions token-price-oracle/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ type Config struct {
LogFileMaxSize int
LogFileMaxAge int
LogCompress bool

// 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
Expand Down Expand Up @@ -112,6 +116,21 @@ func LoadConfig(ctx *cli.Context) (*Config, error) {
LogCompress: ctx.Bool(flags.LogCompressFlag.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
}

// 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

Expand Down
17 changes: 17 additions & 0 deletions token-price-oracle/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,19 @@ var (
Usage: "The RSA private key for external sign",
EnvVar: prefixEnvVar("EXTERNAL_SIGN_RSA_PRIV"),
}

// Gas fee flags (optional - if set, use as max cap instead of dynamic)
GasFeeCapFlag = cli.Uint64Flag{
Name: "gas-fee-cap",
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: "Max gas tip cap in wei (if set, actual tip = min(dynamic, this value))",
EnvVar: prefixEnvVar("GAS_TIP_CAP"),
}
)

var requiredFlags = []cli.Flag{
Expand Down Expand Up @@ -210,6 +223,10 @@ var optionalFlags = []cli.Flag{
ExternalSignChainFlag,
ExternalSignUrlFlag,
ExternalSignRsaPrivFlag,

// Gas fee
GasFeeCapFlag,
GasTipCapFlag,
}

// Flags contains the list of configuration options available to the binary.
Expand Down
18 changes: 18 additions & 0 deletions token-price-oracle/updater/tx_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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
Expand Down Expand Up @@ -193,6 +198,19 @@ 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 {
caps, err := client.CalculateGasCaps(ctx, m.l2Client)
if err != nil {
return err
}

auth.GasTipCap = caps.TipCap
auth.GasFeeCap = caps.FeeCap
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)
Expand Down