wp-evm-algebra-interfaces 0.1.12

sol!-generated bindings for Algebra V1.9 protocol interfaces
Documentation
//! `IAlgebraPeripheryRouter` — Algebra-family periphery surface.
//!
//! Mirrors the Uniswap V3 `Multicall + PeripheryPayments` shape carried
//! by Algebra V1.9 NFPMs (QuickSwap on Polygon being the canonical
//! Algebra-family facade in this workspace), **but with two function
//! renames**:
//!
//! - `unwrapWETH9(uint256,address)` → `unwrapWNativeToken(uint256,address)`
//!   (selector changes from `0x49404b7c` → `0x69bc35b2`)
//! - `refundETH()` → `refundNativeToken()`
//!   (selector changes from `0x12210e8a` → `0x41865270`)
//!
//! `multicall(bytes[])` (`0xac9650d8`) and `sweepToken(address,uint256,address)`
//! (`0xdf2ab5bb`) are byte-identical to V3.
//!
//! **Algebra V1.9 deliberately renamed the WETH-specific functions to
//! native-token-generic names** — this is *not* a Polygon-vs-Ethereum
//! cosmetic rename, it is an upstream Algebra design choice (the same
//! contracts deployed to BSC / other chains carry the same Algebra
//! names regardless of the chain's native token). Names AND selectors
//! genuinely differ from V3 / Ramses / Velodrome — reuse via
//! `IPeripheryRouter` / `IRamsesPeripheryRouter` / `ISlipstreamPeripheryRouter`
//! was never possible.
//!
//! Verified 2026-05-29 via `eth_call` against the deployed QuickSwap
//! Polygon NFPM `0x8eF88E4c7CfbbaC1C163f7eddd4B578792201de6`
//! ("Algebra Positions NFT-V1"):
//!
//! - `multicall([])` → returns canonical `bytes[](0)` encoding.
//! - `unwrapWNativeToken(0, dead)` → success (`0x`); `(MAX, dead)` →
//!   reverts with `"Insufficient WNativeToken"` (proves the fn body
//!   actually runs).
//! - `sweepToken(WMATIC, 0, dead)` → success; `(WMATIC, MAX, dead)` →
//!   reverts with `"Insufficient token"`.
//! - `refundNativeToken()` → success (`0x`).
//! - The V3-named siblings (`unwrapWETH9`, `refundETH`) revert with bare
//!   `0x` — they don't exist on this contract.
//!
//! **Why a family-local declaration vs reusing `wp-evm-v3-interfaces`?**
//! Decision #2 of plan 2026-05-29-quickswap-collect-fees-native-unwrap
//! (L21 follow-up Slice 3): the Algebra NFPM is a semantically distinct
//! family (different upstream, different naming), so coupling the
//! Algebra family to a V3-named interface creates a cross-family
//! dependency with no semantic basis. **Doubly justified here**
//! (vs Slices 1-2 where V3 reuse would have been *byte-identical*):
//! Algebra's renaming means the V3 names *don't even exist* on the
//! deployed contract — reuse was structurally impossible.
//!
//! This module is used by `wp_evm_quickswap::plan_collect_fees`
//! (L21 follow-up Slice 3) to compose
//! `multicall(collect, unwrapWNativeToken, sweepToken)` calldata for the
//! `recipient == Address::ZERO` native-output sentinel.

use alloy_sol_types::sol;

