Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add uniswap V2 WETH-USDC swap example #1353

Merged
merged 11 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions crates/revm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ 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 }
ethers-contract = { version = "2.0.14", default-features = false }
anyhow = "1.0.82"
criterion = "0.5"
Expand Down Expand Up @@ -136,6 +137,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 = ["std", "serde-json", "ethersdb"]

[[bench]]
name = "bench"
path = "benches/bench.rs"
Expand Down
348 changes: 348 additions & 0 deletions examples/uniswap_v2_usdc_swap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
use alloy_sol_types::{sol, SolCall, SolValue};
use ethers_providers::{Http, Provider};
use revm::{
db::{CacheDB, EmptyDB, EmptyDBTyped, EthersDB},
primitives::{
address, keccak256, AccountInfo, Address, Bytes, ExecutionResult, Output, TransactTo, U256,
},
Database, Evm,
};
use std::ops::Div;
use std::{convert::Infallible, sync::Arc};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Provider::<Http>::try_from(
"https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
)?;

let client = Arc::new(client);
let mut ethersdb = EthersDB::new(Arc::clone(&client), None).unwrap();
let mut cache_db = CacheDB::new(EmptyDB::default());

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

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

// USDC uses a proxy pattern so we have to fetch implementation address
let usdc_impl_slot: U256 = "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3"
.parse()
.unwrap();
let usdc_impl_raw = ethersdb.storage(usdc, usdc_impl_slot).unwrap();
let usdc_impl: Address = format!("{:x}", usdc_impl_raw)[24..].parse().unwrap();

// populate basic data
for addr in [weth, usdc, usdc_weth_pair, uniswap_v2_router, usdc_impl] {
let acc_info = ethersdb.basic(addr).unwrap().unwrap();
cache_db.insert_account_info(addr, acc_info);
}

cache_db
.insert_account_storage(usdc, usdc_impl_slot, usdc_impl_raw)
.unwrap();

// populate WETH balance for USDC-WETH pair
let weth_balance_slot = U256::from(3);

let pair_weth_balance_slot = keccak256((usdc_weth_pair, weth_balance_slot).abi_encode());

let value = ethersdb
.storage(weth, pair_weth_balance_slot.into())
.unwrap();
cache_db
.insert_account_storage(weth, pair_weth_balance_slot.into(), value)
.unwrap();

// populate USDC balance for USDC-WETH pair
let usdc_balance_slot = U256::from(9);

let pair_usdc_balance_slot = keccak256((usdc_weth_pair, usdc_balance_slot).abi_encode());

let value = ethersdb
.storage(usdc, pair_usdc_balance_slot.into())
.unwrap();
cache_db
.insert_account_storage(usdc, pair_usdc_balance_slot.into(), value)
.unwrap();

// 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);

// populate UniswapV2 pair slots
let usdc_weth_pair_address = usdc_weth_pair;
let pair_acc_info = ethersdb.basic(usdc_weth_pair_address).unwrap().unwrap();
cache_db.insert_account_info(usdc_weth_pair_address, pair_acc_info);
for i in 0..=12 {
let storage_slot = U256::from(i);
let value = ethersdb
.storage(usdc_weth_pair_address, storage_slot)
.unwrap();
cache_db
.insert_account_storage(usdc_weth_pair_address, storage_slot, value)
.unwrap();
}

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

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

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).await?;

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

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

Ok(())
}

async fn balance_of(
token: Address,
address: Address,
cache_db: &mut CacheDB<EmptyDBTyped<Infallible>>,
) -> anyhow::Result<u128> {
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 => panic!("'balance_of' execution failed: {result:?}"),
};

let balance: u128 = match <u128>::abi_decode(&value, false) {
Ok(balance) => balance,
Err(e) => panic!("'balance_of' decode failed: {:?}", e),
};

Ok(balance)
}

async fn get_amount_out(
amount_in: U256,
reserve_in: U256,
reserve_out: U256,
cache_db: &mut CacheDB<EmptyDBTyped<Infallible>>,
) -> anyhow::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 => panic!("'get_amount_out' execution failed: {result:?}"),
};

let amount_out: u128 = match <u128>::abi_decode(&value, false) {
Ok(amount_out) => amount_out,
Err(e) => panic!("'get_amount_out' decode failed: {:?}", e),
};

Ok(U256::from(amount_out))
}

async fn get_reserves(
pair_address: Address,
cache_db: &mut CacheDB<EmptyDBTyped<Infallible>>,
) -> anyhow::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 => panic!("'get_reserves' execution failed: {result:?}"),
};

let (reserve0, reserve1): (u128, u128) = match <(u128, u128, u32)>::abi_decode(&value, false) {
Ok((reserve0, reserve1, _)) => (reserve0, reserve1),
Err(e) => panic!("'get_reserves' decode failed: {:?}", e),
};
onbjerg marked this conversation as resolved.
Show resolved Hide resolved

Ok((U256::from(reserve0), U256::from(reserve1)))
}

async fn swap(
from: Address,
pool_address: Address,
target: Address,
amount_out: U256,
is_token0: bool,
cache_db: &mut CacheDB<EmptyDBTyped<Infallible>>,
) -> anyhow::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 => panic!("'swap' execution failed: {result:?}"),
};

Ok(())
}

async fn transfer(
from: Address,
to: Address,
amount: U256,
token: Address,
cache_db: &mut CacheDB<EmptyDBTyped<Infallible>>,
) -> anyhow::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 result: bool = match ref_tx {
ExecutionResult::Success {
output: Output::Call(value),
..
} => {
let success: bool = match <bool>::abi_decode(&value, false) {
Ok(balance) => balance,
Err(e) => panic!("'transfer' decode failed: {:?}", e),
};
success
}
result => panic!("'transfer' execution failed: {result:?}"),
};

if !result {
panic!("transfer failed");
}

Ok(())
}
Loading