Skip to content

Commit 3f5bb76

Browse files
authored
feat: Add uniswap V2 WETH-USDC swap example (bluealloy#1353)
* 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
1 parent 1f44e4c commit 3f5bb76

File tree

3 files changed

+312
-2
lines changed

3 files changed

+312
-2
lines changed

Diff for: Cargo.lock

+27-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: crates/revm/Cargo.toml

+7
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ alloy-rpc-types = {git = "https://github.com/alloy-rs/alloy.git", optional = tru
4646
alloy-transport = {git = "https://github.com/alloy-rs/alloy.git", optional = true, default-features = false }
4747

4848
[dev-dependencies]
49+
alloy-sol-types = { version = "0.7.0", default-features = false, features = ["std"] }
4950
ethers-contract = { version = "2.0.14", default-features = false }
5051
anyhow = "1.0.82"
5152
criterion = "0.5"
5253
indicatif = "0.17"
54+
reqwest = { version = "0.12" }
5355

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

141+
[[example]]
142+
name = "uniswap_v2_usdc_swap"
143+
path = "../../examples/uniswap_v2_usdc_swap.rs"
144+
required-features = ["alloydb"]
145+
139146
[[bench]]
140147
name = "bench"
141148
path = "benches/bench.rs"

Diff for: examples/uniswap_v2_usdc_swap.rs

+278
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
use alloy_provider::{network::Ethereum, ProviderBuilder, RootProvider};
2+
use alloy_sol_types::{sol, SolCall, SolValue};
3+
use alloy_transport_http::Http;
4+
use anyhow::{anyhow, Result};
5+
use reqwest::Client;
6+
use revm::{
7+
db::{AlloyDB, CacheDB},
8+
primitives::{
9+
address, keccak256, AccountInfo, Address, Bytes, ExecutionResult, Output, TransactTo, U256,
10+
},
11+
Evm,
12+
};
13+
use std::ops::Div;
14+
use std::sync::Arc;
15+
16+
type AlloyCacheDB = CacheDB<AlloyDB<Http<Client>, Ethereum, Arc<RootProvider<Http<Client>>>>>;
17+
18+
#[tokio::main]
19+
async fn main() -> Result<()> {
20+
let client = ProviderBuilder::new()
21+
.on_reqwest_http(
22+
"https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27"
23+
.parse()
24+
.unwrap(),
25+
)
26+
.unwrap();
27+
let client = Arc::new(client);
28+
let mut cache_db = CacheDB::new(AlloyDB::new(client, None));
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 = U256::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: keccak256(Bytes::new()),
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+
Ok(())
85+
}
86+
87+
fn balance_of(token: Address, address: Address, cache_db: &mut AlloyCacheDB) -> Result<U256> {
88+
sol! {
89+
function balanceOf(address account) public returns (uint256);
90+
}
91+
92+
let encoded = balanceOfCall { account: address }.abi_encode();
93+
94+
let mut evm = Evm::builder()
95+
.with_db(cache_db)
96+
.modify_tx_env(|tx| {
97+
// 0x1 because calling USDC proxy from zero address fails
98+
tx.caller = address!("0000000000000000000000000000000000000001");
99+
tx.transact_to = TransactTo::Call(token);
100+
tx.data = encoded.into();
101+
tx.value = U256::from(0);
102+
})
103+
.build();
104+
105+
let ref_tx = evm.transact().unwrap();
106+
let result = ref_tx.result;
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, false)?;
117+
118+
Ok(balance)
119+
}
120+
121+
async 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 = Evm::builder()
140+
.with_db(cache_db)
141+
.modify_tx_env(|tx| {
142+
tx.caller = address!("0000000000000000000000000000000000000000");
143+
tx.transact_to = TransactTo::Call(uniswap_v2_router);
144+
tx.data = encoded.into();
145+
tx.value = U256::from(0);
146+
})
147+
.build();
148+
149+
let ref_tx = evm.transact().unwrap();
150+
let result = ref_tx.result;
151+
152+
let value = match result {
153+
ExecutionResult::Success {
154+
output: Output::Call(value),
155+
..
156+
} => value,
157+
result => return Err(anyhow!("'getAmountOut' execution failed: {result:?}")),
158+
};
159+
160+
let amount_out = <U256>::abi_decode(&value, false)?;
161+
162+
Ok(amount_out)
163+
}
164+
165+
fn get_reserves(pair_address: Address, cache_db: &mut AlloyCacheDB) -> Result<(U256, U256)> {
166+
sol! {
167+
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
168+
}
169+
170+
let encoded = getReservesCall {}.abi_encode();
171+
172+
let mut evm = Evm::builder()
173+
.with_db(cache_db)
174+
.modify_tx_env(|tx| {
175+
tx.caller = address!("0000000000000000000000000000000000000000");
176+
tx.transact_to = TransactTo::Call(pair_address);
177+
tx.data = encoded.into();
178+
tx.value = U256::from(0);
179+
})
180+
.build();
181+
182+
let ref_tx = evm.transact().unwrap();
183+
let result = ref_tx.result;
184+
185+
let value = match result {
186+
ExecutionResult::Success {
187+
output: Output::Call(value),
188+
..
189+
} => value,
190+
result => return Err(anyhow!("'getReserves' execution failed: {result:?}")),
191+
};
192+
193+
let (reserve0, reserve1, _) = <(U256, U256, u32)>::abi_decode(&value, false)?;
194+
195+
Ok((reserve0, reserve1))
196+
}
197+
198+
fn swap(
199+
from: Address,
200+
pool_address: Address,
201+
target: Address,
202+
amount_out: U256,
203+
is_token0: bool,
204+
cache_db: &mut AlloyCacheDB,
205+
) -> Result<()> {
206+
sol! {
207+
function swap(uint amount0Out, uint amount1Out, address target, bytes callback) external;
208+
}
209+
210+
let amount0_out = if is_token0 { amount_out } else { U256::from(0) };
211+
let amount1_out = if is_token0 { U256::from(0) } else { amount_out };
212+
213+
let encoded = swapCall {
214+
amount0Out: amount0_out,
215+
amount1Out: amount1_out,
216+
target,
217+
callback: Bytes::new(),
218+
}
219+
.abi_encode();
220+
221+
let mut evm = Evm::builder()
222+
.with_db(cache_db)
223+
.modify_tx_env(|tx| {
224+
tx.caller = from;
225+
tx.transact_to = TransactTo::Call(pool_address);
226+
tx.data = encoded.into();
227+
tx.value = U256::from(0);
228+
})
229+
.build();
230+
231+
let ref_tx = evm.transact_commit().unwrap();
232+
233+
match ref_tx {
234+
ExecutionResult::Success { .. } => {}
235+
result => return Err(anyhow!("'swap' execution failed: {result:?}")),
236+
};
237+
238+
Ok(())
239+
}
240+
241+
fn transfer(
242+
from: Address,
243+
to: Address,
244+
amount: U256,
245+
token: Address,
246+
cache_db: &mut AlloyCacheDB,
247+
) -> Result<()> {
248+
sol! {
249+
function transfer(address to, uint amount) external returns (bool);
250+
}
251+
252+
let encoded = transferCall { to, amount }.abi_encode();
253+
254+
let mut evm = Evm::builder()
255+
.with_db(cache_db)
256+
.modify_tx_env(|tx| {
257+
tx.caller = from;
258+
tx.transact_to = TransactTo::Call(token);
259+
tx.data = encoded.into();
260+
tx.value = U256::from(0);
261+
})
262+
.build();
263+
264+
let ref_tx = evm.transact_commit().unwrap();
265+
let success: bool = match ref_tx {
266+
ExecutionResult::Success {
267+
output: Output::Call(value),
268+
..
269+
} => <bool>::abi_decode(&value, false)?,
270+
result => return Err(anyhow!("'transfer' execution failed: {result:?}")),
271+
};
272+
273+
if !success {
274+
return Err(anyhow!("'transfer' failed"));
275+
}
276+
277+
Ok(())
278+
}

0 commit comments

Comments
 (0)