@@ -27,7 +27,11 @@ use apollo_l1_gas_price_types::errors::{
2727 L1GasPriceClientError ,
2828 L1GasPriceProviderError ,
2929} ;
30- use apollo_l1_gas_price_types:: { MockL1GasPriceProviderClient , PriceInfo } ;
30+ use apollo_l1_gas_price_types:: {
31+ MockExchangeRateOracleClientTrait ,
32+ MockL1GasPriceProviderClient ,
33+ PriceInfo ,
34+ } ;
3135use apollo_protobuf:: consensus:: {
3236 BuildParam ,
3337 CommitmentParts ,
@@ -89,6 +93,7 @@ fn expected_l2_gas_info_for_build_proposal_defaults() -> L2GasInfo {
8993 l2_gas_used : GasAmount ( 0 ) ,
9094 }
9195}
96+ use crate :: snip35:: { compute_fee_actual, FEE_PROPOSAL_WINDOW_SIZE } ;
9297use crate :: utils:: { apply_fee_transformations, make_gas_price_params} ;
9398
9499const TEST_PROPOSAL_COMMITMENT : ProposalCommitment = ProposalCommitment ( PARTIAL_BLOCK_HASH . 0 ) ;
@@ -1601,3 +1606,137 @@ async fn test_initialize_fee_proposals_window(
16011606 context. initialize_fee_proposals_window ( start_height) . await . unwrap ( ) ;
16021607 assert_eq ! ( context. fee_proposals_window, expected_window) ;
16031608}
1609+
1610+ #[ derive( Clone ) ]
1611+ enum OracleBehavior {
1612+ /// No oracle is configured (`strk_to_usd_oracle = None`).
1613+ NotConfigured ,
1614+ /// Oracle is configured and `fetch_rate` returns `Ok(rate)`.
1615+ Ok ( u128 ) ,
1616+ /// Oracle is configured and `fetch_rate` returns `Err(_)`.
1617+ Err ,
1618+ }
1619+
1620+ // fee_actual = 10 gwei => margin bounds [9_980_039_920, 10_020_000_000].
1621+ #[ rstest]
1622+ #[ case:: no_fee_actual_freezes_at_l2_gas_price(
1623+ None ,
1624+ OracleBehavior :: NotConfigured ,
1625+ GasPrice ( 7_000_000_000 ) ,
1626+ GasPrice ( 7_000_000_000 )
1627+ ) ]
1628+ #[ case:: no_oracle_freezes_at_fee_actual(
1629+ Some ( GasPrice ( 10_000_000_000 ) ) ,
1630+ OracleBehavior :: NotConfigured ,
1631+ GasPrice ( 7_000_000_000 ) ,
1632+ GasPrice ( 10_000_000_000 )
1633+ ) ]
1634+ #[ case:: oracle_zero_rate_freezes_at_fee_actual(
1635+ Some ( GasPrice ( 10_000_000_000 ) ) ,
1636+ OracleBehavior :: Ok ( 0 ) ,
1637+ GasPrice ( 7_000_000_000 ) ,
1638+ GasPrice ( 10_000_000_000 )
1639+ ) ]
1640+ #[ case:: oracle_err_freezes_at_fee_actual(
1641+ Some ( GasPrice ( 10_000_000_000 ) ) ,
1642+ OracleBehavior :: Err ,
1643+ GasPrice ( 7_000_000_000 ) ,
1644+ GasPrice ( 10_000_000_000 )
1645+ ) ]
1646+ #[ case:: oracle_target_in_bounds_returns_target(
1647+ Some ( GasPrice ( 10_000_000_000 ) ) ,
1648+ OracleBehavior :: Ok ( 300_000_000_000_000_000 ) ,
1649+ GasPrice ( 7_000_000_000 ) ,
1650+ GasPrice ( 10_000_000_000 )
1651+ ) ]
1652+ #[ case:: oracle_target_above_clamps_to_upper(
1653+ Some ( GasPrice ( 10_000_000_000 ) ) ,
1654+ OracleBehavior :: Ok ( 1 ) ,
1655+ GasPrice ( 7_000_000_000 ) ,
1656+ GasPrice ( 10_020_000_000 )
1657+ ) ]
1658+ #[ case:: oracle_target_below_clamps_to_lower(
1659+ Some ( GasPrice ( 10_000_000_000 ) ) ,
1660+ OracleBehavior :: Ok ( 1_000_000_000_000_000_000_000 ) ,
1661+ GasPrice ( 7_000_000_000 ) ,
1662+ GasPrice ( 9_980_039_920 )
1663+ ) ]
1664+ #[ tokio:: test]
1665+ async fn test_compute_snip35_fee_proposal (
1666+ #[ case] fee_actual : Option < GasPrice > ,
1667+ #[ case] oracle_behavior : OracleBehavior ,
1668+ #[ case] l2_gas_price : GasPrice ,
1669+ #[ case] expected_fee_proposal : GasPrice ,
1670+ ) {
1671+ let ( mut deps, _network) = create_test_and_network_deps ( ) ;
1672+ deps. setup_default_expectations ( ) ;
1673+ deps. strk_to_usd_oracle = match oracle_behavior {
1674+ OracleBehavior :: NotConfigured => None ,
1675+ OracleBehavior :: Ok ( rate) => {
1676+ let mut mock = MockExchangeRateOracleClientTrait :: new ( ) ;
1677+ mock. expect_fetch_rate ( ) . returning ( move |_| Ok ( rate) ) ;
1678+ Some ( Arc :: new ( mock) )
1679+ }
1680+ OracleBehavior :: Err => {
1681+ let mut mock = MockExchangeRateOracleClientTrait :: new ( ) ;
1682+ mock. expect_fetch_rate ( ) . returning ( |_| {
1683+ Err ( ExchangeRateOracleClientError :: RequestError ( "test" . to_string ( ) ) )
1684+ } ) ;
1685+ Some ( Arc :: new ( mock) )
1686+ }
1687+ } ;
1688+
1689+ let mut context = deps. build_context ( ) ;
1690+ context. l2_gas_price = l2_gas_price;
1691+ let proposal = context. compute_snip35_fee_proposal ( fee_actual, 0 ) . await ;
1692+ assert_eq ! ( proposal, expected_fee_proposal) ;
1693+ }
1694+
1695+ #[ tokio:: test]
1696+ async fn test_compute_snip35_fee_proposal_converges_to_oracle_target ( ) {
1697+ // (strk_usd_rate, fee_target, n_blocks_until_convergence_with_buffer).
1698+ // 75 gwei bootstrap -> 100 gwei target at +33% reaches by block ~795.
1699+ // 100 gwei -> 60 gwei at -40% reaches by block ~1410.
1700+ let phases: [ ( u128 , GasPrice , u64 ) ; 2 ] = [
1701+ ( 30_000_000_000_000_000 , GasPrice ( 100_000_000_000 ) , 800 ) ,
1702+ ( 50_000_000_000_000_000 , GasPrice ( 60_000_000_000 ) , 1420 ) ,
1703+ ] ;
1704+
1705+ let ( mut deps, _network) = create_test_and_network_deps ( ) ;
1706+ deps. setup_default_expectations ( ) ;
1707+ let mut mock = MockExchangeRateOracleClientTrait :: new ( ) ;
1708+ let mut seq = mockall:: Sequence :: new ( ) ;
1709+ for & ( rate, _, n_blocks) in & phases {
1710+ mock. expect_fetch_rate ( )
1711+ . times ( usize:: try_from ( n_blocks) . unwrap ( ) )
1712+ . in_sequence ( & mut seq)
1713+ . returning ( move |_| Ok ( rate) ) ;
1714+ }
1715+ deps. strk_to_usd_oracle = Some ( Arc :: new ( mock) ) ;
1716+ let mut context = deps. build_context ( ) ;
1717+
1718+ // Bootstrap the window with 75 gwei (the $0.04 target).
1719+ let window_size = u64:: try_from ( FEE_PROPOSAL_WINDOW_SIZE ) . unwrap ( ) ;
1720+ for h in 0 ..window_size {
1721+ context. record_fee_proposal ( BlockNumber ( h) , Some ( GasPrice ( 75_000_000_000 ) ) ) ;
1722+ }
1723+
1724+ let mut height = window_size;
1725+ for ( phase_idx, ( _, fee_target, n_blocks) ) in phases. into_iter ( ) . enumerate ( ) {
1726+ for _ in 0 ..n_blocks {
1727+ let h = BlockNumber ( height) ;
1728+ let fee_actual = compute_fee_actual ( & context. fee_proposals_window , h)
1729+ . expect ( "window stays complete across the loop" ) ;
1730+ let proposal = context. compute_snip35_fee_proposal ( Some ( fee_actual) , 0 ) . await ;
1731+ context. record_fee_proposal ( h, Some ( proposal) ) ;
1732+ height += 1 ;
1733+ }
1734+ let final_fee_actual =
1735+ compute_fee_actual ( & context. fee_proposals_window , BlockNumber ( height) )
1736+ . expect ( "window stays complete across the loop" ) ;
1737+ assert_eq ! (
1738+ final_fee_actual, fee_target,
1739+ "phase {phase_idx}: fee_actual did not reach fee_target after {n_blocks} blocks" ,
1740+ ) ;
1741+ }
1742+ }
0 commit comments