REVM — моделирование реальности блокчейна

[Аннотация]

ГМ.

Вы просыпаетесь утром, чувствуя себя очень уверенным в своем вчерашнем прогрессе. Теперь вы собираетесь начать работу над своим смарт-контрактом Solidity. «О, это не должно быть так сложно; в конце концов, это просто Solidity».

И действительно, в конце концов, это просто Solidity.

Тем не менее, теперь вы выполняете свой первый функциональный вызов, на написание которого у вас ушло всего 5 минут. «О, я быстрый», — думаете вы. Это слишком знакомый вызов «обмена» для парного контракта Uniswap V2, который вы хотели бы запустить с помощью своего бота MEV. Еще один шаг, и вы знаете, что все готово.

Но жизнь нелегка. Вы развертываете свой контракт в тестовой сети или основной сети, используя Remix / Foundry / Hardhat / Brownie (вы называете это). И как только вы запустите «своп»,

EVM REVERT 0x\b2…

Г.Н.

Повторять…

Вот как выглядит разработка смарт-контрактов из-за кулис. Оставляя в стороне славу, большая часть нашего времени посвящается тестированию и моделированию вызовов наших контрактов, чтобы убедиться, что изменения состояния происходят так, как ожидалось.

Даже с удивительными инструментами, доступными для помощи нам сейчас, это все еще может стать довольно ошеломляющим.

