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
1 change: 1 addition & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ gas_buffer = 1.2 # More buffer for inaccurate multicall3 estimations
block_time_seconds = 2
receipt_timeout_blocks = 30 # 60 seconds total (30 blocks * 2s)
rpc_request_timeout_seconds = 20
flashblocks = true # Use BlockId::latest() for gas estimation (Base Flashblocks)

# Base Sepolia Testnet - Chain ID 84532
[transaction.chains.base-sepolia]
Expand Down
87 changes: 74 additions & 13 deletions src/chain/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ pub struct EvmProvider {
eip712_version_cache: Arc<tokio::sync::RwLock<std::collections::HashMap<Address, (String, std::time::Instant)>>>,
/// Token manager for dynamic contract selection based on token configuration
token_manager: Arc<TokenManager>,
/// Use BlockId::latest() instead of BlockId::pending() for gas estimation.
/// For chains with sub-200ms block production (e.g., Base Flashblocks).
flashblocks: bool,
}

impl EvmProvider {
Expand All @@ -240,6 +243,7 @@ impl EvmProvider {
eip1559: bool,
network: Network,
token_manager: Arc<TokenManager>,
flashblocks: bool,
) -> Result<Self, Box<dyn std::error::Error>> {
let chain = EvmChain::try_from(network)?;
let signer_addresses: Vec<Address> =
Expand Down Expand Up @@ -335,9 +339,14 @@ impl EvmProvider {
})?;

// Create RPC client with custom HTTP client
let client = RpcClient::builder()
let mut client = RpcClient::builder()
.http_with_client(http_client, url);

// Flashblocks chains have sub-200ms blocks; override Alloy's 7s default poll interval
if flashblocks {
client = client.with_poll_interval(std::time::Duration::from_millis(200));
}

// Create nonce manager explicitly so we can store a reference for error handling
let nonce_manager = PendingNonceManager::default();

Expand Down Expand Up @@ -368,6 +377,7 @@ impl EvmProvider {
nonce_manager,
eip712_version_cache: Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
token_manager,
flashblocks,
})
}

Expand Down Expand Up @@ -440,6 +450,8 @@ pub trait MetaEvmProvider {
fn eip712_cache(&self) -> &Arc<tokio::sync::RwLock<std::collections::HashMap<Address, (String, std::time::Instant)>>>;
/// Returns reference to token manager for dynamic contract selection.
fn token_manager(&self) -> &TokenManager;
/// Whether this provider targets a flashblocks chain (sub-200ms blocks).
fn flashblocks(&self) -> bool;

/// Sends a meta-transaction to the network.
fn send_transaction(
Expand Down Expand Up @@ -481,6 +493,10 @@ impl MetaEvmProvider for EvmProvider {
&self.token_manager
}

fn flashblocks(&self) -> bool {
self.flashblocks
}

/// Send a meta-transaction with provided `to`, `calldata`, and automatically selected signer.
///
/// This method constructs a transaction from the provided [`MetaTransaction`], automatically
Expand Down Expand Up @@ -563,28 +579,39 @@ impl MetaEvmProvider for EvmProvider {
"Using receipt timeout for transaction"
);

// Apply gas buffer if configured
// Apply gas buffer if configured.
// When flashblocks is enabled, always do explicit gas estimation with BlockId::latest()
// because Alloy's GasFiller uses BlockId::pending() which is unreliable on chains
// with sub-200ms block production (e.g., Base Flashblocks).
let gas_buffer = config
.as_ref()
.map(|c| c.transaction.gas_buffer_for_network(&network_str))
.unwrap_or(1.0);

if gas_buffer > 1.0 {
if gas_buffer > 1.0 || self.flashblocks {
let block_id = if self.flashblocks {
BlockId::latest()
} else {
BlockId::pending()
};
let estimated_gas = self
.inner
.estimate_gas(txr.clone())
.block(block_id)
.await
.map_err(|e| {
FacilitatorLocalError::ContractCall(format!("Gas estimation failed: {e:?}"))
})?;

let buffered_gas = (estimated_gas as f64 * gas_buffer) as u64;
let effective_buffer = if gas_buffer > 1.0 { gas_buffer } else { 1.0 };
let buffered_gas = (estimated_gas as f64 * effective_buffer) as u64;
tracing::debug!(
estimated_gas,
gas_buffer,
gas_buffer = effective_buffer,
buffered_gas,
flashblocks = self.flashblocks,
network = %network_str,
"Applied gas buffer to transaction"
"Applied gas estimation to transaction"
);
txr = txr.with_gas_limit(buffered_gas);
}
Expand Down Expand Up @@ -711,7 +738,13 @@ impl FromEnvByNetworkBuild for EvmProvider {
)
};

let provider = EvmProvider::try_new(wallet, &rpc_url, is_eip1559, network, token_manager).await?;
// Read flashblocks setting from chain config
let flashblocks = crate::config::FacilitatorConfig::from_env()
.ok()
.and_then(|c| c.transaction.chains.get(&network.to_string()).map(|cc| cc.flashblocks))
.unwrap_or(false);

let provider = EvmProvider::try_new(wallet, &rpc_url, is_eip1559, network, token_manager, flashblocks).await?;
Ok(Some(provider))
}
}
Expand Down Expand Up @@ -741,7 +774,7 @@ where

