1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
//! Algebra V1.9 pool mutable-state reads.
use alloy_sol_types::sol;
sol! {
interface IAlgebraPoolState {
function globalState() external view returns (
uint160 price,
int24 tick,
uint16 fee,
uint16 timepointIndex,
uint8 communityFeeToken0,
uint8 communityFeeToken1,
bool unlocked
);
function liquidity() external view returns (uint128);
/// Algebra V1 pool's cumulative LP fee accumulator for token0,
/// in `X128` fixed-point. NOTE the name is `totalFeeGrowth0Token`
/// in Algebra V1's `AlgebraPool.sol`, not `feeGrowthGlobal0X128`
/// like Uniswap V3 — different naming, same semantics (LP-net
/// fee growth, protocol cut already deducted). Verified
/// against canonical QuickSwap pool 0x7b92…a710 on Polygon —
/// `totalFeeGrowth0Token()` returns real state, the
/// Uniswap-named alternative reverts.
function totalFeeGrowth0Token() external view returns (uint256);
function totalFeeGrowth1Token() external view returns (uint256);
function ticks(int24 tick) external view returns (
uint128 liquidityTotal,
int128 liquidityDelta,
uint256 outerFeeGrowth0Token,
uint256 outerFeeGrowth1Token,
int56 outerTickCumulative,
uint160 outerSecondsPerLiquidity,
uint32 outerSecondsSpent,
bool initialized
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_sol_types::SolCall;
#[test]
fn selectors_are_locked() {
assert_eq!(IAlgebraPoolState::globalStateCall::SELECTOR, [0xe7, 0x6c, 0x01, 0xe4]);
assert_eq!(IAlgebraPoolState::liquidityCall::SELECTOR, [0x1a, 0x68, 0x65, 0x02]);
// Algebra V1 names — verified on-chain against QuickSwap pool
// 0x7b92…a710 on Polygon (both at FORK_BLOCK 86_077_558 and
// latest). Wrong Uniswap-style names would revert here AND
// miss this lock; the on-chain decode test in
// `wp-evm-algebra-provider`'s fork-parity suite is the only
// safety net for catching ABI drift downstream.
assert_eq!(IAlgebraPoolState::totalFeeGrowth0TokenCall::SELECTOR, [0x63, 0x78, 0xae, 0x44]);
assert_eq!(IAlgebraPoolState::totalFeeGrowth1TokenCall::SELECTOR, [0xec, 0xde, 0xcf, 0x42]);
assert_eq!(IAlgebraPoolState::ticksCall::SELECTOR, [0xf3, 0x0d, 0xba, 0x93]);
}
/// Lock the `IAlgebraPoolState::totalFeeGrowth0Token` decode against
/// real-world bytes from the canonical QuickSwap V3 USDC.e/USDT
/// pool (`0x7b925e617aefd7fb3a93abe3a701135d7a1ba710`) on Polygon at
/// block `86_077_558` (`0x520F046`).
///
/// **Why this test exists (audit Slice 2 — plan PR #225):** the
/// keccak-only selector lock above is tautological — both sides
/// derive from the same declared name, so an interface that
/// declared `feeGrowthGlobal0Token` (Uniswap-style) would still
/// match its own selector even though the deployed Algebra pool
/// only exposes `totalFeeGrowth0Token` (PR #224, the bug this
/// audit was provoked by). This real-bytes lock proves the
/// declared name actually exists on the on-chain ABI and that the
/// uint256 return decodes — so a future "consistency-rename" back
/// to the V3 name fails loudly here, not silently in production.
///
/// Captured 2026-05-28 via:
/// cast rpc eth_call --rpc-url <POLYGON_MAINNET_RPC> \
/// '{"to":"0x7b925e617aefd7fb3a93abe3a701135d7a1ba710",\
/// "data":"0x6378ae44"}' \
/// 0x520F046
#[test]
#[allow(non_snake_case)]
fn real_quickswap_totalFeeGrowth0Token_decodes() {
use alloy_primitives::U256;
let raw = alloy_primitives::hex!(
"00000000000000000000000000000000003c54f30345461bbcf7625c69498981" // totalFeeGrowth0Token
);
let decoded = IAlgebraPoolState::totalFeeGrowth0TokenCall::abi_decode_returns(&raw)
.expect("decode uint256");
assert_eq!(
decoded,
U256::from_str_radix("313260787374489352611272212014008705", 10).unwrap(),
);
}
/// Lock the `IAlgebraPoolState::totalFeeGrowth1Token` decode against
/// real-world bytes from the canonical QuickSwap V3 USDC.e/USDT
/// pool on Polygon at block `86_077_558` (`0x520F046`).
///
/// Companion to `real_quickswap_totalFeeGrowth0Token_decodes`. Same
/// rationale: the `totalFeeGrowth*Token` name pair is the exact
/// surface fixed by PR #224 — silently mis-naming it back to
/// Uniswap V3's `feeGrowthGlobal*X128` would pass the selector
/// lock but revert on-chain. Locking against a real captured
/// return prevents the regression.
///
/// Captured 2026-05-28 via:
/// cast rpc eth_call --rpc-url <POLYGON_MAINNET_RPC> \
/// '{"to":"0x7b925e617aefd7fb3a93abe3a701135d7a1ba710",\
/// "data":"0xecdecf42"}' \
/// 0x520F046
#[test]
#[allow(non_snake_case)]
fn real_quickswap_totalFeeGrowth1Token_decodes() {
use alloy_primitives::U256;
let raw = alloy_primitives::hex!(
"0000000000000000000000000000000000452c6a1861f1ce2b262918b7a43f2f" // totalFeeGrowth1Token
);
let decoded = IAlgebraPoolState::totalFeeGrowth1TokenCall::abi_decode_returns(&raw)
.expect("decode uint256");
assert_eq!(
decoded,
U256::from_str_radix("359169314992738225562812068319739695", 10).unwrap(),
);
}
/// Lock the `IAlgebraPoolState::liquidity` decode against real-world
/// bytes from the canonical QuickSwap V3 USDC.e/USDT pool on Polygon
/// at block `86_077_558` (`0x520F046`).
///
/// `liquidity()` returns a single `uint128` (in-range active
/// liquidity). Same selector and shape as V3 — the real-bytes lock
/// is the second layer of defense (alongside the selector lock)
/// that proves the declared name decodes against the deployed
/// Algebra pool.
///
/// Captured 2026-05-28 via:
/// cast rpc eth_call --rpc-url <POLYGON_MAINNET_RPC> \
/// '{"to":"0x7b925e617aefd7fb3a93abe3a701135d7a1ba710",\
/// "data":"0x1a686502"}' \
/// 0x520F046
#[test]
fn real_quickswap_liquidity_decodes() {
let raw = alloy_primitives::hex!(
"000000000000000000000000000000000000000000000000000008d714295a71" // liquidity (u128)
);
let decoded =
IAlgebraPoolState::liquidityCall::abi_decode_returns(&raw).expect("decode uint128");
assert_eq!(decoded, 9_719_849_245_297u128);
}
/// Lock the 7-tuple decode of `IAlgebraPoolState::globalStateCall`
/// against real-world bytes from the canonical QuickSwap V3
/// USDC.e/USDT pool (`0x7b925e617aefd7fb3a93abe3a701135d7a1ba710`)
/// on Polygon at block `86_077_558` (`0x520F046`).
///
/// **Algebra-vs-V3 nuance:** Algebra's `globalState()` returns a
/// 7-tuple but with **different field names AND a different type
/// at slot 4** than V3's `slot0()`:
/// `(price, tick, fee, timepointIndex, communityFeeToken0,
/// communityFeeToken1, unlocked)` — NOT V3's
/// `(sqrtPriceX96, tick, observationIndex, observationCardinality,
/// observationCardinalityNext, feeProtocol, unlocked)`. Semantics
/// at slots 2/3/4/5 diverge; slot 4 is `u8 communityFeeToken0` in
/// Algebra vs `u16 observationCardinalityNext` in V3 (both encode
/// to a 32-byte ABI word, so a wrong type wouldn't change the
/// captured bytes — only the `assert_eq!` type would mismatch).
/// Anyone auto-completing the field names from V3 would silently
/// break this test. The tuple **length** (7) happens to match.
///
/// At this block: dynamic-fee Algebra pool reports a `fee` of
/// `10` bps and `communityFeeToken0 = communityFeeToken1 = 150`
/// (15% community-fee share, the QuickSwap V1 default), with the
/// pool idle (`unlocked = true`).
///
/// Captured 2026-05-28 via:
/// cast rpc eth_call --rpc-url <POLYGON_MAINNET_RPC> \
/// '{"to":"0x7b925e617aefd7fb3a93abe3a701135d7a1ba710",\
/// "data":"0xe76c01e4"}' \
/// 0x520F046
#[test]
#[allow(non_snake_case)]
fn real_quickswap_globalState_decodes_7_tuple() {
use alloy_primitives::{aliases::U160, U256};
use IAlgebraPoolState::globalStateReturn;
let raw = alloy_primitives::hex!(
"0000000000000000000000000000000000000000fff41240e6d6248c64e0a1d2" // price (u160)
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc" // tick (i24, -4)
"000000000000000000000000000000000000000000000000000000000000000a" // fee (u16)
"0000000000000000000000000000000000000000000000000000000000001ad2" // timepointIndex (u16)
"0000000000000000000000000000000000000000000000000000000000000096" // communityFeeToken0 (u8)
"0000000000000000000000000000000000000000000000000000000000000096" // communityFeeToken1 (u8)
"0000000000000000000000000000000000000000000000000000000000000001" // unlocked (bool)
);
let decoded: globalStateReturn =
IAlgebraPoolState::globalStateCall::abi_decode_returns(&raw).expect("decode 7-tuple");
// Lock every field — exact values pinned at Polygon block 86_077_558.
assert_eq!(
decoded.price,
U160::from(U256::from_str_radix("79213741604250796873618596306", 10).unwrap()),
);
assert_eq!(decoded.tick.as_i32(), -4);
assert_eq!(decoded.fee, 10u16);
assert_eq!(decoded.timepointIndex, 6_866u16);
// 150 = 0x96 — QuickSwap V1 default community-fee share (15%).
assert_eq!(decoded.communityFeeToken0, 150u8);
assert_eq!(decoded.communityFeeToken1, 150u8);
assert!(decoded.unlocked);
}
/// Lock the 8-tuple decode of `IAlgebraPoolState::ticksCall` against
/// real-world bytes from the canonical QuickSwap V3 USDC.e/USDT
/// pool at tick `-180` (the `tickLower` of position 1962 — the
/// same fixture used by the existing fork-parity test and by the
/// `real_quickswap_positions_return_decodes_11_tuple` NFPM test).
/// Captured at Polygon block `86_077_558` (`0x520F046`).
///
/// **Algebra-vs-V3 nuance:** Algebra's `ticks(int24)` 8-tuple uses
/// `outer*` naming (Algebra V1 source idiom), NOT V3's `Outside*`
/// naming. The field list is
/// `(liquidityTotal, liquidityDelta, outerFeeGrowth0Token,
/// outerFeeGrowth1Token, outerTickCumulative,
/// outerSecondsPerLiquidity, outerSecondsSpent, initialized)` —
/// NOT V3's
/// `(liquidityGross, liquidityNet, feeGrowthOutside0X128,
/// feeGrowthOutside1X128, tickCumulativeOutside,
/// secondsPerLiquidityOutsideX128, secondsOutside, initialized)`.
/// A V3-shaped auto-completion would silently fail.
///
/// Tick `-180` is initialized at this block (it's the lower bound
/// of the active LP position 1962). At this fixture
/// `liquidityTotal == liquidityDelta` — both numerical values are
/// locked below directly from the captured bytes; the standalone
/// equality is a property of this particular boundary state and
/// is not asserted as a structural invariant.
///
/// Captured 2026-05-28 via:
/// cast rpc eth_call --rpc-url <POLYGON_MAINNET_RPC> \
/// '{"to":"0x7b925e617aefd7fb3a93abe3a701135d7a1ba710",\
/// "data":"0xf30dba93ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4c"}' \
/// 0x520F046
#[test]
fn real_quickswap_ticks_return_decodes_8_tuple() {
use alloy_primitives::{
aliases::{I56, U160},
U256,
};
use IAlgebraPoolState::ticksReturn;
let raw = alloy_primitives::hex!(
"0000000000000000000000000000000000000000000000000000015e572f2183" // liquidityTotal (u128)
"0000000000000000000000000000000000000000000000000000015e572f2183" // liquidityDelta (i128)
"00000000000000000000000000000000002ab7f8589d59b52a8748390aed29dd" // outerFeeGrowth0Token (u256)
"000000000000000000000000000000000031c0cb10bc54384ce178a5d061dad3" // outerFeeGrowth1Token (u256)
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffa336de3" // outerTickCumulative (i56)
"00000000000000000000000000000000000bd50522e3bfebaac7ce0555dd7bd6" // outerSecondsPerLiquidity (u160)
"00000000000000000000000000000000000000000000000000000000632b3041" // outerSecondsSpent (u32)
"0000000000000000000000000000000000000000000000000000000000000001" // initialized (bool)
);
let decoded: ticksReturn =
IAlgebraPoolState::ticksCall::abi_decode_returns(&raw).expect("decode 8-tuple");
// Lock every field — exact values pinned at Polygon block 86_077_558.
assert_eq!(decoded.liquidityTotal, 1_504_701_260_163u128);
assert_eq!(decoded.liquidityDelta, 1_504_701_260_163i128);
assert_eq!(
decoded.outerFeeGrowth0Token,
U256::from_str_radix("221807825025140404141599441057294813", 10).unwrap(),
);
assert_eq!(
decoded.outerFeeGrowth1Token,
U256::from_str_radix("258332857208533998751065914378148563", 10).unwrap(),
);
assert_eq!(decoded.outerTickCumulative, I56::try_from(-97_292_829i64).unwrap());
assert_eq!(
decoded.outerSecondsPerLiquidity,
U160::from_str_radix("61435825628096295303193804270304214", 10).unwrap(),
);
assert_eq!(decoded.outerSecondsSpent, 1_663_774_785u32);
assert!(decoded.initialized);
}
}