wp-evm-v3-core 0.1.1

Pure data + quote + plan for v3-family (CL) DEXes — no async dependencies
Documentation
//! Pure CREATE2 pool address derivation for V3-pattern DEXes.
//!
//! Mirrors Uniswap V3's `PoolAddress.computeAddress`:
//!
//! ```solidity
//! pool = address(uint160(uint256(keccak256(abi.encodePacked(
//!     hex'ff', factory,
//!     keccak256(abi.encode(token0, token1, fee)),
//!     POOL_INIT_CODE_HASH
//! )))));
//! ```
//!
//! Pancake V3 and Sushi V3 reuse the same schema; their facades pin a
//! different `factory` + `init_code_hash` via `V3ProtocolConfig`.
//! Forks that deploy pools through a separate deployer set
//! `V3ProtocolConfig::pool_deployer`.

use alloy_primitives::{keccak256, Address, B256, U256};
use alloy_sol_types::SolValue;

/// Derive the V3 pool address for `(token_a, token_b, fee)` under the
/// given protocol configuration. Tokens may be passed in any order — the
/// function sorts them internally so `token0 < token1`.
///
/// Pure: no I/O, no allocation beyond the 96-byte salt preimage.
pub fn compute(
    deployer: Address,
    init_code_hash: B256,
    token_a: Address,
    token_b: Address,
    fee: u32,
) -> Address {
    debug_assert!(token_a != token_b, "pool_address::compute requires distinct tokens");
    debug_assert!(fee < (1u32 << 24), "V3 fee must fit in uint24");

    let (token0, token1) = if token_a < token_b { (token_a, token_b) } else { (token_b, token_a) };

    // abi.encode(token0, token1, uint24(fee)) is byte-identical to
    // abi.encode(token0, token1, uint256(fee)) for fee < 2^24, since
    // both pad to 32-byte words. We use U256 to avoid pulling in a
    // dedicated uint24 sol-type alias.
    let salt = keccak256((token0, token1, U256::from(fee)).abi_encode());
    deployer.create2(salt, init_code_hash)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::data::{SwapRouterKind, V3ProtocolConfig};
    use alloy_primitives::{address, b256};

    /// Canonical Uniswap V3 mainnet config — kept local so this test
    /// does not depend on the facade crate (`wp-evm-uniswap-v3`), which
    /// would create a back-reference.
    const UNISWAP_V3_MAINNET: V3ProtocolConfig = V3ProtocolConfig {
        factory: address!("1F98431c8aD98523631AE4a59f267346ea31F984"),
        pool_deployer: None,
        router: address!("E592427A0AEce92De3Edee1F18E0157C05861564"),
        swap_router_kind: SwapRouterKind::V1,
        position_mgr: address!("C36442b4a4522E871399CD717aBDD847Ab11FE88"),
        init_code_hash: b256!("e34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"),
        fee_tiers: &[100, 500, 3000, 10000],
        multicall: address!("cA11bde05977b3631167028862bE2a173976CA11"),
        quoter: None,
    };

    const USDC: Address = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
    const WETH: Address = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
    /// USDC / WETH 0.05% pool on Uniswap V3 mainnet.
    /// Source: <https://etherscan.io/address/0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640>
    const USDC_WETH_500_POOL: Address = address!("88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640");

    #[test]
    fn canonical_uniswap_v3_usdc_weth_500_matches_mainnet() {
        let pool = compute(
            UNISWAP_V3_MAINNET.pool_deployer.unwrap_or(UNISWAP_V3_MAINNET.factory),
            UNISWAP_V3_MAINNET.init_code_hash,
            USDC,
            WETH,
            500,
        );
        assert_eq!(pool, USDC_WETH_500_POOL);
    }

    #[test]
    fn token_order_does_not_change_result() {
        let forward = compute(
            UNISWAP_V3_MAINNET.pool_deployer.unwrap_or(UNISWAP_V3_MAINNET.factory),
            UNISWAP_V3_MAINNET.init_code_hash,
            USDC,
            WETH,
            500,
        );
        let reversed = compute(
            UNISWAP_V3_MAINNET.pool_deployer.unwrap_or(UNISWAP_V3_MAINNET.factory),
            UNISWAP_V3_MAINNET.init_code_hash,
            WETH,
            USDC,
            500,
        );
        assert_eq!(forward, reversed);
        assert_eq!(forward, USDC_WETH_500_POOL);
    }

    #[test]
    fn different_fee_tier_yields_different_address() {
        let p500 = compute(
            UNISWAP_V3_MAINNET.pool_deployer.unwrap_or(UNISWAP_V3_MAINNET.factory),
            UNISWAP_V3_MAINNET.init_code_hash,
            USDC,
            WETH,
            500,
        );
        let p3000 = compute(
            UNISWAP_V3_MAINNET.pool_deployer.unwrap_or(UNISWAP_V3_MAINNET.factory),
            UNISWAP_V3_MAINNET.init_code_hash,
            USDC,
            WETH,
            3000,
        );
        assert_ne!(p500, p3000);
    }

    #[test]
    fn fee_zero_produces_deterministic_nonzero_address() {
        let pool = compute(
            UNISWAP_V3_MAINNET.pool_deployer.unwrap_or(UNISWAP_V3_MAINNET.factory),
            UNISWAP_V3_MAINNET.init_code_hash,
            USDC,
            WETH,
            0,
        );
        assert_ne!(pool, Address::ZERO);
        assert_eq!(
            pool,
            compute(
                UNISWAP_V3_MAINNET.pool_deployer.unwrap_or(UNISWAP_V3_MAINNET.factory),
                UNISWAP_V3_MAINNET.init_code_hash,
                USDC,
                WETH,
                0,
            )
        );
    }

    #[test]
    fn fee_max_uint24_produces_deterministic_nonzero_address() {
        let max_uint24 = (1u32 << 24) - 1;
        let pool = compute(
            UNISWAP_V3_MAINNET.pool_deployer.unwrap_or(UNISWAP_V3_MAINNET.factory),
            UNISWAP_V3_MAINNET.init_code_hash,
            USDC,
            WETH,
            max_uint24,
        );
        assert_ne!(pool, Address::ZERO);
        assert_eq!(
            pool,
            compute(
                UNISWAP_V3_MAINNET.pool_deployer.unwrap_or(UNISWAP_V3_MAINNET.factory),
                UNISWAP_V3_MAINNET.init_code_hash,
                USDC,
                WETH,
                max_uint24,
            )
        );
    }

    #[test]
    #[cfg(debug_assertions)]
    #[should_panic(expected = "distinct tokens")]
    fn identical_tokens_panics_in_debug() {
        let _ = compute(
            UNISWAP_V3_MAINNET.pool_deployer.unwrap_or(UNISWAP_V3_MAINNET.factory),
            UNISWAP_V3_MAINNET.init_code_hash,
            USDC,
            USDC,
            500,
        );
    }

    #[test]
    fn compute_with_pancake_deployer_matches_real_pool() {
        let pancake_factory: Address = address!("0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865");
        let pancake_deployer: Address = address!("41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9");
        let pancake_init_code_hash: B256 =
            b256!("6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2");

        let usdt: Address = address!("55d398326f99059fF775485246999027B3197955");
        let usdc: Address = address!("8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d");
        let expected_pool: Address = address!("92b7807bF19b7DDdf89b706143896d05228f3121");

        let computed_with_deployer =
            compute(pancake_deployer, pancake_init_code_hash, usdt, usdc, 100);
        assert_eq!(computed_with_deployer, expected_pool);

        let computed_with_factory =
            compute(pancake_factory, pancake_init_code_hash, usdt, usdc, 100);
        assert_ne!(computed_with_factory, expected_pool);
    }
}