1use alloy_primitives::{aliases::U96, keccak256, Address, B256, U256};
12use wp_evm_base::evm::sign_extend_i24;
13use wp_evm_ramses_interfaces::periphery::nfpm::IRamsesNonfungiblePositionManager;
14use wp_evm_velodrome_interfaces::periphery::nfpm::IVelodromeNonfungiblePositionManager;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct RamsesPositionView {
21 pub token_id: U256,
23 pub token0: Address,
25 pub token1: Address,
27 pub tick_spacing: i32,
30 pub tick_lower: i32,
32 pub tick_upper: i32,
34 pub liquidity: u128,
36 pub fee_growth_inside_0_last_x128: U256,
38 pub fee_growth_inside_1_last_x128: U256,
40 pub tokens_owed_0_stale: u128,
42 pub tokens_owed_1_stale: u128,
44}
45
46impl RamsesPositionView {
47 pub fn from_nfpm_returns(
49 token_id: U256,
50 ret: &IRamsesNonfungiblePositionManager::positionsReturn,
51 ) -> Self {
52 Self {
53 token_id,
54 token0: ret.token0,
55 token1: ret.token1,
56 tick_spacing: sign_extend_i24(ret.tickSpacing),
57 tick_lower: sign_extend_i24(ret.tickLower),
58 tick_upper: sign_extend_i24(ret.tickUpper),
59 liquidity: ret.liquidity,
60 fee_growth_inside_0_last_x128: ret.feeGrowthInside0LastX128,
61 fee_growth_inside_1_last_x128: ret.feeGrowthInside1LastX128,
62 tokens_owed_0_stale: ret.tokensOwed0,
63 tokens_owed_1_stale: ret.tokensOwed1,
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct VelodromePositionRow {
71 pub nonce: U96,
73 pub operator: Address,
75 pub token0: Address,
77 pub token1: Address,
79 pub tick_spacing: i32,
81 pub tick_lower: i32,
83 pub tick_upper: i32,
85 pub liquidity: u128,
87 pub fee_growth_inside_0_last_x128: U256,
89 pub fee_growth_inside_1_last_x128: U256,
91 pub tokens_owed_0_stale: u128,
93 pub tokens_owed_1_stale: u128,
95}
96
97impl VelodromePositionRow {
98 pub fn from_nfpm_returns(
100 _token_id: U256,
101 ret: &IVelodromeNonfungiblePositionManager::positionsReturn,
102 ) -> Self {
103 Self {
104 nonce: ret.nonce,
105 operator: ret.operator,
106 token0: ret.token0,
107 token1: ret.token1,
108 tick_spacing: sign_extend_i24(ret.tickSpacing),
109 tick_lower: sign_extend_i24(ret.tickLower),
110 tick_upper: sign_extend_i24(ret.tickUpper),
111 liquidity: ret.liquidity,
112 fee_growth_inside_0_last_x128: ret.feeGrowthInside0LastX128,
113 fee_growth_inside_1_last_x128: ret.feeGrowthInside1LastX128,
114 tokens_owed_0_stale: ret.tokensOwed0,
115 tokens_owed_1_stale: ret.tokensOwed1,
116 }
117 }
118}
119
120pub fn position_key(owner: Address, index: U256, tick_lower: i32, tick_upper: i32) -> B256 {
136 assert!(
137 (-8_388_608..=8_388_607).contains(&tick_lower),
138 "tick_lower {tick_lower} outside int24 range",
139 );
140 assert!(
141 (-8_388_608..=8_388_607).contains(&tick_upper),
142 "tick_upper {tick_upper} outside int24 range",
143 );
144
145 let mut buf = [0u8; 58];
146 buf[..20].copy_from_slice(owner.as_slice());
147 buf[20..52].copy_from_slice(&index.to_be_bytes::<32>());
148 buf[52..55].copy_from_slice(&i24_to_be_bytes(tick_lower));
149 buf[55..58].copy_from_slice(&i24_to_be_bytes(tick_upper));
150 keccak256(buf)
151}
152
153fn i24_to_be_bytes(tick: i32) -> [u8; 3] {
154 let bytes = tick.to_be_bytes();
155 [bytes[1], bytes[2], bytes[3]]
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use alloy_primitives::{address, aliases::I24, b256};
162
163 #[test]
177 fn position_key_synthetic_reference_vector() {
178 let key = position_key(
179 address!("000000000000000000000000000000000000dEaD"),
180 U256::ZERO,
181 -887_188,
182 887_188,
183 );
184 assert_eq!(key, b256!("3b5f5df2f36078cb5495dbec069ca73eed9072adad9c8b0dcaced3689b802b73"),);
185 }
186
187 #[test]
195 fn position_key_real_shadow_position_matches_pool() {
196 let nfpm = address!("12E66C8F215DdD5d48d150c8f46aD0c6fB0F4406");
197 let token_id = U256::from(1_156_688u64);
198 let key = position_key(nfpm, token_id, -887_250, 887_250);
199 assert_eq!(key, b256!("4b6f08061b30865c2b49c44f15339f787b09d20d2429d42fbe3ed5de6199776c"),);
200 }
201
202 #[test]
205 fn from_nfpm_returns_sign_extends_ticks_and_preserves_fields() {
206 let ret = IRamsesNonfungiblePositionManager::positionsReturn {
207 token0: address!("0000000000000000000000000000000000000001"),
208 token1: address!("0000000000000000000000000000000000000002"),
209 tickSpacing: I24::try_from(50i32).unwrap(),
210 tickLower: I24::try_from(-120i32).unwrap(),
211 tickUpper: I24::try_from(240i32).unwrap(),
212 liquidity: 42,
213 feeGrowthInside0LastX128: U256::from(7),
214 feeGrowthInside1LastX128: U256::from(8),
215 tokensOwed0: 9,
216 tokensOwed1: 10,
217 };
218 let view = RamsesPositionView::from_nfpm_returns(U256::from(1), &ret);
219 assert_eq!(view.token_id, U256::from(1));
220 assert_eq!(view.token0, address!("0000000000000000000000000000000000000001"));
221 assert_eq!(view.token1, address!("0000000000000000000000000000000000000002"));
222 assert_eq!(view.tick_spacing, 50);
223 assert_eq!(view.tick_lower, -120);
224 assert_eq!(view.tick_upper, 240);
225 assert_eq!(view.liquidity, 42);
226 assert_eq!(view.fee_growth_inside_0_last_x128, U256::from(7));
227 assert_eq!(view.fee_growth_inside_1_last_x128, U256::from(8));
228 assert_eq!(view.tokens_owed_0_stale, 9);
229 assert_eq!(view.tokens_owed_1_stale, 10);
230 }
231
232 #[test]
236 fn from_nfpm_returns_decodes_real_shadow_active_position() {
237 let raw = alloy_primitives::hex!(
238 "000000000000000000000000039e2fb66102314ce7b64ce5ce3e5183bc94ad38"
239 "00000000000000000000000050c42deacd8fc9773493ed674b675be577f2634b"
240 "0000000000000000000000000000000000000000000000000000000000000032"
241 "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2762e"
242 "00000000000000000000000000000000000000000000000000000000000d89d2"
243 "00000000000000000000000000000000000000000000000342c6d2155c8af6b6"
244 "00000000000000000000000000000000003c34f4fcee3e358e2c80986cddcc72"
245 "000000000000000000000000000000000000029cdd807204b5d3d79e9ac52169"
246 "0000000000000000000000000000000000000000000000000000000000000000"
247 "0000000000000000000000000000000000000000000000000000000000000000"
248 );
249 use alloy_sol_types::SolCall;
250 let ret = IRamsesNonfungiblePositionManager::positionsCall::abi_decode_returns(&raw)
251 .expect("fixture decodes");
252
253 let view = RamsesPositionView::from_nfpm_returns(U256::from(1_156_688u64), &ret);
254
255 assert_eq!(view.token_id, U256::from(1_156_688u64));
256 assert_eq!(view.token0, address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"));
257 assert_eq!(view.token1, address!("50c42deacd8fc9773493ed674b675be577f2634b"));
258 assert_eq!(view.tick_spacing, 50);
259 assert_eq!(view.tick_lower, -887_250);
260 assert_eq!(view.tick_upper, 887_250);
261 assert_eq!(view.liquidity, 60_151_996_462_209_365_686u128);
262 assert_eq!(
263 view.fee_growth_inside_0_last_x128,
264 U256::from(312_611_906_761_373_619_763_565_392_889_236_594u128),
265 );
266 assert_eq!(
267 view.fee_growth_inside_1_last_x128,
268 U256::from(52_992_964_027_640_673_521_461_442_716_009u128),
269 );
270 assert_eq!(view.tokens_owed_0_stale, 0);
271 assert_eq!(view.tokens_owed_1_stale, 0);
272 }
273
274 #[test]
275 #[should_panic(expected = "tick_lower")]
276 fn position_key_panics_on_oversized_tick_lower() {
277 position_key(Address::ZERO, U256::ZERO, 8_388_608, 0);
278 }
279
280 #[test]
281 #[should_panic(expected = "tick_upper")]
282 fn position_key_panics_on_oversized_tick_upper() {
283 position_key(Address::ZERO, U256::ZERO, 0, -8_388_609);
284 }
285}