// Perform payment validation WITHOUT balance check (we'll batch it with signature validation)
let (contract, payment, eip712_domain) =
assert_valid_payment(self.inner(), self.chain(), payload, requirements, Some(self.eip712_cache()), true, self.token_manager()).await?;
assert_valid_payment(self.inner(), self.chain(), payload, requirements, Some(self.eip712_cache()), true, self.token_manager(), self.flashblocks()).await?;

let signed_message = SignedMessage::extract(&payment, &eip712_domain)?;
let payer = signed_message.address;
Expand Down Expand Up @@ -804,6 +837,7 @@ where
token_contract = %transfer_call.contract_address,
otel.kind = "client",
)),
self.flashblocks(),
)
.await
.map_err(|e| categorize_transport_error(e, "batched verification multicall"))?;
Expand Down Expand Up @@ -865,6 +899,7 @@ where
token_contract = %transfer_call.contract_address,
otel.kind = "client",
)),
self.flashblocks(),
)
.await
.map_err(|e| categorize_transport_error(e, "batched verification multicall"))?;
Expand Down Expand Up @@ -917,6 +952,7 @@ where
token_contract = %transfer_call.contract_address,
otel.kind = "client",
)),
self.flashblocks(),
)
.await
.map_err(|e| categorize_transport_error(e, "batched verification multicall"))?;
Expand Down Expand Up @@ -992,6 +1028,7 @@ where
token_contract = %transfer_call.contract_address,
otel.kind = "client",
)),
self.flashblocks(),
)
.await
.map_err(|e| categorize_transport_error(e, "batched verification multicall"))?;
Expand Down Expand Up @@ -1044,6 +1081,7 @@ where
token_contract = %transfer_call.contract_address,
otel.kind = "client",
)),
self.flashblocks(),
)
.await
.map_err(|e| categorize_transport_error(e, "batched verification multicall"))?;
Expand Down Expand Up @@ -1094,6 +1132,7 @@ where
token_contract = %transfer_call.contract_address,
otel.kind = "client",
)),
self.flashblocks(),
)
.await
.map_err(|e| categorize_transport_error(e, "batched verification multicall"))?;
Expand Down Expand Up @@ -1142,7 +1181,7 @@ where
let payload = &request.payment_payload;
let requirements = &request.payment_requirements;
let (contract, payment, eip712_domain) =
assert_valid_payment(self.inner(), self.chain(), payload, requirements, Some(self.eip712_cache()), false, self.token_manager()).await?;
assert_valid_payment(self.inner(), self.chain(), payload, requirements, Some(self.eip712_cache()), false, self.token_manager(), self.flashblocks()).await?;

