Uniswap v3 with ICP in Rust

I’m trying to implement Uniswap v3 with Rust. The transaction seems to be processed, but I’m seeing an error on Etherscan: “Warning! Error encountered during contract execution [execution reverted]”.

Etherscan Transaction: Sepolia Transaction Hash (Txhash) Details | Etherscan

Here’s my Rust code for the swap:

const UNISWAP_ROUTER_ADDRESS: &str = "0xeE567Fe1712Faf6149d80dA1E6934E354124CfE3";
const USDC_ADDRESS: &str = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238";

#[update]
pub async fn swap_eth_to_usdc() -> String {
    let caller = validate_caller_not_anonymous();
    
    let usdc_address = Address::from_str(USDC_ADDRESS).expect("Invalid USDC address");
    let router_address = Address::from_str(UNISWAP_ROUTER_ADDRESS).expect("Invalid router address");
    let router_alloy_address: AlloyAddress = AlloyAddress::from_slice(router_address.as_ref());

    // ETH to swap in Wei and minimum USDC to receive
    let eth_amount = U256::from(5 * 10_u128.pow(16)); // 0.05 ETH
    let min_usdc = U256::from(1 * 10_u128.pow(6)); // Minimum 1 USDC with 6 decimals
    let chain_id = read_state(|s| s.ethereum_network().chain_id());
    let nonce = nat_to_u64(transaction_count(Some(caller), Some(BlockTag::Latest)).await);
    let (gas_limit, max_fee_per_gas, max_priority_fee_per_gas) = estimate_transaction_fees();

    // Data payload for Uniswap's `swapExactETHForTokens` function
    let swap_data = format!(
        // "0x7ff36ab500000000000000000000000000000000000000000000000000000000000000000000000000000000000000{}000000000000000000000000{}",
        "0xb6f9de95{}{}",
        hex::encode(usdc_address.as_ref()),
        hex::encode(router_alloy_address.as_ref() as &[u8; 20]) // Explicitly cast to &[u8; 20]
    );

    // Create and sign transaction
    let transaction = TxEip1559 {
        chain_id,
        nonce,
        gas_limit,
        max_fee_per_gas,
        max_priority_fee_per_gas,
        to: TxKind::Call(router_alloy_address),
        value: eth_amount,
        access_list: Default::default(),
        input: swap_data.into(),
    };

    let wallet = EthereumWallet::new(caller).await;
    let tx_hash = transaction.signature_hash().0;
    let (raw_signature, recovery_id) = wallet.sign_with_ecdsa(tx_hash).await;
    let signature = Signature::from_bytes_and_parity(&raw_signature, recovery_id.is_y_odd())
        .expect("Failed to create a signature");
    let signed_tx = transaction.into_signed(signature);

    let raw_transaction_hash = *signed_tx.hash();
    let mut tx_bytes: Vec<u8> = vec![];
    TxEnvelope::from(signed_tx).encode_2718(&mut tx_bytes);
    let raw_transaction_hex = format!("0x{}", hex::encode(&tx_bytes));
    ic_cdk::println!(
        "Sending raw transaction hex {} with transaction hash {}",
        raw_transaction_hex,
        raw_transaction_hash
    );

    // Send transaction
    let single_rpc_service = read_state(|s| s.single_evm_rpc_service());
    let (result,) = EVM_RPC
        .eth_send_raw_transaction(
            single_rpc_service,
            None,
            raw_transaction_hex.clone(),
            2_000_000_000_u128,
        )
        .await
        .unwrap_or_else(|e| {
            panic!(
                "Failed to send raw transaction {}, error: {:?}",
                raw_transaction_hex, e
            )
        });

    ic_cdk::println!(
        "Result of sending raw transaction {}: {:?}",
        raw_transaction_hex,
        result
    );

    raw_transaction_hash.to_string()
}
2 Likes

It is difficult to say why transaction is reverting as the error message can be caused by a number of issues. These are some situations I have run into revert errors:

  • Not enough ETH on from account to cover gas
  • No from address specified with call
  • No chain id specified
  • Parameter format errors
  • plus others I don’t remember…

Have you considered giving ic-alloy a try? It provides an abstraction layer on top of the EVM RPC canister and simplifies interactions a lot.

This example function does a Uniswap V3 swap:

