diff --git a/common/version/version.go b/common/version/version.go index a9b3af2c6..bb222f3ad 100644 --- a/common/version/version.go +++ b/common/version/version.go @@ -5,7 +5,7 @@ import ( "runtime/debug" ) -var tag = "v4.4.65" +var tag = "v4.4.66" var commit = func() string { if info, ok := debug.ReadBuildInfo(); ok { diff --git a/rollup/internal/config/relayer.go b/rollup/internal/config/relayer.go index f6b022c54..1a1d3a1a4 100644 --- a/rollup/internal/config/relayer.go +++ b/rollup/internal/config/relayer.go @@ -68,12 +68,22 @@ type RelayerConfig struct { FinalizeBundleWithoutProofTimeoutSec uint64 `json:"finalize_bundle_without_proof_timeout_sec"` } +// AlternativeGasTokenConfig The configuration for handling token exchange rates when updating the gas price oracle. +type AlternativeGasTokenConfig struct { + Enabled bool `json:"enabled"` + Mode string `json:"mode"` + FixedExchangeRate float64 `json:"fixed_exchange_rate"` // fixed exchange rate of L2 gas token / L1 gas token + TokenSymbolPair string `json:"token_symbol_pair"` // The pair should be L2 gas token symbol + L1 gas token symbol +} + // GasOracleConfig The config for updating gas price oracle. type GasOracleConfig struct { // MinGasPrice store the minimum gas price to set. MinGasPrice uint64 `json:"min_gas_price"` // GasPriceDiff is the minimum percentage of gas price difference to update gas oracle. GasPriceDiff uint64 `json:"gas_price_diff"` + // AlternativeGasTokenConfig The configuration for handling token exchange rates when updating the gas price oracle. + AlternativeGasTokenConfig *AlternativeGasTokenConfig `json:"alternative_gas_token_config"` // The following configs are only for updating L1 gas price, used for sender in L2. // The weight for L1 base fee. diff --git a/rollup/internal/controller/relayer/l1_relayer.go b/rollup/internal/controller/relayer/l1_relayer.go index d7f857263..ed2f39eb1 100644 --- a/rollup/internal/controller/relayer/l1_relayer.go +++ b/rollup/internal/controller/relayer/l1_relayer.go @@ -21,6 +21,7 @@ import ( "scroll-tech/rollup/internal/config" "scroll-tech/rollup/internal/controller/sender" "scroll-tech/rollup/internal/orm" + rutils "scroll-tech/rollup/internal/utils" ) // Layer1Relayer is responsible for updating L1 gas price oracle contract on L2. @@ -151,6 +152,31 @@ func (r *Layer1Relayer) ProcessGasPriceOracle() { baseFee = block.BaseFee } + // include the token exchange rate in the fee data if alternative gas token enabled + if r.cfg.GasOracleConfig.AlternativeGasTokenConfig != nil && r.cfg.GasOracleConfig.AlternativeGasTokenConfig.Enabled { + // The exchange rate represent the number of native token on L1 required to exchange for 1 native token on L2. + var exchangeRate float64 + switch r.cfg.GasOracleConfig.AlternativeGasTokenConfig.Mode { + case "Fixed": + exchangeRate = r.cfg.GasOracleConfig.AlternativeGasTokenConfig.FixedExchangeRate + case "BinanceApi": + exchangeRate, err = rutils.GetExchangeRateFromBinanceApi(r.cfg.GasOracleConfig.AlternativeGasTokenConfig.TokenSymbolPair, 5) + if err != nil { + log.Error("Failed to get gas token exchange rate from Binance api", "tokenSymbolPair", r.cfg.GasOracleConfig.AlternativeGasTokenConfig.TokenSymbolPair, "err", err) + return + } + default: + log.Error("Invalid alternative gas token mode", "mode", r.cfg.GasOracleConfig.AlternativeGasTokenConfig.Mode) + return + } + if exchangeRate == 0 { + log.Error("Invalid exchange rate", "exchangeRate", exchangeRate) + return + } + baseFee = uint64(math.Ceil(float64(baseFee) / exchangeRate)) + blobBaseFee = uint64(math.Ceil(float64(blobBaseFee) / exchangeRate)) + } + if r.shouldUpdateGasOracle(baseFee, blobBaseFee, isCurie) { // It indicates the committing batch has been stuck for a long time, it's likely that the L1 gas fee spiked. // If we are not committing batches due to high fees then we shouldn't update fees to prevent users from paying high l1_data_fee diff --git a/rollup/internal/controller/relayer/l2_relayer.go b/rollup/internal/controller/relayer/l2_relayer.go index 63e5cbdce..a3a4f0960 100644 --- a/rollup/internal/controller/relayer/l2_relayer.go +++ b/rollup/internal/controller/relayer/l2_relayer.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "math/big" "sort" "strings" @@ -324,6 +325,32 @@ func (r *Layer2Relayer) ProcessGasPriceOracle() { return } suggestGasPriceUint64 := uint64(suggestGasPrice.Int64()) + + // include the token exchange rate in the fee data if alternative gas token enabled + if r.cfg.GasOracleConfig.AlternativeGasTokenConfig != nil && r.cfg.GasOracleConfig.AlternativeGasTokenConfig.Enabled { + // The exchange rate represent the number of native token on L1 required to exchange for 1 native token on L2. + var exchangeRate float64 + switch r.cfg.GasOracleConfig.AlternativeGasTokenConfig.Mode { + case "Fixed": + exchangeRate = r.cfg.GasOracleConfig.AlternativeGasTokenConfig.FixedExchangeRate + case "BinanceApi": + exchangeRate, err = rutils.GetExchangeRateFromBinanceApi(r.cfg.GasOracleConfig.AlternativeGasTokenConfig.TokenSymbolPair, 5) + if err != nil { + log.Error("Failed to get gas token exchange rate from Binance api", "tokenSymbolPair", r.cfg.GasOracleConfig.AlternativeGasTokenConfig.TokenSymbolPair, "err", err) + return + } + default: + log.Error("Invalid alternative gas token mode", "mode", r.cfg.GasOracleConfig.AlternativeGasTokenConfig.Mode) + return + } + if exchangeRate == 0 { + log.Error("Invalid exchange rate", "exchangeRate", exchangeRate) + return + } + suggestGasPriceUint64 = uint64(math.Ceil(float64(suggestGasPriceUint64) * exchangeRate)) + suggestGasPrice = new(big.Int).SetUint64(suggestGasPriceUint64) + } + expectedDelta := r.lastGasPrice * r.gasPriceDiff / gasPriceDiffPrecision if r.lastGasPrice > 0 && expectedDelta == 0 { expectedDelta = 1 diff --git a/rollup/internal/utils/exchange_rate.go b/rollup/internal/utils/exchange_rate.go new file mode 100644 index 000000000..72c32681f --- /dev/null +++ b/rollup/internal/utils/exchange_rate.go @@ -0,0 +1,73 @@ +package utils + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "github.com/scroll-tech/go-ethereum/log" +) + +var BinanceApiEndpoint string = "https://api.binance.com/api/v3/ticker/price?symbol=%s" + +type BinanceResponse struct { + Price string `json:"price"` +} + +func GetExchangeRateFromBinanceApi(tokenSymbolPair string, maxRetries int) (float64, error) { + for i := 0; i < maxRetries; i++ { + if i > 0 { + time.Sleep(5 * time.Second) + } + + // make HTTP GET request + resp, err := http.Get(fmt.Sprintf(BinanceApiEndpoint, tokenSymbolPair)) + if err != nil { + log.Error("error making HTTP request", "err", err) + continue + } + defer func() { + err = resp.Body.Close() + if err != nil { + log.Error("error closing response body", "err", err) + } + }() + + // check for successful response + if resp.StatusCode != http.StatusOK { + log.Error("unexpected status code", "code", resp.StatusCode) + continue + } + + // read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Error("error reading response body", "err", err) + continue + } + + // unmarshal JSON response + var data BinanceResponse + err = json.Unmarshal(body, &data) + if err != nil { + log.Error("error unmarshaling JSON", "err", err) + continue + } + + // convert price string to float64 + price, err := strconv.ParseFloat(data.Price, 64) + if err != nil { + log.Error("error parsing price string", "err", err) + continue + } + + // successful response, return price + return price, nil + } + + // all retries failed, return error + return 0, fmt.Errorf("failed to get exchange rate after %d retries", maxRetries) +}