From 0ba1dab76b6cd1f0623286dc9fdbc758ca2812f5 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:13:26 +0900 Subject: [PATCH 1/4] add cleo v4 deployment config --- crates/gem_evm/src/uniswap/deployment/v4.rs | 15 ++ crates/swapper/src/uniswap/README.md | 148 ++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 crates/swapper/src/uniswap/README.md diff --git a/crates/gem_evm/src/uniswap/deployment/v4.rs b/crates/gem_evm/src/uniswap/deployment/v4.rs index e2cc45c44..e6693e7fa 100644 --- a/crates/gem_evm/src/uniswap/deployment/v4.rs +++ b/crates/gem_evm/src/uniswap/deployment/v4.rs @@ -75,6 +75,21 @@ pub fn get_uniswap_deployment_by_chain(chain: &Chain) -> Option { permit2, universal_router: "0xEf740bf23aCaE26f6492B10de645D6B98dC8Eaf3", }), + Chain::Celo => Some(V4Deployment { + quoter: "0x28566da1093609182dff2cb2a91cfd72e61d66cd", + permit2, + universal_router: "0xcb695bc5d3aa22cad1e6df07801b061a05a0233a", + }), + Chain::Monad => Some(V4Deployment { + quoter: "0xa222dd357a9076d1091ed6aa2e16c9742dd26891", + permit2, + universal_router: "0x0d97dc33264bfc1c226207428a79b26757fb9dc3", + }), + Chain::Ink => Some(V4Deployment { + quoter: "0x3972c00f7ed4885e145823eb7c655375d275a1c5", + permit2, + universal_router: "0x112908dac86e20e7241b0927479ea3bf935d1fa0", + }), _ => None, } } diff --git a/crates/swapper/src/uniswap/README.md b/crates/swapper/src/uniswap/README.md new file mode 100644 index 000000000..e24b02246 --- /dev/null +++ b/crates/swapper/src/uniswap/README.md @@ -0,0 +1,148 @@ +# Uniswap Swap Module + +This module implements on-chain swap support for Uniswap V3 and V4 (and forks like PancakeSwap, Aerodrome, Oku, Wagmi) across multiple EVM chains. + +## Architecture + +The implementation is split across two crates: + +### `gem_evm::uniswap` — Contract addresses and ABI encoding + +Located in `core/crates/gem_evm/src/uniswap/`: + +| File/Dir | Purpose | +|----------|---------| +| `deployment/mod.rs` | `Deployment` trait, Permit2 address map, provider-by-contract lookup | +| `deployment/v3.rs` | V3 deployment addresses per chain (QuoterV2 + UniversalRouter) | +| `deployment/v4.rs` | V4 deployment addresses per chain (V4Quoter + UniversalRouter) | +| `path.rs` | `BasePair` (native/stables/alternatives per chain), path encoding | +| `contracts/` | Solidity ABI types (generated via `alloy-sol-macro`) | +| `command.rs` | UniversalRouter command encoding | +| `mod.rs` | `FeeTier` enum | + +### `swapper::uniswap` — Swap logic and quoting + +Located in `core/crates/swapper/src/uniswap/`: + +| File/Dir | Purpose | +|----------|---------| +| `v3/provider.rs` | `UniswapV3` — implements `Swapper` trait for V3 pools | +| `v4/provider.rs` | `UniswapV4` — implements `Swapper` trait for V4 pools | +| `universal_router/` | Per-DEX router configs (fee tiers, deployment lookup) | +| `swap_route.rs` | Route building (direct and two-hop) | +| `quote_result.rs` | Best-quote selection from batch RPC results | +| `fee_token.rs` | Fee token preference logic | +| `deadline.rs` | Signature deadline calculation | +| `default.rs` | Factory functions to create boxed swapper instances | + +## How a Swap Works + +1. **Quote request** arrives with `from_asset`, `to_asset`, `value`, and `slippage` +2. **Deployment lookup** — find QuoterV2/V4Quoter + UniversalRouter for the chain +3. **Path building** — construct direct paths (all fee tiers) and two-hop paths through intermediaries from `BasePair` +4. **Batch RPC** — call Quoter contract with all path combinations in parallel batches +5. **Best quote** — select the path with the highest output amount +6. **Swap data** — encode UniversalRouter commands with Permit2 approval if needed + +### V3 vs V4 Differences + +| Aspect | V3 | V4 | +|--------|----|----| +| Quoter contract | QuoterV2 | V4Quoter | +| Native token address | WETH (wrapped) | `address(0)` | +| Pool identifier | path-encoded (token+fee+token) | PoolKey struct (currency0, currency1, fee, tickSpacing, hooks) | +| Default gas limit | 500,000 | 300,000 | +| `get_base_pair` call | `weth_as_native: true` | `weth_as_native: false` | + +### V3 Fork Support + +V3 uses the `UniversalRouterProvider` trait to abstract over Uniswap forks: + +| Provider | Chains | Fee Tiers | +|----------|--------|-----------| +| Uniswap V3 | Ethereum, Optimism, Arbitrum, Polygon, AvalancheC, Base, SmartChain, ZkSync, Celo, Blast, World, Unichain, Monad, Stable | 100, 500, 3000, 10000 | +| PancakeSwap V3 | SmartChain, OpBNB, Arbitrum, Linea, Base | 100, 500, 2500, 10000 | +| Aerodrome | Base | 100, 400, 500, 3000, 10000 | +| Oku | Sonic, Mantle, Gnosis, Plasma | 100, 500, 3000, 10000 | +| Wagmi | Sonic | 500, 1500, 3000, 10000 | + +## Current V4 Chain Support + +Ethereum, Optimism, Arbitrum, Polygon, AvalancheC, Base, SmartChain, Blast, World, Unichain, Celo, Monad, Ink + +## How to Add a New Chain + +### Prerequisites + +The chain must already exist in `primitives::Chain` and `primitives::EVMChain`. + +### Steps + +#### 1. Add Permit2 address (`gem_evm/src/uniswap/deployment/mod.rs`) + +Add the chain to `get_uniswap_permit2_by_chain`. Most chains use the standard Permit2: + +```rust +Chain::Ethereum +| Chain::Optimism +| Chain::NewChain // <-- add here +=> Some("0x000000000022D473030F116dDEE9F6B43aC78BA3"), +``` + +ZkSync-family chains use a different address. + +#### 2. Add deployment addresses + +**For V3** (`gem_evm/src/uniswap/deployment/v3.rs`): + +Add to `get_uniswap_router_deployment_by_chain`: + +```rust +Chain::NewChain => Some(V3Deployment { + quoter_v2: "0x...", // QuoterV2 address + permit2, + universal_router: "0x...", // UniversalRouter address +}), +``` + +**For V4** (`gem_evm/src/uniswap/deployment/v4.rs`): + +Add to `get_uniswap_deployment_by_chain`: + +```rust +Chain::NewChain => Some(V4Deployment { + quoter: "0x...", // V4Quoter address + permit2, + universal_router: "0x...", // UniversalRouter address +}), +``` + +Find addresses at: +- V3: https://docs.uniswap.org/contracts/v3/reference/deployments/ +- V4: https://docs.uniswap.org/contracts/v4/deployments +- All: https://github.com/Uniswap/contracts/blob/main/deployments/index.md + +#### 3. Add base pairs (`gem_evm/src/uniswap/path.rs`) + +In `get_base_pair`, add token addresses for the chain: + +- **BTC** (WBTC equivalent) — empty string if none +- **USDC** — required for routing +- **USDT** — empty string if none + +These tokens are used as intermediaries for two-hop routing (e.g., TOKEN → USDC → TOKEN). + +#### 4. Verify (no code needed) + +Both V3 and V4 providers auto-discover supported chains: +- V3: `get_deployment_by_chain()` returns `Some` → chain is supported +- V4: `get_uniswap_deployment_by_chain()` returns `Some` → chain is supported + +The `GemSwapper` in gemstone registers both providers at startup. No additional wiring is needed. + +#### 5. Test + +```bash +cargo clippy -p gem_evm -p swapper -- -D warnings +cargo test -p swapper +``` From cf5fbb647e7d78b81ebc4fcf5371a1058066c731 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:57:39 +0900 Subject: [PATCH 2/4] extend okx chain support --- crates/swapper/src/proxy/provider.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index 01135463c..e512dd273 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -104,7 +104,17 @@ impl ProxyProvider { } pub fn new_okx(rpc_provider: Arc) -> Self { - Self::new_with_path(SwapperProvider::Okx, "okx", vec![SwapperChainAsset::All(Chain::Solana)], rpc_provider) + Self::new_with_path( + SwapperProvider::Okx, + "okx", + vec![ + SwapperChainAsset::All(Chain::Solana), + SwapperChainAsset::All(Chain::Manta), + SwapperChainAsset::All(Chain::Mantle), + SwapperChainAsset::All(Chain::XLayer), + ], + rpc_provider, + ) } pub fn new_cetus_aggregator(rpc_provider: Arc) -> Self { From afe64c3a4c408bf3099797b3e5f6fae42ab3958c Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:32:35 +0900 Subject: [PATCH 3/4] use checksum addresses --- crates/gem_evm/src/uniswap/deployment/v4.rs | 12 +- crates/swapper/src/uniswap/README.md | 148 -------------------- 2 files changed, 6 insertions(+), 154 deletions(-) delete mode 100644 crates/swapper/src/uniswap/README.md diff --git a/crates/gem_evm/src/uniswap/deployment/v4.rs b/crates/gem_evm/src/uniswap/deployment/v4.rs index e6693e7fa..918b03875 100644 --- a/crates/gem_evm/src/uniswap/deployment/v4.rs +++ b/crates/gem_evm/src/uniswap/deployment/v4.rs @@ -76,19 +76,19 @@ pub fn get_uniswap_deployment_by_chain(chain: &Chain) -> Option { universal_router: "0xEf740bf23aCaE26f6492B10de645D6B98dC8Eaf3", }), Chain::Celo => Some(V4Deployment { - quoter: "0x28566da1093609182dff2cb2a91cfd72e61d66cd", + quoter: "0x28566da1093609182dFf2cB2A91CFD72e61d66cd", permit2, - universal_router: "0xcb695bc5d3aa22cad1e6df07801b061a05a0233a", + universal_router: "0xcb695bc5D3Aa22cAD1E6DF07801b061a05A0233A", }), Chain::Monad => Some(V4Deployment { - quoter: "0xa222dd357a9076d1091ed6aa2e16c9742dd26891", + quoter: "0xa222Dd357A9076d1091Ed6Aa2e16C9742dD26891", permit2, - universal_router: "0x0d97dc33264bfc1c226207428a79b26757fb9dc3", + universal_router: "0x0D97Dc33264bfC1c226207428A79b26757fb9dc3", }), Chain::Ink => Some(V4Deployment { - quoter: "0x3972c00f7ed4885e145823eb7c655375d275a1c5", + quoter: "0x3972C00f7ed4885e145823eb7C655375d275A1C5", permit2, - universal_router: "0x112908dac86e20e7241b0927479ea3bf935d1fa0", + universal_router: "0x112908daC86e20e7241B0927479Ea3Bf935d1fa0", }), _ => None, } diff --git a/crates/swapper/src/uniswap/README.md b/crates/swapper/src/uniswap/README.md deleted file mode 100644 index e24b02246..000000000 --- a/crates/swapper/src/uniswap/README.md +++ /dev/null @@ -1,148 +0,0 @@ -# Uniswap Swap Module - -This module implements on-chain swap support for Uniswap V3 and V4 (and forks like PancakeSwap, Aerodrome, Oku, Wagmi) across multiple EVM chains. - -## Architecture - -The implementation is split across two crates: - -### `gem_evm::uniswap` — Contract addresses and ABI encoding - -Located in `core/crates/gem_evm/src/uniswap/`: - -| File/Dir | Purpose | -|----------|---------| -| `deployment/mod.rs` | `Deployment` trait, Permit2 address map, provider-by-contract lookup | -| `deployment/v3.rs` | V3 deployment addresses per chain (QuoterV2 + UniversalRouter) | -| `deployment/v4.rs` | V4 deployment addresses per chain (V4Quoter + UniversalRouter) | -| `path.rs` | `BasePair` (native/stables/alternatives per chain), path encoding | -| `contracts/` | Solidity ABI types (generated via `alloy-sol-macro`) | -| `command.rs` | UniversalRouter command encoding | -| `mod.rs` | `FeeTier` enum | - -### `swapper::uniswap` — Swap logic and quoting - -Located in `core/crates/swapper/src/uniswap/`: - -| File/Dir | Purpose | -|----------|---------| -| `v3/provider.rs` | `UniswapV3` — implements `Swapper` trait for V3 pools | -| `v4/provider.rs` | `UniswapV4` — implements `Swapper` trait for V4 pools | -| `universal_router/` | Per-DEX router configs (fee tiers, deployment lookup) | -| `swap_route.rs` | Route building (direct and two-hop) | -| `quote_result.rs` | Best-quote selection from batch RPC results | -| `fee_token.rs` | Fee token preference logic | -| `deadline.rs` | Signature deadline calculation | -| `default.rs` | Factory functions to create boxed swapper instances | - -## How a Swap Works - -1. **Quote request** arrives with `from_asset`, `to_asset`, `value`, and `slippage` -2. **Deployment lookup** — find QuoterV2/V4Quoter + UniversalRouter for the chain -3. **Path building** — construct direct paths (all fee tiers) and two-hop paths through intermediaries from `BasePair` -4. **Batch RPC** — call Quoter contract with all path combinations in parallel batches -5. **Best quote** — select the path with the highest output amount -6. **Swap data** — encode UniversalRouter commands with Permit2 approval if needed - -### V3 vs V4 Differences - -| Aspect | V3 | V4 | -|--------|----|----| -| Quoter contract | QuoterV2 | V4Quoter | -| Native token address | WETH (wrapped) | `address(0)` | -| Pool identifier | path-encoded (token+fee+token) | PoolKey struct (currency0, currency1, fee, tickSpacing, hooks) | -| Default gas limit | 500,000 | 300,000 | -| `get_base_pair` call | `weth_as_native: true` | `weth_as_native: false` | - -### V3 Fork Support - -V3 uses the `UniversalRouterProvider` trait to abstract over Uniswap forks: - -| Provider | Chains | Fee Tiers | -|----------|--------|-----------| -| Uniswap V3 | Ethereum, Optimism, Arbitrum, Polygon, AvalancheC, Base, SmartChain, ZkSync, Celo, Blast, World, Unichain, Monad, Stable | 100, 500, 3000, 10000 | -| PancakeSwap V3 | SmartChain, OpBNB, Arbitrum, Linea, Base | 100, 500, 2500, 10000 | -| Aerodrome | Base | 100, 400, 500, 3000, 10000 | -| Oku | Sonic, Mantle, Gnosis, Plasma | 100, 500, 3000, 10000 | -| Wagmi | Sonic | 500, 1500, 3000, 10000 | - -## Current V4 Chain Support - -Ethereum, Optimism, Arbitrum, Polygon, AvalancheC, Base, SmartChain, Blast, World, Unichain, Celo, Monad, Ink - -## How to Add a New Chain - -### Prerequisites - -The chain must already exist in `primitives::Chain` and `primitives::EVMChain`. - -### Steps - -#### 1. Add Permit2 address (`gem_evm/src/uniswap/deployment/mod.rs`) - -Add the chain to `get_uniswap_permit2_by_chain`. Most chains use the standard Permit2: - -```rust -Chain::Ethereum -| Chain::Optimism -| Chain::NewChain // <-- add here -=> Some("0x000000000022D473030F116dDEE9F6B43aC78BA3"), -``` - -ZkSync-family chains use a different address. - -#### 2. Add deployment addresses - -**For V3** (`gem_evm/src/uniswap/deployment/v3.rs`): - -Add to `get_uniswap_router_deployment_by_chain`: - -```rust -Chain::NewChain => Some(V3Deployment { - quoter_v2: "0x...", // QuoterV2 address - permit2, - universal_router: "0x...", // UniversalRouter address -}), -``` - -**For V4** (`gem_evm/src/uniswap/deployment/v4.rs`): - -Add to `get_uniswap_deployment_by_chain`: - -```rust -Chain::NewChain => Some(V4Deployment { - quoter: "0x...", // V4Quoter address - permit2, - universal_router: "0x...", // UniversalRouter address -}), -``` - -Find addresses at: -- V3: https://docs.uniswap.org/contracts/v3/reference/deployments/ -- V4: https://docs.uniswap.org/contracts/v4/deployments -- All: https://github.com/Uniswap/contracts/blob/main/deployments/index.md - -#### 3. Add base pairs (`gem_evm/src/uniswap/path.rs`) - -In `get_base_pair`, add token addresses for the chain: - -- **BTC** (WBTC equivalent) — empty string if none -- **USDC** — required for routing -- **USDT** — empty string if none - -These tokens are used as intermediaries for two-hop routing (e.g., TOKEN → USDC → TOKEN). - -#### 4. Verify (no code needed) - -Both V3 and V4 providers auto-discover supported chains: -- V3: `get_deployment_by_chain()` returns `Some` → chain is supported -- V4: `get_uniswap_deployment_by_chain()` returns `Some` → chain is supported - -The `GemSwapper` in gemstone registers both providers at startup. No additional wiring is needed. - -#### 5. Test - -```bash -cargo clippy -p gem_evm -p swapper -- -D warnings -cargo test -p swapper -``` From 947cf453560711379d4ca4a58775006febe9c5f2 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:32:37 +0900 Subject: [PATCH 4/4] enable swap for xlayer, respect approval from proxy for okx --- crates/primitives/src/chain_config.rs | 2 +- crates/swapper/src/proxy/provider.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/primitives/src/chain_config.rs b/crates/primitives/src/chain_config.rs index eefe3c52c..826aab6f0 100644 --- a/crates/primitives/src/chain_config.rs +++ b/crates/primitives/src/chain_config.rs @@ -1175,7 +1175,7 @@ static CHAIN_CONFIGS: &[ChainConfig] = &[ minimum_account_balance: None, block_time: 2_000, rank: 30, - is_swap_supported: false, + is_swap_supported: true, is_nft_supported: false, is_utxo: false, evm: Some(EvmChainConfig { diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index e512dd273..bb37c8b00 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -47,6 +47,10 @@ where } pub async fn check_approval_and_limit(&self, quote: &Quote, quote_data: &SwapQuoteData) -> Result<(Option, Option), SwapperError> { + if let Some(ref approval) = quote_data.approval { + return Ok((Some(approval.clone()), quote_data.gas_limit.clone())); + } + let request = "e.request; let from_asset = request.from_asset.asset_id(); @@ -171,11 +175,7 @@ impl ProxyProvider { Self::new_with_path( SwapperProvider::Relay, "relay", - vec![ - SwapperChainAsset::All(Chain::Hyperliquid), - SwapperChainAsset::All(Chain::Manta), - SwapperChainAsset::All(Chain::Berachain), - ], + vec![SwapperChainAsset::All(Chain::Hyperliquid), SwapperChainAsset::All(Chain::Berachain)], rpc_provider, ) }