use crate::{
    evm::utils::{get_rpc_service, get_signer},
    IUniswapV3SwapRouter, UNISWAP_V3_SWAP_ROUTER,
};
use alloy::{
    network::EthereumWallet,
    primitives::{aliases::U24, Address, U160, U256},
    providers::ProviderBuilder,
    transports::icp::IcpConfig,
};

pub async fn swap(
    token_in: Address,
    token_out: Address,
    fee: U24,
    amount_in: U256,
    amount_out_minimum: U256,
) -> Result<String, String> {
    let (signer, recipient) = get_signer();
    let wallet = EthereumWallet::from(signer);
    let rpc_service = get_rpc_service();
    let config = IcpConfig::new(rpc_service);
    let provider = ProviderBuilder::new()
        .with_recommended_fillers()
        .wallet(wallet)
        .on_icp(config);

    let args = IUniswapV3SwapRouter::ExactInputSingleParams {
        tokenIn: token_in,
        tokenOut: token_out,
        fee,
        recipient,
        amountIn: amount_in,
        amountOutMinimum: amount_out_minimum,
        sqrtPriceLimitX96: U160::from(0),
    };

    let v3_swap_router = IUniswapV3SwapRouter::new(UNISWAP_V3_SWAP_ROUTER, provider.clone());

    match v3_swap_router.exactInputSingle(args).send().await {
        Ok(res) => Ok(format!("{}", res.tx_hash())),
        Err(e) => Err(e.to_string()),
    }
}

See full example repository here: GitHub - ic-alloy/ic-alloy-dca: A semi-autonomous agent, swapping ERC-20 tokens on Uniswap for you.

1 Like

Thank you for the solution, I am trying to implement this but got few errors:

 dfx canister --ic call basic_ethereum swap_eth_to_usdc '()'
Please enter the passphrase for your identity: [hidden]                                                              
Decryption complete.
Error: Failed update call.
Caused by: The replica returned a rejection error: reject code CanisterError, reject message Error from Canister zkrig-uqaaa-aaaap-qkmiq-cai: Canister called `ic0.trap` with message: Panicked at 'called `Option::unwrap()` on a `None` value', src/lib.rs:111:35
Canister Backtrace:
ic_cdk::api::trap
ic_cdk::printer::set_panic_hook::{{closure}}
std::panicking::rust_panic_with_hook
std::panicking::begin_panic_handler::{{closure}}
std::sys::backtrace::__rust_end_short_backtrace
rust_begin_unwind
core::panicking::panic_fmt
core::panicking::panic
core::option::unwrap_failed
std::thread::local::LocalKey<T>::with
basic_ethereum::__canister_method_swap_eth_to_usdc::{{closure}}
ic_cdk::futures::spawn
canister_update swap_eth_to_usdc
.
Consider gracefully handling failures from this canister or altering the canister to handle exceptions. See documentation: http://internetcomputer.org/docs/current/references/execution-errors#trapped-explicitly, error code None

My code:

use alloy::{
    signers::icp::IcpSigner,
    transports::icp::{RpcApi, RpcService as AlloyRpcService},
};

use alloy::{
    primitives::{address as alloyAddress, aliases::U24, Address as AlloyPrimitivesAddress},
    sol,
};
use ic_cdk::export_candid;
use ic_cdk_timers::TimerId;
use serde::{Serialize};
use std::cell::RefCell;

use alloy::{
    network::EthereumWallet as AllowNetworkEthereumWallet,
    primitives::{U160},
    providers::ProviderBuilder,
    transports::icp::IcpConfig,
};

pub const EVM_RPC_CANISTER_ID: Principal =
    Principal::from_slice(b"\x00\x00\x00\x00\x02\x30\x00\xCC\x01\x01"); // 7hfb6-caaaa-aaaar-qadga-cai
pub const EVM_RPC: EvmRpcCanister = EvmRpcCanister(EVM_RPC_CANISTER_ID);

#[init]
pub fn init(maybe_init: Option<InitArg>) {
    if let Some(init_arg) = maybe_init {
        init_state(init_arg)
    }
}

const USDC_ADDRESS: &str = "0xf08a50178dfcde18524640ea6618a1f965821715";

pub const UNISWAP_V3_SWAP_ROUTER: AlloyPrimitivesAddress = alloyAddress!("3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E");
pub const UNISWAP_V3_FACTORY: AlloyPrimitivesAddress = alloyAddress!("0227628f3F023bb0B980b67D528571c95c6DaC1c");

