diff --git a/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context_test.rs b/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context_test.rs index d2e0a8c6a4c..791c684aee4 100644 --- a/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context_test.rs +++ b/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context_test.rs @@ -26,7 +26,11 @@ use apollo_l1_gas_price_types::errors::{ L1GasPriceClientError, L1GasPriceProviderError, }; -use apollo_l1_gas_price_types::{MockL1GasPriceProviderClient, PriceInfo}; +use apollo_l1_gas_price_types::{ + MockExchangeRateOracleClientTrait, + MockL1GasPriceProviderClient, + PriceInfo, +}; use apollo_protobuf::consensus::{ BuildParam, CommitmentParts, @@ -89,6 +93,15 @@ fn expected_l2_gas_info_for_build_proposal_defaults() -> L2GasInfo { l2_gas_used: GasAmount(0), } } + +use crate::snip35::{ + compute_fee_proposal, + compute_fee_target, + FEE_PROPOSAL_MARGIN_PPT, + ORACLE_L2_GAS_FLOOR_MAX_FRI, + ORACLE_L2_GAS_FLOOR_MIN_FRI, + TARGET_ATTO_USD_PER_L2_GAS, +}; use crate::utils::{apply_fee_transformations, make_gas_price_params}; static TEST_PROPOSAL_COMMITMENT: LazyLock = LazyLock::new(|| { @@ -1544,3 +1557,57 @@ async fn test_first_height_keeps_sync_provided_l2_gas_price() { expected_price, context.l2_gas_price.0 ); } + +/// E2E test: SNIP-35 oracle → fee_proposal → ProposalInit → validate → decision_reached. +/// Verifies the full flow from STRK/USD oracle price to a fee_proposal that appears in the +/// built proposal and passes validation. +#[tokio::test] +async fn snip35_fee_proposal_e2e_flow() { + // STRK/USD rate: $0.50 with 18 decimals. + const STRK_USD_RATE: u128 = 500_000_000_000_000_000; + + let (mut deps, mut network) = create_test_and_network_deps(); + deps.setup_deps_for_build(SetupDepsArgs::default()); + + // Create a mock oracle that returns the known STRK/USD rate. + let mut mock_oracle = MockExchangeRateOracleClientTrait::new(); + mock_oracle.expect_fetch_rate().returning(|_| Ok(STRK_USD_RATE)); + deps.strk_to_usd_oracle = Some(Arc::new(mock_oracle)); + + // Build context (SNIP-35 is always enabled with hardcoded constants). + let mut context = deps.build_context(); + + // Build a proposal. + context.set_height_and_round(BlockNumber(0), 0).await.unwrap(); + let _fin_receiver = context.build_proposal(BuildParam::default(), TIMEOUT).await.unwrap(); + + // Extract ProposalInit from the network. + let (_, mut receiver) = network.outbound_proposal_receiver.next().await.unwrap(); + let ProposalPart::Init(init) = receiver.next().await.unwrap() else { + panic!("Expected ProposalPart::Init"); + }; + + // Compute the expected fee_proposal. + // During initiation (< 10 blocks), fee_actual falls back to l2_gas_price (min_gas_price). + let min_gas_price = VersionedConstants::latest_constants().min_gas_price; + let fee_target = compute_fee_target( + TARGET_ATTO_USD_PER_L2_GAS, + STRK_USD_RATE, + ORACLE_L2_GAS_FLOOR_MIN_FRI, + ORACLE_L2_GAS_FLOOR_MAX_FRI, + ); + let expected_fee_proposal = + compute_fee_proposal(Some(fee_target), min_gas_price, FEE_PROPOSAL_MARGIN_PPT); + + assert_eq!( + init.fee_proposal_fri, + Some(expected_fee_proposal), + "fee_proposal in ProposalInit should match: fee_target={fee_target:?}, \ + min_gas_price={min_gas_price:?}, expected={expected_fee_proposal:?}" + ); + // Sanity: fee_proposal should be nonzero. + assert!( + init.fee_proposal_fri.expect("fee_proposal must be Some at V0_14_3+").0 > 0, + "fee_proposal should be nonzero when oracle is active" + ); +}