1use alloy_primitives::{address, b256, Address, B256, U256};
2use alloy_provider::{network::Ethereum, Provider};
3use alloy_sol_types::SolCall;
4use anyhow::{anyhow, Result};
5use wp_evm_base::{chain::Chain, types::SlippageBps};
6use wp_evm_ramses_provider as ramses;
7use wp_evm_ramses_provider::data::RamsesProtocolConfig;
8use wp_evm_v3_provider::plan::PeripherySelectors;
9use wp_evm_velodrome_interfaces::periphery::router::ISlipstreamPeripheryRouter;
10
11const SELECTORS: PeripherySelectors = PeripherySelectors {
16 multicall: ISlipstreamPeripheryRouter::multicallCall::SELECTOR,
17 unwrap_native: ISlipstreamPeripheryRouter::unwrapWETH9Call::SELECTOR,
18 sweep_token: ISlipstreamPeripheryRouter::sweepTokenCall::SELECTOR,
19 refund_native: ISlipstreamPeripheryRouter::refundETHCall::SELECTOR,
20};
21
22pub use wp_evm_ramses_provider::data::{
23 CollectFeesParams, ExactInParams, ExactOutParams, PlanFragment, PoolState, PositionState,
24 Quote, RamsesAddLiquidityParams, RemoveAndCollectParams, RemoveLiquidityParams,
25};
26pub use wp_evm_ramses_provider::position::{
27 position_key, RamsesPositionView, VelodromePositionRow,
28};
29pub use wp_evm_ramses_provider::position_views::{PositionFees, PositionViewEntry};
30pub use wp_evm_ramses_provider::quote::QuoteError;
31pub use wp_evm_ramses_provider::Enumeration;
32
33pub const CONFIG: RamsesProtocolConfig = RamsesProtocolConfig {
34 factory: address!("5e7BB104d84c7CB9B682AaC2F3d509f5F406809A"),
35 pool_deployer: Address::ZERO,
36 router: address!("BE6D8f0d05cC4be24d5167a3eF062215bE6D18a5"),
37 position_mgr: address!("827922686190790b37229fd06084350E74485b72"),
38 init_code_hash: b256!("ffb9af9ea6d9e39da47392ecc7055277b9915b8bfc9f83f105821b7791a6ae30"),
39 tick_spacings: &[1, 50, 100, 200, 2000],
40 multicall: address!("cA11bde05977b3631167028862bE2a173976CA11"),
41 quoter: Some(address!("254cF9E1E6e233aa1AC962CB9B05b2cfeAaE15b0")),
42 voter: address!("16613524e02ad97eDfeF371bC883F2F5d6C480A5"),
43};
44
45pub fn config_for_chain(chain: Chain) -> Option<&'static RamsesProtocolConfig> {
46 match chain {
47 Chain::Base => Some(&CONFIG),
48 Chain::Ethereum
49 | Chain::Arbitrum
50 | Chain::Optimism
51 | Chain::Polygon
52 | Chain::Bsc
53 | Chain::Sonic
54 | Chain::HyperEvm
55 | Chain::Avalanche
56 | Chain::Celo => None,
57 }
58}
59
60pub fn factory(chain: Chain) -> Option<Address> {
61 config_for_chain(chain).map(|c| c.factory)
62}
63pub fn pool_deployer(chain: Chain) -> Option<Address> {
64 config_for_chain(chain).map(|c| c.pool_deployer)
65}
66pub fn position_manager(chain: Chain) -> Option<Address> {
67 config_for_chain(chain).map(|c| c.position_mgr)
68}
69pub fn router(chain: Chain) -> Option<Address> {
70 config_for_chain(chain).map(|c| c.router)
71}
72pub fn quoter(chain: Chain) -> Option<Address> {
73 config_for_chain(chain).and_then(|c| c.quoter)
74}
75pub fn multicall(chain: Chain) -> Option<Address> {
76 config_for_chain(chain).map(|c| c.multicall)
77}
78pub fn init_code_hash(chain: Chain) -> Option<B256> {
79 config_for_chain(chain).map(|c| c.init_code_hash)
80}
81pub fn voter(chain: Chain) -> Option<Address> {
82 config_for_chain(chain).map(|c| c.voter)
83}
84pub fn supports(chain: Chain) -> bool {
85 config_for_chain(chain).is_some()
86}
87
88pub async fn pool_state<P: Provider<Ethereum>>(
89 provider: &P,
90 chain: Chain,
91 pool: Address,
92) -> Result<PoolState> {
93 let cfg =
94 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
95 ramses::hydrate::pool_state_velodrome(provider, cfg.multicall, pool).await
98}
99
100pub async fn position_state<P: Provider<Ethereum>>(
101 provider: &P,
102 chain: Chain,
103 token_id: U256,
104) -> Result<PositionState> {
105 let cfg =
106 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
107 ramses::hydrate::position_state_slipstream(provider, cfg.multicall, cfg.position_mgr, token_id)
108 .await
109}
110
111pub async fn position_views<P: Provider<Ethereum>>(
112 provider: &P,
113 chain: Chain,
114 token_ids: &[U256],
115) -> Result<Vec<PositionViewEntry<VelodromePositionRow>>> {
116 let cfg =
117 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
118 ramses::position_views::velodrome_position_views(
119 provider,
120 cfg.multicall,
121 cfg.position_mgr,
122 token_ids,
123 )
124 .await
125}
126
127pub async fn position_views_with_nfpm<P: Provider<Ethereum>>(
128 provider: &P,
129 chain: Chain,
130 nfpm: Address,
131 token_ids: &[U256],
132) -> Result<Vec<PositionViewEntry<VelodromePositionRow>>> {
133 let multicall =
134 config_for_chain(chain).map(|cfg| cfg.multicall).unwrap_or(ramses::MULTICALL3_ADDRESS);
135 ramses::position_views::velodrome_position_views(provider, multicall, nfpm, token_ids).await
136}
137
138pub async fn enumerate_owner_token_ids<P: Provider<Ethereum>>(
139 provider: &P,
140 chain: Chain,
141 nfpm: Address,
142 owner: Address,
143) -> Result<Enumeration> {
144 let multicall =
145 config_for_chain(chain).map(|cfg| cfg.multicall).unwrap_or(ramses::MULTICALL3_ADDRESS);
146 ramses::enumerate_owner_token_ids(provider, multicall, nfpm, owner, chain).await
147}
148
149pub async fn populate_positions_fees<P: Provider<Ethereum>>(
150 provider: &P,
151 chain: Chain,
152 entries: &mut [PositionViewEntry<VelodromePositionRow>],
153) -> Result<()> {
154 let cfg =
155 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
156 ramses::position_views::velodrome_populate_position_fees(
157 provider,
158 cfg.multicall,
159 entries,
160 |v| pool_address(chain, v.token0, v.token1, v.tick_spacing, None),
161 )
162 .await
163}
164
165pub fn quote_exact_in(s: &PoolState, p: &ExactInParams) -> Result<Quote, QuoteError> {
166 ramses::quote::exact_in(s, p)
167}
168pub fn quote_exact_out(s: &PoolState, p: &ExactOutParams) -> Result<Quote, QuoteError> {
169 ramses::quote::exact_out(s, p)
170}
171
172pub async fn populate_ticks<P: Provider<Ethereum>>(
173 provider: &P,
174 chain: Chain,
175 pool: Address,
176 state: &mut PoolState,
177) -> Result<()> {
178 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
179 ramses::populate_ticks::populate_ticks(provider, pool, state).await
180}
181
182pub async fn quote_online_exact_in<P: Provider<Ethereum>>(
183 provider: &P,
184 chain: Chain,
185 state: &PoolState,
186 params: &ExactInParams,
187) -> Result<Quote> {
188 let cfg =
189 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
190 let quoter =
191 cfg.quoter.ok_or_else(|| anyhow!("Aerodrome quoter not registered on {chain:?}"))?;
192 ramses::quote_online::quote_online_exact_in(provider, quoter, state, params).await
193}
194
195pub fn plan_swap_exact_in(
196 s: &PoolState,
197 q: &Quote,
198 p: &ExactInParams,
199 slippage: SlippageBps,
200 deadline: u64,
201 chain: Chain,
202) -> Result<PlanFragment> {
203 let cfg =
204 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
205 Ok(ramses::plan::swap_exact_in(s, q, p, slippage, deadline, cfg.router))
206}
207
208pub fn plan_add_liquidity(
209 p: &RamsesAddLiquidityParams,
210 slippage: SlippageBps,
211 deadline: u64,
212 chain: Chain,
213) -> Result<PlanFragment> {
214 let cfg =
215 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
216 Ok(ramses::plan::add_liquidity_slipstream(p, slippage, deadline, cfg.position_mgr))
217}
218
219pub fn plan_add_liquidity_with_min(
224 p: &RamsesAddLiquidityParams,
225 amount0_min: U256,
226 amount1_min: U256,
227 deadline: u64,
228 chain: Chain,
229) -> Result<PlanFragment> {
230 let cfg =
231 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
232 Ok(ramses::plan::add_liquidity_slipstream_with_min(
233 p,
234 amount0_min,
235 amount1_min,
236 deadline,
237 cfg.position_mgr,
238 ))
239}
240
241#[allow(clippy::too_many_arguments)]
242pub fn plan_increase_liquidity(
243 token_id: U256,
244 token0: Address,
245 token1: Address,
246 amount0_desired: U256,
247 amount1_desired: U256,
248 slippage: SlippageBps,
249 deadline: u64,
250 chain: Chain,
251) -> Result<PlanFragment> {
252 let cfg =
253 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
254 Ok(ramses::plan::increase_liquidity(
255 token_id,
256 token0,
257 token1,
258 amount0_desired,
259 amount1_desired,
260 slippage,
261 deadline,
262 cfg.position_mgr,
263 ))
264}
265
266pub fn plan_remove_liquidity(
267 p: &RemoveLiquidityParams,
268 deadline: u64,
269 chain: Chain,
270) -> Result<PlanFragment> {
271 let cfg =
272 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
273 Ok(ramses::plan::remove_liquidity(p, deadline, cfg.position_mgr))
274}
275
276pub fn plan_remove_liquidity_and_collect(
290 p: &RemoveAndCollectParams,
291 deadline: u64,
292 chain: Chain,
293) -> Result<PlanFragment> {
294 let cfg =
295 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
296
297 let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_remove_and_collect(
298 p,
299 cfg.position_mgr,
300 chain,
301 )?;
302
303 let mut core_params = (*p).clone();
304 core_params.recipient = wrap.effective_collect_recipient;
305
306 let frag = ramses::plan::remove_liquidity_and_collect(&core_params, deadline, cfg.position_mgr);
307 wp_evm_v3_provider::plan::compose_native_remove_collect_multicall(frag, &wrap, SELECTORS)
308}
309
310pub fn plan_collect_fees(p: &CollectFeesParams, chain: Chain) -> Result<PlanFragment> {
311 let cfg =
312 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
313
314 let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_collect(p, cfg.position_mgr, chain)?;
325
326 let core_params = CollectFeesParams {
332 token_id: p.token_id,
333 recipient: wrap.effective_recipient,
334 token0: p.token0,
335 token1: p.token1,
336 caller: p.caller,
337 };
338
339 let frag = ramses::plan::collect_fees(&core_params, cfg.position_mgr);
340 Ok(wp_evm_v3_provider::plan::compose_native_collect_multicall(frag, &wrap, SELECTORS))
341}
342
343pub fn pool_address(
344 chain: Chain,
345 token_a: Address,
346 token_b: Address,
347 tick_spacing: i32,
348 init_code_hash_override: Option<B256>,
349) -> Option<Address> {
350 let cfg = config_for_chain(chain)?;
351 let init_code_hash = init_code_hash_override.unwrap_or(cfg.init_code_hash);
352 Some(ramses::pool_address::compute(cfg.factory, init_code_hash, token_a, token_b, tick_spacing))
353}
354
355pub async fn pending_emissions<P: Provider<Ethereum>>(
356 provider: &P,
357 chain: Chain,
358 pool: Address,
359 account: Address,
360 token_id: U256,
361) -> Result<Option<wp_evm_ramses_provider::velodrome_gauge::VelodromePendingEmissions>> {
362 let cfg =
363 config_for_chain(chain).ok_or_else(|| anyhow!("Aerodrome not deployed on {chain:?}"))?;
364 wp_evm_ramses_provider::velodrome_gauge::pending_emissions(
365 provider,
366 cfg.multicall,
367 cfg.voter,
368 pool,
369 account,
370 token_id,
371 )
372 .await
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn config_for_chain_returns_some_for_base_only() {
381 assert_eq!(config_for_chain(Chain::Base), Some(&CONFIG));
382 for unsupported in [
383 Chain::Ethereum,
384 Chain::Arbitrum,
385 Chain::Optimism,
386 Chain::Polygon,
387 Chain::Bsc,
388 Chain::Sonic,
389 Chain::Avalanche,
390 Chain::Celo,
391 ] {
392 assert!(
393 config_for_chain(unsupported).is_none(),
394 "aerodrome should not surface {unsupported:?}",
395 );
396 }
397 }
398
399 #[test]
404 fn plan_add_liquidity_with_min_threads_precomputed_mins_at_position_manager() {
405 let p = RamsesAddLiquidityParams {
406 token0: address!("4200000000000000000000000000000000000006"),
407 token1: address!("833589fCD6eDb6E08f4c7C32D4f71b54bda02913"),
408 tick_spacing: 100,
409 tick_lower: -887_200,
410 tick_upper: 887_200,
411 amount0_desired: U256::from(1_000_000u64),
412 amount1_desired: U256::from(500_000_000_000_000u64),
413 recipient: address!("0000000000000000000000000000000000000099"),
414 };
415 let m0 = U256::from(123_456u64);
416 let m1 = U256::from(789_012u64);
417 let frag = plan_add_liquidity_with_min(&p, m0, m1, 9_999_999_999, Chain::Base)
418 .expect("Base supported");
419 assert_eq!(frag.calls.len(), 1);
420 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
421 assert_eq!(frag.approvals.len(), 2);
422
423 let bare = ramses::plan::add_liquidity_slipstream_with_min(
424 &p,
425 m0,
426 m1,
427 9_999_999_999,
428 CONFIG.position_mgr,
429 );
430 assert_eq!(frag.calls[0].calldata, bare.calls[0].calldata);
431 let other = ramses::plan::add_liquidity_slipstream_with_min(
432 &p,
433 m0 + U256::from(1u64),
434 m1,
435 9_999_999_999,
436 CONFIG.position_mgr,
437 );
438 assert_ne!(frag.calls[0].calldata, other.calls[0].calldata);
439 }
440
441 #[test]
442 fn pool_address_matches_canonical_aerodrome_weth_usdc_cl100() {
443 let pool = pool_address(
444 Chain::Base,
445 address!("4200000000000000000000000000000000000006"),
446 address!("833589fCD6eDb6E08f4c7C32D4f71b54bda02913"),
447 100,
448 None,
449 )
450 .expect("Base supported");
451 assert_eq!(pool, address!("b2cc224c1c9feE385f8ad6a55b4d94E92359DC59"));
452 }
453
454 #[test]
455 fn pool_address_token_order_independent() {
456 let weth = address!("4200000000000000000000000000000000000006");
457 let usdc = address!("833589fCD6eDb6E08f4c7C32D4f71b54bda02913");
458 assert_eq!(
459 pool_address(Chain::Base, weth, usdc, 100, None),
460 pool_address(Chain::Base, usdc, weth, 100, None)
461 );
462 }
463
464 #[test]
465 fn plan_collect_fees_native_recipient_emits_multicall_with_unwrap_and_sweep() {
466 let params = CollectFeesParams {
470 token_id: U256::from(1u64),
471 recipient: Address::ZERO,
472 token0: address!("4200000000000000000000000000000000000006"), token1: address!("833589fCD6eDb6E08f4c7C32D4f71b54bda02913"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
475 };
476 let frag = plan_collect_fees(¶ms, Chain::Base).expect("Base supported");
477
478 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
479 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
480 assert_eq!(frag.value, U256::ZERO);
481 assert_eq!(frag.calls[0].value, U256::ZERO);
482 assert!(
483 frag.calls[0].calldata.windows(4).any(|w| w == [0x49, 0x40, 0x4b, 0x7c]),
484 "native collect multicall must include unwrapWETH9(uint256,address) tail"
485 );
486 assert!(
487 frag.calls[0].calldata.windows(4).any(|w| w == [0xdf, 0x2a, 0xb5, 0xbb]),
488 "native collect multicall must include sweepToken(address,uint256,address) tail"
489 );
490 }
491
492 #[test]
493 fn plan_collect_fees_non_native_recipient_passthrough() {
494 let params = CollectFeesParams {
495 token_id: U256::from(1u64),
496 recipient: address!("0000000000000000000000000000000000000099"),
497 token0: address!("4200000000000000000000000000000000000006"),
498 token1: address!("833589fCD6eDb6E08f4c7C32D4f71b54bda02913"),
499 caller: Address::ZERO,
500 };
501 let frag = plan_collect_fees(¶ms, Chain::Base).expect("Base supported");
502 let bare = ramses::plan::collect_fees(¶ms, CONFIG.position_mgr);
503
504 assert_ne!(
505 &frag.calls[0].calldata[..4],
506 &[0xac, 0x96, 0x50, 0xd8],
507 "non-native case must NOT be wrapped in multicall"
508 );
509 assert_eq!(
510 frag.calls[0].calldata, bare.calls[0].calldata,
511 "non-native pass-through must stay byte-identical to bare collect()"
512 );
513 }
514
515 #[test]
516 fn plan_collect_fees_no_native_side_rejects() {
517 let params = CollectFeesParams {
520 token_id: U256::from(1u64),
521 recipient: Address::ZERO,
522 token0: address!("833589fCD6eDb6E08f4c7C32D4f71b54bda02913"), token1: address!("50c5725949A6F0c72E6C4a641F24049A917DB0Cb"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
525 };
526 let err = plan_collect_fees(¶ms, Chain::Base).unwrap_err();
527 let msg = format!("{err:#}");
528 assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
529 }
530
531 fn fixture_remove_and_collect_params_weth_paired() -> RemoveAndCollectParams {
537 RemoveAndCollectParams {
538 token_id: U256::from(1u64),
539 liquidity: 1_000_000u128,
540 amount0_min: Some(U256::from(100u64)),
541 amount1_min: Some(U256::from(200u64)),
542 recipient: Address::ZERO,
543 token0: address!("4200000000000000000000000000000000000006"), token1: address!("833589fCD6eDb6E08f4c7C32D4f71b54bda02913"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
546 burn: false,
547 }
548 }
549
550 #[test]
551 fn plan_remove_liquidity_and_collect_native_recipient_emits_4_call_multicall() {
552 let params = fixture_remove_and_collect_params_weth_paired();
554 let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Base)
555 .expect("Base supported");
556
557 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
558 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
559 assert_eq!(frag.value, U256::ZERO);
560 assert!(frag.approvals.is_empty());
561
562 use alloy_sol_types::SolValue;
563 let (inner,): (Vec<alloy_primitives::Bytes>,) =
564 <(Vec<alloy_primitives::Bytes>,)>::abi_decode_params(&frag.calls[0].calldata[4..])
565 .expect("decode outer multicall params");
566 assert_eq!(inner.len(), 4, "expected 4 inner calls");
567 assert_eq!(&inner[0][..4], &[0x0c, 0x49, 0xcc, 0xbe], "inner[0] = decreaseLiquidity");
568 assert_eq!(&inner[1][..4], &[0xfc, 0x6f, 0x78, 0x65], "inner[1] = collect");
569 assert_eq!(&inner[2][..4], &[0x49, 0x40, 0x4b, 0x7c], "inner[2] = unwrapWETH9");
570 assert_eq!(&inner[3][..4], &[0xdf, 0x2a, 0xb5, 0xbb], "inner[3] = sweepToken");
571 }
572
573 #[test]
574 fn plan_remove_liquidity_and_collect_non_native_recipient_passthrough() {
575 let mut params = fixture_remove_and_collect_params_weth_paired();
576 params.recipient = address!("0000000000000000000000000000000000000099");
577 let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Base)
578 .expect("Base supported");
579 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
580 let bare =
581 ramses::plan::remove_liquidity_and_collect(¶ms, 9_999_999_999, CONFIG.position_mgr);
582 assert_eq!(
583 frag.calls[0].calldata, bare.calls[0].calldata,
584 "non-native pass-through must stay byte-identical to bare multicall([decrease, collect])"
585 );
586 }
587
588 #[test]
589 fn plan_remove_liquidity_and_collect_no_native_side_rejects() {
590 let mut params = fixture_remove_and_collect_params_weth_paired();
591 params.token0 = address!("833589fCD6eDb6E08f4c7C32D4f71b54bda02913"); params.token1 = address!("50c5725949A6F0c72E6C4a641F24049A917DB0Cb"); let err =
594 plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Base).unwrap_err();
595 let msg = format!("{err:#}");
596 assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
597 }
598
599 #[test]
604 fn factory_returns_chain_specific_address_via_layer2() {
605 assert_eq!(factory(Chain::Base), Some(CONFIG.factory));
606 for unsupported in [
607 Chain::Ethereum,
608 Chain::Arbitrum,
609 Chain::Optimism,
610 Chain::Polygon,
611 Chain::Bsc,
612 Chain::Sonic,
613 Chain::Avalanche,
614 Chain::Celo,
615 ] {
616 assert_eq!(factory(unsupported), None);
617 }
618 }
619
620 #[tokio::test]
625 async fn pool_state_routes_to_chain_specific_multicall() {
626 let Some(rpc) = std::env::var("BASE_RPC_URL").ok() else {
627 eprintln!(
628 "SKIP pool_state_routes_to_chain_specific_multicall: \
629 set BASE_RPC_URL to a Base archive RPC to enable"
630 );
631 return;
632 };
633 let anvil = alloy::node_bindings::Anvil::new().fork(rpc).spawn();
634 let provider = alloy::providers::ProviderBuilder::new().connect_http(anvil.endpoint_url());
635
636 let base_pool = address!("b2cc224c1c9feE385f8ad6a55b4d94E92359DC59");
639 let state = pool_state(&provider, Chain::Base, base_pool)
640 .await
641 .expect("Base pool_state must succeed via Layer 2 chain-aware routing");
642 assert!(state.liquidity > 0, "Real on-chain Aerodrome pool should have non-zero liquidity");
643 }
644
645 #[test]
646 fn pool_address_with_override_uses_override_not_config_hash() {
647 let weth = address!("4200000000000000000000000000000000000006");
648 let usdc = address!("833589fCD6eDb6E08f4c7C32D4f71b54bda02913");
649 let custom_hash = b256!("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
650
651 let with_override =
652 pool_address(Chain::Base, weth, usdc, 100, Some(custom_hash)).expect("Base supported");
653 let without_override =
654 pool_address(Chain::Base, weth, usdc, 100, None).expect("Base supported");
655
656 assert_ne!(with_override, without_override);
657 }
658}