pub const MAX_ALLOWANCE: U256 = U256::MAX;

sol!(
    #[sol(rpc)]
    "sol/IUniswapV3SwapRouter.sol"
);

sol!(
    #[sol(rpc)]
    "sol/IUniswapV3Factory.sol"
);

sol!(
    #[sol(rpc)]
    "sol/IUniswapV3PoolState.sol"
);

sol!(
    #[sol(rpc)]
    "sol/IERC20.sol"
);

#[derive(Serialize, Deserialize, CandidType)]
pub struct CanisterSettingsDto {
    pub owner: String,
    pub token_in_address: String,
    pub token_in_name: String,
    pub token_out_address: String,
    pub token_out_name: String,
    pub fee: u64,
    pub amount_in: u64,
    pub slippage: u64,
    pub interval: u64,
}

#[derive(Default)]
pub struct State {
    // Settings
    owner: String,
    token_in_address: AlloyPrimitivesAddress,
    token_in_name: String,
    token_out_address: AlloyPrimitivesAddress,
    token_out_name: String,
    fee: U24,
    amount_in: U256,
    slippage: U256,
    interval: u64,

    // Runtime
    timer_id: Option<TimerId>,
    signer: Option<IcpSigner>,
    canister_eth_address: Option<AlloyPrimitivesAddress>,
    uniswap_v3_pool_address: Option<AlloyPrimitivesAddress>,
}

thread_local! {
    static STATE: RefCell<State> = RefCell::new(State::default());
}

export_candid!();

pub fn get_rpc_service() -> AlloyRpcService {
    AlloyRpcService::Custom(RpcApi {
        url: "https://ic-alloy-evm-rpc-proxy.kristofer-977.workers.dev/eth-sepolia".to_string(),
        headers: None,
    })
}

pub fn get_signer() -> (IcpSigner, AlloyPrimitivesAddress) {
    STATE.with_borrow(|state| {
        (
            state.signer.as_ref().unwrap().clone(),
            state.canister_eth_address.unwrap(),
        )
    })
}

pub async fn swap(
    token_in: AlloyPrimitivesAddress,
    token_out: AlloyPrimitivesAddress,
    fee: U24,
    amount_in: U256,
    amount_out_minimum: U256,
) -> Result<String, String> {
    let (signer, recipient) = get_signer();
    let wallet = AllowNetworkEthereumWallet::from(signer);
    let rpc_service = get_rpc_service();
    let config = IcpConfig::new(rpc_service);
    let provider = ProviderBuilder::new()
        .with_recommended_fillers()
        .wallet(wallet)
        .on_icp(config);

    let args = IUniswapV3SwapRouter::ExactInputSingleParams {
        tokenIn: token_in,
        tokenOut: token_out,
        fee,
        recipient,
        amountIn: amount_in,
        amountOutMinimum: amount_out_minimum,
        sqrtPriceLimitX96: U160::from(0),
    };

    let v3_swap_router = IUniswapV3SwapRouter::new(UNISWAP_V3_SWAP_ROUTER, provider.clone());

    match v3_swap_router.exactInputSingle(args).send().await {
        Ok(res) => Ok(format!("{}", res.tx_hash())),
        Err(e) => Err(e.to_string()),
    }
}

#[update]
pub async fn swap_eth_to_usdc() -> Result<String, String> {
    let fee: U24 = U24::from(3000);
    let amount_in: U256 = U256::from(1_000_000_000_000_000u64);
    let amount_out_minimum: U256 = U256::from(0);
    let token_in = AlloyPrimitivesAddress::from_str("0x0000000000000000000000000000000000000000").map_err(|e| e.to_string())?;
    let token_out = AlloyPrimitivesAddress::from_str("0xf08a50178dfcde18524640ea6618a1f965821715").map_err(|e| e.to_string())?;

    swap(token_in, token_out, fee, amount_in, amount_out_minimum).await
}

It seems you are never initializing state.canister_eth_address and state.signer. That would cause the error on line 111.

Could it be you are missing the init function? See example here: ic-alloy-dca/src/agent/src/service/init_upgrade.rs at main · ic-alloy/ic-alloy-dca · GitHub

Bro, just fork Uniswap V3 on Bitfinity which runs EVM in a canister.