Skip to content

Commit

Permalink
feat: Add uniswap V2 WETH-USDC swap example (bluealloy#1353)
Browse files Browse the repository at this point in the history
* Add uniswap V2 WETH-USDC swap example

* Use alloy primitives

* Use revm primitives

* Use alloydb

* Use only necessary pair slots

* typo

* Simplify error handling

* Improve numbers decoding

* Dont use panic for errors

* Remove unused feature

* Use alloydb instead of manually fetching data
  • Loading branch information
pawurb committed Apr 29, 2024
1 parent 1f44e4c commit 3f5bb76
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 2 deletions.
29 changes: 27 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/revm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ alloy-rpc-types = {git = "https://github.com/alloy-rs/alloy.git", optional = tru
alloy-transport = {git = "https://github.com/alloy-rs/alloy.git", optional = true, default-features = false }

[dev-dependencies]
alloy-sol-types = { version = "0.7.0", default-features = false, features = ["std"] }
ethers-contract = { version = "2.0.14", default-features = false }
anyhow = "1.0.82"
criterion = "0.5"
indicatif = "0.17"
reqwest = { version = "0.12" }

alloy-provider = { git = "https://github.com/alloy-rs/alloy.git", default-features = false, features = ["reqwest"] }
# needed for enabling TLS to use HTTPS connections when testing alloy DB
Expand Down Expand Up @@ -136,6 +138,11 @@ name = "db_by_ref"
path = "../../examples/db_by_ref.rs"
required-features = ["std", "serde-json"]

[[example]]
name = "uniswap_v2_usdc_swap"
path = "../../examples/uniswap_v2_usdc_swap.rs"
required-features = ["alloydb"]

[[bench]]
name = "bench"
path = "benches/bench.rs"
Expand Down
278 changes: 278 additions & 0 deletions examples/uniswap_v2_usdc_swap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
use alloy_provider::{network::Ethereum, ProviderBuilder, RootProvider};
use alloy_sol_types::{sol, SolCall, SolValue};
use alloy_transport_http::Http;
use anyhow::{anyhow, Result};
use reqwest::Client;
use revm::{
db::{AlloyDB, CacheDB},
primitives::{
address, keccak256, AccountInfo, Address, Bytes, ExecutionResult, Output, TransactTo, U256,
},
Evm,
};
use std::ops::Div;
use std::sync::Arc;

type AlloyCacheDB = CacheDB<AlloyDB<Http<Client>, Ethereum, Arc<RootProvider<Http<Client>>>>>;

#[tokio::main]
async fn main() -> Result<()> {
let client = ProviderBuilder::new()
.on_reqwest_http(
"https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27"
.parse()
.unwrap(),
)
.unwrap();
let client = Arc::new(client);
let mut cache_db = CacheDB::new(AlloyDB::new(client, None));

// Random empty account
let account = address!("18B06aaF27d44B756FCF16Ca20C1f183EB49111f");

let weth = address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
let usdc = address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48");
let usdc_weth_pair = address!("B4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc");

let weth_balance_slot = U256::from(3);

// give our test account some fake WETH and ETH
let one_ether = U256::from(1_000_000_000_000_000_000u128);
let hashed_acc_balance_slot = keccak256((account, weth_balance_slot).abi_encode());
cache_db
.insert_account_storage(weth, hashed_acc_balance_slot.into(), one_ether)
.unwrap();

let acc_info = AccountInfo {
nonce: 0_u64,
balance: one_ether,
code_hash: keccak256(Bytes::new()),
code: None,
};
cache_db.insert_account_info(account, acc_info);

let acc_weth_balance_before = balance_of(weth, account, &mut cache_db)?;
println!("WETH balance before swap: {}", acc_weth_balance_before);
let acc_usdc_balance_before = balance_of(usdc, account, &mut cache_db)?;
println!("USDC balance before swap: {}", acc_usdc_balance_before);

let (reserve0, reserve1) = get_reserves(usdc_weth_pair, &mut cache_db)?;

let amount_in = one_ether.div(U256::from(10));

// calculate USDC amount out
let amount_out = get_amount_out(amount_in, reserve1, reserve0, &mut cache_db).await?;

// transfer WETH to USDC-WETH pair
transfer(account, usdc_weth_pair, amount_in, weth, &mut cache_db)?;

// execute low-level swap without using UniswapV2 router
swap(
account,
usdc_weth_pair,
account,
amount_out,
true,
&mut cache_db,
)?;

let acc_weth_balance_after = balance_of(weth, account, &mut cache_db)?;
println!("WETH balance after swap: {}", acc_weth_balance_after);
let acc_usdc_balance_after = balance_of(usdc, account, &mut cache_db)?;
println!("USDC balance after swap: {}", acc_usdc_balance_after);

Ok(())
}

fn balance_of(token: Address, address: Address, cache_db: &mut AlloyCacheDB) -> Result<U256> {
sol! {
function balanceOf(address account) public returns (uint256);
}

let encoded = balanceOfCall { account: address }.abi_encode();

let mut evm = Evm::builder()
.with_db(cache_db)
.modify_tx_env(|tx| {
// 0x1 because calling USDC proxy from zero address fails
tx.caller = address!("0000000000000000000000000000000000000001");
tx.transact_to = TransactTo::Call(token);
tx.data = encoded.into();
tx.value = U256::from(0);
})
.build();

let ref_tx = evm.transact().unwrap();
let result = ref_tx.result;

let value = match result {
ExecutionResult::Success {
output: Output::Call(value),
..
} => value,
result => return Err(anyhow!("'balanceOf' execution failed: {result:?}")),
};

let balance = <U256>::abi_decode(&value, false)?;

Ok(balance)
}

async fn get_amount_out(
amount_in: U256,
reserve_in: U256,
reserve_out: U256,
cache_db: &mut AlloyCacheDB,
) -> Result<U256> {
let uniswap_v2_router = address!("7a250d5630b4cf539739df2c5dacb4c659f2488d");
sol! {
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut);
}

let encoded = getAmountOutCall {
amountIn: amount_in,
reserveIn: reserve_in,
reserveOut: reserve_out,
}
.abi_encode();

let mut evm = Evm::builder()
.with_db(cache_db)
.modify_tx_env(|tx| {
tx.caller = address!("0000000000000000000000000000000000000000");
tx.transact_to = TransactTo::Call(uniswap_v2_router);
tx.data = encoded.into();
tx.value = U256::from(0);
})
.build();

let ref_tx = evm.transact().unwrap();
let result = ref_tx.result;

let value = match result {
ExecutionResult::Success {
output: Output::Call(value),
..
} => value,
result => return Err(anyhow!("'getAmountOut' execution failed: {result:?}")),
};

let amount_out = <U256>::abi_decode(&value, false)?;

Ok(amount_out)
}

fn get_reserves(pair_address: Address, cache_db: &mut AlloyCacheDB) -> Result<(U256, U256)> {
sol! {
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}

let encoded = getReservesCall {}.abi_encode();

let mut evm = Evm::builder()
.with_db(cache_db)
.modify_tx_env(|tx| {
tx.caller = address!("0000000000000000000000000000000000000000");
tx.transact_to = TransactTo::Call(pair_address);
tx.data = encoded.into();
tx.value = U256::from(0);
})
.build();

let ref_tx = evm.transact().unwrap();
let result = ref_tx.result;

let value = match result {
ExecutionResult::Success {
output: Output::Call(value),
..
} => value,
result => return Err(anyhow!("'getReserves' execution failed: {result:?}")),
};

let (reserve0, reserve1, _) = <(U256, U256, u32)>::abi_decode(&value, false)?;

Ok((reserve0, reserve1))
}

fn swap(
from: Address,
pool_address: Address,
target: Address,
amount_out: U256,
is_token0: bool,
cache_db: &mut AlloyCacheDB,
) -> Result<()> {
sol! {
function swap(uint amount0Out, uint amount1Out, address target, bytes callback) external;
}

let amount0_out = if is_token0 { amount_out } else { U256::from(0) };
let amount1_out = if is_token0 { U256::from(0) } else { amount_out };

let encoded = swapCall {
amount0Out: amount0_out,
amount1Out: amount1_out,
target,
callback: Bytes::new(),
}
.abi_encode();

let mut evm = Evm::builder()
.with_db(cache_db)
.modify_tx_env(|tx| {
tx.caller = from;
tx.transact_to = TransactTo::Call(pool_address);
tx.data = encoded.into();
tx.value = U256::from(0);
})
.build();

let ref_tx = evm.transact_commit().unwrap();

match ref_tx {
ExecutionResult::Success { .. } => {}
result => return Err(anyhow!("'swap' execution failed: {result:?}")),
};

Ok(())
}

fn transfer(
from: Address,
to: Address,
amount: U256,
token: Address,
cache_db: &mut AlloyCacheDB,
) -> Result<()> {
sol! {
function transfer(address to, uint amount) external returns (bool);
}

let encoded = transferCall { to, amount }.abi_encode();

let mut evm = Evm::builder()
.with_db(cache_db)
.modify_tx_env(|tx| {
tx.caller = from;
tx.transact_to = TransactTo::Call(token);
tx.data = encoded.into();
tx.value = U256::from(0);
})
.build();

let ref_tx = evm.transact_commit().unwrap();
let success: bool = match ref_tx {
ExecutionResult::Success {
output: Output::Call(value),
..
} => <bool>::abi_decode(&value, false)?,
result => return Err(anyhow!("'transfer' execution failed: {result:?}")),
};

if !success {
return Err(anyhow!("'transfer' failed"));
}

Ok(())
}

0 comments on commit 3f5bb76

Please sign in to comment.