sol! {
    /// Algebra-family periphery surface — `multicall` /
    /// `unwrapWNativeToken` / `refundNativeToken` / `sweepToken`.
    ///
    /// `multicall` and `sweepToken` selectors are byte-identical to the
    /// Uniswap V3 SwapRouter / NFPM periphery.
    /// `unwrapWNativeToken` and `refundNativeToken` are Algebra-renamed
    /// equivalents of V3's `unwrapWETH9` / `refundETH` (different
    /// names → different selectors).
    ///
    /// Verified 2026-05-29 against QuickSwap Polygon NFPM:
    /// - QuickSwap Polygon: `0x8eF88E4c7CfbbaC1C163f7eddd4B578792201de6`
    interface IAlgebraPeripheryRouter {
        /// Execute multiple peripheral calls atomically. Outer call is
        /// **payable** — native-in flows thread `msg.value` through.
        /// Used by L21 follow-up Slice 3 to wrap `collect` with
        /// `unwrapWNativeToken` + `sweepToken` tail calls. Selector: `0xac9650d8`.
        function multicall(bytes[] calldata data) external payable returns (bytes[] memory results);

        /// Unwrap WNativeToken (wrapped native — WMATIC on Polygon) held by
        /// the NFPM into native MATIC and forward to `recipient`.
        ///
        /// Algebra-renamed equivalent of Uniswap V3's `unwrapWETH9`.
        /// Selector: `0x69bc35b2` (differs from V3's `0x49404b7c`).
        function unwrapWNativeToken(uint256 amountMinimum, address recipient) external payable;

        /// Refund any native token left in the NFPM to `msg.sender`. Not
        /// used by Slice 3 (collect has no over-supplied native), but
        /// included for symmetry with V3's IPeripheryRouter declaration.
        ///
        /// Algebra-renamed equivalent of Uniswap V3's `refundETH`.
        /// Selector: `0x41865270` (differs from V3's `0x12210e8a`).
        function refundNativeToken() external payable;

        /// Sweep ERC20 balance held by the NFPM to `recipient`. Used as
        /// the third inner call in the native collect composition to
        /// extract the non-native side of the position pair after the
        /// NFPM `collect` has deposited both sides.
        ///
        /// Byte-identical to V3 / Ramses / Velodrome `sweepToken`.
        /// Selector: `0xdf2ab5bb`.
        function sweepToken(address token, uint256 amountMinimum, address recipient) external payable;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_sol_types::SolCall;

    #[test]
    fn multicall_selector_locked() {
        assert_eq!(
            IAlgebraPeripheryRouter::multicallCall::SELECTOR,
            [0xac, 0x96, 0x50, 0xd8],
            "multicall(bytes[]) selector must remain 0xac9650d8 (shared with V3 SwapRouter / NFPM)"
        );
    }

    #[test]
    fn unwrap_wnative_token_selector_locked() {
        // Algebra renamed unwrapWETH9 → unwrapWNativeToken. Selector
        // differs from V3's 0x49404b7c — verified 2026-05-29 against
        // QuickSwap Polygon NFPM (the V3-named function does NOT
        // exist on the deployed contract).
        assert_eq!(
            IAlgebraPeripheryRouter::unwrapWNativeTokenCall::SELECTOR,
            [0x69, 0xbc, 0x35, 0xb2],
            "unwrapWNativeToken(uint256,address) selector must remain 0x69bc35b2"
        );
    }

    #[test]
    fn refund_native_token_selector_locked() {
        // Algebra renamed refundETH → refundNativeToken. Selector
        // differs from V3's 0x12210e8a.
        assert_eq!(
            IAlgebraPeripheryRouter::refundNativeTokenCall::SELECTOR,
            [0x41, 0x86, 0x52, 0x70],
            "refundNativeToken() selector must remain 0x41865270"
        );
    }

    #[test]
    fn sweep_token_selector_locked() {
        assert_eq!(
            IAlgebraPeripheryRouter::sweepTokenCall::SELECTOR,
            [0xdf, 0x2a, 0xb5, 0xbb],
            "sweepToken(address,uint256,address) selector must remain 0xdf2ab5bb"
        );
    }

    /// Lock the `multicall(bytes[])` return decode against real-world
    /// bytes from the QuickSwap Polygon NFPM
    /// (`0x8eF88E4c7CfbbaC1C163f7eddd4B578792201de6`) on Polygon mainnet
    /// at block `0x5300000` (86_966_272 decimal, captured 2026-05-29).
    ///
    /// **Why this test exists (audit Slice 4 — plan PR #225 retro L1
    /// and R14 retro L1):** every NEW `sol!` function declaration in
    /// an interfaces crate gets BOTH a selector lock test (which is
    /// tautological — both sides derive from the same declared name)
    /// AND a real captured-bytes decode test (which proves the
    /// declared name actually exists on the deployed contract and
    /// decodes to the declared return shape).
    ///
    /// The natural decode target for `multicall(bytes[])` is the
    /// empty-array invocation — `multicall([])` succeeds on any
    /// contract that exposes the function, returns an empty `bytes[]`,
    /// and produces the canonical ABI encoding for an empty dynamic
    /// array: head (offset = `0x20`) + length (`0`). Capturing these
    /// 64 bytes locks the **return shape** against silent
    /// `multicall` ABI drift.
    ///
    /// `unwrapWNativeToken` + `sweepToken` + `refundNativeToken` have
    /// no return values, so per audit Tier C discipline a selector lock
    /// alone suffices for those three — input-side functions don't
    /// carry return-shape ABI risk. (The fn bodies were verified at
    /// plan-drafting time via balance-check reverts — see module
    /// docstring.)
    ///
    /// Captured 2026-05-29 via:
    ///   cast rpc eth_call \
    ///     --rpc-url https://polygon-rpc.com (or Alchemy) \
    ///     '{"to":"0x8eF88E4c7CfbbaC1C163f7eddd4B578792201de6",\
    ///       "data":"0xac9650d80000000000000000000000000000000000000000000000000000000000000020\
    ///               0000000000000000000000000000000000000000000000000000000000000000"}' \
    ///     0x5300000
    #[test]
    fn real_quickswap_polygon_multicall_empty_array_decodes() {
        let raw = alloy_primitives::hex!(
            "0000000000000000000000000000000000000000000000000000000000000020" // bytes[] head offset
            "0000000000000000000000000000000000000000000000000000000000000000" // bytes[] length = 0
        );
        let decoded = IAlgebraPeripheryRouter::multicallCall::abi_decode_returns(&raw)
            .expect("decode bytes[] return");

        assert!(
            decoded.is_empty(),
            "multicall([]) must return empty bytes[]; got len {}",
            decoded.len()
        );
    }
}