let signed_message = SignedMessage::extract(&payment, &eip712_domain)?;
let payer = signed_message.address;
Expand Down Expand Up @@ -1378,7 +1417,7 @@ impl EvmProvider {

// Validate payment and extract contract, payment data, and EIP-712 domain
let (contract, payment, eip712_domain) =
assert_valid_payment(self.inner(), self.chain(), payload, requirements, Some(self.eip712_cache()), false, &self.token_manager).await?;
assert_valid_payment(self.inner(), self.chain(), payload, requirements, Some(self.eip712_cache()), false, &self.token_manager, self.flashblocks).await?;

let signed_message = SignedMessage::extract(&payment, &eip712_domain)?;
let payer = signed_message.address;
Expand Down Expand Up @@ -1876,17 +1915,26 @@ fn is_unsupported_pending_error<E: std::fmt::Debug>(error: &E) -> bool {
has_error
}

/// Helper function to call a contract method with automatic fallback to "latest" block tag
/// Call a contract method with automatic fallback to "latest" block tag
/// if the RPC doesn't support "pending".
///
/// Takes async blocks that produce the call results.
/// When `flashblocks` is true, skips the `try_call` (which uses the default "pending" block
/// tag) and goes directly to `retry_call` (which uses "latest"). On chains with sub-200ms
/// block production (e.g., Base Flashblocks) the "pending" state is unreliable and can cause
/// simulations to revert with stale/inconsistent state.
async fn call_with_fallback<T, E>(
try_call: impl std::future::Future<Output = Result<T, E>>,
retry_call: impl std::future::Future<Output = Result<T, E>>,
flashblocks: bool,
) -> Result<T, E>
where
E: std::fmt::Debug,
{
if flashblocks {
tracing::trace!("Flashblocks enabled, using latest block directly for contract call");
return retry_call.await;
}

match try_call.await {
Ok(result) => {
tracing::trace!("Contract call succeeded on first attempt");
Expand Down Expand Up @@ -1956,6 +2004,7 @@ async fn assert_enough_balance<P: Provider>(
token_contract: &Erc3009Contract<P>,
sender: &EvmAddress,
max_amount_required: U256,
flashblocks: bool,
) -> Result<(), FacilitatorLocalError> {
let balance = match token_contract {
Erc3009Contract::PackedBytes(packed_abi) => match packed_abi {
Expand All @@ -1982,6 +2031,7 @@ async fn assert_enough_balance<P: Provider>(
sender = %sender,
otel.kind = "client"
)),
flashblocks,
)
.await
.map_err(|e| categorize_transport_error(e, "balance query"))?
Expand Down Expand Up @@ -2011,6 +2061,7 @@ async fn assert_enough_balance<P: Provider>(
sender = %sender,
otel.kind = "client"
)),
flashblocks,
)
.await
.map_err(|e| categorize_transport_error(e, "balance query"))?
Expand Down Expand Up @@ -2038,6 +2089,7 @@ async fn assert_enough_balance<P: Provider>(
sender = %sender,
otel.kind = "client"
)),
flashblocks,
)
.await
.map_err(|e| categorize_transport_error(e, "balance query"))?
Expand Down Expand Up @@ -2130,6 +2182,7 @@ async fn assert_domain<P: Provider>(
asset_address: &Address,
requirements: &PaymentRequirements,
version_cache: Option<&Arc<tokio::sync::RwLock<std::collections::HashMap<Address, (String, std::time::Instant)>>>>,
flashblocks: bool,
) -> Result<Eip712Domain, FacilitatorLocalError> {
let usdc = USDCDeployment::by_network(payload.network);
let name = requirements
Expand Down Expand Up @@ -2189,6 +2242,7 @@ async fn assert_domain<P: Provider>(
"fetch_eip712_version",
otel.kind = "client",
)),
flashblocks,
)
.await
.map_err(|e| categorize_transport_error(e, "fetch EIP-712 version"))?
Expand All @@ -2214,6 +2268,7 @@ async fn assert_domain<P: Provider>(
"fetch_eip712_domain",
otel.kind = "client",
)),
flashblocks,
)
.await
.map_err(|e| categorize_transport_error(e, "fetch EIP-712 domain"))?;
Expand All @@ -2238,6 +2293,7 @@ async fn assert_domain<P: Provider>(
"fetch_eip712_domain",
otel.kind = "client",
)),
flashblocks,
)
.await
.map_err(|e| categorize_transport_error(e, "fetch EIP-712 domain"))?;
Expand Down Expand Up @@ -2273,6 +2329,7 @@ async fn assert_domain<P: Provider>(
"fetch_eip712_version",
otel.kind = "client",
)),
flashblocks,
)
.await
.map_err(|e| categorize_transport_error(e, "fetch EIP-712 version"))?
Expand All @@ -2298,6 +2355,7 @@ async fn assert_domain<P: Provider>(
"fetch_eip712_domain",
otel.kind = "client",
)),
flashblocks,
)
.await
.map_err(|e| categorize_transport_error(e, "fetch EIP-712 domain"))?;
Expand All @@ -2322,6 +2380,7 @@ async fn assert_domain<P: Provider>(
"fetch_eip712_domain",
otel.kind = "client",
)),
flashblocks,
)
.await
.map_err(|e| categorize_transport_error(e, "fetch EIP-712 domain"))?;
Expand Down Expand Up @@ -2409,6 +2468,7 @@ async fn assert_valid_payment<P: Provider + Clone>(
version_cache: Option<&Arc<tokio::sync::RwLock<std::collections::HashMap<Address, (String, std::time::Instant)>>>>,
skip_balance_check: bool,
token_manager: &TokenManager,
flashblocks: bool,
) -> Result<(Erc3009Contract<P>, ExactEvmPayment, Eip712Domain), FacilitatorLocalError> {
let payment_payload = match &payload.payload {
ExactPaymentPayload::Evm(payload) => payload,
Expand Down Expand Up @@ -2497,14 +2557,15 @@ async fn assert_valid_payment<P: Provider + Clone>(
create_erc3009_contract(crate::tokens::SignatureFormat::PackedBytes, "abi/USDC.json", asset_address, provider.clone())
};

let domain = assert_domain(chain, &contract, payload, &asset_address, requirements, version_cache).await?;
let domain = assert_domain(chain, &contract, payload, &asset_address, requirements, version_cache, flashblocks).await?;

let amount_required = requirements.max_amount_required.0;
if !skip_balance_check {
assert_enough_balance(
&contract,
&payment_payload.authorization.from,
amount_required,
flashblocks,
)
.await?;
}
Expand Down
32 changes: 32 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ pub struct ChainConfig {
/// If not set, uses the global transaction.gas_buffer.
#[serde(default)]
pub gas_buffer: Option<f64>,
/// Use `BlockId::latest()` instead of `BlockId::pending()` for gas estimation.
/// Enable for chains with sub-200ms block production (e.g., Base Flashblocks)
/// where the "pending" block state is unreliable.
#[serde(default)]
pub flashblocks: bool,
}

impl ChainConfig {
Expand Down Expand Up @@ -661,4 +666,31 @@ enabled = false
let config4: FacilitatorConfig = toml::from_str(config4_str).unwrap();
assert!(!config4.batch_settlement.is_enabled_anywhere());
}

#[test]
fn test_flashblocks_defaults_to_false() {
let config_str = r#"
[transaction.chains.base]
block_time_seconds = 2
receipt_timeout_blocks = 30
rpc_request_timeout_seconds = 20
"#;
let config: FacilitatorConfig = toml::from_str(config_str).unwrap();
let base = config.transaction.chains.get("base").unwrap();
assert!(!base.flashblocks);
}

#[test]
fn test_flashblocks_parses_true() {
let config_str = r#"
[transaction.chains.base]
block_time_seconds = 2
receipt_timeout_blocks = 30
rpc_request_timeout_seconds = 20
flashblocks = true
"#;
let config: FacilitatorConfig = toml::from_str(config_str).unwrap();
let base = config.transaction.chains.get("base").unwrap();
assert!(base.flashblocks);
}
}