wp-evm-algebra-core 0.1.14

Pure data + quote + plan for Algebra-family CL DEXes
Documentation
//! `AlgebraPositionView` — lossless 11-field mirror of Algebra V1.9 NFPM
//! `positions(uint256)` plus the shared position-key helper.

use alloy_primitives::{keccak256, Address, B256, U256};
use wp_evm_algebra_interfaces::periphery::{
    nfpm::IAlgebraNonfungiblePositionManager, nfpm_custom_deployer::IAlgebraCustomDeployerNfpm,
};
use wp_evm_base::evm::sign_extend_i24;

/// Faithful Rust mirror of Algebra V1.9 NFPM's 11-field `positions()`
/// return tuple. Algebra omits V3's `fee` field because pools are keyed by
/// token pair only and fee is dynamic pool state.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlgebraPositionView {
    /// NFT token ID this position belongs to. Echoed from caller input.
    pub token_id: U256,
    /// Permit nonce.
    pub nonce: U256,
    /// ERC-721 approved operator.
    pub operator: Address,
    /// Pool token0.
    pub token0: Address,
    /// Pool token1.
    pub token1: Address,
    /// Lower tick, sign-extended from int24.
    pub tick_lower: i32,
    /// Upper tick, sign-extended from int24.
    pub tick_upper: i32,
    /// Position liquidity.
    pub liquidity: u128,
    /// Last-touch fee-growth snapshot for token0.
    pub fee_growth_inside_0_last_x128: U256,
    /// Last-touch fee-growth snapshot for token1.
    pub fee_growth_inside_1_last_x128: U256,
    /// Stale token0 owed snapshot from NFPM.
    pub tokens_owed_0_stale: u128,
    /// Stale token1 owed snapshot from NFPM.
    pub tokens_owed_1_stale: u128,
}

impl AlgebraPositionView {
    /// Decode an Algebra NFPM `positions(uint256)` return tuple.
    pub fn from_nfpm_returns(
        token_id: U256,
        ret: &IAlgebraNonfungiblePositionManager::positionsReturn,
    ) -> Self {
        Self {
            token_id,
            nonce: U256::from(ret.nonce),
            operator: ret.operator,
            token0: ret.token0,
            token1: ret.token1,
            tick_lower: sign_extend_i24(ret.tickLower),
            tick_upper: sign_extend_i24(ret.tickUpper),
            liquidity: ret.liquidity,
            fee_growth_inside_0_last_x128: ret.feeGrowthInside0LastX128,
            fee_growth_inside_1_last_x128: ret.feeGrowthInside1LastX128,
            tokens_owed_0_stale: ret.tokensOwed0,
            tokens_owed_1_stale: ret.tokensOwed1,
        }
    }
}

/// 12-field mirror of a custom-pool-deployer Algebra NFPM `positions()` return,
/// adding the per-position `deployer` (used as the CREATE2 custom deployer).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomDeployerPositionView {
    pub token_id: U256,
    pub nonce: U256,
    pub operator: Address,
    pub token0: Address,
    pub token1: Address,
    /// Per-position custom pool deployer (CREATE2 salt input; non-zero for custom pools).
    pub deployer: Address,
    pub tick_lower: i32,
    pub tick_upper: i32,
    pub liquidity: u128,
    pub fee_growth_inside_0_last_x128: U256,
    pub fee_growth_inside_1_last_x128: U256,
    pub tokens_owed_0_stale: u128,
    pub tokens_owed_1_stale: u128,
}

impl CustomDeployerPositionView {
    pub fn from_nfpm_returns(
        token_id: U256,
        ret: &IAlgebraCustomDeployerNfpm::positionsReturn,
    ) -> Self {
        Self {
            token_id,
            nonce: U256::from(ret.nonce),
            operator: ret.operator,
            token0: ret.token0,
            token1: ret.token1,
            deployer: ret.deployer,
            tick_lower: sign_extend_i24(ret.tickLower),
            tick_upper: sign_extend_i24(ret.tickUpper),
            liquidity: ret.liquidity,
            fee_growth_inside_0_last_x128: ret.feeGrowthInside0LastX128,
            fee_growth_inside_1_last_x128: ret.feeGrowthInside1LastX128,
            tokens_owed_0_stale: ret.tokensOwed0,
            tokens_owed_1_stale: ret.tokensOwed1,
        }
    }
}

