1use alloy_primitives::{aliases::I24, Address, U256};
7use alloy_provider::{network::Ethereum, Provider};
8use alloy_sol_types::SolCall;
9use anyhow::Result;
10use wp_evm_multicall::{aggregate3, IMulticall3};
11use wp_evm_v3_core::position::PositionView;
12use wp_evm_v3_interfaces::periphery::nfpm::INonfungiblePositionManagerView;
13
14#[derive(Debug, Clone)]
19pub struct PositionFees {
20 pub pool: Address,
21 pub current_tick: i32,
22 pub sqrt_price_x96: U256,
23 pub in_range: bool,
24 pub current_owed_0: U256,
25 pub current_owed_1: U256,
26}
27
28#[derive(Debug, Clone)]
34pub struct PositionViewEntry<V> {
35 pub token_id: U256,
37 pub result: std::result::Result<(V, Address), String>,
39 pub fees: Option<PositionFees>,
42}
43
44pub async fn position_views<P: Provider<Ethereum>>(
52 provider: &P,
53 multicall: Address,
54 nfpm: Address,
55 token_ids: &[U256],
56) -> Result<Vec<PositionViewEntry<PositionView>>> {
57 let mut calls: Vec<IMulticall3::Call3> = Vec::with_capacity(token_ids.len() * 2);
58 for tid in token_ids {
59 calls.extend(build_token_calls(nfpm, *tid));
60 }
61
62 let raw_results = aggregate3(provider, multicall, calls).await?;
63 let expected = token_ids.len() * 2;
64 anyhow::ensure!(
65 raw_results.len() == expected,
66 "multicall returned {} results for {} token IDs; expected {}",
67 raw_results.len(),
68 token_ids.len(),
69 expected,
70 );
71
72 Ok(token_ids
73 .iter()
74 .zip(raw_results.chunks(2))
75 .map(|(tid, chunk)| decode_one_position(*tid, chunk))
76 .collect())
77}
78
79fn build_token_calls(nfpm: Address, token_id: U256) -> [IMulticall3::Call3; 2] {
80 let mk = |data: Vec<u8>| IMulticall3::Call3 {
81 target: nfpm,
82 allowFailure: true,
83 callData: data.into(),
84 };
85 [
86 mk(INonfungiblePositionManagerView::positionsCall { tokenId: token_id }.abi_encode()),
87 mk(INonfungiblePositionManagerView::ownerOfCall { tokenId: token_id }.abi_encode()),
88 ]
89}
90
91fn decode_one_position(
92 token_id: U256,
93 chunk: &[IMulticall3::Result],
94) -> PositionViewEntry<PositionView> {
95 debug_assert_eq!(chunk.len(), 2);
96 match try_decode(token_id, chunk) {
97 Ok(decoded) => PositionViewEntry { token_id, result: Ok(decoded), fees: None },
98 Err(err) => PositionViewEntry { token_id, result: Err(err.to_string()), fees: None },
99 }
100}
101
102fn try_decode(token_id: U256, chunk: &[IMulticall3::Result]) -> Result<(PositionView, Address)> {
103 let need = |idx: usize, label: &str| -> Result<&[u8]> {
104 let r = &chunk[idx];
105 if !r.success {
106 anyhow::bail!("{label} reverted (token may not exist or NFPM mismatch)");
107 }
108 Ok(r.returnData.as_ref())
109 };
110
111 let positions_ret =
112 INonfungiblePositionManagerView::positionsCall::abi_decode_returns(need(0, "positions")?)?;
113 let owner =
114 INonfungiblePositionManagerView::ownerOfCall::abi_decode_returns(need(1, "ownerOf")?)?;
115
116 let view = PositionView::from_nfpm_returns(token_id, &positions_ret);
117 Ok((view, owner))
118}
119
120#[derive(Debug, Clone)]
123pub struct PoolGlobals {
124 pub current_tick_i24: I24,
125 pub sqrt_price_x96: U256,
126 pub fg_global_0: U256,
127 pub fg_global_1: U256,
128}
129
130pub trait PoolFeeGrowthSource {
134 fn pool_globals_calls(&self, pool: Address) -> [IMulticall3::Call3; 4];
135 fn tick_calls(
136 &self,
137 pool: Address,
138 tick_lower: I24,
139 tick_upper: I24,
140 ) -> [IMulticall3::Call3; 2];
141 fn decode_pool_globals(&self, chunk: &[IMulticall3::Result]) -> Option<PoolGlobals>;
142 fn decode_tick_outside(
144 &self,
145 chunk: &[IMulticall3::Result],
146 ) -> Option<(U256, U256, U256, U256)>;
147}
148
149pub struct V3PoolFeeGrowthSource;
151
152impl PoolFeeGrowthSource for V3PoolFeeGrowthSource {
153 fn pool_globals_calls(&self, pool: Address) -> [IMulticall3::Call3; 4] {
154 use wp_evm_v3_interfaces::pool::state::IUniswapV3PoolState;
155
156 let mk = |data: Vec<u8>| IMulticall3::Call3 {
157 target: pool,
158 allowFailure: true,
159 callData: data.into(),
160 };
161 [
162 mk(IUniswapV3PoolState::slot0Call {}.abi_encode()),
163 mk(IUniswapV3PoolState::liquidityCall {}.abi_encode()),
164 mk(IUniswapV3PoolState::feeGrowthGlobal0X128Call {}.abi_encode()),
165 mk(IUniswapV3PoolState::feeGrowthGlobal1X128Call {}.abi_encode()),
166 ]
167 }
168
169 fn tick_calls(
170 &self,
171 pool: Address,
172 tick_lower: I24,
173 tick_upper: I24,
174 ) -> [IMulticall3::Call3; 2] {
175 use wp_evm_v3_interfaces::pool::state::IUniswapV3PoolState;
176
177 let mk = |data: Vec<u8>| IMulticall3::Call3 {
178 target: pool,
179 allowFailure: true,
180 callData: data.into(),
181 };
182 [
183 mk(IUniswapV3PoolState::ticksCall { tick: tick_lower }.abi_encode()),
184 mk(IUniswapV3PoolState::ticksCall { tick: tick_upper }.abi_encode()),
185 ]
186 }
187
188 fn decode_pool_globals(&self, chunk: &[IMulticall3::Result]) -> Option<PoolGlobals> {
189 use wp_evm_v3_interfaces::pool::state::IUniswapV3PoolState;
190
191 if chunk.iter().any(|r| !r.success) {
192 return None;
193 }
194 let slot0 =
195 IUniswapV3PoolState::slot0Call::abi_decode_returns(&chunk[0].returnData).ok()?;
196 let _liquidity =
197 IUniswapV3PoolState::liquidityCall::abi_decode_returns(&chunk[1].returnData).ok()?;
198 let fg_global_0 =
199 IUniswapV3PoolState::feeGrowthGlobal0X128Call::abi_decode_returns(&chunk[2].returnData)
200 .ok()?;
201 let fg_global_1 =
202 IUniswapV3PoolState::feeGrowthGlobal1X128Call::abi_decode_returns(&chunk[3].returnData)
203 .ok()?;
204 Some(PoolGlobals {
205 current_tick_i24: slot0.tick,
206 sqrt_price_x96: U256::from(slot0.sqrtPriceX96),
207 fg_global_0,
208 fg_global_1,
209 })
210 }
211
212 fn decode_tick_outside(
213 &self,
214 chunk: &[IMulticall3::Result],
215 ) -> Option<(U256, U256, U256, U256)> {
216 use wp_evm_v3_interfaces::pool::state::IUniswapV3PoolState;
217
218 if chunk.iter().any(|r| !r.success) {
219 return None;
220 }
221 let tick_lower_info =
222 IUniswapV3PoolState::ticksCall::abi_decode_returns(&chunk[0].returnData).ok()?;
223 let tick_upper_info =
224 IUniswapV3PoolState::ticksCall::abi_decode_returns(&chunk[1].returnData).ok()?;
225 Some((
226 tick_lower_info.feeGrowthOutside0X128,
227 tick_lower_info.feeGrowthOutside1X128,
228 tick_upper_info.feeGrowthOutside0X128,
229 tick_upper_info.feeGrowthOutside1X128,
230 ))
231 }
232}
233
234#[derive(Debug)]
237struct DedupCallPlan {
238 pub calls: Vec<IMulticall3::Call3>,
243 pub unique_pools: Vec<Address>,
246 pub position_to_pool_idx: Vec<usize>,
250}
251
252fn build_dedup_call_plan<S: PoolFeeGrowthSource>(
256 targets: &[(usize, Address, (i32, i32))],
257 source: &S,
258) -> DedupCallPlan {
259 use std::collections::HashMap;
260
261 let mut unique_pools: Vec<Address> = Vec::new();
265 let mut pool_to_idx: HashMap<Address, usize> = HashMap::new();
266 for (_, pool, _) in targets {
267 if !pool_to_idx.contains_key(pool) {
268 pool_to_idx.insert(*pool, unique_pools.len());
269 unique_pools.push(*pool);
270 }
271 }
272
273 let position_to_pool_idx: Vec<usize> =
275 targets.iter().map(|(_, pool, _)| pool_to_idx[pool]).collect();
276
277 let mut calls: Vec<IMulticall3::Call3> =
279 Vec::with_capacity(4 * unique_pools.len() + 2 * targets.len());
280
281 for pool in &unique_pools {
282 calls.extend(source.pool_globals_calls(*pool));
283 }
284 for (_, pool, (tick_lower, tick_upper)) in targets {
285 let tl = i32_to_i24_truncating(*tick_lower);
286 let tu = i32_to_i24_truncating(*tick_upper);
287 calls.extend(source.tick_calls(*pool, tl, tu));
288 }
289
290 DedupCallPlan { calls, unique_pools, position_to_pool_idx }
291}
292
293fn i32_to_i24_truncating(tick: i32) -> I24 {
314 let bytes = tick.to_be_bytes();
315 I24::from_be_bytes::<3>([bytes[1], bytes[2], bytes[3]])
316}
317
318#[derive(Debug, Clone, Copy)]
320pub struct FeeMathInputs {
321 pub tick_lower: i32,
322 pub tick_upper: i32,
323 pub liquidity: u128,
324 pub fee_growth_inside_0_last_x128: U256,
325 pub fee_growth_inside_1_last_x128: U256,
326 pub tokens_owed_0_stale: u128,
327 pub tokens_owed_1_stale: u128,
328}
329
330pub async fn populate_position_fees_with<P, V, S>(
340 provider: &P,
341 multicall: Address,
342 entries: &mut [PositionViewEntry<V>],
343 source: &S,
344 derive_pool: impl Fn(&V) -> Option<Address>,
345 extract: impl Fn(&V) -> FeeMathInputs,
346) -> Result<()>
347where
348 P: Provider<Ethereum>,
349 V: Clone,
350 S: PoolFeeGrowthSource,
351{
352 use wp_evm_amm_math::fee_growth::get_fee_growth_inside;
353 use wp_evm_base::evm::sign_extend_i24;
354
355 let mut targets: Vec<(usize, Address, V, FeeMathInputs)> = Vec::new();
360 for (idx, entry) in entries.iter().enumerate() {
361 if let Ok((view, _owner)) = &entry.result {
362 let pool = match derive_pool(view) {
363 Some(addr) => addr,
364 None => continue,
365 };
366 targets.push((idx, pool, view.clone(), extract(view)));
367 }
368 }
369
370 if targets.is_empty() {
371 return Ok(());
372 }
373
374 let tick_targets: Vec<(usize, Address, (i32, i32))> = targets
377 .iter()
378 .map(|(idx, pool, _view, inputs)| (*idx, *pool, (inputs.tick_lower, inputs.tick_upper)))
379 .collect();
380 let plan = build_dedup_call_plan(&tick_targets, source);
381
382 let raw = aggregate3(provider, multicall, plan.calls).await?;
383 let expected = 4 * plan.unique_pools.len() + 2 * targets.len();
384 anyhow::ensure!(
385 raw.len() == expected,
386 "phase-2 multicall returned {} results for K={} pools + N={} positions; expected {}",
387 raw.len(),
388 plan.unique_pools.len(),
389 targets.len(),
390 expected,
391 );
392
393 let globals_section = &raw[..4 * plan.unique_pools.len()];
396 let pool_globals: Vec<Option<PoolGlobals>> = (0..plan.unique_pools.len())
397 .map(|k| source.decode_pool_globals(&globals_section[4 * k..4 * (k + 1)]))
398 .collect();
399
400 let ticks_offset = 4 * plan.unique_pools.len();
403 for (n, (entry_idx, pool, _view, inputs)) in targets.iter().enumerate() {
404 let pool_idx = plan.position_to_pool_idx[n];
405 let Some(globals) = &pool_globals[pool_idx] else {
406 continue;
408 };
409 let tick_chunk = &raw[ticks_offset + 2 * n..ticks_offset + 2 * (n + 1)];
410 let Some((lower0, lower1, upper0, upper1)) = source.decode_tick_outside(tick_chunk) else {
411 continue;
412 };
413
414 let current_tick = sign_extend_i24(globals.current_tick_i24);
415 let in_range = inputs.tick_lower <= current_tick && current_tick < inputs.tick_upper;
416
417 let (fg_inside_0, fg_inside_1) = get_fee_growth_inside(
418 inputs.tick_lower,
419 inputs.tick_upper,
420 current_tick,
421 globals.fg_global_0,
422 globals.fg_global_1,
423 lower0,
424 lower1,
425 upper0,
426 upper1,
427 );
428
429 let (current_owed_0, current_owed_1) =
430 current_tokens_owed(*inputs, fg_inside_0, fg_inside_1);
431
432 entries[*entry_idx].fees = Some(PositionFees {
433 pool: *pool,
434 current_tick,
435 sqrt_price_x96: globals.sqrt_price_x96,
436 in_range,
437 current_owed_0,
438 current_owed_1,
439 });
440 }
441
442 Ok(())
443}
444
445pub async fn populate_position_fees<P: Provider<Ethereum>>(
446 provider: &P,
447 multicall: Address,
448 entries: &mut [PositionViewEntry<PositionView>],
449 derive_pool: impl Fn(&PositionView) -> Option<Address>,
450) -> Result<()> {
451 populate_position_fees_with(
452 provider,
453 multicall,
454 entries,
455 &V3PoolFeeGrowthSource,
456 derive_pool,
457 |v| FeeMathInputs {
458 tick_lower: v.tick_lower,
459 tick_upper: v.tick_upper,
460 liquidity: v.liquidity,
461 fee_growth_inside_0_last_x128: v.fee_growth_inside_0_last_x128,
462 fee_growth_inside_1_last_x128: v.fee_growth_inside_1_last_x128,
463 tokens_owed_0_stale: v.tokens_owed_0_stale,
464 tokens_owed_1_stale: v.tokens_owed_1_stale,
465 },
466 )
467 .await
468}
469
470fn current_tokens_owed(
471 inputs: FeeMathInputs,
472 fee_growth_inside_0x128: U256,
473 fee_growth_inside_1x128: U256,
474) -> (U256, U256) {
475 let (accrued_0, accrued_1) = wp_evm_amm_math::fee_growth::get_tokens_owed(
476 inputs.fee_growth_inside_0_last_x128,
477 inputs.fee_growth_inside_1_last_x128,
478 inputs.liquidity,
479 fee_growth_inside_0x128,
480 fee_growth_inside_1x128,
481 );
482 (
483 U256::from(inputs.tokens_owed_0_stale) + accrued_0,
484 U256::from(inputs.tokens_owed_1_stale) + accrued_1,
485 )
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use alloy_primitives::address;
492
493 #[test]
494 fn current_tokens_owed_includes_stale_tokens_owed_snapshot() {
495 use alloy_primitives::aliases::{I24, U24, U96};
496
497 let positions_ret = INonfungiblePositionManagerView::positionsReturn {
498 nonce: U96::ZERO,
499 operator: Address::ZERO,
500 token0: Address::ZERO,
501 token1: Address::ZERO,
502 fee: U24::from(500u32),
503 tickLower: I24::try_from(0i32).unwrap(),
504 tickUpper: I24::try_from(60i32).unwrap(),
505 liquidity: 1u128,
506 feeGrowthInside0LastX128: U256::ZERO,
507 feeGrowthInside1LastX128: U256::ZERO,
508 tokensOwed0: 7u128,
509 tokensOwed1: 11u128,
510 };
511 let view = PositionView::from_nfpm_returns(U256::from(1u64), &positions_ret);
512 let q128 = U256::from(1u64) << 128usize;
513
514 let inputs = FeeMathInputs {
515 tick_lower: view.tick_lower,
516 tick_upper: view.tick_upper,
517 liquidity: view.liquidity,
518 fee_growth_inside_0_last_x128: view.fee_growth_inside_0_last_x128,
519 fee_growth_inside_1_last_x128: view.fee_growth_inside_1_last_x128,
520 tokens_owed_0_stale: view.tokens_owed_0_stale,
521 tokens_owed_1_stale: view.tokens_owed_1_stale,
522 };
523 let (owed_0, owed_1) = current_tokens_owed(inputs, q128, q128);
524
525 assert_eq!(owed_0, U256::from(8u64));
526 assert_eq!(owed_1, U256::from(12u64));
527 }
528
529 #[test]
534 fn dedup_call_plan_emits_4k_plus_2n_calls() {
535 let pool_a = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
536 let pool_b = address!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
537
538 let targets: Vec<(usize, Address, (i32, i32))> =
540 vec![(0, pool_a, (-1000, 1000)), (1, pool_a, (-2000, 2000)), (2, pool_b, (-500, 500))];
541
542 let plan = build_dedup_call_plan(&targets, &V3PoolFeeGrowthSource);
543 assert_eq!(plan.calls.len(), 14, "expected 4*K + 2*N = 4*2 + 2*3");
544 assert_eq!(plan.unique_pools.len(), 2, "K = 2 unique pools");
545 assert_eq!(plan.unique_pools[0], pool_a, "pool_a registered first");
546 assert_eq!(plan.unique_pools[1], pool_b);
547 assert_eq!(plan.position_to_pool_idx, vec![0, 0, 1]);
548 }
549
550 #[test]
553 fn dedup_call_plan_single_pool_collapses_globals() {
554 let pool = address!("cccccccccccccccccccccccccccccccccccccccc");
555 let targets: Vec<(usize, Address, (i32, i32))> =
556 (0..14).map(|i| (i, pool, (-1000 + i as i32 * 10, 1000 + i as i32 * 10))).collect();
557
558 let plan = build_dedup_call_plan(&targets, &V3PoolFeeGrowthSource);
559 assert_eq!(plan.calls.len(), 32);
561 assert_eq!(plan.unique_pools.len(), 1);
562 assert_eq!(plan.position_to_pool_idx, vec![0; 14]);
563 }
564
565 #[test]
569 fn dedup_call_plan_all_distinct_pools_no_degradation() {
570 let targets: Vec<(usize, Address, (i32, i32))> = (0..5)
573 .map(|i| {
574 let mut bytes = [0u8; 20];
575 bytes[19] = i as u8 + 1;
576 (i, Address::from(bytes), (-100 * (i as i32 + 1), 100 * (i as i32 + 1)))
577 })
578 .collect();
579
580 let plan = build_dedup_call_plan(&targets, &V3PoolFeeGrowthSource);
581 assert_eq!(plan.calls.len(), 30);
582 assert_eq!(plan.unique_pools.len(), 5);
583 }
584
585 #[test]
586 fn decode_pool_globals_and_tick_chunk_decode_results_and_reverts_return_none() {
587 use alloy_primitives::aliases::{I24, I56, U160};
588 use wp_evm_v3_interfaces::pool::state::IUniswapV3PoolState;
589
590 let ok = |return_data: Vec<u8>| IMulticall3::Result {
591 success: true,
592 returnData: return_data.into(),
593 };
594
595 let slot0 = IUniswapV3PoolState::slot0Return {
596 sqrtPriceX96: U160::from(123u64),
597 tick: I24::try_from(-42i32).unwrap(),
598 observationIndex: 1,
599 observationCardinality: 2,
600 observationCardinalityNext: 3,
601 feeProtocol: 0,
602 unlocked: true,
603 };
604 let tick_lower = IUniswapV3PoolState::ticksReturn {
605 liquidityGross: 10u128,
606 liquidityNet: -5i128,
607 feeGrowthOutside0X128: U256::from(100u64),
608 feeGrowthOutside1X128: U256::from(200u64),
609 tickCumulativeOutside: I56::try_from(-9i64).unwrap(),
610 secondsPerLiquidityOutsideX128: U160::from(300u64),
611 secondsOutside: 400,
612 initialized: true,
613 };
614 let tick_upper = IUniswapV3PoolState::ticksReturn {
615 liquidityGross: 20u128,
616 liquidityNet: 5i128,
617 feeGrowthOutside0X128: U256::from(101u64),
618 feeGrowthOutside1X128: U256::from(201u64),
619 tickCumulativeOutside: I56::try_from(9i64).unwrap(),
620 secondsPerLiquidityOutsideX128: U160::from(301u64),
621 secondsOutside: 401,
622 initialized: true,
623 };
624
625 let chunk = [
626 ok(IUniswapV3PoolState::slot0Call::abi_encode_returns(&slot0)),
627 ok(IUniswapV3PoolState::liquidityCall::abi_encode_returns(&123_456u128)),
628 ok(IUniswapV3PoolState::feeGrowthGlobal0X128Call::abi_encode_returns(&U256::from(
629 1_000u64,
630 ))),
631 ok(IUniswapV3PoolState::feeGrowthGlobal1X128Call::abi_encode_returns(&U256::from(
632 2_000u64,
633 ))),
634 ok(IUniswapV3PoolState::ticksCall::abi_encode_returns(&tick_lower)),
635 ok(IUniswapV3PoolState::ticksCall::abi_encode_returns(&tick_upper)),
636 ];
637
638 let source = V3PoolFeeGrowthSource;
639 let globals = source.decode_pool_globals(&chunk[..4]).expect("globals decode");
640 let (lower0, lower1, upper0, upper1) =
641 source.decode_tick_outside(&chunk[4..]).expect("ticks decode");
642 assert_eq!(globals.current_tick_i24, slot0.tick);
643 assert_eq!(globals.sqrt_price_x96, U256::from(slot0.sqrtPriceX96));
644 assert_eq!(globals.fg_global_0, U256::from(1_000u64));
645 assert_eq!(globals.fg_global_1, U256::from(2_000u64));
646 assert_eq!(lower0, U256::from(100u64));
647 assert_eq!(lower1, U256::from(200u64));
648 assert_eq!(upper0, U256::from(101u64));
649 assert_eq!(upper1, U256::from(201u64));
650
651 let mut reverted_globals = chunk[..4].to_vec();
652 reverted_globals[3].success = false;
653 assert!(source.decode_pool_globals(&reverted_globals).is_none());
654
655 let mut reverted_ticks = chunk[4..].to_vec();
656 reverted_ticks[1].success = false;
657 assert!(source.decode_tick_outside(&reverted_ticks).is_none());
658 }
659
660 #[test]
667 fn i32_to_i24_truncating_roundtrips_via_sign_extend() {
668 use wp_evm_base::evm::sign_extend_i24;
669
670 for tick in [-887_220i32, -8_388_608, -60, -1, 0, 1, 60, 887_220, 8_388_607] {
671 let i24 = i32_to_i24_truncating(tick);
672 assert_eq!(sign_extend_i24(i24), tick, "round-trip failed at tick={tick}");
673 }
674 }
675
676 #[test]
704 fn arbitrum_position_5435993_math_snapshot_lock() {
705 use alloy_primitives::aliases::{I24, I56, U160, U24, U96};
706 use wp_evm_amm_math::fee_growth::get_fee_growth_inside;
707 use wp_evm_base::evm::sign_extend_i24;
708 use wp_evm_v3_interfaces::pool::state::IUniswapV3PoolState;
709
710 let positions_ret = INonfungiblePositionManagerView::positionsReturn {
712 nonce: U96::ZERO,
713 operator: Address::ZERO,
714 token0: address!("0c880f6761F1af8d9Aa9C466984b80DAb9a8c9e8"), token1: address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"), fee: U24::from(3_000u32),
717 tickLower: I24::try_from(-76_200i32).unwrap(),
718 tickUpper: I24::try_from(-72_780i32).unwrap(),
719 liquidity: 137_776_574_478_361_638_142u128,
720 feeGrowthInside0LastX128: U256::from_str_radix(
721 "115792089237316195423570985008687907851891836270363301344313529832249190462215",
722 10,
723 )
724 .unwrap(),
725 feeGrowthInside1LastX128: U256::from_str_radix(
726 "115792089237316195423570985008687907853268070783021718997865526573101277782114",
727 10,
728 )
729 .unwrap(),
730 tokensOwed0: 0u128,
731 tokensOwed1: 0u128,
732 };
733 let view = PositionView::from_nfpm_returns(U256::from(5_435_993u64), &positions_ret);
734
735 let slot0 = IUniswapV3PoolState::slot0Return {
737 sqrtPriceX96: U160::from_str_radix("1866984747909073049766330368", 10).unwrap(),
738 tick: I24::try_from(-74_964i32).unwrap(),
739 observationIndex: 1566,
740 observationCardinality: 1800,
741 observationCardinalityNext: 1800,
742 feeProtocol: 102,
743 unlocked: true,
744 };
745 let liquidity_pool = 0u128; let fg_global_0 =
747 U256::from_str_radix("3257941510536840956130253336502569755530", 10).unwrap();
748 let fg_global_1 =
749 U256::from_str_radix("90146056400431291924115652136134661340", 10).unwrap();
750 let tick_lower_info = IUniswapV3PoolState::ticksReturn {
751 liquidityGross: 1_025_821_222_695_237_314_208u128,
752 liquidityNet: 1_025_821_222_695_237_314_208i128,
753 feeGrowthOutside0X128: U256::from_str_radix(
754 "3011970555915117741071824734093997682470",
755 10,
756 )
757 .unwrap(),
758 feeGrowthOutside1X128: U256::from_str_radix(
759 "89996042868724336997184963186756559285",
760 10,
761 )
762 .unwrap(),
763 tickCumulativeOutside: I56::try_from(-6_183_495_934_001i64).unwrap(),
764 secondsPerLiquidityOutsideX128: U160::from_str_radix(
765 "1055476276114891623306785093325462239337919411",
766 10,
767 )
768 .unwrap(),
769 secondsOutside: 1_768_271_064,
770 initialized: true,
771 };
772 let tick_upper_info = IUniswapV3PoolState::ticksReturn {
773 liquidityGross: 527_757_663_279_894_400_755u128,
774 liquidityNet: -178_735_004_268_239_931_963i128,
775 feeGrowthOutside0X128: U256::from_str_radix(
776 "1619785920262637406907822566892852684677",
777 10,
778 )
779 .unwrap(),
780 feeGrowthOutside1X128: U256::from_str_radix(
781 "2061540973486906648897049606535835620",
782 10,
783 )
784 .unwrap(),
785 tickCumulativeOutside: I56::try_from(-3_908_878_221_061i64).unwrap(),
786 secondsPerLiquidityOutsideX128: U160::from_str_radix("130589885041679980488482", 10)
787 .unwrap(),
788 secondsOutside: 58_739_145,
789 initialized: true,
790 };
791
792 let ok = |data: Vec<u8>| IMulticall3::Result { success: true, returnData: data.into() };
795 let chunk = [
796 ok(IUniswapV3PoolState::slot0Call::abi_encode_returns(&slot0)),
797 ok(IUniswapV3PoolState::liquidityCall::abi_encode_returns(&liquidity_pool)),
798 ok(IUniswapV3PoolState::feeGrowthGlobal0X128Call::abi_encode_returns(&fg_global_0)),
799 ok(IUniswapV3PoolState::feeGrowthGlobal1X128Call::abi_encode_returns(&fg_global_1)),
800 ok(IUniswapV3PoolState::ticksCall::abi_encode_returns(&tick_lower_info)),
801 ok(IUniswapV3PoolState::ticksCall::abi_encode_returns(&tick_upper_info)),
802 ];
803 let source = V3PoolFeeGrowthSource;
804 let globals = source
805 .decode_pool_globals(&chunk[..4])
806 .expect("frozen Arbitrum pool globals must decode cleanly");
807 let (lower0, lower1, upper0, upper1) = source
808 .decode_tick_outside(&chunk[4..])
809 .expect("frozen Arbitrum ticks must decode cleanly");
810
811 let current_tick = sign_extend_i24(globals.current_tick_i24);
813 assert_eq!(current_tick, -74_964, "current_tick decoded from frozen slot0");
814
815 let in_range = view.tick_lower <= current_tick && current_tick < view.tick_upper;
816 assert!(in_range, "position 5435993 was in-range at the snapshot block");
817
818 let (fg_inside_0, fg_inside_1) = get_fee_growth_inside(
819 view.tick_lower,
820 view.tick_upper,
821 current_tick,
822 globals.fg_global_0,
823 globals.fg_global_1,
824 lower0,
825 lower1,
826 upper0,
827 upper1,
828 );
829
830 let inputs = FeeMathInputs {
834 tick_lower: view.tick_lower,
835 tick_upper: view.tick_upper,
836 liquidity: view.liquidity,
837 fee_growth_inside_0_last_x128: view.fee_growth_inside_0_last_x128,
838 fee_growth_inside_1_last_x128: view.fee_growth_inside_1_last_x128,
839 tokens_owed_0_stale: view.tokens_owed_0_stale,
840 tokens_owed_1_stale: view.tokens_owed_1_stale,
841 };
842 let (current_owed_0, current_owed_1) =
843 current_tokens_owed(inputs, fg_inside_0, fg_inside_1);
844
845 assert_eq!(
847 current_owed_0,
848 U256::from(1_754_557_829_256_627_897u128),
849 "current_owed_0 (PENDLE wei) drifted from frozen Arbitrum reference",
850 );
851 assert_eq!(
852 current_owed_1,
853 U256::from(953_585_198_240_599u128),
854 "current_owed_1 (WETH wei) drifted from frozen Arbitrum reference",
855 );
856 }
857}