Проблема усугубляется, когда мы пытаемся переопределить ценности хранилища и создать среду, специально адаптированную к нашим потребностям. Например, когда:

  • мы хотим смоделировать ценовое влияние обмена 1 000 000 WETH на пул Uniswap (чтобы увидеть, каково это стать китом ?но у нас не так много WETH,
  • Мы хотим определить, имеем ли мы дело с токеном-приманкой, фактически не покупая их,
  • Мы хотим смоделировать наш арбитражный путь, чтобы увидеть, будет ли он прибыльным

Нам приходится возиться со значениями хранилища по умолчанию и вставлять значения, которые мы хотели бы протестировать.

Все это на самом деле можно сделать олдскульным способом, не прибегая к причудливым методам, углубившись в код GitHub с открытым исходным кодом и документацию, чтобы проверить, является ли интересующий токен приманкой.

Кроме того, вы можете самостоятельно изучать математику DEX, что заставит вас изучить различные протоколы, такие как Uniswap, Curve, Balancer и тому подобное.

Однако быстро становится очевидным, что этот подход вообще не масштабируем.

? Так что в настоящее время все крутые ребята используют так называемую симуляцию EVM. И мы собираемся исследовать эту захватывающую территорию, используя фантастическую библиотеку Rust, известную как REVM:

GitHub — bluealloy/revm: Виртуальная машина Rust Ethereum (revm) Написан ли EVM на rust, то есть…

Виртуальная машина Rust Ethereum (revm) Написан ли EVM на rust, ориентированном на скорость и простоту — GitHub …

github.com

На самом деле я затронул эту тему в своем предыдущем сообщении в блоге:

Использование REVM для создания сэндвич-бота

В последнем посте, Как я провожу свои дни, наблюдая за Mempool (Часть 1): Прогнозирование транзакций с помощью трассировки EVM, мы…

medium.com

где я обещал создать сэндвич-бота с помощью REVM, но не сделал этого (извините за это ? Мы скоро доберемся туда!) … Хотя мой предыдущий пост в блоге, безусловно, может дать некоторую предысторию, это не является обязательным требованием. Любой, кто еще не читал его, может погрузиться прямо в этот пост и без проблем следить за ним.

В этом сообщении блога:

  1. Мы углубимся и посмотрим на внутреннюю работу REVM, на то, как он работает и что мы можем сделать с библиотекой. Мы узнаем, как: (аиспользовать revm для выполнения трассировки EVM, (б) находить значения слотов хранения переменных контракта и (в) обходить шаблон прокси для получения байт-кода контракта реализации и многое другое.
  2. Мы также увидим, что сам по себе REVM иногда может быть «слишком низкоуровневым», поэтому мы будем использовать foundry-evm (https://github.com/foundry-rs/foundry/tree/master/crates/evm) для работы с интерфейсом более высокого уровня. Вы будете удивлены, увидев, что foundry-evm может позаботиться обо всем, что мы сделали выше, с помощью одной строки кода.
  3. Наконец, мы также будем использовать метод eth_call, чтобы показать вам, как все это можно сделать без каких-либо причудливых методов, если мы просто имеем дело с симуляцией одной транзакции.

Мы попытаемся смоделировать простую функцию «swap» в Uniswap V2, чтобы проиллюстрировать, как это можно сделать.

Давайте повеселимся! ?

⭐️ Пользователи Python и JavaScript также могут извлечь значительную выгоду из этих функций. И у Pythonistas на самом деле есть возможность использовать библиотеку pyrevm, которая является родной оболочкой вокруг revm, использующей PyO3. Это очень быстро, согласно тестам, проведенным в https://github.com/ziyadedher/evm-bench(Но что случилось с JS?)

Однако стоит отметить, что библиотека последний раз обновлялась девять месяцев назад и может не предлагать полные функции revm. Итак, изначально я намеревался рассказать о pyrevm в этом сообщении в блоге, но мне пришлось отложить это.

Я обязательно разберусь с контентом pyrevm, как только копну глубже.

GitHub — gakonst/pyrevm: Оболочка Python вокруг https://github.com/bluealloy/revm/ с помощью PyO3

Оболочка Python вокруг https://github.com/bluealloy/revm/ с помощью PyO3 — GitHub — gakonst/pyrevm: Оболочка Python вокруг…

github.com

Что такое симуляция EVM и почему так серьезно?

Моделирование EVM не является новой концепцией; Это то, с чем мы все знакомы и чем регулярно занимаемся.

Существует два основных метода моделирования транзакций:

  1. Автономное моделирование: При таком подходе все вычисления выполняются локально, не полагаясь на сетевые подключения (подключение к узлу RPC). Этот метод, как правило, намного быстрее, поскольку большинство узких мест возникает из-за сетевых подключений.
  2. Онлайн-симуляция: транзакции выполняются с использованием машины EVM, часто через узел блокчейна. Этот метод называется «онлайн», потому что он включает в себя взаимодействие с сетью блокчейна.

Однако есть еще один способ выполнения транзакций без отправки их непосредственно на узел. Вы можете изолировать компонент компьютера EVM от узла и запустить его локально, что значительно ускорит моделирование.

Таким образом, вы уменьшаете количество вызовов RPC к узлу, сводя к минимуму задержку в сети. Тем не менее, нам по-прежнему нужен доступ к узлу для получения значений хранилища, поэтому моделирование EVM подпадает под категорию «Онлайн-моделирование».

Учебник по REVM начинается здесь

Запачкать руки — лучший способ понять технологию. Если все, что я сказал выше, было слишком расплывчатым, начните печатать вместе со мной, и все станет кристально ясным.

Давайте начнем новый проект Rust. Я объясню все построчно, чтобы люди, которые не слишком знакомы с использованием Rust, могли следить за этим.

Весь код, используемый в этом сообщении блога, доступен в моем репозитории Github:

GitHub — solidquant/revm-is-all-you-need: учебные пособия и примеры моделирования EVM (revm / foundry-evm…

Учебные пособия и примеры моделирования EVM (revm / foundry-evm / eth_call) — GitHub — solidquant/revm-is-all-you-need: EVM…

github.com

??ПРЕДУПРЕЖДЕНИЕ, ПРЕЖДЕ ЧЕМ МЫ НАЧНЕМ??

Этот пост в блоге пойдет в кроличью нору ?? — точно так же, как я исследовал эти темы. Так что, естественно, это будет усложняться довольно быстро, но в конце концов это также будет очень полезно. Так что не торопитесь. Разбейте вещи на части и попытайтесь понять, как работает каждый из приведенных здесь примеров кода. Может быть, попытка втиснуть все в эту единственную статью была слишком надуманной, ха-ха.

О, но не волнуйтесь. Это не так уж и плохо, если вы потратите время ?

⚡️⚡️ Для читателей, которые где-то застряли, не стесняйтесь присоединиться к серверу Discord, где более 500 участников уже ежедневно обсуждают темы, связанные с MEV. Я подумываю о создании отдельного канала для симуляции EVM и делюсь еще несколькими идеями:

Присоединяйтесь к серверу Solid Quant Discord!

Квантовые любители, занимающиеся квантовыми вещами — в основном связанными с MEV на данный момент. | 506 членов

discord.com

✅ Инициализация

Выполните следующую команду:▶️ cargo new revm-is-all-you-need

Это создаст для вас пустой каталог проекта Rust. Затем скопируйте и вставьте приведенные ниже зависимости в файл Cargo.toml (Cargo.toml):[package]
name = «revm-is-all-you-need»
version = «0.1.0»
edition = «2021»

[dependencies]
dotenv = «0.15.0»
futures = «0.3.5»
anyhow = «1.0.70»
tokio = { version = «1.29.0», features = [«full»] }
tokio-stream = { version = «0.1», features = [‘sync’] }

ethers-core = «2.0»
ethers-providers = «2.0»
ethers-contract = «2.0»
ethers = { version = «2.0», features = [«abigen», «ws»]}

foundry-evm = { git = «https://github.com/solidquant/foundry.git», branch = «version-fix» }
anvil = { git = «https://github.com/solidquant/foundry.git», branch = «version-fix» }

revm = { version = «3.3.0», features = [«ethersdb»] }

eth-encode-packed = «0.1.0»

colored = «2.0.0»
log = «0.4.17»
fern = {version = «0.6.2», features = [«colored»]}
chrono = «0.4.23»

Давайте позаботимся обо всех зависимостях с самого начала. Подключать его по одному — хлопотно.

Здесь следует отметить следующее:

  • Мы используем разветвленную версию проекта Foundry из предыдущего коммита (https://github.com/solidquant/foundry). Это необходимо для того, чтобы избежать конфликтов версий. Ethers-rs, Foundry и REVM постоянно обновляют свои проекты, и это удивительно. Однако недостатком является то, что эти частые обновления иногда могут приводить к конфликтам версий. Пока используйте разветвленную версию, и как только вы освоитесь, обновите свою кодовую базу до более новых версий.

Теперь самое лучшее в любом уроке. Запускаем основную функцию и смотрим, работает ли установка.

Введите это в свой src/main.rs:use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
println!(«Hello, world!»);
Ok(())
}

Мы будем использовать «anyhow», чтобы упростить обработку ошибок. Проект надо строить и выплевывать на ходовой грузовой пробег:Hello, world!

Если бы только все шаги учебника были такими простыми. Но скоро все станет намного более беспокойным, так что держитесь крепче. ?

✅ Создание экземпляра EVM

Давайте создадим еще один файл Rust в нашем каталоге src (src/revm_examples.rs ):use revm::{
db::{CacheDB, EmptyDB, InMemoryDB},
EVM,
};

pub fn create_evm_instance() -> EVM<InMemoryDB> {
let db = CacheDB::new(EmptyDB::default());
let mut evm = EVM::new();
evm.database(db);
evm
}

Мы создаем простую функцию, которая возвращает экземпляр EVM с помощью InMemoryDB, что эквивалентно CacheDB<EmptyDB>>. Это создаст чистую среду EVM, в которой нет значений хранилища. Таким образом, никаких счетов или контрактов пока не существует. Нам придется разбираться со всем самостоятельно.

Далее мы создадим еще одну короткую функцию для настройки некоторых базовых конфигураций EVM (src/revm_examples.rs):// imports

// create_evm_instance

// add this ?
pub fn evm_env_setup(evm: &mut EVM<InMemoryDB>) {
// overriding some default env values to make it more efficient for testing
evm.env.cfg.limit_contract_code_size = Some(0x100000);
evm.env.cfg.disable_block_gas_limit = true;
evm.env.cfg.disable_base_fee = true;
}

Есть несколько конфигураций, которые мы можем настроить через поле cfg:

Как видите, мы можем переопределить ограничение на размер контракта по умолчанию, которое по умолчанию составляет 0x6000 (= 24 576 байт), и проверки баланса, а также лимиты газа блока и проверки базовой платы. В нашем примере я установил ограничение на размер контракта равным 0x100000 (= 104 8576 байт), чтобы немного ослабить ограничения для простоты тестирования.

✅ Получение баланса токенов ERC-20

Мы попробуем вызвать функцию ERC-20 «balanceOf», чтобы проверить, какая часть баланса токенов удерживается нашим смоделированным пользователем (src/revm_examples.rs):// add all the necessary imports ?
use anyhow::{anyhow, Result};
use bytes::Bytes;
use ethers::{
abi::{self, parse_abi},
prelude::*,
providers::Middleware,
types::{
transaction::eip2930::AccessList, BlockId, BlockNumber, Eip1559TransactionRequest,
NameOrAddress, H160, U256,
},
};
use log::info;
use revm::{
db::{CacheDB, EmptyDB, EthersDB, InMemoryDB},
primitives::Bytecode,
primitives::{
keccak256, AccountInfo, ExecutionResult, Log, Output, TransactTo, TxEnv, B160,
U256 as rU256,
},
Database, EVM,
};
use std::{str::FromStr, sync::Arc};

// create_evm_instance

// evm_env_setup

// add this ?
#[derive(Debug, Clone)]
pub struct TxResult {
pub output: Bytes,
pub logs: Option<Vec<Log>>,
pub gas_used: u64,
pub gas_refunded: u64,
}

pub fn get_token_balance(evm: &mut EVM<InMemoryDB>, token: H160, account: H160) -> Result<U256> {
let erc20_abi = BaseContract::from(parse_abi(&[
«function balanceOf(address) external view returns (uint256)»,
])?);
let calldata = erc20_abi.encode(«balanceOf», account)?;

evm.env.tx.caller = account.into();
evm.env.tx.transact_to = TransactTo::Call(token.into());
evm.env.tx.data = calldata.0;

// This will fail, because the token contract has not been deployed yet
let result = match evm.transact_ref() {
Ok(result) => result,
Err(e) => return Err(anyhow!(«EVM call failed: {e:?}»)),
};
let tx_result = match result.result {
ExecutionResult::Success {
gas_used,
gas_refunded,
output,
logs,
..
} => match output {
Output::Call(o) => TxResult {
output: o,
logs: Some(logs),
gas_used,
gas_refunded,
},
Output::Create(o, _) => TxResult {
output: o,
logs: Some(logs),
gas_used,
gas_refunded,
},
},
ExecutionResult::Revert { gas_used, output } => {
return Err(anyhow!(
«EVM REVERT: {:?} / Gas used: {:?}»,
output,
gas_used
))
}
ExecutionResult::Halt {
reason, gas_used, ..
} => return Err(anyhow!(«EVM HALT: {:?} / Gas used: {:?}», reason, gas_used)),
};
let decoded_output = erc20_abi.decode_output(«balanceOf», tx_result.output)?;
Ok(decoded_output)
}

Мы пройдемся по всему построчно.

Во-первых, мы создаем экземпляр BaseContract из простого ABI ERC-20, содержащего одно определение функции:let erc20_abi = BaseContract::from(parse_abi(&[
«function balanceOf(address) external view returns (uint256)»,
])?);
let calldata = erc20_abi.encode(«balanceOf», account)?;

При этом мы кодируем вызов функции в «balanceOf», что приведет к следующему, если мы попытаемся распечатать «calldata»:Bytes(0x70a08231000000000000000000000000e2b5a9c1e325511a227ef527af38c3a7b65afa1d)

Затем нам нужно настроить переменные транзакции, прежде чем мы сможем отправить вызов:evm.env.tx.caller = account.into();
evm.env.tx.transact_to = TransactTo::Call(token.into());
evm.env.tx.data = calldata.0;

Ничего сложного, мы устанавливаем значения только для трех переменных:

  • Вызывающий абонент: кто вызывает функцию (от)
  • transact_to: к чему мы призываем
  • data: входные данные нашей транзакции (input)

После этого мы запускаем нашу транзакцию, как показано ниже:let result = match evm.transact_ref() {
Ok(result) => result,
Err(e) => return Err(anyhow!(«EVM call failed: {e:?}»)),
};

Вы можете видеть, что мы вызываем функцию «transact_ref» на нашем EVM для запуска нашей транзакции. На самом деле, существует в общей сложности шесть способов выполнения транзакций в revm, а именно:

  • transact: выполнить tx без записи в БД, возвращает состояния
  • inspect: выполняем tx с помощью инспектора, и без записи в БД возвращает состояния
  • transact_commit: вызовите «transact» и зафиксируйте изменения состояния в БД
  • inspect_commit: вызовите «inspect» с помощью инспектора и зафиксируйте изменения состояния в БД
  • transact_ref: вызывайте «transact» и не фиксируйте изменения в БД
  • inspect_ref: вызовите «inspect» с помощью инспектора и не фиксируйте изменения в БД

Итак, по сути, мы вызываем transact_ref/inspect_ref для выполнения транзакций без каких-либо изменений в БД.

После успешного запуска транзакции мы получим структуру ResultAndState, которая выглядит примерно так:

(Сделайте краткий снимок того, как выглядят эти данные. Пока мы будем использовать только поле результатано есть и поле состояния, к которому мы скоро перейдем. Ничто не пропадет даром.)

который мы сопоставляем с полем результата с помощью следующего кода:let tx_result = match result.result {
ExecutionResult::Success {
gas_used,
gas_refunded,
output,
logs,
..
} => match output {
Output::Call(o) => TxResult {
output: o,
logs: Some(logs),
gas_used,
gas_refunded,
},
Output::Create(o, _) => TxResult {
output: o,
logs: Some(logs),
gas_used,
gas_refunded,
},
},
ExecutionResult::Revert { gas_used, output } => {
return Err(anyhow!(
«EVM REVERT: {:?} / Gas used: {:?}»,
output,
gas_used
))
}
ExecutionResult::Halt {
reason, gas_used, ..
} => return Err(anyhow!(«EVM HALT: {:?} / Gas used: {:?}», reason, gas_used)),
};

Поскольку ExecutionResult может быть трех типов (Success / Revert / Halt), мы обрабатываем их все и извлекаем интересующие нас переменные. Попробуйте бегло просмотреть определение ExecutionResult:

Мы извлекаем gas_used, gas_refunded, журналы и выходные данные и создаем отдельную переменную структуры TxResult, которая определяется как:pub struct TxResult {
pub output: Bytes,
pub logs: Option<Vec<Log>>,
pub gas_used: u64,
pub gas_refunded: u64,
}

Наконец, мы декодируем выходные данные Bytes с помощью экземпляра erc20_abi BaseContract:let decoded_output = erc20_abi.decode_output(«balanceOf», tx_result.output)?;
Ok(decoded_output)

? Сейчас мы попробуем запустить этот код. Я хочу сказать вам заранее, что это не удастся, так что не паникуйте! Я расскажу вам, почему это не удастся.

Создайте новый файл в каталоге src (src/lib.rs):pub mod revm_examples;
pub mod constants;
pub mod utils;

Создайте еще один файл в каталоге src (src/constants.rs):use ethers::{
prelude::Lazy,
types::{Address, Bytes},
};
use std::str::FromStr;

pub fn get_env(key: &str) -> String {
std::env::var(key).unwrap()
}

#[derive(Debug, Clone)]
pub struct Env {
pub https_url: String,
pub wss_url: String,
}

impl Env {
pub fn new() -> Self {
Env {
https_url: get_env(«HTTPS_URL»),
wss_url: get_env(«WSS_URL»),
}
}
}

pub static ZERO_ADDRESS: Lazy<Address> =
Lazy::new(|| Address::from_str(«0x0000000000000000000000000000000000000000»).unwrap());

pub static SIMULATOR_CODE: Lazy<Bytes> = Lazy::new(|| {
«0x608060405234801561001057600080fd5b50600436106100365760003560e01c8063054d50d41461003b57806364bfce6f14610061575b600080fd5b61004e6100493660046106e4565b610089565b6040519081526020015b60405180910390f35b61007461006f36600461072c565b6101ae565b60408051928352602083019190915201610058565b60008084116100f35760405162461bcd60e51b815260206004820152602b60248201527f556e697377617056324c6962726172793a20494e53554646494349454e545f4960448201526a1394155517d05353d5539560aa1b60648201526084015b60405180910390fd5b6000831180156101035750600082115b6101605760405162461bcd60e51b815260206004820152602860248201527f556e697377617056324c6962726172793a20494e53554646494349454e545f4c604482015267495155494449545960c01b60648201526084016100ea565b600061016e856103e561078f565b9050600061017c848361078f565b905060008261018d876103e861078f565b61019791906107a6565b90506101a381836107b9565b979650505050505050565b6000806101c56001600160a01b03851686886104ef565b600080600080886001600160a01b0316630902f1ac6040518163ffffffff1660e01b8152600401606060405180830381865afa158015610209573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061022d91906107f2565b506001600160701b031691506001600160701b03169150866001600160a01b0316886001600160a01b0316101561026957819350809250610270565b8093508192505b50506040516370a0823160e01b81526001600160a01b03888116600483015260009184918916906370a0823190602401602060405180830381865afa1580156102bd573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102e19190610842565b6102eb919061085b565b604051630153543560e21b8152600481018290526024810185905260448101849052909150309063054d50d490606401602060405180830381865afa158015610338573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061035c9190610842565b6040516370a0823160e01b81523060048201529095506000906001600160a01b038816906370a0823190602401602060405180830381865afa1580156103a6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103ca9190610842565b9050600080886001600160a01b03168a6001600160a01b0316106103f0578760006103f4565b6000885b6040805160008152602081019182905263022c0d9f60e01b90915291935091506001600160a01b038c169063022c0d9f906104389085908590309060248101610892565b600060405180830381600087803b15801561045257600080fd5b505af1158015610466573d6000803e3d6000fd5b50506040516370a0823160e01b81523060048201528592506001600160a01b038c1691506370a0823190602401602060405180830381865afa1580156104b0573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104d49190610842565b6104de919061085b565b965050505050505094509492505050565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b179052610541908490610546565b505050565b600061055b6001600160a01b038416836105a9565b9050805160001415801561058057508080602001905181019061057e91906108e2565b155b1561054157604051635274afe760e01b81526001600160a01b03841660048201526024016100ea565b60606105b7838360006105c0565b90505b92915050565b6060814710156105e55760405163cd78605960e01b81523060048201526024016100ea565b600080856001600160a01b031684866040516106019190610904565b60006040518083038185875af1925050503d806000811461063e576040519150601f19603f3d011682016040523d82523d6000602084013e610643565b606091505b509150915061065386838361065f565b925050505b9392505050565b6060826106745761066f826106bb565b610658565b815115801561068b57506001600160a01b0384163b155b156106b457604051639996b31560e01b81526001600160a01b03851660048201526024016100ea565b5080610658565b8051156106cb5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b6000806000606084860312156106f957600080fd5b505081359360208301359350604090920135919050565b80356001600160a01b038116811461072757600080fd5b919050565b6000806000806080858703121561074257600080fd5b8435935061075260208601610710565b925061076060408601610710565b915061076e60608601610710565b905092959194509250565b634e487b7160e01b600052601160045260246000fd5b80820281158282048414176105ba576105ba610779565b808201808211156105ba576105ba610779565b6000826107d657634e487b7160e01b600052601260045260246000fd5b500490565b80516001600160701b038116811461072757600080fd5b60008060006060848603121561080757600080fd5b610810846107db565b925061081e602085016107db565b9150604084015163ffffffff8116811461083757600080fd5b809150509250925092565b60006020828403121561085457600080fd5b5051919050565b818103818111156105ba576105ba610779565b60005b83811015610889578181015183820152602001610871565b50506000910152565b84815283602082015260018060a01b038316604082015260806060820152600082518060808401526108cb8160a085016020870161086e565b601f01601f19169190910160a00195945050505050565b6000602082840312156108f457600080fd5b8151801515811461065857600080fd5b6000825161091681846020870161086e565b919091019291505056fea26469706673582212201d6da94f2d6ac0535f5153da5aac14a1f6ef19d15801986cfe2b2d6fab019c6564736f6c63430008140033»
.parse()
.unwrap()
});

Не обращайте внимания на переменную SIMULATOR_CODE на данный момент, мы будем использовать ее через мгновение.

Еще один (src/utils.rs), который должен помочь с нашим ведением журнала:use anyhow::{self, Result};
use fern::colors::{Color, ColoredLevelConfig};
use log::LevelFilter;

pub fn setup_logger() -> Result<()> {
let colors = ColoredLevelConfig {
trace: Color::Cyan,
debug: Color::Magenta,
info: Color::Green,
warn: Color::Red,
error: Color::BrightRed,
..ColoredLevelConfig::new()
};

fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
«{}[{}] {}»,
chrono::Local::now().format(«[%H:%M:%S]»),
colors.color(record.level()),
message
))
})
.chain(std::io::stdout())
.level(log::LevelFilter::Error)
.level_for(«revm_is_all_you_need», LevelFilter::Info)
.apply()?;

Ok(())
}

Затем перейдите в (src/main.rs):use anyhow::Result;
use ethers::{
providers::{Middleware, Provider, Ws},
types::{BlockNumber, H160},
};
use log::info;
use std::{str::FromStr, sync::Arc};

use revm_is_all_you_need::constants::Env;
use revm_is_all_you_need::revm_examples::{
create_evm_instance, evm_env_setup, get_token_balance
};
use revm_is_all_you_need::utils::setup_logger;

#[tokio::main]
async fn main() -> Result<()> {
dotenv::dotenv().ok();
setup_logger()?;

let mut evm = create_evm_instance();
evm_env_setup(&mut evm);

let user = H160::from_str(«0xE2b5A9c1e325511a227EF527af38c3A7B65AFA1d»).unwrap();

let weth = H160::from_str(«0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2»).unwrap();
let usdt = H160::from_str(«0xdAC17F958D2ee523a2206206994597C13D831ec7»).unwrap();
let usdc = H160::from_str(«0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48»).unwrap();
let dai = H160::from_str(«0x6B175474E89094C44Da98b954EedeAC495271d0F»).unwrap();

let weth_balance = get_token_balance(&mut evm, weth, user);
info!(«WETH balance: {:?}», weth_balance);

Ok(())
}

Мы будем называть нашу функцию «get_token_balance» с помощью WETH:let weth_balance = get_token_balance(&mut evm, weth, user);

Теперь выполните:▶️ cargo run

И получаем такую ошибку:

[09:19:27][INFO] WETH balance: Err(Invalid name: please ensure the contract and method you’re calling exist! failed to decode empty bytes. if you’re using jsonrpc this is likely due to jsonrpc returning `0x` in case contract or method don’t exist)

Сообщение об ошибке любезно сообщает нам, что у нас нет контракта и метода, который мы вызываем. Но это странно, потому что мы называем контракт WETH, который должен быть там.

?Вы помните, как я говорил, что наш экземпляр EVM с серверной базой данных, работающей под управлением EmptyDB, будет чистым листом? Это я и имел в виду. Все привычные контракты еще не будут существовать в нашем блокчейн-мире. Поэтому мы должны создавать их один за другим, прежде чем мы сможем начать взаимодействовать с ним.

Мы увидим, как мы можем это сделать, в следующем разделе.

✅ Строим наш мир с нуля

Теперь самое интересное, что также можно считать нашим самым большим кошмаром, когда мы имеем дело с оборотом.

Прежде чем мы сможем начать, давайте представим, что мы пытаемся здесь сделать:

Куклы покемонов в продаже от Tmon (корейская социальная торговля)

Итак, мы все знаем, что Дитто из Pokémon обладает способностью превращаться в других покемонов, верно? Но есть что-то не так в том, как выглядит его преобразованная версия.

Это аналогия, которую мы проводим с нашими смоделированными средами. В конце концов, он может быть не совсем полным, но он все равно будет функционировать так, как задумано, что позволит нам получить результаты моделирования.

Давайте поднимем эту вечеринку на ступеньку выше!

Наконец-то мы готовы протестировать нашу функцию «swap» из Uniswap V2. Для этого мы создадим простой смарт-контракт для решения задачи, и мы сделаем это, выполнив:▶️ forge init contracts —no-commit

При этом будет создан шаблон проекта Foundry в каталоге контрактов. Создайте новый файл в каталоге contracts/src (contracts/src/Simulator.sol) :// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import «./interfaces/IUniswapV2Pair.sol»;
import «./interfaces/IUniswapV2Router02.sol»;
import «./interfaces/IUniswapV3Pool.sol»;
import «./interfaces/IERC20.sol»;

import «./utils/SafeERC20.sol»;

contract Simulator {
using SafeERC20 for IERC20;

function v2SimulateSwap(
uint256 amountIn,
address targetPair,
address inputToken,
address outputToken
) external returns (uint256 amountOut, uint256 realAfterBalance) {
// 1. Check if you can transfer the token
// Some honeypot tokens won’t allow you to transfer tokens
IERC20(inputToken).safeTransfer(targetPair, amountIn);

uint256 reserveIn;
uint256 reserveOut;

{
(uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(targetPair)
.getReserves();

if (inputToken < outputToken) {
reserveIn = reserve0;
reserveOut = reserve1;
} else {
reserveIn = reserve1;
reserveOut = reserve0;
}
}

// 2. Calculate the amount out you are supposed to get if the token isn’t taxed
uint256 actualAmountIn = IERC20(inputToken).balanceOf(targetPair) —
reserveIn;
amountOut = this.getAmountOut(actualAmountIn, reserveIn, reserveOut);

// If the token is taxed, you won’t receive amountOut back, and the swap will revert
uint256 outBalanceBefore = IERC20(outputToken).balanceOf(address(this));

(uint256 amount0Out, uint256 amount1Out) = inputToken < outputToken
? (uint256(0), amountOut)
: (amountOut, uint256(0));
IUniswapV2Pair(targetPair).swap(
amount0Out,
amount1Out,
address(this),
new bytes(0)
);

// 3. Check the real balance of outputToken after the swap
realAfterBalance =
IERC20(outputToken).balanceOf(address(this)) —
outBalanceBefore;
}

function getAmountOut(
uint256 amountIn,
uint256 reserveIn,
uint256 reserveOut
) external pure returns (uint256 amountOut) {
require(amountIn > 0, «UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT»);
require(
reserveIn > 0 && reserveOut > 0,
«UniswapV2Library: INSUFFICIENT_LIQUIDITY»
);
uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
amountOut = numerator / denominator;
}
}

? Важно использовать интерфейс контракта SafeERC20, потому что не все токены ERC-20 используют один и тот же интерфейс, и SafeERC20 поможет нам решить эту проблему.

❗️ Токен USDT — это хорошо известный токен ERC-20, который демонстрирует такое поведение. Tether не возвращает логическое значение для своей функции «передачи», в отличие от стандартных токенов ERC-20.

Кроме того, контракт на симулятор относительно прост.

Чтобы выполнить функцию «v2SimulateSwap» в нашем контракте Simulator, мы должны передать определенное количество inputToken (удерживаемое контрактом Simulator) в контракт targetPair, прежде чем мы сможем вызвать функцию «swap».

Это означает, что нам нужно обладать определенным количеством inputToken перед проведением этой симуляции. Этого можно достичь, переопределив значения хранилища с помощью revm.

? Переопределение сопоставлений «балансов» в токенах ERC-20

Мы немного знаем о слотах для хранения. Мы понимаем, что, изучив, как переменные организованы в смарт-контракте, мы можем определить слоты хранения для этих переменных.

Одним из широко известных примеров является отображение «балансов» в токене WETH, который, как известно, использует слот хранения 3 для хранения балансов пользователей. Это можно легко определить с помощью таких инструментов, как Slither:

GitHub — crytic/slither: Статический анализатор для Solidity

Статический анализатор прочности. Внесите свой вклад в разработку crytic/slither, создав учетную запись на GitHub.

github.com

? Нам нужна эта информация, потому что для переопределения значений состояния с помощью revm или любых других механизмов моделирования мы должны вводить значения непосредственно в слот хранения. Поэтому, если мы знаем о значениях слотов хранения для сопоставлений «балансов» токенов ERC-20, мы можем моделировать транзакции, которые требуют баланса этих токенов, даже если на самом деле у нас их нет.

Мы быстро рассмотрим демонстрацию того, как использовать Slither, чтобы выяснить структуру хранения смарт-контрактов, проверенных на Etherscan. Так я изначально вычислял значения слотов хранения для различных смарт-контрактов. Если на вашем компьютере установлен Python, попробуйте запустить:▶️ pip install slither-analyzer

Затем получите адрес контракта для WETH, а именно:

0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

и выполните:▶️ slither-read-storage 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 —json weth_storage.json

При этом будет выведен макет хранилища в файле weth_storage.json:{
«name»: {
«name»: «name»,
«type_string»: «string»,
«slot»: 0,
«size»: 256,
«offset»: 0,
«value»: null,
«elems»: {}
},
«symbol»: {
«name»: «symbol»,
«type_string»: «string»,
«slot»: 1,
«size»: 256,
«offset»: 0,
«value»: null,
«elems»: {}
},
«decimals»: {
«name»: «decimals»,
«type_string»: «uint8»,
«slot»: 2,
«size»: 8,
«offset»: 0,
«value»: null,
«elems»: {}
},
«balanceOf»: {
«name»: «balanceOf»,
«type_string»: «mapping(address => uint256)»,
«slot»: 3, // ? right here!!
«size»: 256,
«offset»: 0,
«value»: null,
«elems»: {}
},
«allowance»: {
«name»: «allowance»,
«type_string»: «mapping(address => mapping(address => uint256))»,
«slot»: 4,
«size»: 256,
«offset»: 0,
«value»: null,
«elems»: {}
}
}

И мы находим то, что ищем. Это сопоставления «balanceOf» со значением слота, равным 3.

Все это хорошо; Однако настоящая проблема возникает, когда мы сталкиваемся с контрактами, использующими прокси-шаблоны, и нам необходимо определить значения слотов хранения для нескольких других токенов.

Попробуйте выполнить приведенную выше команду с помощью USDC (а именно: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48):▶️ slither-read-storage 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 —json usdc_storage.json

Этот подход может столкнуться с трудностями с такими токенами, как USDC, поскольку они используют контракт с прокси-шаблоном. Следовательно, их хранение организовано так, как ожидалось, но их код реализации развертывается через другой адрес контракта, который мы должны различать самостоятельно.

Чтобы обойти эту проблему, мы обратимся к другим способам поиска значений слотов хранения.

Во-первых, мы рассмотрим, как использовать трассировку EVM в сочетании с revm для идентификации затронутых адресов и слотов хранения после выполнения транзакции:

  1. Использование трассировки EVM (как на Geth, так и на Parity)
  2. Использование REVM

Впоследствии мы углубимся в процесс определения адресов реализации прокси-контрактов, что позволит нам взаимодействовать с такими токенами, как USDC.

Слоты для хранения, Слоты для хранения

1️⃣ Использование трассировки EVM для получения значений слотов хранения

Один из способов выяснить значение слота хранения — трассировка EVM.

Это то, с чем мы уже немного разбирались в предыдущем сообщении в блоге о просмотре мемпула:

Как я провожу свои дни Просмотр Mempool (Часть 1): Прогнозирование транзакций с помощью EVM Tracing

Как я начал с наблюдения за птицами до наблюдения за мемпулами, и как я сейчас создаю своего бота-сэндвича

medium.com

Мы сделаем то же самое снова, но на этот раз с дополнительным штрихом:

  • получить адреса, которые были затронуты во время выполнения транзакции,
  • Извлеките все слоты хранения, которые изменили состояния, из каждого из этих затронутых адресов.

Создайте новый файл в каталоге src (src/trace.rs):use anyhow::Result;
use ethers::prelude::*;
use ethers_providers::Middleware;
use std::sync::Arc;

pub async fn get_state_diff<M: Middleware + ‘static>(
provider: Arc<M>,
tx: Eip1559TransactionRequest,
block_number: U64,
) -> Result<GethTrace> {
let trace = provider
.debug_trace_call(
tx,
Some(block_number.into()),
GethDebugTracingCallOptions {
tracing_options: GethDebugTracingOptions {
disable_storage: None,
disable_stack: None,
enable_memory: None,
enable_return_data: None,
tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::PreStateTracer,
)),
tracer_config: None,
timeout: None,
},
state_overrides: None,
},
)
.await?;

Ok(trace)
}

И обновите файл (src/lib.rs):pub mod revm_examples;
pub mod constants;
pub mod utils;
pub mod trace;

Мы создаем простую функцию (get_state_diff), вызывающую конечную точку RPC debug_traceTransaction на узлах Geth.

Читатели, работающие с личными узлами стиля четности, такими как Erigon, могут ссылаться на приведенный ниже код, чтобы добиться чего-то подобного с конечными точками трассировки:

evm-tracing-samples/rust/src/trace.rs на главной странице · Solidquant/EVM-Tracing-Samples

Примеры скриптов, использующих EVM Tracing в Python и Rust — evm-tracing-samples/rust/src/trace.rs на главной странице ·…

github.com

Приведенный выше код выглядит довольно устрашающе, но это не что иное, как простая оболочка для создания приведенного ниже фрейма запроса:{
‘id’: 1,
‘method’: ‘debug_traceTransaction’,
‘jsonrpc’: ‘2.0’,
‘params’: [
tx_hash,
{‘tracer’: ‘prestateTracer’}
]
}

Как видите, мы используем prestateTracer для отслеживания любых изменений состояния с вызовом транзакции.

Перейдите в (src/revm_examples.rs) и добавьте следующее, прежде чем мы сможем увидеть, что делает эта функция строка за строкой:// previous imports

// add these imports ?
use crate::constants::SIMULATOR_CODE;
use crate::trace::get_state_diff;

// create_evm_instance

// evm_env_setup

// get_token_balance

// add this ?
pub async fn geth_and_revm_tracing<M: Middleware + ‘static>(
evm: &mut EVM<InMemoryDB>,
provider: Arc<M>,
token: H160,
account: H160,
) -> Result<i32> {
let erc20_abi = BaseContract::from(parse_abi(&[
«function balanceOf(address) external view returns (uint256)»,
])?);
let calldata = erc20_abi.encode(«balanceOf», account)?;

let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;
let nonce = provider
.get_transaction_count(account, Some(BlockId::Number(BlockNumber::Latest)))
.await?;
let chain_id = provider.get_chainid().await?;

let tx = Eip1559TransactionRequest {
chain_id: Some(chain_id.as_u64().into()),
nonce: Some(nonce),
from: Some(account),
to: Some(NameOrAddress::Address(token)),
gas: None,
value: None,
data: Some(calldata),
max_priority_fee_per_gas: None,
max_fee_per_gas: None,
access_list: AccessList::default(),
};
let geth_trace = get_state_diff(provider.clone(), tx, block.number.unwrap()).await?;
let prestate = match geth_trace {
GethTrace::Known(known) => match known {
GethTraceFrame::PreStateTracer(prestate) => match prestate {
PreStateFrame::Default(prestate_mode) => Some(prestate_mode),
_ => None,
},
_ => None,
},
_ => None,
}
.unwrap();
let geth_touched_accs = prestate.0.keys();
info!(«Geth trace: {:?}», geth_touched_accs);

let token_acc_state = prestate.0.get(&token).ok_or(anyhow!(«no token key»))?;
let token_touched_storage = token_acc_state
.storage
.clone()
.ok_or(anyhow!(«no storage values»))?;

for i in 0..20 {
let slot = keccak256(&abi::encode(&[
abi::Token::Address(account.into()),
abi::Token::Uint(U256::from(i)),
]));
info!(«{} {:?}», i, slot);
match token_touched_storage.get(&slot.into()) {
Some(_) => {
info!(«Balance storage slot: {:?} ({:?})», i, slot);
return Ok(i);
}
None => {}
}
}

Ok(0)
}

  1. Во-первых, мы создаем объект транзакции Eip1559:

let erc20_abi = BaseContract::from(parse_abi(&[
«function balanceOf(address) external view returns (uint256)»,
])?);
let calldata = erc20_abi.encode(«balanceOf», account)?;

let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;
let nonce = provider
.get_transaction_count(account, Some(BlockId::Number(BlockNumber::Latest)))
.await?;
let chain_id = provider.get_chainid().await?;

let tx = Eip1559TransactionRequest {
chain_id: Some(chain_id.as_u64().into()),
nonce: Some(nonce),
from: Some(account),
to: Some(NameOrAddress::Address(token)),
gas: None,
value: None,
data: Some(calldata),
max_priority_fee_per_gas: None,
max_fee_per_gas: None,
access_list: AccessList::default(),
};

Это не так уж сложно. Мы извлекаем информацию о блоке, одноразовом номере chain_id с помощью вызовов RPC и соответствующим образом заполняем структуру Eip1559TransactionRequest.

2. Далее запускаем debug_traceTransaction и получаем все затронутые адреса, запустив функцию «balanceOf». Мы можем ожидать, что наша машина EVM коснется отображения балансов контракта токена ERC-20.let geth_trace = get_state_diff(provider.clone(), tx, block.number.unwrap()).await?;
let prestate = match geth_trace {
GethTrace::Known(known) => match known {
GethTraceFrame::PreStateTracer(prestate) => match prestate {
PreStateFrame::Default(prestate_mode) => Some(prestate_mode),
_ => None,
},
_ => None,
},
_ => None,
}
.unwrap();
let geth_touched_accs = prestate.0.keys();
info!(«Geth trace: {:?}», geth_touched_accs);

Посмотрим, что это выведет. Перейдите по ссылке (src/main.rs):// previous imports

// add this ?
use revm_is_all_you_need::revm_examples::{
create_evm_instance, evm_env_setup, get_token_balance, geth_and_revm_tracing
};

#[tokio::main]
async fn main() -> Result<()> {
// previous code here

// add this ?
let env = Env::new();
let ws = Ws::connect(&env.wss_url).await.unwrap();
let provider = Arc::new(Provider::new(ws));

match geth_and_revm_tracing(&mut evm, provider.clone(), weth, user).await {
Ok(_) => {}
Err(e) => info!(«Tracing error: {e:?}»),
}

Ok(())
}

и запустите код:

[14:27:07] [ИНФОРМАЦИЯ] След гетов: [0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5, 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2, 0xe2b5a9c1e325511a227ef527af38c3a7b65afa1d]

Мы видим, что запуск функции «balanceOf» на WETH затрагивает три вышеуказанных адреса, и нас интересует 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 адрес.

3. Наконец, мы отфильтровываем контракт WETH из нашего предварительного штата BTreeMap, выполнив:let token_acc_state = prestate.0.get(&token).ok_or(anyhow!(«no token key»))?;
let token_touched_storage = token_acc_state
.storage
.clone()
.ok_or(anyhow!(«no storage values»))?;

for i in 0..20 {
let slot = keccak256(&abi::encode(&[
abi::Token::Address(account.into()),
abi::Token::Uint(U256::from(i)),
]));
info!(«{} {:?}», i, slot);
match token_touched_storage.get(&slot.into()) {
Some(_) => {
info!(«Balance storage slot: {:?} ({:?})», i, slot);
return Ok(i);
}
None => {}
}
}

Мы извлекаем все слоты хранения, которые были затронуты из контракта WETH, доступ к которым можно получить через поле хранилища AccountState, которое можно получить, выполнив prestate.0.get(&token):

Мы начинаем перебирать числа от 0 до 20 и вычисляем слот для хранения наших желаемых «балансовых» отображений, содержащих баланс нашего целевого счета. И получаем примерно так:

Мы видим, что значение слота хранения балансовых отображений WETH составляет: 3

Это именно то, что мы хотели увидеть.

2️⃣ Использование REVM для получения значений слотов хранения

Это было весело, но давайте не будем забывать, что это учебник по revm, а не по гетам. Причина, по которой я показал вам это, заключается в том, чтобы проиллюстрировать идею о том, что те же самые результаты могут быть воспроизведены только с помощью revm.

Теперь самое время вспомнить, что наша первая попытка вызвать функцию «balanceOf» с помощью revm не удалась, потому что мы использовали чистую версию экземпляра EVM. Это будет по-другому, если мы развернем контракт токена перед вызовом функции.

Для этого создадим еще одну функцию в нашем (src/revm_examples.rs):// previous code here…

// add this ?
pub async fn revm_contract_deploy_and_tracing<M: Middleware + ‘static>(
evm: &mut EVM<InMemoryDB>,
provider: Arc<M>,
token: H160,
account: H160,
) -> Result<i32> {
// deploy contract to EVM
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

let mut ethersdb = EthersDB::new(provider.clone(), Some(block.number.unwrap().into())).unwrap();

let token_acc_info = ethersdb.basic(token.into()).unwrap().unwrap();
evm.db
.as_mut()
.unwrap()
.insert_account_info(token.into(), token_acc_info);

let erc20_abi = BaseContract::from(parse_abi(&[
«function balanceOf(address) external view returns (uint256)»,
])?);
let calldata = erc20_abi.encode(«balanceOf», account)?;

evm.env.tx.caller = account.into();
evm.env.tx.transact_to = TransactTo::Call(token.into());
evm.env.tx.data = calldata.0.clone();

let result = match evm.transact_ref() {
Ok(result) => result,
Err(e) => return Err(anyhow!(«EVM call failed: {e:?}»)),
};
let token_b160: B160 = token.into();
let token_acc = result.state.get(&token_b160).unwrap();
let token_touched_storage = token_acc.storage.clone();
info!(«Touched storage slots: {:?}», token_touched_storage);

for i in 0..20 {
let slot = keccak256(&abi::encode(&[
abi::Token::Address(account.into()),
abi::Token::Uint(U256::from(i)),
]));
let slot: rU256 = U256::from(slot).into();
match token_touched_storage.get(&slot) {
Some(_) => {
info!(«Balance storage slot: {:?} ({:?})», i, slot);
return Ok(i);
}
None => {}
}
}

Ok(0)
}

Код начнет казаться знакомым некоторым читателям, но не волнуйтесь, я все равно буду просматривать их строка за строкой, потому что это то, что мне нравится делать.

На этот раз мы собираемся использовать EthersDB из revm для получения базовой информации об учетной записи о токене, который мы хотим развернуть, и внедрить ее в базу данных EVM. Обратите внимание, что это приведет к трем асинхронным вызовам нашей конечной точки RPC, что видно из определения функции:let f = async {
let nonce = self.client.get_transaction_count(add, self.block_number);
let balance = self.client.get_balance(add, self.block_number);
let code = self.client.get_code(add, self.block_number);
tokio::join!(nonce, balance, code)
};

Вот почему использование этого метода без осторожности может занять очень много времени.

Давайте продолжим.

Во-первых, мы создаем экземпляр EthersDB:// deploy contract to EVM
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

let mut ethersdb = EthersDB::new(provider.clone(), Some(block.number.unwrap().into())).unwrap();

let token_acc_info = ethersdb.basic(token.into()).unwrap().unwrap();
evm.db
.as_mut()
.unwrap()
.insert_account_info(token.into(), token_acc_info);

Запуск базовой функции с помощью EthersDB вернет структуру AccountInstance, которая выглядит следующим образом:

Мы видим, что мы можем получить байт-код, развернутый в целевой учетной записи, используя этот метод из основной сети. Таким образом, простое внедрение этой информации об учетной записи в базу данных EVM эквивалентно развертыванию смарт-контрактов на этом адресе.

Затем мы снова попробуем сделать вызов «balanceOf» к адресу токена, на этот раз:let erc20_abi = BaseContract::from(parse_abi(&[
«function balanceOf(address) external view returns (uint256)»,
])?);
let calldata = erc20_abi.encode(«balanceOf», account)?;

evm.env.tx.caller = account.into();
evm.env.tx.transact_to = TransactTo::Call(token.into());
evm.env.tx.data = calldata.0.clone();

let result = match evm.transact_ref() {
Ok(result) => result,
Err(e) => return Err(anyhow!(«EVM call failed: {e:?}»)),
};

К настоящему времени мы все должны быть хорошо знакомы с тем, как мы вызываем транзакции с помощью экземпляра EVM. Но на этот раз мы собираемся получить доступ к затронутым значениям слота хранения, возвращаемым revm, выполнив следующие действия:let token_b160: B160 = token.into();
let token_acc = result.state.get(&token_b160).unwrap();
let token_touched_storage = token_acc.storage.clone();
info!(«Touched storage slots: {:?}», token_touched_storage);

Выполнение этого приведет к следующему:

[16:05:56] [ИНФОРМАЦИЯ] Затронутые слоты хранения: {0xb4dd106494c3538bbf85de3412632df633f8b5c7be6ffb607fdbabd4700d7d57_U256: StorageSlot { original_value: 0x0_U256, present_value: 0x0_U256 }}

Вуаля, вот и все. Мы видим, что получаем что-то очень похожее на то, что у нас было с трассировкой Geth EVM.

Мы можем видеть, какие слоты для хранения были затронуты и как изменились значения (состояние pre/post).

Итак, мы делаем то же самое, что и с трассировками Geth EVM:for i in 0..20 {
let slot = keccak256(&abi::encode(&[
abi::Token::Address(account.into()),
abi::Token::Uint(U256::from(i)),
]));
let slot: rU256 = U256::from(slot).into();
match token_touched_storage.get(&slot) {
Some(_) => {
info!(«Balance storage slot: {:?} ({:?})», i, slot);
return Ok(i);
}
None => {}
}
}

Перейдите по ссылке (src/main.rs):// previous imports

// fix this ?
use revm_is_all_you_need::revm_examples::{
create_evm_instance, evm_env_setup, get_token_balance, geth_and_revm_tracing,
revm_contract_deploy_and_tracing,
};

#[tokio::main]
async fn main() -> Result<()> {
// previous code here…

// add this ?
match revm_contract_deploy_and_tracing(&mut evm, provider.clone(), weth, user).await {
Ok(_) => {}
Err(e) => info!(«Tracing error: {e:?}»),
}

Сделав это, и запустив грузовой пробег, мы получим тот же результат, что и раньше:

[16:05:56] [ИНФОРМАЦИЯ] Слот для хранения весов: 3 (0xb4dd106494c3538bbf85de3412632df633f8b5c7be6ffb607fdbabd4700d7d57_U256)

Шаблоны прокси, шаблоны прокси

Теперь, когда мы знаем, как использовать revm как способ получения изменений значения хранилища, давайте рассмотрим крайний случай, когда мы пытаемся развернуть контракт с помощью шаблонов прокси.

Одним из таких примеров является токен USDC. В приведенном выше примере мы имели дело с токеном WETH, который не использует шаблон прокси, поэтому мы могли развернуть контракт токена так же легко, как:// deploy contract to EVM
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

let mut ethersdb = EthersDB::new(provider.clone(), Some(block.number.unwrap().into())).unwrap();

let token_acc_info = ethersdb.basic(token.into()).unwrap().unwrap();
evm.db
.as_mut()
.unwrap()
.insert_account_info(token.into(), token_acc_info);

Однако, если мы сделаем это с USDC, мы увидим из определения контракта в Etherscan, что мы получим байт-код чего-то, называемого контрактом UpgradeabilityProxy:

Это довольно удобно, так как мы сразу видим, что мы можем получить доступ к адресу контракта на реализацию через переменную IMPLEMENTATION_SLOT.

Но настоящий кошмар начинается, когда мы понимаем, что существует множество способов реализации шаблона прокси-сервера, и что UpgradeabilityProxy является лишь одним из многих способов сделать это.

Таким образом, мы создаем простую функцию, которая может охватывать 4 различных шаблона прокси (логика/маяк EIP-1967, реализация OpenZeppelin и логика EIP-1822) в новом файле, который мы назовем (src/tokens.rs):use anyhow::Result;
use ethers::prelude::*;
use ethers_core::types::{BlockId, BlockNumber, TxHash, H160, U256};
use std::sync::Arc;
use tokio::task::JoinSet;

use crate::constants::ZERO_ADDRESS;

pub async fn get_implementation<M: Middleware + ‘static>(
provider: Arc<M>,
token: H160,
block_number: U64,
) -> Result<Option<H160>> {
// adapted from: https://github.com/gnosis/evm-proxy-detection/blob/main/src/index.ts
let eip_1967_logic_slot =
U256::from(«0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc»);
let eip_1967_beacon_slot =
U256::from(«0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50»);
let open_zeppelin_implementation_slot =
U256::from(«0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3»);
let eip_1822_logic_slot =
U256::from(«0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7»);

let implementation_slots = vec![
eip_1967_logic_slot,
eip_1967_beacon_slot,
open_zeppelin_implementation_slot,
eip_1822_logic_slot,
];

let mut set = JoinSet::new();

for slot in implementation_slots {
let _provider = provider.clone();
let fut = tokio::spawn(async move {
_provider
.get_storage_at(token, TxHash::from_uint(&slot), Some(block_number.into()))
.await
});
set.spawn(fut);
}

while let Some(res) = set.join_next().await {
let out = res???;
let implementation = H160::from(out);
if implementation != *ZERO_ADDRESS {
return Ok(Some(implementation));
}
}

Ok(None)
}

И обновить (src/lib.rs):pub mod revm_examples;
pub mod constants;
pub mod utils;
pub mod trace;
pub mod tokens;

Это упрощенная версия:

evm-proxy-detection/src/index.ts в main · gnosis/evm-proxy-detection

Обнаружение прокси-контрактов и их целевых адресов с помощью функции запросов JSON-RPC, совместимой с EIP-1193 …

github.com

Таким образом, читатели, которые хотят получить более полный способ решения этой проблемы, могут обратиться к приведенной выше ссылке.

Используя функцию «get_implementation», мы в основном вызываем четыре разных слота с помощью eth_getStorageAt вызова, чтобы получить доступ к значениям каждого слота и посмотреть, хранят ли они адрес реализации. Если возвращаемое значение не равно ZERO ADDRESS, мы знаем, что байт-код контракта реализации находится за шаблоном прокси-сервера, и мы должны использовать этот адрес для получения кода для нашего целевого контракта.

Немного сбивает с толку, правда?

REVM — это все, что вам нужно

Мы прошли долгий путь. Давайте быстро подведем итоги того, что мы рассмотрели до сих пор:

  • мы знаем, как создать экземпляр EVM,
  • мы знаем, как использовать revm так же, как и с трассировкой Geth EVM,
  • мы знаем, как получить адрес реализации прокси,
  • Мы знаем, как вставлять значения хранилища с помощью слота хранения

И, используя эти методы, мы, наконец, смоделируем нашу функцию v2SimulateSwap в нашем контракте Simulator.

Давайте перейдем к (src/revm_examples.rs) и добавим:// previous code here…

// add this ?
pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
evm: &mut EVM<InMemoryDB>,
provider: Arc<M>,
account: H160,
factory: H160,
target_pair: H160,
input_token: H160,
output_token: H160,
input_balance_slot: i32,
output_balance_slot: i32,
input_token_implementation: Option<H160>,
output_token_implementation: Option<H160>,
) -> Result<(U256, U256)> {
Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

Мы будем реализовывать эту функцию по одной строке за раз.

Давайте быстро вспомним, какой была сигнатура функции v2SimulateSwap:function v2SimulateSwap(
uint256 amountIn,
address targetPair,
address inputToken,
address outputToken
) external returns (uint256 amountOut, uint256 realAfterBalance)

Имея это в виду, мы в первую очередь запрашиваем последнюю информацию о блоках с помощью нашего провайдера:// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
evm: &mut EVM<InMemoryDB>,
provider: Arc<M>,
account: H160,
factory: H160,
target_pair: H160,
input_token: H160,
output_token: H160,
input_balance_slot: i32,
output_balance_slot: i32,
input_token_implementation: Option<H160>,
output_token_implementation: Option<H160>,
) -> Result<(U256, U256)> {
// add this ?
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

Затем мы создаем EthersDB, чтобы мы могли выполнять вызовы к конечным точкам нашего узла:// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

// add this ?
let mut ethersdb = EthersDB::new(provider.clone(), Some(block.number.unwrap().into())).unwrap();

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

Наша база данных EthersDB будет выполнять вызовы к последним данным блока.

Теперь мы извлечем изменяемую ссылку нашей БД, как показано ниже:// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

let mut ethersdb = EthersDB::new(provider.clone(), Some(block.number.unwrap().into())).unwrap();

// add this ?
let db = evm.db.as_mut().unwrap();

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

это может быть доступно из нашего экземпляра EVM. Мы будем использовать это для развертывания контрактов и внедрения значений хранилища.

✅ Настройка баланса пользователя

Затем мы создаем основную учетную запись пользователя и вводим ей баланс в 10 ETH:// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

let mut ethersdb = EthersDB::new(provider.clone(), Some(block.number.unwrap().into())).unwrap();

let db = evm.db.as_mut().unwrap();

// add this ?
let ten_eth = rU256::from(10)
.checked_mul(rU256::from(10).pow(rU256::from(18)))
.unwrap();

// Set user: give the user enough ETH to pay for gas
let user_acc_info = AccountInfo::new(ten_eth, 0, Bytecode::default());
db.insert_account_info(account.into(), user_acc_info);

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

Теперь, когда у нас настроена основная учетная запись пользователя, нам нужно развернуть наш контракт Simulator в базе данных EVM (мы даем ему случайный адрес):// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

let mut ethersdb = EthersDB::new(provider.clone(), Some(block.number.unwrap().into())).unwrap();

let db = evm.db.as_mut().unwrap();

let ten_eth = rU256::from(10)
.checked_mul(rU256::from(10).pow(rU256::from(18)))
.unwrap();

// Set user: give the user enough ETH to pay for gas
let user_acc_info = AccountInfo::new(ten_eth, 0, Bytecode::default());
db.insert_account_info(account.into(), user_acc_info);

// add this ?
// Deploy Simulator contract
let simulator_address = H160::from_str(«0xF2d01Ee818509a9540d8324a5bA52329af27D19E»).unwrap();
let simulator_acc_info = AccountInfo::new(
rU256::ZERO,
0,
Bytecode::new_raw((*SIMULATOR_CODE.0).into()),
);
db.insert_account_info(simulator_address.into(), simulator_acc_info);

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

Поскольку нам нужно развернуть контракты для входного токена, выходного токена и пары Uniswap V2 для этих двух токенов, мы просто используем контракт Uniswap V2 Factory для создания новой пары:// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
// previous code here…

// add this ?
// Deploy necessary contracts to simulate Uniswap V2 swap
let input_token_address = match input_token_implementation {
Some(implementation) => implementation,
None => input_token,
};
let output_token_address = match output_token_implementation {
Some(implementation) => implementation,
None => output_token,
};
let input_token_acc_info = ethersdb.basic(input_token_address.into()).unwrap().unwrap();
let output_token_acc_info = ethersdb
.basic(output_token_address.into())
.unwrap()
.unwrap();
let factory_acc_info = ethersdb.basic(factory.into()).unwrap().unwrap();

db.insert_account_info(input_token.into(), input_token_acc_info);
db.insert_account_info(output_token.into(), output_token_acc_info);
db.insert_account_info(factory.into(), factory_acc_info);

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

Но помните, что наши токены могут скрываться за контрактом шаблона прокси, поэтому мы получаем значения Optional input_token_implementation / output_token_implementation и используем эти адреса в качестве входных данных для нашего базового вызова EthersDB, если это не значение None. При этом мы развернули три контракта:

  • входной токен,
  • выходной токен,
  • Фабрика Uniswap V2

✅ Создайте новую пару с помощью Uniswap V2 Factory

На этот раз мы попробуем создать новую пару с вызовом нашего контракта Factory:// previous code here…

// add this ?
pub fn get_tx_result(result: ExecutionResult) -> Result<TxResult> {
let output = match result {
ExecutionResult::Success {
gas_used,
gas_refunded,
output,
logs,
..
} => match output {
Output::Call(o) => TxResult {
output: o,
logs: Some(logs),
gas_used,
gas_refunded,
},
Output::Create(o, _) => TxResult {
output: o,
logs: Some(logs),
gas_used,
gas_refunded,
},
},
ExecutionResult::Revert { gas_used, output } => {
return Err(anyhow!(
«EVM REVERT: {:?} / Gas used: {:?}»,
output,
gas_used
))
}
ExecutionResult::Halt { reason, .. } => return Err(anyhow!(«EVM HALT: {:?}», reason)),
};

Ok(output)
}

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
// previous code here…

// add this ?
// Deploy pair contract using factory
let factory_abi = BaseContract::from(parse_abi(&[
«function createPair(address,address) external returns (address)»,
])?);
let calldata = factory_abi.encode(«createPair», (input_token, output_token))?;

let gas_price = rU256::from(100)
.checked_mul(rU256::from(10).pow(rU256::from(9)))
.unwrap();

// Create a pair contract using the factory contract
let create_pair_tx = TxEnv {
caller: account.into(),
gas_limit: 5000000,
gas_price: gas_price,
gas_priority_fee: None,
transact_to: TransactTo::Call(factory.into()),
value: rU256::ZERO,
data: calldata.0,
chain_id: None,
nonce: None,
access_list: Default::default(),
};
evm.env.tx = create_pair_tx;

let result = match evm.transact_commit() {
Ok(result) => result,
Err(e) => return Err(anyhow!(«EVM call failed: {:?}», e)),
};
let result = get_tx_result(result)?;
let pair_address: H160 = factory_abi.decode_output(«createPair», result.output)?;
info!(«Pair created: {:?}», pair_address);

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

Обратите внимание, что мы используем 100 GWEI в качестве gas_price.

Мы также используем несколько иной способ проведения нашей транзакции. На этот раз мы делаем это с помощью структуры TxEnv. В остальном синтаксис должен казаться вам знакомым.

Мы получаем TxResult, выполняя вызов transact_commit с помощью нашего экземпляра EVM. Сделав это, мы видим, что наша пара была успешно создана.

✅ Получение логов после выполнения транзакции

Преимущество использования revm заключается в том, что это действительно то же самое, что и выполнение транзакций на узле Ethereum. Выполнение транзакции также даст вам доступ к выходным журналам, которые мы получаем следующим образом:// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
// previous code here…

// add this ?
// parse PairCreated event to get token0 / token1
let pair_created_log = &result.logs.unwrap()[0];
let token0: B160 = pair_created_log.topics[1].into();
let token1: B160 = pair_created_log.topics[2].into();
info!(«Token 0: {:?} / Token 1: {:?}», token0, token1);

// Check if the target_pair is equal to the pair created address
assert_eq!(target_pair, pair_address);

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

Мы проверяем, что token0 и token1 являются значениями, которые мы ожидали.

✅ Впрыскиваем резервы в пару

Когда наш парный контракт готов, мы извлекаем исходные данные о резервах из нашего узла:// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
// previous code here…

// add this ?
// There’re no reserves in the pool, so we inject the reserves that we retrieve with ethersdb
// The storage slot of reserves is: 8
let db = evm.db.as_mut().unwrap();
let reserves_slot = rU256::from(8);
let original_reserves = ethersdb
.storage(pair_address.into(), reserves_slot)
.unwrap();
db.insert_account_storage(pair_address.into(), reserves_slot, original_reserves)?;

// Check that the reserves are set correctly
let pair_abi = BaseContract::from(parse_abi(&[
«function getReserves() external view returns (uint112,uint112,uint32)»,
])?);
let calldata = pair_abi.encode(«getReserves», ())?;
let get_reserves_tx = TxEnv {
caller: account.into(),
gas_limit: 5000000,
gas_price: gas_price,
gas_priority_fee: None,
transact_to: TransactTo::Call(target_pair.into()),
value: rU256::ZERO,
data: calldata.0,
chain_id: None,
nonce: None,
access_list: Default::default(),
};
evm.env.tx = get_reserves_tx;

let result = match evm.transact_ref() {
Ok(result) => result,
Err(e) => return Err(anyhow!(«EVM call failed: {:?}», e)),
};
let result = get_tx_result(result.result)?;
let reserves: (U256, U256, U256) = pair_abi.decode_output(«getReserves», result.output)?;
info!(«Pair reserves: {:?}», reserves);

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

После этого мы пробуем вызвать функцию «getReserves», чтобы убедиться, что резервы пары установлены правильно. (используя transact_refпотому что мы не вносим никаких изменений с отслеживанием состояния)

Мы почти закончили.

✅ Настройка баланса парных входных/выходных токенов

Если мы изучим функцию «своп» Uniswap V2 Pair:

Мы можем заметить, что он извлекает баланс token0token1, выполняя вызовы контрактов токенов.

Это означает, что наш недавно развернутый парный контракт должен иметь реальные балансы токенов для выполнения реального обмена. К счастью для нас, мы уже знаем, как это сделать:// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
// previous code here…

// add this ?
// We actually have to feed the input/output token balance to pair contract (to perform real swaps)
let db = evm.db.as_mut().unwrap();

let (balance_slot_0, balance_slot_1) = if token0 == input_token.into() {
(input_balance_slot, output_balance_slot)
} else {
(output_balance_slot, input_balance_slot)
};
info!(
«Balance slot 0: {:?} / slot 1: {:?}»,
balance_slot_0, balance_slot_1
);

let pair_token0_slot = keccak256(&abi::encode(&[
abi::Token::Address(target_pair.into()),
abi::Token::Uint(U256::from(balance_slot_0)),
]));
db.insert_account_storage(token0, pair_token0_slot.into(), reserves.0.into())?;

let pair_token1_slot = keccak256(&abi::encode(&[
abi::Token::Address(target_pair.into()),
abi::Token::Uint(U256::from(balance_slot_1)),
]));
db.insert_account_storage(token1, pair_token1_slot.into(), reserves.1.into())?;

// Check that balance is set correctly
let token_abi = BaseContract::from(parse_abi(&[
«function balanceOf(address) external view returns (uint256)»,
])?);
for token in vec![token0, token1] {
let calldata = token_abi.encode(«balanceOf», target_pair)?;
evm.env.tx.caller = account.into();
evm.env.tx.transact_to = TransactTo::Call(token);
evm.env.tx.data = calldata.0;
let result = match evm.transact_ref() {
Ok(result) => result,
Err(e) => return Err(anyhow!(«EVM call failed: {:?}», e)),
};
let result = get_tx_result(result.result)?;
let balance: U256 = token_abi.decode_output(«balanceOf», result.output)?;
info!(«{:?}: {:?}», token, balance);
}

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

После того, как мы установили балансы токенов для парного контракта, мы проверяем, правильно ли они установлены, вызывая функции «balanceOf» для каждого из них.

✅ Контракт Inject Simulator с балансом входного токена

Чтобы выполнить своп, мы должны убедиться, что в нашем симуляторе достаточно входного баланса токенов:// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
// previous code here…

// add this ?
// feed simulator with input_token balance
let db = evm.db.as_mut().unwrap();

let slot_in = keccak256(&abi::encode(&[
abi::Token::Address(simulator_address.into()),
abi::Token::Uint(U256::from(input_balance_slot)),
]));
db.insert_account_storage(input_token.into(), slot_in.into(), ten_eth)?;

Ok((U256::zero(), U256::zero())) // ? placeholder for now, will update later
}

✅ v2SimulateSwap

Наконец-то мы можем выполнить наш своп:// previous code here…

pub async fn revm_v2_simulate_swap<M: Middleware + ‘static>(
// params…
) -> Result<(U256, U256)> {
// previous code here…

// add this ?
// run v2SimulateSwap
let amount_in = U256::from(1)
.checked_mul(U256::from(10).pow(U256::from(18)))
.unwrap();
let simulator_abi = BaseContract::from(
parse_abi(&[
«function v2SimulateSwap(uint256,address,address,address) external returns (uint256, uint256)»,
])?
);
let calldata = simulator_abi.encode(
«v2SimulateSwap»,
(amount_in, target_pair, input_token, output_token),
)?;
let v2_simulate_swap_tx = TxEnv {
caller: account.into(),
gas_limit: 5000000,
gas_price: gas_price,
gas_priority_fee: None,
transact_to: TransactTo::Call(simulator_address.into()),
value: rU256::ZERO,
data: calldata.0,
chain_id: None,
nonce: None,
access_list: Default::default(),
};
evm.env.tx = v2_simulate_swap_tx;

let result = match evm.transact_commit() {
Ok(result) => result,
Err(e) => return Err(anyhow!(«EVM call failed: {:?}», e)),
};
let result = get_tx_result(result)?;
let out: (U256, U256) = simulator_abi.decode_output(«v2SimulateSwap», result.output)?;
info!(«Amount out: {:?}», out);

Ok(out)
}

Теперь мы можем попробовать запустить этот код из нашего файла main.rs (src/main.rs):// previous code here…

// fix this ?
use revm_is_all_you_need::revm_examples::{
create_evm_instance, evm_env_setup, get_token_balance, geth_and_revm_tracing,
revm_contract_deploy_and_tracing, revm_v2_simulate_swap,
};
use revm_is_all_you_need::tokens::get_implementation;

#[tokio::main]
async fn main() -> Result<()> {
// previous code here…

// add this ?
let block = provider
.get_block(BlockNumber::Latest)
.await
.unwrap()
.unwrap();

let uniswap_v2_factory = H160::from_str(«0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f»).unwrap();
let weth_usdt_pair = H160::from_str(«0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852»).unwrap();

let weth_balance_slot =
revm_contract_deploy_and_tracing(&mut evm, provider.clone(), weth, user)
.await
.unwrap();
let usdt_balance_slot =
revm_contract_deploy_and_tracing(&mut evm, provider.clone(), usdt, user)
.await
.unwrap();

let weth_implementation = get_implementation(provider.clone(), weth, block.number.unwrap())
.await
.unwrap();
let usdt_implementation = get_implementation(provider.clone(), usdt, block.number.unwrap())
.await
.unwrap();

info!(«WETH proxy: {:?}», weth_implementation);
info!(«USDT proxy: {:?}», usdt_implementation);

match revm_v2_simulate_swap(
&mut evm,
provider.clone(),
user,
uniswap_v2_factory,
weth_usdt_pair,
weth,
usdt,
weth_balance_slot,
usdt_balance_slot,
weth_implementation,
usdt_implementation,
)
.await
{
Ok(_) => {}
Err(e) => info!(«v2SimulateSwap revm failed: {e:?}»),
}

Ok(())
}

Выпуск:

Мы видим, что обмен 1 WETH даст нам:

[18:31:35] [ИНФОРМАЦИЯ] Сумма: (1588027787, 1588027787)

USDT обратно. Что эквивалентно:

1588027787 / 10 ** 6 = 1588.027787 USDT

Это кажется правильным ?

Хорошо, REVM — это не все, что вам нужно.

Если вы похожи на меня, вам, вероятно, интересно, как вы сюда попали.

Я имею в виду, что все это работает только для имитации одного обмена? Какой смысл использовать revm, если один своп будет таким сложным.

Вот почему вам нужно использовать интерфейс более высокого уровня, чтобы справиться со всем этим.

Вот тут-то и может проявить себя литейщик-эвм.

Единственное, что вам нужно сделать, чтобы использовать foundry-evm, — это переключить EmptyDB на SharedBackend foundry-evm. Вот где происходит все волшебство.

Создайте новый файл с именем (src/foundry_examples.rs):use anyhow::{anyhow, Result};
use ethers::{
abi::{self, parse_abi},
prelude::*,
providers::Middleware,
types::{BlockNumber, H160, U256},
};
use foundry_evm::{
executor::{
fork::{BlockchainDb, BlockchainDbMeta, SharedBackend},
Bytecode, TransactTo, TxEnv,
},
revm::{
db::CacheDB,
primitives::{keccak256, AccountInfo, U256 as rU256},
EVM,
},
};
use log::info;
use std::{collections::BTreeSet, str::FromStr, sync::Arc};

use crate::constants::SIMULATOR_CODE;
use crate::revm_examples::get_tx_result;

pub async fn foundry_v2_simulate_swap<M: Middleware + ‘static>(
provider: Arc<M>,
account: H160,
target_pair: H160,
input_token: H160,
output_token: H160,
input_balance_slot: i32,
) -> Result<(U256, U256)> {
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

// pay attention to this part ?
let shared_backend = SharedBackend::spawn_backend_thread(
provider.clone(),
BlockchainDb::new(
BlockchainDbMeta {
cfg_env: Default::default(),
block_env: Default::default(),
hosts: BTreeSet::from([«».to_string()]),
},
None,
),
Some(block.number.unwrap().into()),
);
let db = CacheDB::new(shared_backend);

let mut evm = EVM::new();
evm.database(db);

// I’ll reveal the rest of the code right below
// …
Ok(out)
}

Как видно из вышеизложенного, мы используем нечто, называемое SharedBackend, которое является способом разветвления основной сети в Foundry.

Помните, как нам приходилось вставлять значения в те значения хранилища, которые мы собирались использовать в нашем примере кода revm?

Что ж, больше не нужно использовать SharedBackend. Эта серверная система будет автоматически извлекать для нас всю информацию об учетной записи и значения хранилища из-за кулис всякий раз, когда мы пытаемся получить доступ к значениям, которые ранее не вызывались.

Как это удобно!

Итак, остальная часть кода — это то, с чем мы уже знакомы:pub async fn foundry_v2_simulate_swap<M: Middleware + ‘static>(
provider: Arc<M>,
account: H160,
target_pair: H160,
input_token: H160,
output_token: H160,
input_balance_slot: i32,
) -> Result<(U256, U256)> {
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

let shared_backend = SharedBackend::spawn_backend_thread(
provider.clone(),
BlockchainDb::new(
BlockchainDbMeta {
cfg_env: Default::default(),
block_env: Default::default(),
hosts: BTreeSet::from([«».to_string()]),
},
None,
),
Some(block.number.unwrap().into()),
);
let db = CacheDB::new(shared_backend);

let mut evm = EVM::new();
evm.database(db);

evm.env.cfg.limit_contract_code_size = Some(0x100000);
evm.env.cfg.disable_block_gas_limit = true;
evm.env.cfg.disable_base_fee = true;

evm.env.block.number = rU256::from(block.number.unwrap().as_u64() + 1);

let fork_db = evm.db.as_mut().unwrap();

let ten_eth = rU256::from(10)
.checked_mul(rU256::from(10).pow(rU256::from(18)))
.unwrap();

// Set user: give the user enough ETH to pay for gas
let user_acc_info = AccountInfo::new(ten_eth, 0, Bytecode::default());
fork_db.insert_account_info(account.into(), user_acc_info);

// Deploy Simulator contract
let simulator_address = H160::from_str(«0xF2d01Ee818509a9540d8324a5bA52329af27D19E»).unwrap();
let simulator_acc_info = AccountInfo::new(
rU256::ZERO,
0,
Bytecode::new_raw((*SIMULATOR_CODE.0).into()),
);
fork_db.insert_account_info(simulator_address.into(), simulator_acc_info);

let balance_slot = keccak256(&abi::encode(&[
abi::Token::Address(simulator_address.into()),
abi::Token::Uint(U256::from(input_balance_slot)),
]));
fork_db.insert_account_storage(input_token.into(), balance_slot.into(), ten_eth)?;

// run v2SimulateSwap
let amount_in = U256::from(1)
.checked_mul(U256::from(10).pow(U256::from(18)))
.unwrap();
let simulator_abi = BaseContract::from(
parse_abi(&[
«function v2SimulateSwap(uint256,address,address,address) external returns (uint256, uint256)»,
])?
);
let calldata = simulator_abi.encode(
«v2SimulateSwap»,
(amount_in, target_pair, input_token, output_token),
)?;

let gas_price = rU256::from(100)
.checked_mul(rU256::from(10).pow(rU256::from(9)))
.unwrap();
let v2_simulate_swap_tx = TxEnv {
caller: account.into(),
gas_limit: 5000000,
gas_price: gas_price,
gas_priority_fee: None,
transact_to: TransactTo::Call(simulator_address.into()),
value: rU256::ZERO,
data: calldata.0,
chain_id: None,
nonce: None,
access_list: Default::default(),
};
evm.env.tx = v2_simulate_swap_tx;

let result = match evm.transact_commit() {
Ok(result) => result,
Err(e) => return Err(anyhow!(«EVM call failed: {:?}», e)),
};
let result = get_tx_result(result)?;
let out: (U256, U256) = simulator_abi.decode_output(«v2SimulateSwap», result.output)?;
info!(«Amount out: {:?}», out);

Ok(out)
}

Очень просто.

Обновление (src/lib.rs):pub mod revm_examples;
pub mod constants;
pub mod utils;
pub mod trace;
pub mod tokens;
pub mod foundry_examples;

И добавьте следующее в наш (src/main.rs):match foundry_v2_simulate_swap(
provider.clone(),
user,
weth_usdt_pair,
weth,
usdt,
weth_balance_slot,
)
.await
{
Ok(_) => {}
Err(e) => info!(«v2SimulateSwap foundry evm failed: {e:?}»),
}

Выпуск:

[18:47:53] [ИНФОРМАЦИЯ] Сумма: (1586529481, 1586529481)

Мы видим, что получаем тот же результат, что и наша версия revm.

Симуляете одиночные транзакции? Используйте eth_call!

Знаете ли вы, что мы могли бы сделать все, что мы сделали выше, без помощи новой библиотеки, а вместо этого проложить себе путь вверх только с помощью эфиров?

Мы можем использовать что-то под названием CallBuilder для отправки eth_call запросов в нашу конечную точку RPC, которая описана здесь:

Ethers.rs: Библиотека Ethereum для Rust

Книга обо всем, что связано с эфирами.

www.gakonst.com

Вы не получите полную картину только из этой документации, но поиск по некоторым примерам кода в репозитории ethers-rs Github поможет вам достичь этой цели.

Создайте новый файл с именем (src/eth_call_examples.rs):use anyhow::{anyhow, Result};
use ethers::{
abi::{self, parse_abi},
core::utils::keccak256,
prelude::*,
providers::{call_raw::RawCall, Provider, Ws},
types::{spoof, TransactionRequest, H160, U256},
};
use log::info;
use std::{str::FromStr, sync::Arc};

use crate::constants::SIMULATOR_CODE;

pub async fn eth_call_v2_simulate_swap(
provider: Arc<Provider<Ws>>,
account: H160,
target_pair: H160,
input_token: H160,
output_token: H160,
input_balance_slot: i32,
) -> Result<(U256, U256)> {
// Shows how you can spoof multiple storage slots
// but also shows that you can only test one transaction at a time
let block = provider
.get_block(BlockNumber::Latest)
.await?
.ok_or(anyhow!(«failed to retrieve block»))?;

let ten_eth = U256::from(10)
.checked_mul(U256::from(10).pow(U256::from(18)))
.unwrap();

// Spoof user balance with 10 ETH (for gas fees)
let mut state = spoof::state();
state.account(account).balance(ten_eth).nonce(0.into());

// Create Simulator contract with bytecode injection
let simulator_address = H160::from_str(«0xF2d01Ee818509a9540d8324a5bA52329af27D19E»).unwrap();
state
.account(simulator_address)
.code((*SIMULATOR_CODE).clone());

// Spoof simulator input token balance
let input_balance_slot = keccak256(&abi::encode(&[
abi::Token::Address(simulator_address),
abi::Token::Uint(U256::from(input_balance_slot)),
]));
state.account(input_token).store(
input_balance_slot.into(),
H256::from_low_u64_be(ten_eth.as_u64()),
);

let one_eth = ten_eth.checked_div(U256::from(10)).unwrap();
let simulator_abi = BaseContract::from(
parse_abi(&[
«function v2SimulateSwap(uint256,address,address,address) external returns (uint256, uint256)»,
])?
);
let calldata = simulator_abi.encode(
«v2SimulateSwap»,
(one_eth, target_pair, input_token, output_token),
)?;

let gas_price = U256::from(100)
.checked_mul(U256::from(10).pow(U256::from(9)))
.unwrap();
let tx = TransactionRequest::default()
.from(account)
.to(simulator_address)
.value(U256::zero())
.data(calldata.0)
.nonce(U256::zero())
.gas(5000000)
.gas_price(gas_price)
.chain_id(1)
.into();
let result = provider
.call_raw(&tx)
.state(&state)
.block(block.number.unwrap().into())
.await?;
let out: (U256, U256) = simulator_abi.decode_output(«v2SimulateSwap», result)?;
info!(«v2SimulateSwap eth_call result: {:?}», out);

Ok(out)
}

, красота этого кода. Это такой простой способ подмены данных и моделирования контрактов, которые вы еще не развернули в сети. Все, что вам нужно, это байт-код контракта.

Давайте попробуем запустить этот код после того, как мы обновим наш (src/lib.rs):pub mod revm_examples;
pub mod constants;
pub mod utils;
pub mod trace;
pub mod tokens;
pub mod foundry_examples;
pub mod eth_call_examples;

И добавьте следующее в (src/main.rs)::match eth_call_v2_simulate_swap(
provider.clone(),
user,
weth_usdt_pair,
weth,
usdt,
weth_balance_slot,
)
.await
{
Ok(_) => {}
Err(e) => info!(«v2SimulateSwap eth_call failed: {e:?}»),
}

Выпуск:

[18:57:03] [INFO] v2SimulateSwap eth_call результат: (1588524014, 1588524014)

Как и ожидалось, мы видим точно такие же результаты.

Покажите мне несколько контрольных показателей. Насколько быстро работает REVM?

После реализации трех различных методов проведения моделирования EVM нам может быть интересно узнать об их соответствующей производительности

Чтобы оценить это, я провел простой тест, чтобы сравнить время, затрачиваемое каждым методом на завершение сложной симуляции, подобной той, которую мы провели сегодня.

(Эти симуляции могут быть выполнены по-разному, поэтому, пожалуйста, отнеситесь к результатам здесь с долей скептицизма!)

Вы можете перейти по следующей ссылке, чтобы ознакомиться с подробными сведениями о том, как проводились эти тесты:

revm-is-all-you-need/src/benchmarks.rs на главной странице · SolidQuant/Revm — это все, что вам нужно

Учебные пособия и примеры моделирования EVM (revm / foundry-evm / eth_call) — revm-is-all-you-need/src/benchmarks.rs на главной странице ·…

github.com

Введите нашего первого игрока:

? 1. Обороты:

? 2. Литейный цех-ЭВМ:

? 3. eth_call:

?⚡️Это довольно неожиданно, не правда ли?

Обороты занимали: 0,06 секунды в среднем

Foundry-EVM занял: 0,12 секунды в среднем

и eth_call занял: 0,005 секунды в среднем

с помощью персонального узла Geth.

Я также хотел бы поделиться результатами тех же тестов с использованием узла Alchemy для развлечения:

Обороты занимали: 1,02 секунды в среднем

Литейный завод-EVM занял: 4,7 секунды в среднем

и eth_call заняло: 0,25 секунды в среднем

Как и ожидалось, моделирование оказалось чрезмерно трудоемким при использовании службы общедоступных узлов.

На основании полученных результатов можно сделать предварительный вывод о том, что eth_call является наиболее эффективным подходом к моделированию EVM.

Тем не менее, важно иметь полное представление о сильных и слабых сторонах каждого метода, чтобы выборочно применять их в соответствии с нашими конкретными требованиями.

Причина, по которой eth_call был самым быстрым в нашем случае, заключается в том, что нам пришлось сделать несколько вызовов конечной точки узла, пытаясь получить значения хранилища.

Поэтому, когда речь идет о моделировании одной транзакции, особенно той, которая включает в себя доступ к нескольким значениям хранилища в рамках сложной транзакции, eth_call представляется предпочтительным методом.

И наоборот, в сценариях, где обновления нашего экземпляра EVM в режиме реального времени могут быть достигнуты путем подписки на потоки событий или при работе с пакетными транзакциями, использование revm или foundry-evm является более практичным выбором.

Каждый из этих методов имеет свои уникальные варианты использования, и крайне важно потратить время на понимание технологии, прежде чем делать окончательные выводы.

Источник