example_uniswap_v2_usdc_swap/
main.rs

1//! Example of uniswap getReserves() call emulation.
2#![cfg_attr(not(test), warn(unused_crate_dependencies))]
3
4use alloy_eips::BlockId;
5use alloy_provider::{network::Ethereum, DynProvider, Provider, ProviderBuilder};
6use alloy_sol_types::{sol, SolCall, SolValue};
7use anyhow::{anyhow, Result};
8use revm::{
9    context::TxEnv,
10    context_interface::result::{ExecutionResult, Output},
11    database::{AlloyDB, CacheDB},
12    database_interface::WrapDatabaseAsync,
13    primitives::{address, keccak256, Address, Bytes, StorageKey, TxKind, KECCAK_EMPTY, U256},
14    state::AccountInfo,
15    Context, ExecuteCommitEvm, ExecuteEvm, MainBuilder, MainContext,
16};
17use std::ops::Div;
18
19type AlloyCacheDB = CacheDB<WrapDatabaseAsync<AlloyDB<Ethereum, DynProvider>>>;
20
21#[tokio::main]
22async fn main() -> Result<()> {
23    // Initialize the Alloy provider and database
24    let rpc_url = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27";
25    let provider = ProviderBuilder::new().connect(rpc_url).await?.erased();
26
27    let alloy_db = WrapDatabaseAsync::new(AlloyDB::new(provider, BlockId::latest())).unwrap();
28    let mut cache_db = CacheDB::new(alloy_db);
29
30    // Random empty account
31    let account = address!("18B06aaF27d44B756FCF16Ca20C1f183EB49111f");
32
33    let weth = address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
34    let usdc = address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48");
35    let usdc_weth_pair = address!("B4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc");
36
37    let weth_balance_slot = StorageKey::from(3);
38
39    // Give our test account some fake WETH and ETH
40    let one_ether = U256::from(1_000_000_000_000_000_000u128);
41    let hashed_acc_balance_slot = keccak256((account, weth_balance_slot).abi_encode());
42    cache_db
43        .insert_account_storage(weth, hashed_acc_balance_slot.into(), one_ether)
44        .unwrap();
45
46    let acc_info = AccountInfo {
47        nonce: 0_u64,
48        balance: one_ether,
49        code_hash: KECCAK_EMPTY,
50        code: None,
51    };
52    cache_db.insert_account_info(account, acc_info);
53
54    let acc_weth_balance_before = balance_of(weth, account, &mut cache_db)?;
55    println!("WETH balance before swap: {}", acc_weth_balance_before);
56    let acc_usdc_balance_before = balance_of(usdc, account, &mut cache_db)?;
57    println!("USDC balance before swap: {}", acc_usdc_balance_before);
58
59    let (reserve0, reserve1) = get_reserves(usdc_weth_pair, &mut cache_db)?;
60
61    let amount_in = one_ether.div(U256::from(10));
62
63    // Calculate USDC amount out
64    let amount_out = get_amount_out(amount_in, reserve1, reserve0, &mut cache_db).await?;
65
66    // Transfer WETH to USDC-WETH pair
67    transfer(account, usdc_weth_pair, amount_in, weth, &mut cache_db)?;
68
69    // Execute low-level swap without using UniswapV2 router
70    swap(
71        account,
72        usdc_weth_pair,
73        account,
74        amount_out,
75        true,
76        &mut cache_db,
77    )?;
78
79    let acc_weth_balance_after = balance_of(weth, account, &mut cache_db)?;
80    println!("WETH balance after swap: {}", acc_weth_balance_after);
81    let acc_usdc_balance_after = balance_of(usdc, account, &mut cache_db)?;
82    println!("USDC balance after swap: {}", acc_usdc_balance_after);
83
84    println!("OK");
85    Ok(())
86}
87
88fn balance_of(token: Address, address: Address, alloy_db: &mut AlloyCacheDB) -> Result<U256> {
89    sol! {
90        function balanceOf(address account) public returns (uint256);
91    }
92
93    let encoded = balanceOfCall { account: address }.abi_encode();
94
95    let mut evm = Context::mainnet().with_db(alloy_db).build_mainnet();
96
97    let result = evm
98        .transact(TxEnv {
99            // 0x1 because calling USDC proxy from zero address fails
100            caller: address!("0000000000000000000000000000000000000001"),
101            kind: TxKind::Call(token),
102            data: encoded.into(),
103            value: U256::from(0),
104            ..Default::default()
105        })
106        .unwrap();
107
108    let value = match result {
109        ExecutionResult::Success {
110            output: Output::Call(value),
111            ..
112        } => value,
113        result => return Err(anyhow!("'balanceOf' execution failed: {result:?}")),
114    };
115
116    let balance = <U256>::abi_decode(&value)?;
117
118    Ok(balance)
119}
120
121async fn get_amount_out(
122    amount_in: U256,
123    reserve_in: U256,
124    reserve_out: U256,
125    cache_db: &mut AlloyCacheDB,
126) -> Result<U256> {
127    let uniswap_v2_router = address!("7a250d5630b4cf539739df2c5dacb4c659f2488d");
128    sol! {
129        function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut);
130    }
131
132    let encoded = getAmountOutCall {
133        amountIn: amount_in,
134        reserveIn: reserve_in,
135        reserveOut: reserve_out,
136    }
137    .abi_encode();
138
139    let mut evm = Context::mainnet().with_db(cache_db).build_mainnet();
140
141    let result = evm
142        .transact(TxEnv {
143            caller: address!("0000000000000000000000000000000000000000"),
144            kind: TxKind::Call(uniswap_v2_router),
145            data: encoded.into(),
146            value: U256::from(0),
147            ..Default::default()
148        })
149        .unwrap();
150
151    let value = match result {
152        ExecutionResult::Success {
153            output: Output::Call(value),
154            ..
155        } => value,
156        result => return Err(anyhow!("'getAmountOut' execution failed: {result:?}")),
157    };
158
159    let amount_out = <U256>::abi_decode(&value)?;
160
161    Ok(amount_out)
162}
163
164fn get_reserves(pair_address: Address, cache_db: &mut AlloyCacheDB) -> Result<(U256, U256)> {
165    sol! {
166        function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
167    }
168
169    let encoded = getReservesCall {}.abi_encode();
170
171    let mut evm = Context::mainnet().with_db(cache_db).build_mainnet();
172
173    let result = evm
174        .transact(TxEnv {
175            caller: address!("0000000000000000000000000000000000000000"),
176            kind: TxKind::Call(pair_address),
177            data: encoded.into(),
178            value: U256::from(0),
179            ..Default::default()
180        })
181        .unwrap();
182
183    let value = match result {
184        ExecutionResult::Success {
185            output: Output::Call(value),
186            ..
187        } => value,
188        result => return Err(anyhow!("'getReserves' execution failed: {result:?}")),
189    };
190
191    let (reserve0, reserve1, _) = <(U256, U256, u32)>::abi_decode(&value)?;
192
193    Ok((reserve0, reserve1))
194}
195
196fn swap(
197    from: Address,
198    pool_address: Address,
199    target: Address,
200    amount_out: U256,
201    is_token0: bool,
202    cache_db: &mut AlloyCacheDB,
203) -> Result<()> {
204    sol! {
205        function swap(uint amount0Out, uint amount1Out, address target, bytes callback) external;
206    }
207
208    let amount0_out = if is_token0 { amount_out } else { U256::from(0) };
209    let amount1_out = if is_token0 { U256::from(0) } else { amount_out };
210
211    let encoded = swapCall {
212        amount0Out: amount0_out,
213        amount1Out: amount1_out,
214        target,
215        callback: Bytes::new(),
216    }
217    .abi_encode();
218
219    let mut evm = Context::mainnet().with_db(cache_db).build_mainnet();
220
221    let tx = TxEnv {
222        caller: from,
223        kind: TxKind::Call(pool_address),
224        data: encoded.into(),
225        value: U256::from(0),
226        nonce: 1,
227        ..Default::default()
228    };
229
230    let ref_tx = evm.transact_commit(tx).unwrap();
231
232    match ref_tx {
233        ExecutionResult::Success { .. } => {}
234        result => return Err(anyhow!("'swap' execution failed: {result:?}")),
235    };
236
237    Ok(())
238}
239
240fn transfer(
241    from: Address,
242    to: Address,
243    amount: U256,
244    token: Address,
245    cache_db: &mut AlloyCacheDB,
246) -> Result<()> {
247    sol! {
248        function transfer(address to, uint amount) external returns (bool);
249    }
250
251    let encoded = transferCall { to, amount }.abi_encode();
252
253    let mut evm = Context::mainnet().with_db(cache_db).build_mainnet();
254
255    let tx = TxEnv {
256        caller: from,
257        kind: TxKind::Call(token),
258        data: encoded.into(),
259        value: U256::from(0),
260        ..Default::default()
261    };
262
263    let ref_tx = evm.transact_commit(tx).unwrap();
264    let success: bool = match ref_tx {
265        ExecutionResult::Success {
266            output: Output::Call(value),
267            ..
268        } => <bool>::abi_decode(&value)?,
269        result => return Err(anyhow!("'transfer' execution failed: {result:?}")),
270    };
271
272    if !success {
273        return Err(anyhow!("'transfer' failed"));
274    }
275
276    Ok(())
277}