/// Compute Algebra pool position key. Algebra V1.9 uses the same packed
/// `(owner, tickLower, tickUpper)` key shape as V3.
///
/// **Panics** in both debug and release if either tick is outside the
/// `int24` range `[-2^23, 2^23 - 1]`. The function is `pub`, so callers
/// can pass arbitrary `i32`; an out-of-range tick would silently truncate
/// in `i24_to_be_bytes` and produce a wrong hash, so we trip loudly
/// instead. (Uniswap-style ticks live in `[-887_272, 887_272]` — well
/// inside int24 — so realistic callers never hit this.)
pub fn position_key(owner: Address, tick_lower: i32, tick_upper: i32) -> B256 {
    assert!(
        (-8_388_608..=8_388_607).contains(&tick_lower),
        "tick_lower {tick_lower} outside int24 range",
    );
    assert!(
        (-8_388_608..=8_388_607).contains(&tick_upper),
        "tick_upper {tick_upper} outside int24 range",
    );

    let mut buf = [0u8; 26];
    buf[..20].copy_from_slice(owner.as_slice());
    buf[20..23].copy_from_slice(&i24_to_be_bytes(tick_lower));
    buf[23..26].copy_from_slice(&i24_to_be_bytes(tick_upper));
    keccak256(buf)
}

fn i24_to_be_bytes(tick: i32) -> [u8; 3] {
    let bytes = tick.to_be_bytes();
    [bytes[1], bytes[2], bytes[3]]
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_primitives::{address, aliases::I24};

    #[test]
    fn position_key_matches_v3_reference_vector() {
        let owner = address!("000000000000000000000000000000000000dEaD");
        let key = position_key(owner, -887_220, 887_220);
        assert_eq!(
            key,
            alloy_primitives::b256!(
                "1de7686f3203885e60772ba1129871b5a5d3c9b611ecdb79223ca2e8da545eb2"
            )
        );
    }

    #[test]
    fn from_nfpm_returns_sign_extends_ticks() {
        let ret = IAlgebraNonfungiblePositionManager::positionsReturn {
            nonce: alloy_primitives::aliases::U96::ZERO,
            operator: Address::ZERO,
            token0: address!("0000000000000000000000000000000000000001"),
            token1: address!("0000000000000000000000000000000000000002"),
            tickLower: I24::try_from(-120i32).unwrap(),
            tickUpper: I24::try_from(240i32).unwrap(),
            liquidity: 42,
            feeGrowthInside0LastX128: U256::from(7),
            feeGrowthInside1LastX128: U256::from(8),
            tokensOwed0: 9,
            tokensOwed1: 10,
        };
        let view = AlgebraPositionView::from_nfpm_returns(U256::from(1), &ret);
        assert_eq!(view.tick_lower, -120);
        assert_eq!(view.tick_upper, 240);
        assert_eq!(view.tokens_owed_1_stale, 10);
    }
}

#[cfg(test)]
mod custom_deployer_tests {
    use super::*;
    use alloy_primitives::{address, aliases::U88};

    #[test]
    fn maps_deployer_and_tokens_owed() {
        let deployer = address!("5D433A94A4a2aA8f9AA34D8D15692Dc2E9960584");
        let ret = IAlgebraCustomDeployerNfpm::positionsReturn {
            nonce: U88::from(1u64),
            operator: Address::ZERO,
            token0: address!("2222222222222222222222222222222222222222"),
            token1: address!("3333333333333333333333333333333333333333"),
            deployer,
            tickLower: alloy_primitives::aliases::I24::try_from(-60i32).unwrap(),
            tickUpper: alloy_primitives::aliases::I24::try_from(60i32).unwrap(),
            liquidity: 5u128,
            feeGrowthInside0LastX128: U256::ZERO,
            feeGrowthInside1LastX128: U256::ZERO,
            tokensOwed0: 1000u128,
            tokensOwed1: 2000u128,
        };
        let v = CustomDeployerPositionView::from_nfpm_returns(U256::from(42u64), &ret);
        assert_eq!(v.deployer, deployer);
        assert_eq!(v.tick_lower, -60);
        assert_eq!(v.tokens_owed_0_stale, 1000);
        assert_eq!(v.tokens_owed_1_stale, 2000);
    }
}