├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── README.md ├── bins ├── lst-mev │ ├── Cargo.toml │ └── src │ │ └── main.rs └── mempool-monitor │ ├── Cargo.toml │ └── src │ ├── main.rs │ ├── pool.rs │ └── utils.rs ├── contracts ├── foundry.toml ├── src │ ├── Simulator.sol │ └── interfaces │ │ ├── IBalancerV2Pool.sol │ │ ├── IBalancerV2Vault.sol │ │ ├── ICurveV2Pool.sol │ │ ├── IERC20.sol │ │ ├── IERC4626.sol │ │ ├── IMevEth.sol │ │ ├── IUniswapV3Pool.sol │ │ └── IWETH.sol └── test │ └── Simulator.t.sol ├── crates ├── evm-fork-db │ ├── Cargo.toml │ └── src │ │ ├── backend.rs │ │ ├── cache.rs │ │ ├── database.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ └── types.rs ├── shared │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── logging.rs │ │ └── utils.rs └── simulator │ ├── Cargo.toml │ └── src │ ├── abi.rs │ ├── bytecode.rs │ ├── evm.rs │ ├── lib.rs │ └── traits │ ├── mod.rs │ ├── simulator.rs │ └── uniswap_v3.rs ├── rust-toolchain.toml ├── rustfmt.toml └── taplo.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | 4 | # Mac 5 | .DS_Store 6 | 7 | # Rust 8 | /target 9 | 10 | # Foundry 11 | /contracts/cache 12 | /contracts/lib 13 | /contracts/out 14 | 15 | # Project specific 16 | /logs 17 | /cache 18 | *.log 19 | .env -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.largeFileOptimizations": false, 4 | "rust-analyzer.rustfmt.extraArgs": ["+nightly"], 5 | "rust-analyzer.cargo.runBuildScripts": true, 6 | "rust-analyzer.procMacro.enable": true, 7 | "rust-analyzer.checkOnSave.command": "check", 8 | "rust-analyzer.checkOnSave.extraArgs": ["--target-dir", "/tmp/rust-analyzer-check"], 9 | "rust-analyzer.files.excludeDirs": ["**/target"], 10 | "rust-analyzer.workspace.discoverConfig": null, 11 | "[rust]": { 12 | "editor.defaultFormatter": "rust-lang.rust-analyzer", 13 | "editor.formatOnSave": true 14 | }, 15 | "[toml]": { 16 | "editor.defaultFormatter": "tamasfe.even-better-toml", 17 | "editor.formatOnSave": true 18 | }, 19 | "evenBetterToml.formatter.alignEntries": true, 20 | "evenBetterToml.formatter.arrayAutoExpand": true, 21 | "evenBetterToml.formatter.reorderKeys": true 22 | } 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*", "bins/*"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | version = "0.1.0" 7 | 8 | [workspace.dependencies] 9 | anyhow = "1.0.94" 10 | const_format = "0.2.32" 11 | csv = "1.1" 12 | derivative = "2.2.0" 13 | dotenv = "0.15.0" 14 | evm-fork-db = { path = "./crates/evm-fork-db" } 15 | eyre = "0.6.12" 16 | futures = "0.3.30" 17 | futures-util = "0.3" 18 | itertools = "0.11.0" 19 | mempool-monitor = { path = "./bins/mempool-monitor" } 20 | parking_lot = "0.12" 21 | serde = "1.0" 22 | serde_json = "1.0" 23 | shared = { path = "./crates/shared" } 24 | simulator = { path = "./crates/simulator" } 25 | thiserror = "1" 26 | tokio = { version = "1.39.3", features = ["full"] } 27 | tracing = "0.1.40" 28 | tracing-appender = "0.2.3" 29 | tracing-subscriber = "0.3.18" 30 | url = "2" 31 | 32 | # Alloy 33 | alloy = { version = "0.8", features = [ 34 | "eips", 35 | "full", 36 | "hyper", 37 | "json-rpc", 38 | "node-bindings", 39 | "rpc-client", 40 | "rpc-types-debug", 41 | "rpc-types-trace", 42 | "signer-aws", 43 | "signer-gcp", 44 | "signer-keystore", 45 | "signer-ledger", 46 | "signer-mnemonic", 47 | "signer-trezor", 48 | "signer-yubihsm", 49 | ] } 50 | 51 | alloy-consensus = { version = "0.8", default-features = false } 52 | alloy-network = "0.8" 53 | alloy-primitives = { version = "0.8", features = ["rand"] } 54 | alloy-provider = { version = "0.8", features = [ 55 | "reqwest", 56 | "pubsub", 57 | "ws", 58 | "ipc", 59 | "debug-api", 60 | "anvil-node", 61 | "anvil-api", 62 | ] } 63 | alloy-rpc-client = "0.8" 64 | alloy-rpc-types = "0.8" 65 | alloy-rpc-types-eth = { version = "0.8" } 66 | alloy-rpc-types-trace = { version = "0.8" } 67 | alloy-serde = { version = "0.8", default-features = false } 68 | alloy-sol-types = "0.8" 69 | alloy-transport = { version = "0.8", default-features = false } 70 | alloy-transport-http = "0.8" 71 | 72 | # Revm 73 | revm = { version = "18.0.0", default-features = false, features = ["std", "serde"] } 74 | 75 | # Reth 76 | reth = { git = "https://github.com/paradigmxyz/reth" } 77 | reth-chainspec = { git = "https://github.com/paradigmxyz/reth" } 78 | reth-db = { git = "https://github.com/paradigmxyz/reth" } 79 | reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth" } 80 | reth-node-types = { git = "https://github.com/paradigmxyz/reth" } 81 | reth-provider = { git = "https://github.com/paradigmxyz/reth" } 82 | 83 | # Foundry 84 | foundry-evm = { git = "https://github.com/foundry-rs/foundry" } 85 | foundry-evm-core = { git = "https://github.com/foundry-rs/foundry" } 86 | 87 | [profile.release] 88 | codegen-units = 1 89 | debug = true 90 | opt-level = 3 91 | overflow-checks = true 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EVM MEV projects 2 | 3 | ## Setup 4 | 5 | Create a .env file and set the following variables: 6 | 7 | ```bash 8 | RPC_HTTPS_URL= 9 | RPC_WSS_URL= 10 | ``` 11 | 12 | ## Projects 13 | 14 | ### 1. Mempool monitoring system for EVM chains 15 | 16 | This should work for all EVM compatible chains with the mempool API available. 17 | 18 | Ethereum, BSC, Berachain, etc. 19 | 20 | Run: 21 | 22 | ```bash 23 | cargo run --release --bin mempool-monitor 24 | ``` 25 | 26 | ``` 27 | Tx hash: 0xda2739146e60f5cc0bc55f6ecfce90754995b118a9466f2a7f9e0e69a9e40614 28 | 29 | Tx hash: 0x22ba26731135744e5986b4b977555ca05ebc582aa396f994f5de009fbc39fec6 30 | 2025-01-02T11:04:41.105049Z INFO mempool_monitor: V3: Ok(Log { address: 0xa2542303c9a01c011da9106128d9b0838e6c3e57, data: Swap { sender: 0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad, recipient: 0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad, amount0: -980297773970, amount1: 45000000000000000, sqrtPriceX96: 16890130550038774723541115646484, liquidity: 5290986765420913319, tick: 107248 } }) 31 | 2025-01-02T11:04:41.105318Z INFO mempool_monitor: Transfer: Ok(Log { address: 0x26e550ac11b26f78a04489d5f20f24e3559f7dd9, data: Transfer { from: 0xa2542303c9a01c011da9106128d9b0838e6c3e57, to: 0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad, value: 980297773970 } }) 32 | 2025-01-02T11:04:41.105433Z INFO mempool_monitor: Transfer: Ok(Log { address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2, data: Transfer { from: 0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad, to: 0xa2542303c9a01c011da9106128d9b0838e6c3e57, value: 45000000000000000 } }) 33 | 2025-01-02T11:04:41.105531Z INFO mempool_monitor: Transfer: Ok(Log { address: 0x26e550ac11b26f78a04489d5f20f24e3559f7dd9, data: Transfer { from: 0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad, to: 0x000000fee13a103a10d593b9ae06b3e05f2e7e1c, value: 2450744434 } }) 34 | 2025-01-02T11:04:41.105625Z INFO mempool_monitor: Transfer: Ok(Log { address: 0x26e550ac11b26f78a04489d5f20f24e3559f7dd9, data: Transfer { from: 0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad, to: 0xa278d52813180dbf815ff69f5694d27993807750, value: 977847029536 } }) 35 | 36 | Tx hash: 0x01892e49a9c1ccb57635945bc469ddd02e182f6b4ebed2429ea77d30030158be 37 | ``` 38 | 39 | ### 2. MEV LST arbitrage research 40 | 41 | There are two different LST arbitrage strategies. 42 | 43 | **Strategy #1**: Mint LST and sell on different venue for WETH 44 | 45 | - Buy: Deposit WETH into LST contract and mint LST 46 | - Sell: Swap LST for WETH on different venues (Uniswap V3, Balancer V2, Curve V2) 47 | 48 | **Strategy #2**: Buy LST and directly redeem WETH 49 | 50 | - Buy: Buy LST from different venue (flashloan is possible) 51 | - Sell: Redeem WETH from LST contract 52 | 53 | The strategies have been tested on the following contracts (contracts/src/Simulator.sol): 54 | 55 | ```bash 56 | cd contracts 57 | forge build 58 | forge test --fork-url http://localhost:8545 --fork-block-number 18732930 --via-ir -vv 59 | ``` 60 | 61 | Run Rust script: 62 | 63 | ```bash 64 | cargo run --release --bin lst-mev 65 | ``` 66 | 67 | ``` 68 | 2025-01-02T10:39:48.876042Z INFO lst_mev: Target block number: 18732930 69 | 2025-01-02T10:39:50.272180Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=27718, output=0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000024153000000000000000000000000000000000000000000000000000000000000 70 | 2025-01-02T10:39:50.272649Z INFO lst_mev: amount_in=0, profit=0, took=758ms 71 | 2025-01-02T10:39:53.369634Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=274706, output=0x 72 | 2025-01-02T10:39:53.370173Z INFO lst_mev: amount_in=100000000000000000000, profit=0, took=3097ms 73 | 2025-01-02T10:39:58.768452Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=449999, output=0x 74 | 2025-01-02T10:39:58.769361Z INFO lst_mev: amount_in=200000000000000000000, profit=0, took=5399ms 75 | 2025-01-02T10:40:04.177210Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=449999, output=0x 76 | 2025-01-02T10:40:04.177485Z INFO lst_mev: amount_in=300000000000000000000, profit=0, took=5408ms 77 | 2025-01-02T10:40:09.577390Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=449999, output=0x 78 | 2025-01-02T10:40:09.578368Z INFO lst_mev: amount_in=400000000000000000000, profit=0, took=5400ms 79 | 2025-01-02T10:40:14.981048Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=449999, output=0x 80 | 2025-01-02T10:40:14.982157Z INFO lst_mev: amount_in=500000000000000000000, profit=0, took=5403ms 81 | 2025-01-02T10:40:20.426880Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=449999, output=0x 82 | 2025-01-02T10:40:20.427472Z INFO lst_mev: amount_in=600000000000000000000, profit=0, took=5445ms 83 | 2025-01-02T10:40:25.829888Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=449999, output=0x 84 | 2025-01-02T10:40:25.830212Z INFO lst_mev: amount_in=700000000000000000000, profit=0, took=5402ms 85 | 2025-01-02T10:40:31.278723Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=449999, output=0x 86 | 2025-01-02T10:40:31.279187Z INFO lst_mev: amount_in=800000000000000000000, profit=0, took=5449ms 87 | 2025-01-02T10:40:36.681143Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=449999, output=0x 88 | 2025-01-02T10:40:36.682200Z INFO lst_mev: amount_in=900000000000000000000, profit=0, took=5403ms 89 | 2025-01-02T10:40:42.102395Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=449999, output=0x 90 | 2025-01-02T10:40:42.103234Z INFO lst_mev: amount_in=1000000000000000000000, profit=0, took=5421ms 91 | 2025-01-02T10:40:42.811384Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=27718, output=0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000024153000000000000000000000000000000000000000000000000000000000000 92 | 2025-01-02T10:40:42.812477Z INFO lst_mev: amount_in=0, profit=0, took=709ms 93 | 2025-01-02T10:40:45.371553Z INFO lst_mev: amount_in=10000000000000000000, profit=51759955692787022, took=2558ms 94 | 2025-01-02T10:40:47.944242Z INFO lst_mev: amount_in=20000000000000000000, profit=85564146063761637, took=2572ms 95 | 2025-01-02T10:40:50.479827Z INFO lst_mev: amount_in=30000000000000000000, profit=101460640575642009, took=2535ms 96 | 2025-01-02T10:40:53.040741Z INFO lst_mev: amount_in=40000000000000000000, profit=99497337261482228, took=2560ms 97 | 2025-01-02T10:40:55.665126Z INFO lst_mev: amount_in=50000000000000000000, profit=79721963488201519, took=2624ms 98 | 2025-01-02T10:40:58.226992Z INFO lst_mev: amount_in=60000000000000000000, profit=42182076716036289, took=2561ms 99 | 2025-01-02T10:41:00.771354Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=210548, output=0x 100 | 2025-01-02T10:41:00.771775Z INFO lst_mev: amount_in=70000000000000000000, profit=0, took=2544ms 101 | 2025-01-02T10:41:07.421154Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=210548, output=0x 102 | 2025-01-02T10:41:07.422260Z INFO lst_mev: amount_in=80000000000000000000, profit=0, took=6650ms 103 | 2025-01-02T10:41:10.275527Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=210508, output=0x 104 | 2025-01-02T10:41:10.276169Z INFO lst_mev: amount_in=90000000000000000000, profit=0, took=2853ms 105 | 2025-01-02T10:41:13.352218Z ERROR simulator::traits::simulator: transfer_token reverted. gas_used=274706, output=0x 106 | 2025-01-02T10:41:13.352725Z INFO lst_mev: amount_in=100000000000000000000, profit=0, took=3076ms 107 | 2025-01-02T10:41:15.894471Z INFO lst_mev: amount_in=20000000000000000000, profit=85564146063761637, took=2541ms 108 | 2025-01-02T10:41:18.421215Z INFO lst_mev: amount_in=22000000000000000000, profit=90174525434070175, took=2526ms 109 | 2025-01-02T10:41:20.942218Z INFO lst_mev: amount_in=24000000000000000000, profit=94068981249958115, took=2520ms 110 | 2025-01-02T10:41:23.466565Z INFO lst_mev: amount_in=26000000000000000000, profit=97247896968753875, took=2524ms 111 | 2025-01-02T10:41:26.098962Z INFO lst_mev: amount_in=28000000000000000000, profit=99711655773988905, took=2632ms 112 | 2025-01-02T10:41:43.678804Z INFO lst_mev: amount_in=30000000000000000000, profit=101460640575642009, took=17580ms 113 | 2025-01-02T10:41:46.261869Z INFO lst_mev: amount_in=32000000000000000000, profit=102495234010383417, took=2582ms 114 | 2025-01-02T10:41:48.831844Z INFO lst_mev: amount_in=34000000000000000000, profit=102815818441818585, took=2569ms 115 | 2025-01-02T10:41:51.448207Z INFO lst_mev: amount_in=36000000000000000000, profit=102422775960731740, took=2616ms 116 | 2025-01-02T10:41:54.000841Z INFO lst_mev: amount_in=38000000000000000000, profit=101316488385329168, took=2552ms 117 | 2025-01-02T10:41:56.552372Z INFO lst_mev: amount_in=40000000000000000000, profit=99497337261482228, took=2551ms 118 | 2025-01-02T10:41:59.079354Z INFO lst_mev: amount_in=32000000000000000000, profit=102495234010383417, took=2526ms 119 | 2025-01-02T10:42:01.617871Z INFO lst_mev: amount_in=32400000000000000000, profit=102616459386102771, took=2538ms 120 | 2025-01-02T10:42:10.563430Z INFO lst_mev: amount_in=32800000000000000000, profit=102709127460161738, took=8945ms 121 | 2025-01-02T10:42:13.080826Z INFO lst_mev: amount_in=33200000000000000000, profit=102773241289722716, took=2517ms 122 | 2025-01-02T10:42:15.635795Z INFO lst_mev: amount_in=33600000000000000000, profit=102808803931511744, took=2554ms 123 | 2025-01-02T10:42:18.156779Z INFO lst_mev: amount_in=34000000000000000000, profit=102815818441818585, took=2520ms 124 | 2025-01-02T10:42:20.670691Z INFO lst_mev: amount_in=34400000000000000000, profit=102794287876496794, took=2513ms 125 | 2025-01-02T10:42:23.209864Z INFO lst_mev: amount_in=34800000000000000000, profit=102744215290963804, took=2539ms 126 | 2025-01-02T10:42:25.717200Z INFO lst_mev: amount_in=35200000000000000000, profit=102665603740201003, took=2507ms 127 | 2025-01-02T10:42:28.394149Z INFO lst_mev: amount_in=35600000000000000000, profit=102558456278753807, took=2676ms 128 | 2025-01-02T10:42:30.945808Z INFO lst_mev: amount_in=36000000000000000000, profit=102422775960731740, took=2551ms 129 | 2025-01-02T10:42:33.863319Z INFO lst_mev: amount_in=33600000000000000000, profit=102808803931511744, took=2917ms 130 | 2025-01-02T10:42:36.391940Z INFO lst_mev: amount_in=33680000000000000000, profit=102812490586298093, took=2528ms 131 | 2025-01-02T10:42:38.917716Z INFO lst_mev: amount_in=33760000000000000000, profit=102815035340274773, took=2525ms 132 | 2025-01-02T10:42:41.443063Z INFO lst_mev: amount_in=33840000000000000000, profit=102816438217889313, took=2525ms 133 | 2025-01-02T10:42:43.966183Z INFO lst_mev: amount_in=33920000000000000000, profit=102816699243588540, took=2523ms 134 | 2025-01-02T10:42:46.521395Z INFO lst_mev: amount_in=34000000000000000000, profit=102815818441818585, took=2555ms 135 | 2025-01-02T10:42:49.047994Z INFO lst_mev: amount_in=34080000000000000000, profit=102813795837024880, took=2526ms 136 | 2025-01-02T10:42:51.562487Z INFO lst_mev: amount_in=34160000000000000000, profit=102810631453652159, took=2514ms 137 | 2025-01-02T10:42:54.107006Z INFO lst_mev: amount_in=34240000000000000000, profit=102806325316144462, took=2544ms 138 | 2025-01-02T10:42:56.623710Z INFO lst_mev: amount_in=34320000000000000000, profit=102800877448945126, took=2516ms 139 | 2025-01-02T10:42:59.214992Z INFO lst_mev: amount_in=34400000000000000000, profit=102794287876496794, took=2591ms 140 | 2025-01-02T10:43:01.756678Z INFO lst_mev: amount_in=33840000000000000000, profit=102816438217889313, took=2541ms 141 | 2025-01-02T10:43:04.352131Z INFO lst_mev: amount_in=33856000000000000000, profit=102816581770400096, took=2595ms 142 | 2025-01-02T10:43:06.913458Z INFO lst_mev: amount_in=33872000000000000000, profit=102816679649029843, took=2561ms 143 | 2025-01-02T10:43:09.465551Z INFO lst_mev: amount_in=33888000000000000000, profit=102816731853974120, took=2551ms 144 | 2025-01-02T10:43:12.005174Z INFO lst_mev: amount_in=33904000000000000000, profit=102816738385428495, took=2539ms 145 | 2025-01-02T10:43:14.529674Z INFO lst_mev: amount_in=33920000000000000000, profit=102816699243588540, took=2524ms 146 | 2025-01-02T10:43:17.058747Z INFO lst_mev: amount_in=33936000000000000000, profit=102816614428649820, took=2529ms 147 | 2025-01-02T10:43:19.576308Z INFO lst_mev: amount_in=33952000000000000000, profit=102816483940807900, took=2517ms 148 | 2025-01-02T10:43:22.100193Z INFO lst_mev: amount_in=33968000000000000000, profit=102816307780258345, took=2523ms 149 | 2025-01-02T10:43:24.647652Z INFO lst_mev: amount_in=33984000000000000000, profit=102816085947196720, took=2547ms 150 | 2025-01-02T10:43:27.215085Z INFO lst_mev: amount_in=34000000000000000000, profit=102815818441818585, took=2567ms 151 | 2025-01-02T10:43:29.779698Z INFO lst_mev: amount_in=33888000000000000000, profit=102816731853974120, took=2564ms 152 | 2025-01-02T10:43:32.318523Z INFO lst_mev: amount_in=33891200000000000000, profit=102816736814137928, took=2538ms 153 | 2025-01-02T10:43:34.839785Z INFO lst_mev: amount_in=33894400000000000000, profit=102816739947363706, took=2521ms 154 | 2025-01-02T10:43:37.393912Z INFO lst_mev: amount_in=33897600000000000000, profit=102816741253653017, took=2553ms 155 | 2025-01-02T10:43:39.927115Z INFO lst_mev: amount_in=33900800000000000000, profit=102816740733007425, took=2533ms 156 | 2025-01-02T10:43:42.479784Z INFO lst_mev: amount_in=33904000000000000000, profit=102816738385428495, took=2552ms 157 | 2025-01-02T10:43:45.099726Z INFO lst_mev: amount_in=33907200000000000000, profit=102816734210917793, took=2619ms 158 | 2025-01-02T10:43:47.649808Z INFO lst_mev: amount_in=33910400000000000000, profit=102816728209476881, took=2550ms 159 | 2025-01-02T10:43:50.270009Z INFO lst_mev: amount_in=33913600000000000000, profit=102816720381107326, took=2620ms 160 | 2025-01-02T10:43:52.781355Z INFO lst_mev: amount_in=33916800000000000000, profit=102816710725810690, took=2511ms 161 | 2025-01-02T10:43:55.309825Z INFO lst_mev: amount_in=33920000000000000000, profit=102816699243588540, took=2528ms 162 | 2025-01-02T10:43:57.953424Z INFO lst_mev: amount_in=33894400000000000000, profit=102816739947363706, took=2643ms 163 | 2025-01-02T10:44:00.482888Z INFO lst_mev: amount_in=33895040000000000000, profit=102816740354776435, took=2529ms 164 | 2025-01-02T10:44:07.530011Z INFO lst_mev: amount_in=33895680000000000000, profit=102816740689111718, took=7047ms 165 | 2025-01-02T10:44:10.079693Z INFO lst_mev: amount_in=33896320000000000000, profit=102816740950369568, took=2549ms 166 | 2025-01-02T10:44:12.612519Z INFO lst_mev: amount_in=33896960000000000000, profit=102816741138549995, took=2532ms 167 | 2025-01-02T10:44:15.138110Z INFO lst_mev: amount_in=33897600000000000000, profit=102816741253653017, took=2525ms 168 | 2025-01-02T10:44:17.661694Z INFO lst_mev: amount_in=33898240000000000000, profit=102816741295678641, took=2523ms 169 | 2025-01-02T10:44:20.209114Z INFO lst_mev: amount_in=33898880000000000000, profit=102816741264626881, took=2547ms 170 | 2025-01-02T10:44:22.759357Z INFO lst_mev: amount_in=33899520000000000000, profit=102816741160497750, took=2550ms 171 | 2025-01-02T10:44:25.307550Z INFO lst_mev: amount_in=33900160000000000000, profit=102816740983291260, took=2548ms 172 | 2025-01-02T10:44:41.694183Z INFO lst_mev: amount_in=33900800000000000000, profit=102816740733007425, took=16386ms 173 | 2025-01-02T10:44:44.212556Z INFO lst_mev: amount_in=33897600000000000000, profit=102816741253653017, took=2518ms 174 | 2025-01-02T10:44:46.741539Z INFO lst_mev: amount_in=33897728000000000000, profit=102816741267904332, took=2528ms 175 | 2025-01-02T10:44:49.259094Z INFO lst_mev: amount_in=33897856000000000000, profit=102816741279232552, took=2517ms 176 | 2025-01-02T10:44:51.778752Z INFO lst_mev: amount_in=33897984000000000000, profit=102816741287637678, took=2519ms 177 | 2025-01-02T10:44:54.366425Z INFO lst_mev: amount_in=33898112000000000000, profit=102816741293119706, took=2587ms 178 | 2025-01-02T10:44:56.907789Z INFO lst_mev: amount_in=33898240000000000000, profit=102816741295678641, took=2541ms 179 | 2025-01-02T10:44:59.451519Z INFO lst_mev: amount_in=33898368000000000000, profit=102816741295314479, took=2543ms 180 | 2025-01-02T10:45:01.984355Z INFO lst_mev: amount_in=33898496000000000000, profit=102816741292027222, took=2532ms 181 | 2025-01-02T10:45:04.527087Z INFO lst_mev: amount_in=33898624000000000000, profit=102816741285816869, took=2542ms 182 | 2025-01-02T10:45:07.076429Z INFO lst_mev: amount_in=33898752000000000000, profit=102816741276683422, took=2549ms 183 | 2025-01-02T10:45:09.622929Z INFO lst_mev: amount_in=33898880000000000000, profit=102816741264626881, took=2546ms 184 | 2025-01-02T10:45:09.623075Z INFO lst_mev: Optimized: Optimized { optimized_in: 33898240000000000000, optimized_out: 102816741295678641 } 185 | 2025-01-02T10:45:09.623128Z INFO lst_mev: Optimized amount in: 33898240000000000000 186 | 2025-01-02T10:45:09.623160Z INFO lst_mev: Optimized profit: 102816741295678641 187 | ``` 188 | 189 | Optimized amount in: 33.89824 ETH 190 | Optimized profit: 0.1028 ETH 191 | -------------------------------------------------------------------------------- /bins/lst-mev/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "lst-mev" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | alloy = { workspace = true } 8 | anyhow = { workspace = true } 9 | dotenv = { workspace = true } 10 | revm = { workspace = true } 11 | shared = { workspace = true } 12 | simulator = { workspace = true } 13 | tokio = { workspace = true } 14 | tracing = { workspace = true } 15 | -------------------------------------------------------------------------------- /bins/lst-mev/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::str::FromStr; 3 | use std::time::Instant; 4 | 5 | use alloy::primitives::Address; 6 | use anyhow::Result; 7 | use revm::primitives::U256; 8 | use shared::utils::get_env; 9 | use simulator::evm::EVM; 10 | use simulator::traits::{SimulatorContract, UniswapV3PoolContract}; 11 | use tracing::info; 12 | 13 | #[derive(Debug, Clone)] 14 | struct Optimized { 15 | pub optimized_in: u128, 16 | pub optimized_out: u128, 17 | } 18 | 19 | async fn simulate( 20 | rpc_https_url: &str, 21 | target_block_number: u64, 22 | weth: Address, 23 | target_uniswap_v3_pool: Address, 24 | zfo: bool, 25 | amount_in: u128, 26 | ) -> Result { 27 | let owner = Address::random(); 28 | 29 | let mut evm = EVM::new( 30 | &rpc_https_url, 31 | None, 32 | None, 33 | target_block_number, 34 | weth, 35 | owner, 36 | U256::from(10_u64.pow(18)), // 1 ETH 37 | ) 38 | .await; 39 | 40 | let balance_before = evm.get_token_balance(weth, evm.simulator()).unwrap().0; 41 | 42 | // Perform flashswap arbitrage. 43 | evm.flashswap_lst_arbitrage(target_uniswap_v3_pool, zfo, U256::from(amount_in))?; 44 | 45 | let balance_after = evm.get_token_balance(weth, evm.simulator()).unwrap().0; 46 | 47 | let profit = balance_after.saturating_sub(balance_before); 48 | 49 | match profit.try_into() { 50 | Ok(profit_u64) => Ok(profit_u64), 51 | Err(_) => { 52 | info!("Profit too large for u128, returning 0"); 53 | Ok(0) 54 | } 55 | } 56 | } 57 | 58 | // Quadratic search for optimal amount_in. 59 | async fn optimize_arbitrage( 60 | rpc_https_url: &str, 61 | target_block_number: u64, 62 | weth: Address, 63 | target_uniswap_v3_pool: Address, 64 | zfo: bool, 65 | ) -> Result { 66 | let intervals = 10; 67 | let tolerance = 10_u128.pow(15); // 0.001 ETH 68 | let ceiling = 10_u128.pow(18) * 1000; // 1000 ETH 69 | 70 | let mut min_amount_in = 0; // 0 ETH 71 | let mut max_amount_in = ceiling; 72 | let mut optimized_in = 0; 73 | let mut max_profit = 0; 74 | 75 | while max_amount_in - min_amount_in > tolerance { 76 | let step = (max_amount_in - min_amount_in) / intervals; 77 | if step == 0 { 78 | break; 79 | } 80 | 81 | let mut best_local_profit = 0; 82 | let mut best_local_amount_in = min_amount_in; 83 | 84 | for i in 0..=intervals { 85 | let amount_in = std::cmp::min(min_amount_in + i * step, ceiling); 86 | 87 | let s = Instant::now(); 88 | let profit = simulate( 89 | rpc_https_url, 90 | target_block_number, 91 | weth, 92 | target_uniswap_v3_pool, 93 | zfo, 94 | amount_in, 95 | ) 96 | .await 97 | .unwrap_or(0); 98 | let took = s.elapsed().as_millis(); 99 | info!("amount_in={amount_in}, profit={profit}, took={took}ms"); 100 | 101 | if profit > best_local_profit { 102 | best_local_profit = profit; 103 | best_local_amount_in = amount_in; 104 | } 105 | 106 | if profit > max_profit { 107 | max_profit = profit; 108 | optimized_in = amount_in; 109 | } 110 | 111 | if amount_in == ceiling { 112 | break; 113 | } 114 | } 115 | 116 | if best_local_amount_in == min_amount_in { 117 | min_amount_in = best_local_amount_in; 118 | max_amount_in = std::cmp::min(best_local_amount_in + step, ceiling); 119 | } else if best_local_amount_in == max_amount_in { 120 | min_amount_in = max_amount_in.saturating_sub(step); 121 | // NB: Intentionally leave max_amount_in unchanged. 122 | } else { 123 | min_amount_in = best_local_amount_in.saturating_sub(step); 124 | max_amount_in = std::cmp::min(best_local_amount_in + step, ceiling); 125 | } 126 | } 127 | 128 | let optimized_in: u128 = optimized_in.try_into().unwrap_or(0); 129 | let optimized_out: u128 = max_profit.try_into().unwrap_or(0); 130 | 131 | Ok(Optimized { optimized_in, optimized_out }) 132 | } 133 | 134 | #[tokio::main] 135 | async fn main() -> Result<()> { 136 | // Load environment variables. 137 | dotenv::dotenv().ok(); 138 | 139 | // Setup tracing. 140 | let log_dir = Path::new("logs"); 141 | let _guard = shared::logging::setup_tracing(Some(&log_dir), Some("lst-mev.log")); 142 | 143 | info!("Starting LST MEV simulation"); 144 | 145 | // Log panics as errors. 146 | let default_panic = std::panic::take_hook(); 147 | std::panic::set_hook(Box::new(move |panic_info| { 148 | ::tracing::error!("Application panic; panic={panic_info:?}"); 149 | 150 | default_panic(panic_info); 151 | })); 152 | 153 | let rpc_https_url = get_env("RPC_HTTPS_URL"); 154 | info!("RPC HTTPS URL: {}", rpc_https_url); 155 | 156 | let target_block_number = 18732930; 157 | info!("Target block number: {}", target_block_number); 158 | 159 | let weth = Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); 160 | 161 | let target_uniswap_v3_pool = 162 | Address::from_str("0xDeBead39628F93905dfc3E88003af40bf11189b0").unwrap(); 163 | 164 | let owner = Address::random(); 165 | 166 | let mut evm = EVM::new( 167 | &rpc_https_url, 168 | None, 169 | None, 170 | target_block_number, 171 | weth, 172 | owner, 173 | U256::from(10_u64.pow(18)), // 1 ETH 174 | ) 175 | .await; 176 | 177 | let token0 = evm.token0(target_uniswap_v3_pool).unwrap(); 178 | let zfo = token0 == weth; 179 | 180 | let optimized = 181 | optimize_arbitrage(&rpc_https_url, target_block_number, weth, target_uniswap_v3_pool, zfo) 182 | .await 183 | .unwrap(); 184 | 185 | info!("Optimized: {:?}", optimized); 186 | 187 | info!("Optimized amount in: {}", optimized.optimized_in); 188 | info!("Optimized profit: {}", optimized.optimized_out); 189 | 190 | Ok(()) 191 | } 192 | -------------------------------------------------------------------------------- /bins/mempool-monitor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "mempool-monitor" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | alloy = { workspace = true } 8 | alloy-primitives = { workspace = true } 9 | alloy-provider = { workspace = true } 10 | alloy-rpc-types = { workspace = true, features = ["debug", "trace"] } 11 | alloy-rpc-types-eth = { workspace = true } 12 | alloy-rpc-types-trace = { workspace = true } 13 | alloy-sol-types = { workspace = true } 14 | anyhow = { workspace = true } 15 | csv = { workspace = true } 16 | dotenv = { workspace = true } 17 | futures-util = { workspace = true } 18 | serde = { workspace = true } 19 | serde_json = { workspace = true } 20 | shared = { workspace = true } 21 | simulator = { workspace = true } 22 | tokio = { workspace = true } 23 | tracing = { workspace = true } 24 | -------------------------------------------------------------------------------- /bins/mempool-monitor/src/main.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod pool; 2 | pub(crate) mod utils; 3 | 4 | use std::path::Path; 5 | 6 | use alloy::providers::ext::DebugApi; 7 | use alloy::providers::Provider; 8 | use alloy::sol_types::SolEvent; 9 | use alloy_rpc_types::transaction::TransactionRequest; 10 | use alloy_rpc_types_eth::BlockNumberOrTag; 11 | use alloy_rpc_types_trace::geth::{ 12 | CallConfig, CallFrame, CallLogFrame, GethDebugTracingCallOptions, GethTrace, 13 | }; 14 | use anyhow::Result; 15 | use futures_util::StreamExt; 16 | use shared::utils::{get_env, get_ws_provider}; 17 | use simulator::abi; 18 | use tracing::info; 19 | 20 | use crate::utils::load_pools; 21 | 22 | fn collect_logs(frame: &CallFrame) -> Vec { 23 | std::iter::once(frame) 24 | .flat_map(|f| { 25 | f.logs 26 | .iter() 27 | .cloned() 28 | .chain(f.calls.iter().flat_map(collect_logs)) 29 | }) 30 | .collect() 31 | } 32 | 33 | #[tokio::main] 34 | async fn main() -> Result<()> { 35 | // Load environment variables. 36 | dotenv::dotenv().ok(); 37 | 38 | // Setup tracing. 39 | let log_dir = Path::new("logs"); 40 | let _guard = shared::logging::setup_tracing(Some(&log_dir), Some("mempool-monitor.log")); 41 | 42 | info!("Starting mempool monitor"); 43 | 44 | // Log panics as errors. 45 | let default_panic = std::panic::take_hook(); 46 | std::panic::set_hook(Box::new(move |panic_info| { 47 | ::tracing::error!("Application panic; panic={panic_info:?}"); 48 | 49 | default_panic(panic_info); 50 | })); 51 | 52 | let rpc_wss_url = get_env("RPC_WSS_URL"); 53 | info!("RPC WSS URL: {}", rpc_wss_url); 54 | 55 | let provider = get_ws_provider(&rpc_wss_url).await; 56 | 57 | // Load all Uniswap V2, V3 pools. 58 | let pools = load_pools(&rpc_wss_url, 0).await.unwrap(); 59 | info!("Loaded {} pools", pools.len()); 60 | 61 | let sub = provider.subscribe_pending_transactions().await?; 62 | let mut stream = sub.into_stream(); 63 | 64 | while let Some(tx_hash) = stream.next().await { 65 | if let Ok(Some(tx)) = provider.get_transaction_by_hash(tx_hash).await { 66 | println!("\nTx hash: {}", tx_hash); 67 | 68 | let trace_tx = TransactionRequest::from_transaction(tx); 69 | 70 | let mut config = GethDebugTracingCallOptions::default(); 71 | 72 | let mut call_config = CallConfig::default(); 73 | call_config = call_config.with_log(); 74 | 75 | config.tracing_options.tracer = 76 | Some(alloy_rpc_types_trace::geth::GethDebugTracerType::BuiltInTracer( 77 | alloy_rpc_types_trace::geth::GethDebugBuiltInTracerType::CallTracer, 78 | )); 79 | 80 | config.tracing_options.tracer_config = 81 | serde_json::to_value(call_config).unwrap().into(); 82 | 83 | if let Ok(trace) = provider 84 | .debug_trace_call(trace_tx, BlockNumberOrTag::Latest.into(), config) 85 | .await 86 | { 87 | if let GethTrace::CallTracer(frame) = trace { 88 | let logs = collect_logs(&frame); 89 | 90 | for log in logs.iter() { 91 | if let Some(topics) = &log.topics { 92 | let topic = topics[0]; 93 | 94 | let alloy_log = alloy_primitives::Log { 95 | address: log.address.unwrap(), 96 | data: alloy_primitives::LogData::new( 97 | log.topics.clone().unwrap(), 98 | log.data.clone().unwrap(), 99 | ) 100 | .unwrap(), 101 | }; 102 | 103 | match topic { 104 | abi::IERC20::Transfer::SIGNATURE_HASH => { 105 | let transfer_log = 106 | abi::IERC20::Transfer::decode_log(&alloy_log, false); 107 | 108 | info!("Transfer: {:?}", transfer_log); 109 | } 110 | 111 | abi::CrocSwapDex::CrocSwap::SIGNATURE_HASH => { 112 | let swap_log = 113 | abi::CrocSwapDex::CrocSwap::decode_log(&alloy_log, false); 114 | 115 | info!("Croc: {:?}", swap_log); 116 | } 117 | 118 | abi::IUniswapV2Pair::Swap::SIGNATURE_HASH => { 119 | let swap_log = 120 | abi::IUniswapV2Pair::Swap::decode_log(&alloy_log, false); 121 | 122 | info!("V2: {:?}", swap_log); 123 | } 124 | 125 | abi::IUniswapV3Pool::Swap::SIGNATURE_HASH => { 126 | let swap_log = 127 | abi::IUniswapV3Pool::Swap::decode_log(&alloy_log, false); 128 | 129 | info!("V3: {:?}", swap_log); 130 | } 131 | 132 | _ => {} 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | Ok(()) 142 | } 143 | -------------------------------------------------------------------------------- /bins/mempool-monitor/src/pool.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Address; 2 | use alloy::rpc::types::Log; 3 | use alloy::sol_types::SolEvent; 4 | use anyhow::Result; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::abi; 8 | 9 | #[derive(Debug, Clone, Serialize, Deserialize)] 10 | pub enum Venue { 11 | UniswapV2, 12 | UniswapV3, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub struct Pool { 17 | pub id: Address, 18 | pub token0: Address, 19 | pub token1: Address, 20 | pub fee: u64, 21 | pub venue: Venue, 22 | pub block: u64, 23 | } 24 | 25 | impl TryFrom<&Log> for Pool { 26 | type Error = anyhow::Error; 27 | 28 | fn try_from(log: &Log) -> Result { 29 | let topic = log.data().topics()[0]; 30 | 31 | match topic { 32 | abi::IUniswapV2Factory::PairCreated::SIGNATURE_HASH => { 33 | let pair_log = abi::IUniswapV2Factory::PairCreated::decode_log(&log.inner, false)?; 34 | Ok(Pool { 35 | id: pair_log.data.pair, 36 | token0: pair_log.data.token0, 37 | token1: pair_log.data.token1, 38 | fee: 3000, // uniswap v2 style (0.3%) 39 | venue: Venue::UniswapV2, 40 | block: log.block_number.unwrap_or(0), 41 | }) 42 | } 43 | abi::IUniswapV3Factory::PoolCreated::SIGNATURE_HASH => { 44 | let pool_log = abi::IUniswapV3Factory::PoolCreated::decode_log(&log.inner, false)?; 45 | Ok(Pool { 46 | id: pool_log.data.pool, 47 | token0: pool_log.data.token0, 48 | token1: pool_log.data.token1, 49 | fee: pool_log.data.fee.try_into()?, 50 | venue: Venue::UniswapV3, 51 | block: log.block_number.unwrap_or(0), 52 | }) 53 | } 54 | _ => anyhow::bail!("Unknown event signature: {topic}"), 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /bins/mempool-monitor/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | use std::sync::Arc; 4 | use std::time::Instant; 5 | 6 | use alloy::sol_types::SolEvent; 7 | use alloy_provider::Provider; 8 | use anyhow::Result; 9 | use csv::{Reader, Writer}; 10 | use shared::utils::{get_block_range, get_logs, get_ws_provider}; 11 | use tracing::info; 12 | 13 | use crate::abi; 14 | use crate::pool::Pool; 15 | 16 | fn save_to_csv(pools: &[Pool], path: &Path) -> Result<()> { 17 | let mut writer = Writer::from_path(path)?; 18 | 19 | for pool in pools { 20 | writer.serialize(pool)?; 21 | } 22 | 23 | writer.flush()?; 24 | Ok(()) 25 | } 26 | 27 | fn load_from_csv(path: &Path) -> Result> { 28 | let mut reader = Reader::from_path(path)?; 29 | let mut pools = Vec::new(); 30 | 31 | for result in reader.deserialize() { 32 | let pool: Pool = result?; 33 | pools.push(pool); 34 | } 35 | 36 | Ok(pools) 37 | } 38 | 39 | pub(crate) async fn load_pools(wss_url: &str, from_block: u64) -> Result> { 40 | let provider = Arc::new(get_ws_provider(wss_url).await); 41 | info!("connected to provider"); 42 | 43 | let cache_dir = Path::new("cache"); 44 | if !cache_dir.exists() { 45 | fs::create_dir_all(cache_dir)?; 46 | info!("Created cache directory at {:?}", cache_dir); 47 | } 48 | 49 | let pools_cache_path = cache_dir.join("pools.csv"); 50 | let pools = if pools_cache_path.exists() { load_from_csv(&pools_cache_path)? } else { vec![] }; 51 | 52 | let start_block = pools 53 | .iter() 54 | .map(|pool| pool.block) 55 | .max() 56 | .map_or(from_block, |block| block + 1); 57 | 58 | let end_block = provider.get_block_number().await?; 59 | 60 | if start_block >= end_block { 61 | info!("No new blocks to scan"); 62 | return Ok(pools); 63 | } 64 | 65 | info!("Scanning blocks {start_block} to {end_block}"); 66 | let mut pools = pools; 67 | let events = [ 68 | abi::IUniswapV2Factory::PairCreated::SIGNATURE, 69 | abi::IUniswapV3Factory::PoolCreated::SIGNATURE, 70 | ]; 71 | 72 | // Process blocks in chunks 73 | const CHUNK_SIZE: u64 = 10_000; 74 | for (chunk_start, chunk_end) in get_block_range(start_block, end_block, CHUNK_SIZE) { 75 | let timer = Instant::now(); 76 | 77 | match get_logs(provider.clone(), chunk_start, chunk_end, None, &events).await { 78 | Ok(logs) => { 79 | info!("Processing blocks {chunk_start}-{chunk_end}: found {} logs", logs.len()); 80 | 81 | let new_pools: Vec<_> = logs 82 | .iter() 83 | .filter_map(|log| { 84 | Pool::try_from(log) 85 | .map_err(|e| info!("Failed to parse pool from log: {e}")) 86 | .ok() 87 | }) 88 | .collect(); 89 | 90 | if !new_pools.is_empty() { 91 | info!( 92 | "Added {} new pools in {}ms", 93 | new_pools.len(), 94 | timer.elapsed().as_millis() 95 | ); 96 | pools.extend(new_pools); 97 | } 98 | } 99 | Err(e) => { 100 | info!("Failed to fetch logs for blocks {chunk_start}-{chunk_end}: {e}"); 101 | continue; 102 | } 103 | } 104 | } 105 | 106 | // Save results 107 | if let Err(e) = save_to_csv(&pools, &pools_cache_path) { 108 | info!("Failed to save pools to cache: {e}"); 109 | } else { 110 | info!("Saved {} pools to {:?}", pools.len(), pools_cache_path); 111 | } 112 | 113 | Ok(pools) 114 | } 115 | -------------------------------------------------------------------------------- /contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 7 | -------------------------------------------------------------------------------- /contracts/src/Simulator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/console.sol"; 5 | 6 | import "./interfaces/IERC20.sol"; 7 | import "./interfaces/IERC4626.sol"; 8 | import "./interfaces/IUniswapV3Pool.sol"; 9 | import "./interfaces/IBalancerV2Vault.sol"; 10 | import "./interfaces/ICurveV2Pool.sol"; 11 | 12 | contract Simulator is IFlashLoanRecipient { 13 | constructor() {} 14 | 15 | receive() external payable {} 16 | 17 | function uniswapV3SwapCallback( 18 | int256 amount0Delta, 19 | int256 amount1Delta, 20 | bytes calldata data 21 | ) external { 22 | // no callback protection in place 23 | // use only for testing/simulation purposes 24 | uint256 amountIn = uint256( 25 | amount0Delta > 0 ? amount0Delta : amount1Delta 26 | ); 27 | 28 | if (data.length == 64) { 29 | // regular v3 swap 30 | (address pool, address tokenIn) = abi.decode( 31 | data, 32 | (address, address) 33 | ); 34 | IERC20(tokenIn).transfer(pool, amountIn); 35 | } else { 36 | // flashswap 37 | (address pool, address tokenIn, address tokenOut) = abi.decode( 38 | data, 39 | (address, address, address) 40 | ); 41 | 42 | IERC20 lst20 = IERC20(tokenOut); 43 | IERC4626 lst4626 = IERC4626(tokenOut); 44 | 45 | uint256 lstBalance = lst20.balanceOf(address(this)); 46 | uint256 shares = lst4626.convertToShares(lstBalance); 47 | 48 | // get back WETH 49 | lst4626.redeem(shares, address(this), address(this)); 50 | 51 | // repay loan 52 | IERC20(tokenIn).transfer(pool, amountIn); 53 | } 54 | } 55 | 56 | error UnauthorizedCaller(address caller); 57 | error InvalidSellType(uint256 sellType); 58 | error TradeNotProfitable(int256 profit); 59 | 60 | function receiveFlashLoan( 61 | IERC20[] memory tokens, 62 | uint256[] memory amounts, 63 | uint256[] memory, // feeAmounts, unused 64 | bytes memory data 65 | ) external { 66 | address BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; 67 | if (msg.sender != BALANCER_VAULT) revert UnauthorizedCaller(msg.sender); 68 | 69 | // Get initial state 70 | address borrowedToken = address(tokens[0]); 71 | uint256 initialBalance = amounts[0]; 72 | 73 | // Decode base parameters 74 | (address lst, uint256 sellType) = abi.decode(data, (address, uint256)); 75 | 76 | // Step 1: Deposit borrowed tokens into LST 77 | lstDeposit(lst, borrowedToken, initialBalance); 78 | uint256 lstBalance = IERC20(lst).balanceOf(address(this)); 79 | console.log("LST balance: %s", lstBalance); 80 | 81 | // Step 2: Sell LST tokens based on sell type 82 | _executeSellStrategy(data, sellType, lstBalance); 83 | 84 | // Step 3: Calculate and log profit 85 | uint256 finalBalance = IERC20(borrowedToken).balanceOf(address(this)); 86 | int256 profit = int256(finalBalance) - int256(initialBalance); 87 | console.log("profit: %s", profit); 88 | 89 | if (profit < 0) { 90 | revert TradeNotProfitable(profit); 91 | } 92 | 93 | // Step 4: Repay loan 94 | IERC20(borrowedToken).transfer(BALANCER_VAULT, initialBalance); 95 | } 96 | 97 | function _executeSellStrategy( 98 | bytes memory data, 99 | uint256 sellType, 100 | uint256 amount 101 | ) private { 102 | if (sellType == 0) { 103 | (, , address pool, bool zeroForOne) = abi.decode( 104 | data, 105 | (address, uint256, address, bool) 106 | ); 107 | uniswapV3Swap(pool, zeroForOne, amount); 108 | } else if (sellType == 1) { 109 | (, , bytes32 poolId, address tokenIn, address tokenOut) = abi 110 | .decode(data, (address, uint256, bytes32, address, address)); 111 | balancerV2Swap(poolId, tokenIn, tokenOut, amount); 112 | } else if (sellType == 2) { 113 | (, , address pool, uint256 tokenInIdx, uint256 tokenOutIdx) = abi 114 | .decode(data, (address, uint256, address, uint256, uint256)); 115 | curveV2Swap(pool, tokenInIdx, tokenOutIdx, amount); 116 | } else { 117 | revert InvalidSellType(sellType); 118 | } 119 | } 120 | 121 | // Arbitrage Scenario #1 122 | function flashloanLstArbitrage( 123 | address lst, 124 | address tokenIn, 125 | uint256 amountIn, 126 | bytes memory sellData 127 | ) public { 128 | // performs arbitrage scenario #1 129 | // this is profitable if the sell price is higher than the deposit price 130 | // 1. Buy: deposit into LST (mint) 131 | // 2. Sell: sell LST from different venue 132 | IBalancerV2Vault vaultContract = IBalancerV2Vault( 133 | 0xBA12222222228d8Ba445958a75a0704d566BF2C8 134 | ); 135 | 136 | IERC20[] memory tokens = new IERC20[](1); 137 | tokens[0] = IERC20(tokenIn); 138 | 139 | uint256[] memory amounts = new uint256[](1); 140 | amounts[0] = amountIn; 141 | 142 | bytes memory fullData = abi.encodePacked(abi.encode(lst), sellData); 143 | 144 | vaultContract.flashLoan( 145 | IFlashLoanRecipient(address(this)), 146 | tokens, 147 | amounts, 148 | fullData 149 | ); 150 | } 151 | 152 | // Arbitrage Scanario #2 153 | function flashswapLstArbitrage( 154 | address pool, 155 | bool zeroForOne, 156 | uint256 amountIn 157 | ) public { 158 | // performs arbitrage scenario #2 159 | // 1. Buy: loan LST from Uniswap V3 using flashswap 160 | // 2. Sell: withdraw WETH directly from LST contract 161 | 162 | IUniswapV3Pool v3Pool = IUniswapV3Pool(pool); 163 | 164 | address token0 = v3Pool.token0(); 165 | address token1 = v3Pool.token1(); 166 | 167 | (address tokenIn, address tokenOut) = zeroForOne 168 | ? (token0, token1) 169 | : (token1, token0); 170 | 171 | uint160 sqrtPriceLimitX96 = zeroForOne 172 | ? 4295128740 173 | : 1461446703485210103287273052203988822378723970341; 174 | 175 | bytes memory data = abi.encode(pool, tokenIn, tokenOut); 176 | 177 | v3Pool.swap( 178 | address(this), 179 | zeroForOne, 180 | int256(amountIn), 181 | sqrtPriceLimitX96, 182 | data 183 | ); 184 | } 185 | 186 | function lstDeposit(address lst, address tokenIn, uint256 amountIn) public { 187 | IERC4626 lstContract = IERC4626(lst); 188 | require(lstContract.asset() == tokenIn); 189 | 190 | IERC20(tokenIn).approve(lst, amountIn); 191 | lstContract.deposit(amountIn, address(this)); 192 | } 193 | 194 | // sellType = 0 195 | function uniswapV3Swap( 196 | address pool, 197 | bool zeroForOne, 198 | uint256 amountIn 199 | ) public { 200 | IUniswapV3Pool v3Pool = IUniswapV3Pool(pool); 201 | 202 | address tokenIn = zeroForOne ? v3Pool.token0() : v3Pool.token1(); 203 | 204 | uint160 sqrtPriceLimitX96 = zeroForOne 205 | ? 4295128740 206 | : 1461446703485210103287273052203988822378723970341; 207 | 208 | bytes memory data = abi.encode(pool, tokenIn); 209 | 210 | v3Pool.swap( 211 | address(this), 212 | zeroForOne, 213 | int256(amountIn), 214 | sqrtPriceLimitX96, 215 | data 216 | ); 217 | } 218 | 219 | // sellType = 1 220 | function balancerV2Swap( 221 | bytes32 poolId, 222 | address tokenIn, 223 | address tokenOut, 224 | uint256 amountIn 225 | ) public { 226 | address vault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; 227 | 228 | IERC20 tokenInContract = IERC20(tokenIn); 229 | tokenInContract.approve(vault, 0xFFFFFFFFFFFFFFFFFFFFFFFF); 230 | 231 | IBalancerV2Vault vaultContract = IBalancerV2Vault(vault); 232 | 233 | vaultContract.swap( 234 | IBalancerV2Vault.SingleSwap( 235 | poolId, 236 | IBalancerV2Vault.SwapKind.GIVEN_IN, 237 | IAsset(tokenIn), 238 | IAsset(tokenOut), 239 | amountIn, 240 | new bytes(0) 241 | ), 242 | IBalancerV2Vault.FundManagement( 243 | address(this), 244 | false, 245 | payable(address(this)), 246 | false 247 | ), 248 | 0, 249 | 11533977638873292903519766084849772071321814878804040558617845282038221897728 250 | ); 251 | } 252 | 253 | // sellType = 2 254 | function curveV2Swap( 255 | address pool, 256 | uint256 tokenInIdx, 257 | uint256 tokenOutIdx, 258 | uint256 amountIn 259 | ) public { 260 | ICurveV2Pool v2Pool = ICurveV2Pool(pool); 261 | 262 | address tokenIn = v2Pool.coins(tokenInIdx); 263 | 264 | IERC20 tokenInContract = IERC20(tokenIn); 265 | tokenInContract.approve(pool, amountIn); 266 | 267 | v2Pool.exchange(tokenInIdx, tokenOutIdx, amountIn, 0); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /contracts/src/interfaces/IBalancerV2Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.20; 3 | 4 | interface IBalancerV2Pool { 5 | function getPoolId() external view returns (bytes32); 6 | } 7 | -------------------------------------------------------------------------------- /contracts/src/interfaces/IBalancerV2Vault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.20; 3 | 4 | import "./IERC20.sol"; 5 | 6 | interface IAsset { 7 | // solhint-disable-previous-line no-empty-blocks 8 | } 9 | 10 | interface IBalancerV2Vault { 11 | struct SingleSwap { 12 | bytes32 poolId; 13 | SwapKind kind; 14 | IAsset assetIn; 15 | IAsset assetOut; 16 | uint256 amount; 17 | bytes userData; 18 | } 19 | 20 | struct FundManagement { 21 | address sender; 22 | bool fromInternalBalance; 23 | address payable recipient; 24 | bool toInternalBalance; 25 | } 26 | 27 | enum SwapKind { 28 | GIVEN_IN, 29 | GIVEN_OUT 30 | } 31 | 32 | function swap( 33 | SingleSwap memory singleSwap, 34 | FundManagement memory funds, 35 | uint256 limit, 36 | uint256 deadline 37 | ) external payable returns (uint256); 38 | 39 | function getPoolTokens( 40 | bytes32 poolId 41 | ) 42 | external 43 | view 44 | returns ( 45 | IERC20[] memory tokens, 46 | uint256[] memory balances, 47 | uint256 lastChangeBlock 48 | ); 49 | 50 | function flashLoan( 51 | IFlashLoanRecipient recipient, 52 | IERC20[] memory tokens, 53 | uint256[] memory amounts, 54 | bytes memory userData 55 | ) external; 56 | } 57 | 58 | interface IFlashLoanRecipient { 59 | function receiveFlashLoan( 60 | IERC20[] memory tokens, 61 | uint256[] memory amounts, 62 | uint256[] memory feeAmounts, 63 | bytes memory userData 64 | ) external; 65 | } 66 | -------------------------------------------------------------------------------- /contracts/src/interfaces/ICurveV2Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.20; 3 | 4 | interface ICurveV2Pool { 5 | function get_dy( 6 | uint256 i, 7 | uint256 j, 8 | uint256 dx 9 | ) external returns (uint256); 10 | 11 | function exchange( 12 | uint256 i, 13 | uint256 j, 14 | uint256 dx, 15 | uint256 min_dy 16 | ) external; 17 | 18 | function coins(uint256 index) external returns (address); 19 | } 20 | -------------------------------------------------------------------------------- /contracts/src/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) 3 | 4 | pragma solidity ^0.8.20; 5 | 6 | interface IERC20 { 7 | event Transfer(address indexed from, address indexed to, uint256 value); 8 | event Approval( 9 | address indexed owner, 10 | address indexed spender, 11 | uint256 value 12 | ); 13 | 14 | function name() external view returns (string memory); 15 | 16 | function symbol() external view returns (string memory); 17 | 18 | function decimals() external view returns (uint8); 19 | 20 | function totalSupply() external view returns (uint256); 21 | 22 | function balanceOf(address account) external view returns (uint256); 23 | 24 | function transfer(address to, uint256 value) external returns (bool); 25 | 26 | function allowance( 27 | address owner, 28 | address spender 29 | ) external view returns (uint256); 30 | 31 | function approve(address spender, uint256 value) external returns (bool); 32 | 33 | function transferFrom( 34 | address from, 35 | address to, 36 | uint256 value 37 | ) external returns (bool); 38 | } 39 | -------------------------------------------------------------------------------- /contracts/src/interfaces/IERC4626.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | interface IERC4626 { 5 | function convertToShares(uint256 assets) external view returns (uint256); 6 | function convertToAssets(uint256 shares) external view returns (uint256); 7 | function asset() external view returns (address); 8 | 9 | function deposit( 10 | uint256 assets, 11 | address receiver 12 | ) external returns (uint256 shares); 13 | 14 | function mint( 15 | uint256 shares, 16 | address receiver 17 | ) external returns (uint256 assets); 18 | 19 | function withdraw( 20 | uint256 assets, 21 | address receiver, 22 | address owner 23 | ) external returns (uint256 shares); 24 | 25 | function redeem( 26 | uint256 shares, 27 | address receiver, 28 | address owner 29 | ) external returns (uint256 assets); 30 | 31 | function previewMint(uint256 shares) external view returns (uint256); 32 | function previewDeposit(uint256 assets) external view returns (uint256); 33 | } 34 | -------------------------------------------------------------------------------- /contracts/src/interfaces/IMevEth.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | interface IMevEth { 5 | function MIN_DEPOSIT() external view returns (uint128); 6 | } 7 | -------------------------------------------------------------------------------- /contracts/src/interfaces/IUniswapV3Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.20; 3 | 4 | interface IUniswapV3Pool { 5 | function token0() external view returns (address); 6 | 7 | function token1() external view returns (address); 8 | 9 | function fee() external view returns (uint24); 10 | 11 | function tickSpacing() external view returns (int24); 12 | 13 | function liquidity() external view returns (uint128); 14 | 15 | function slot0() 16 | external 17 | view 18 | returns ( 19 | uint160 sqrtPriceX96, 20 | int24 tick, 21 | uint16 observationIndex, 22 | uint16 observationCardinality, 23 | uint16 observationCardinalityNext, 24 | uint8 feeProtocol, 25 | bool unlocked 26 | ); 27 | 28 | function ticks( 29 | int24 tick 30 | ) 31 | external 32 | view 33 | returns ( 34 | uint128 liquidityGross, 35 | int128 liquidityNet, 36 | uint256 feeGrowthOutside0X128, 37 | uint256 feeGrowthOutside1X128, 38 | int56 tickCumulativeOutside, 39 | uint160 secondsPerLiquidityOutsideX128, 40 | uint32 secondsOutside, 41 | bool initialized 42 | ); 43 | 44 | function swap( 45 | address recipient, 46 | bool zeroForOne, 47 | int256 amountSpecified, 48 | uint160 sqrtPriceLimitX96, 49 | bytes calldata data 50 | ) external returns (int256 amount0, int256 amount1); 51 | } 52 | -------------------------------------------------------------------------------- /contracts/src/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.20; 3 | 4 | interface IWETH { 5 | function deposit() external payable; 6 | 7 | function transfer(address to, uint value) external returns (bool); 8 | 9 | function withdraw(uint) external; 10 | 11 | function balanceOf(address account) external view returns (uint256); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/test/Simulator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | 6 | import "../src/Simulator.sol"; 7 | import "../src/interfaces/IERC20.sol"; 8 | import "../src/interfaces/IWETH.sol"; 9 | import "../src/interfaces/IMevEth.sol"; 10 | import "../src/interfaces/IUniswapV3Pool.sol"; 11 | import "../src/interfaces/IBalancerV2Vault.sol"; 12 | import "../src/interfaces/IBalancerV2Pool.sol"; 13 | import "../src/interfaces/ICurveV2Pool.sol"; 14 | 15 | // forge build 16 | // forge test --fork-url http://localhost:8545 --via-ir -vv 17 | contract SimulatorTest is Test { 18 | Simulator public simulator; 19 | 20 | IWETH weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 21 | 22 | function setUp() public { 23 | simulator = new Simulator(); 24 | } 25 | 26 | function testLstDeposit() public { 27 | // wrap ETH and send to simulator contract 28 | uint256 amountIn = 100000000000000000; // 0.1 ETH 29 | weth.deposit{value: amountIn}(); 30 | 31 | weth.transfer(address(simulator), amountIn); 32 | 33 | // check that WETH has been received 34 | uint256 simulatorWethBalance = weth.balanceOf(address(simulator)); 35 | console.log("simulator weth balance: %s", simulatorWethBalance); 36 | 37 | address targetLst = 0x24Ae2dA0f361AA4BE46b48EB19C91e02c5e4f27E; 38 | 39 | // simulate LST deposit 40 | IERC20 tokenOutContract = IERC20(targetLst); 41 | 42 | uint256 balanceBefore = tokenOutContract.balanceOf(address(simulator)); 43 | 44 | simulator.lstDeposit(targetLst, address(weth), amountIn); 45 | 46 | uint256 balanceAfter = tokenOutContract.balanceOf(address(simulator)); 47 | 48 | console.log("balance before: %s", balanceBefore); 49 | console.log("balance after: %s", balanceAfter); 50 | 51 | assert(balanceAfter > balanceBefore); 52 | } 53 | 54 | function testUniswapV3Swap() public { 55 | // wrap ETH and send to simulator contract 56 | uint256 amountIn = 100000000000000000; // 0.1 ETH 57 | weth.deposit{value: amountIn}(); 58 | 59 | weth.transfer(address(simulator), amountIn); 60 | 61 | // check that WETH has been received 62 | uint256 simulatorWethBalance = weth.balanceOf(address(simulator)); 63 | console.log("simulator weth balance: %s", simulatorWethBalance); 64 | 65 | address targetUniswapV3Pool = 0xDeBead39628F93905dfc3E88003af40bf11189b0; 66 | 67 | IUniswapV3Pool v3Pool = IUniswapV3Pool(targetUniswapV3Pool); 68 | 69 | address token0 = v3Pool.token0(); 70 | address token1 = v3Pool.token1(); 71 | 72 | bool zeroForOne = token0 == address(weth); 73 | 74 | address tokenOut = zeroForOne ? token1 : token0; 75 | 76 | console.log("token0: %s", token0); 77 | console.log("token1: %s", token1); 78 | console.log("zeroForOne: %s", zeroForOne); 79 | 80 | // simulate v3 swap 81 | IERC20 tokenOutContract = IERC20(tokenOut); 82 | 83 | uint256 balanceBefore = tokenOutContract.balanceOf(address(simulator)); 84 | 85 | simulator.uniswapV3Swap(targetUniswapV3Pool, zeroForOne, amountIn); 86 | 87 | uint256 balanceAfter = tokenOutContract.balanceOf(address(simulator)); 88 | 89 | console.log("balance before: %s", balanceBefore); 90 | console.log("balance after: %s", balanceAfter); 91 | 92 | assert(balanceAfter > balanceBefore); 93 | } 94 | 95 | function testFlashswapArbitrage() public { 96 | uint256 amountIn = 100000000000000000; // 0.1 ETH 97 | address targetUniswapV3Pool = 0xDeBead39628F93905dfc3E88003af40bf11189b0; 98 | 99 | IUniswapV3Pool v3Pool = IUniswapV3Pool(targetUniswapV3Pool); 100 | 101 | address token0 = v3Pool.token0(); 102 | 103 | bool zeroForOne = token0 == address(weth); 104 | 105 | uint256 wethBalanceBefore = weth.balanceOf(address(simulator)); 106 | 107 | simulator.flashswapLstArbitrage( 108 | targetUniswapV3Pool, 109 | zeroForOne, 110 | amountIn 111 | ); 112 | 113 | uint256 wethBalanceAfter = weth.balanceOf(address(simulator)); 114 | 115 | int256 profit = int256(wethBalanceAfter) - int256(wethBalanceBefore); 116 | console.log("profit: %s", profit); 117 | } 118 | 119 | function testBalancerV2Swap() public { 120 | // wrap ETH and send to simulator contract 121 | uint256 amountIn = 100000000000000000; // 0.1 ETH 122 | weth.deposit{value: amountIn}(); 123 | 124 | weth.transfer(address(simulator), amountIn); 125 | 126 | // check that WETH has been received 127 | uint256 simulatorWethBalance = weth.balanceOf(address(simulator)); 128 | console.log("simulator weth balance: %s", simulatorWethBalance); 129 | 130 | address balancerV2Vault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; 131 | address targetBalancerV2Pool = 0x05b1a35FdBC43849aA836B00c8861696edce8cC4; 132 | 133 | IBalancerV2Vault vaultContract = IBalancerV2Vault(balancerV2Vault); 134 | 135 | IBalancerV2Pool v2Pool = IBalancerV2Pool(targetBalancerV2Pool); 136 | 137 | bytes32 poolId = v2Pool.getPoolId(); 138 | 139 | console.log("poolId:"); 140 | console.logBytes32(poolId); 141 | 142 | (IERC20[] memory tokens, uint256[] memory balances, ) = vaultContract 143 | .getPoolTokens(poolId); 144 | 145 | for (uint256 i = 0; i < tokens.length; i++) { 146 | console.log("Token %s: %s", i, address(tokens[i])); 147 | console.log("Balance %s: %s", i, balances[i]); 148 | } 149 | 150 | address token0 = address(tokens[0]); 151 | address token1 = address(tokens[1]); 152 | 153 | uint256 reserve0 = balances[0]; 154 | uint256 reserve1 = balances[1]; 155 | 156 | (address tokenIn, address tokenOut) = token0 == address(weth) 157 | ? (token0, token1) 158 | : (token1, token0); 159 | 160 | uint256 reserveIn = token0 == address(weth) ? reserve0 : reserve1; 161 | 162 | // simulate balancer v2 swap 163 | IERC20 tokenOutContract = IERC20(tokenOut); 164 | 165 | uint256 balanceBefore = tokenOutContract.balanceOf(address(simulator)); 166 | 167 | uint256 swapAmountIn = 100000000000000; // 0.0001 ETH 168 | assert(reserveIn >= swapAmountIn); 169 | 170 | simulator.balancerV2Swap(poolId, tokenIn, tokenOut, swapAmountIn); 171 | 172 | uint256 balanceAfter = tokenOutContract.balanceOf(address(simulator)); 173 | 174 | console.log("balance before: %s", balanceBefore); 175 | console.log("balance after: %s", balanceAfter); 176 | 177 | assert(balanceAfter > balanceBefore); 178 | } 179 | 180 | function testCurveV2Swap() public { 181 | // wrap ETH and send to simulator contract 182 | uint256 amountIn = 100000000000000000; // 0.1 ETH 183 | weth.deposit{value: amountIn}(); 184 | 185 | weth.transfer(address(simulator), amountIn); 186 | 187 | // check that WETH has been received 188 | uint256 simulatorWethBalance = weth.balanceOf(address(simulator)); 189 | console.log("simulator weth balance: %s", simulatorWethBalance); 190 | 191 | address targetCurveV2Pool = 0x429cCFCCa8ee06D2B41DAa6ee0e4F0EdBB77dFad; 192 | 193 | ICurveV2Pool v2Pool = ICurveV2Pool(targetCurveV2Pool); 194 | 195 | address token0 = v2Pool.coins(0); 196 | address token1 = v2Pool.coins(1); 197 | 198 | console.log("token0: %s", token0); 199 | console.log("token1: %s", token1); 200 | 201 | (uint256 tokenInIdx, uint256 tokenOutIdx) = token0 == address(weth) 202 | ? (0, 1) 203 | : (1, 0); 204 | 205 | address tokenOut = token0 == address(weth) ? token1 : token0; 206 | 207 | // simulate curve v2 swap 208 | IERC20 tokenOutContract = IERC20(tokenOut); 209 | 210 | uint256 balanceBefore = tokenOutContract.balanceOf(address(simulator)); 211 | 212 | simulator.curveV2Swap( 213 | targetCurveV2Pool, 214 | tokenInIdx, 215 | tokenOutIdx, 216 | amountIn 217 | ); 218 | 219 | uint256 balanceAfter = tokenOutContract.balanceOf(address(simulator)); 220 | 221 | console.log("balance before: %s", balanceBefore); 222 | console.log("balance after: %s", balanceAfter); 223 | 224 | assert(balanceAfter > balanceBefore); 225 | } 226 | 227 | function testUniswapV3Arbitrage() public { 228 | address targetLst = 0x24Ae2dA0f361AA4BE46b48EB19C91e02c5e4f27E; 229 | address tokenIn = address(weth); 230 | uint256 amountIn = 100000000000000000; // 0.1 ETH 231 | 232 | address targetUniswapV3Pool = 0xDeBead39628F93905dfc3E88003af40bf11189b0; 233 | 234 | IUniswapV3Pool v3Pool = IUniswapV3Pool(targetUniswapV3Pool); 235 | address token0 = v3Pool.token0(); 236 | bool zeroForOne = token0 == address(weth); // zeroForOne for buy, flip for sell 237 | 238 | bytes memory sellData = abi.encode(0, targetUniswapV3Pool, !zeroForOne); 239 | 240 | simulator.flashloanLstArbitrage(targetLst, tokenIn, amountIn, sellData); 241 | } 242 | 243 | function testBalancerV2Arbitrage() public { 244 | address targetLst = 0x24Ae2dA0f361AA4BE46b48EB19C91e02c5e4f27E; 245 | uint256 amountIn = 100000000000000; // 0.0001 ETH 246 | 247 | address balancerV2Vault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; 248 | address targetBalancerV2Pool = 0x05b1a35FdBC43849aA836B00c8861696edce8cC4; 249 | 250 | IBalancerV2Vault vaultContract = IBalancerV2Vault(balancerV2Vault); 251 | IBalancerV2Pool v2Pool = IBalancerV2Pool(targetBalancerV2Pool); 252 | bytes32 poolId = v2Pool.getPoolId(); 253 | 254 | (IERC20[] memory tokens, , ) = vaultContract.getPoolTokens(poolId); 255 | 256 | address token0 = address(tokens[0]); 257 | address token1 = address(tokens[1]); 258 | 259 | (address tokenIn, address tokenOut) = token0 == address(weth) 260 | ? (token0, token1) 261 | : (token1, token0); 262 | 263 | bytes memory sellData = abi.encode(1, poolId, tokenIn, tokenOut); 264 | 265 | IMevEth mevEth = IMevEth(targetLst); 266 | uint256 minDeposit = mevEth.MIN_DEPOSIT(); 267 | 268 | if (amountIn > minDeposit) { 269 | simulator.flashloanLstArbitrage( 270 | targetLst, 271 | tokenIn, 272 | amountIn, 273 | sellData 274 | ); 275 | } else { 276 | console.log("amountIn < minDeposit"); 277 | } 278 | } 279 | 280 | function testCurveV2Arbitrage() public { 281 | address targetLst = 0x24Ae2dA0f361AA4BE46b48EB19C91e02c5e4f27E; 282 | address tokenIn = address(weth); 283 | uint256 amountIn = 100000000000000000; // 0.1 ETH 284 | 285 | address targetCurveV2Pool = 0x429cCFCCa8ee06D2B41DAa6ee0e4F0EdBB77dFad; 286 | ICurveV2Pool v2Pool = ICurveV2Pool(targetCurveV2Pool); 287 | 288 | address token0 = v2Pool.coins(0); 289 | 290 | (uint256 tokenInIdx, uint256 tokenOutIdx) = token0 == address(weth) 291 | ? (1, 0) 292 | : (0, 1); // flip because it's sell 293 | 294 | bytes memory sellData = abi.encode( 295 | 2, 296 | targetCurveV2Pool, 297 | tokenInIdx, 298 | tokenOutIdx 299 | ); 300 | 301 | simulator.flashloanLstArbitrage(targetLst, tokenIn, amountIn, sellData); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /crates/evm-fork-db/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "evm-fork-db" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | alloy-consensus = { workspace = true } 8 | alloy-primitives = { workspace = true } 9 | alloy-provider = { workspace = true } 10 | alloy-rpc-types = { workspace = true } 11 | alloy-serde = { workspace = true } 12 | alloy-transport = { workspace = true } 13 | eyre = { workspace = true } 14 | foundry-evm = { workspace = true } 15 | foundry-evm-core = { workspace = true } 16 | futures = { workspace = true } 17 | parking_lot = { workspace = true } 18 | reth = { workspace = true } 19 | reth-chainspec = { workspace = true } 20 | reth-db = { workspace = true } 21 | reth-node-ethereum = { workspace = true } 22 | reth-node-types = { workspace = true } 23 | reth-provider = { workspace = true } 24 | revm = { workspace = true } 25 | serde = { workspace = true } 26 | serde_json = { workspace = true } 27 | thiserror = { workspace = true } 28 | tokio = { workspace = true } 29 | tracing = { workspace = true } 30 | url = { workspace = true } 31 | -------------------------------------------------------------------------------- /crates/evm-fork-db/src/backend.rs: -------------------------------------------------------------------------------- 1 | //! Smart caching and deduplication of requests when using a forking provider. 2 | 3 | use std::collections::VecDeque; 4 | use std::fmt; 5 | use std::future::IntoFuture; 6 | use std::marker::PhantomData; 7 | use std::path::Path; 8 | use std::pin::Pin; 9 | use std::sync::mpsc::{channel as oneshot_channel, Sender as OneshotSender}; 10 | use std::sync::Arc; 11 | 12 | use alloy_primitives::{keccak256, Address, Bytes, B256, U256}; 13 | use alloy_provider::network::{AnyNetwork, AnyRpcBlock, AnyRpcTransaction, AnyTxEnvelope}; 14 | use alloy_provider::Provider; 15 | use alloy_rpc_types::{BlockId, Transaction}; 16 | use alloy_serde::WithOtherFields; 17 | use alloy_transport::Transport; 18 | use eyre::WrapErr; 19 | use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; 20 | use futures::stream::Stream; 21 | use futures::task::{Context, Poll}; 22 | use futures::{Future, FutureExt}; 23 | use reth::primitives::Bytecode as RethBytecode; 24 | use revm::db::DatabaseRef; 25 | use revm::primitives::map::hash_map::Entry; 26 | use revm::primitives::map::{AddressHashMap, HashMap}; 27 | use revm::primitives::{AccountInfo, Bytecode, KECCAK_EMPTY}; 28 | 29 | use crate::cache::{BlockchainDb, FlushJsonBlockCacheDB, MemDb, StorageInfo}; 30 | use crate::error::{DatabaseError, DatabaseResult}; 31 | use crate::types::DBFactory; 32 | 33 | /// Logged when an error is indicative that the user is trying to fork from a 34 | /// non-archive node. 35 | pub const NON_ARCHIVE_NODE_WARNING: &str = "\ 36 | It looks like you're trying to fork from an older block with a non-archive node which is not \ 37 | supported. Please try to change your RPC url to an \ 38 | archive node if the issue persists."; 39 | 40 | // Various future/request type aliases 41 | 42 | type AccountFuture = 43 | Pin, Address)> + Send>>; 44 | type StorageFuture = Pin, Address, U256)> + Send>>; 45 | type BlockHashFuture = Pin, u64)> + Send>>; 46 | type FullBlockFuture = Pin< 47 | Box, Err>, BlockId)> + Send>, 48 | >; 49 | type TransactionFuture = 50 | Pin, B256)> + Send>>; 51 | 52 | type AccountInfoSender = OneshotSender>; 53 | type StorageSender = OneshotSender>; 54 | type BlockHashSender = OneshotSender>; 55 | type FullBlockSender = OneshotSender>; 56 | type TransactionSender = OneshotSender>; 57 | 58 | type AddressData = AddressHashMap; 59 | type StorageData = AddressHashMap; 60 | type BlockHashData = HashMap; 61 | 62 | struct AnyRequestFuture { 63 | sender: OneshotSender>, 64 | future: Pin> + Send>>, 65 | } 66 | 67 | impl fmt::Debug for AnyRequestFuture { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 | f.debug_tuple("AnyRequestFuture") 70 | .field(&self.sender) 71 | .finish() 72 | } 73 | } 74 | 75 | trait WrappedAnyRequest: Unpin + Send + fmt::Debug { 76 | fn poll_inner(&mut self, cx: &mut Context<'_>) -> Poll<()>; 77 | } 78 | 79 | /// @dev Implements `WrappedAnyRequest` for `AnyRequestFuture`. 80 | /// 81 | /// - `poll_inner` is similar to `Future` polling but intentionally consumes the 82 | /// Future and return Future 83 | /// - This design avoids storing `Future` directly, as its type may 84 | /// not be known at compile time. 85 | /// - Instead, the result (`Result`) is sent via the `sender` channel, 86 | /// which enforces type safety. 87 | impl WrappedAnyRequest for AnyRequestFuture 88 | where 89 | T: fmt::Debug + Send + 'static, 90 | Err: fmt::Debug + Send + 'static, 91 | { 92 | fn poll_inner(&mut self, cx: &mut Context<'_>) -> Poll<()> { 93 | match self.future.poll_unpin(cx) { 94 | Poll::Ready(result) => { 95 | let _ = self.sender.send(result); 96 | Poll::Ready(()) 97 | } 98 | Poll::Pending => Poll::Pending, 99 | } 100 | } 101 | } 102 | 103 | /// Request variants that are executed by the provider 104 | enum ProviderRequest { 105 | Account(AccountFuture), 106 | Storage(StorageFuture), 107 | BlockHash(BlockHashFuture), 108 | FullBlock(FullBlockFuture), 109 | Transaction(TransactionFuture), 110 | AnyRequest(Box), 111 | } 112 | 113 | /// The Request type the Backend listens for 114 | #[derive(Debug)] 115 | enum BackendRequest { 116 | /// Fetch the account info 117 | Basic(Address, AccountInfoSender), 118 | /// Fetch a storage slot 119 | Storage(Address, U256, StorageSender), 120 | /// Fetch a block hash 121 | BlockHash(u64, BlockHashSender), 122 | /// Fetch an entire block with transactions 123 | FullBlock(BlockId, FullBlockSender), 124 | /// Fetch a transaction 125 | Transaction(B256, TransactionSender), 126 | /// Sets the pinned block to fetch data from 127 | SetPinnedBlock(BlockId), 128 | 129 | /// Update Address data 130 | UpdateAddress(AddressData), 131 | /// Update Storage data 132 | UpdateStorage(StorageData), 133 | /// Update Block Hashes 134 | UpdateBlockHash(BlockHashData), 135 | /// Any other request 136 | AnyRequest(Box), 137 | } 138 | 139 | /// Handles an internal provider and listens for requests. 140 | /// 141 | /// This handler will remain active as long as it is reachable (request channel 142 | /// still open) and requests are in progress. 143 | #[must_use = "futures do nothing unless polled"] 144 | pub struct BackendHandler { 145 | provider: P, 146 | file_db_factory: Option, 147 | transport: PhantomData, 148 | /// Stores all the data. 149 | db: BlockchainDb, 150 | /// Requests currently in progress 151 | pending_requests: Vec>, 152 | /// Listeners that wait for a `get_account` related response 153 | account_requests: HashMap>, 154 | /// Listeners that wait for a `get_storage_at` response 155 | storage_requests: HashMap<(Address, U256), Vec>, 156 | /// Listeners that wait for a `get_block` response 157 | block_requests: HashMap>, 158 | /// Incoming commands. 159 | incoming: UnboundedReceiver, 160 | /// unprocessed queued requests 161 | queued_requests: VecDeque, 162 | /// The block to fetch data from. 163 | // This is an `Option` so that we can have less code churn in the functions below 164 | block_id: Option, 165 | } 166 | 167 | impl BackendHandler 168 | where 169 | T: Transport + Clone, 170 | P: Provider + Clone + Unpin + 'static, 171 | { 172 | fn new( 173 | provider: P, 174 | file_db_factory: Option, 175 | db: BlockchainDb, 176 | rx: UnboundedReceiver, 177 | block_id: Option, 178 | ) -> Self { 179 | Self { 180 | provider, 181 | file_db_factory, 182 | db, 183 | pending_requests: Default::default(), 184 | account_requests: Default::default(), 185 | storage_requests: Default::default(), 186 | block_requests: Default::default(), 187 | queued_requests: Default::default(), 188 | incoming: rx, 189 | block_id, 190 | transport: PhantomData, 191 | } 192 | } 193 | 194 | /// handle the request in queue in the future. 195 | /// 196 | /// We always check: 197 | /// 1. if the requested value is already stored in the cache, then answer 198 | /// the sender 199 | /// 2. otherwise, fetch it via the provider but check if a request for that 200 | /// value is already in progress (e.g. another Sender just requested the 201 | /// same account) 202 | fn on_request(&mut self, req: BackendRequest) { 203 | match req { 204 | BackendRequest::Basic(addr, sender) => { 205 | trace!(target: "backendhandler", "received request basic address={:?}", addr); 206 | let acc = self.db.accounts().read().get(&addr).cloned(); 207 | if let Some(basic) = acc { 208 | let _ = sender.send(Ok(basic)); 209 | } else { 210 | self.request_account(addr, sender); 211 | } 212 | } 213 | BackendRequest::BlockHash(number, sender) => { 214 | let hash = self 215 | .db 216 | .block_hashes() 217 | .read() 218 | .get(&U256::from(number)) 219 | .cloned(); 220 | if let Some(hash) = hash { 221 | let _ = sender.send(Ok(hash)); 222 | } else { 223 | self.request_hash(number, sender); 224 | } 225 | } 226 | BackendRequest::FullBlock(number, sender) => { 227 | self.request_full_block(number, sender); 228 | } 229 | BackendRequest::Transaction(tx, sender) => { 230 | self.request_transaction(tx, sender); 231 | } 232 | BackendRequest::Storage(addr, idx, sender) => { 233 | // account is already stored in the cache 234 | let value = self 235 | .db 236 | .storage() 237 | .read() 238 | .get(&addr) 239 | .and_then(|acc| acc.get(&idx).copied()); 240 | if let Some(value) = value { 241 | let _ = sender.send(Ok(value)); 242 | } else { 243 | // account present but not storage -> fetch storage 244 | self.request_account_storage(addr, idx, sender); 245 | } 246 | } 247 | BackendRequest::SetPinnedBlock(block_id) => { 248 | self.block_id = Some(block_id); 249 | } 250 | BackendRequest::UpdateAddress(address_data) => { 251 | for (address, data) in address_data { 252 | self.db.accounts().write().insert(address, data); 253 | } 254 | } 255 | BackendRequest::UpdateStorage(storage_data) => { 256 | for (address, data) in storage_data { 257 | self.db.storage().write().insert(address, data); 258 | } 259 | } 260 | BackendRequest::UpdateBlockHash(block_hash_data) => { 261 | for (block, hash) in block_hash_data { 262 | self.db.block_hashes().write().insert(block, hash); 263 | } 264 | } 265 | BackendRequest::AnyRequest(fut) => { 266 | self.pending_requests.push(ProviderRequest::AnyRequest(fut)); 267 | } 268 | } 269 | } 270 | 271 | /// process a request for account's storage 272 | fn request_account_storage(&mut self, address: Address, idx: U256, listener: StorageSender) { 273 | match self.storage_requests.entry((address, idx)) { 274 | Entry::Occupied(mut entry) => { 275 | entry.get_mut().push(listener); 276 | } 277 | Entry::Vacant(entry) => { 278 | trace!(target: "backendhandler", %address, %idx, "preparing storage request"); 279 | entry.insert(vec![listener]); 280 | 281 | let mut use_provider = false; 282 | 283 | if let Some(file_db_factory) = &self.file_db_factory { 284 | let block_number = self.block_id.unwrap().as_u64().unwrap(); 285 | match file_db_factory.history_by_block_number(block_number) { 286 | Ok(state_provider) => { 287 | let fut = Box::pin(async move { 288 | let storage = state_provider 289 | .storage(address, idx.into()) 290 | .map_err(Into::into) 291 | .and_then(|res| Ok(res.unwrap_or(U256::ZERO))); 292 | (storage, address, idx) 293 | }); 294 | self.pending_requests.push(ProviderRequest::Storage(fut)); 295 | } 296 | Err(_) => { 297 | use_provider = true; 298 | } 299 | } 300 | } else { 301 | use_provider = true; 302 | } 303 | 304 | if use_provider { 305 | let provider = self.provider.clone(); 306 | let block_id = self.block_id.unwrap_or_default(); 307 | let fut = Box::pin(async move { 308 | let storage = provider 309 | .get_storage_at(address, idx) 310 | .block_id(block_id) 311 | .await 312 | .map_err(Into::into); 313 | (storage, address, idx) 314 | }); 315 | self.pending_requests.push(ProviderRequest::Storage(fut)); 316 | } 317 | } 318 | } 319 | } 320 | 321 | /// returns the future that fetches the account data 322 | fn get_account_req(&self, address: Address) -> ProviderRequest { 323 | trace!(target: "backendhandler", "preparing account request, address={:?}", address); 324 | 325 | if let Some(file_db_factory) = &self.file_db_factory { 326 | let block_number = self.block_id.unwrap().as_u64().unwrap(); 327 | match file_db_factory.history_by_block_number(block_number) { 328 | Ok(state_provider) => { 329 | let fut = Box::pin(async move { 330 | let balance = match state_provider 331 | .account_balance(&address) 332 | .map_err(Into::into) 333 | .map(|res| res.unwrap_or(U256::ZERO)) 334 | { 335 | Ok(b) => b, 336 | Err(e) => return (Err(e), address), 337 | }; 338 | let nonce = match state_provider 339 | .account_nonce(&address) 340 | .map_err(Into::into) 341 | .map(|res| res.unwrap_or(0)) 342 | { 343 | Ok(n) => n, 344 | Err(e) => return (Err(e), address), 345 | }; 346 | let code = match state_provider 347 | .account_code(&address) 348 | .map_err(Into::into) 349 | .map(|res| res.unwrap_or(RethBytecode::new_raw(Bytes::default()))) 350 | { 351 | Ok(c) => match c.0 { 352 | Bytecode::LegacyRaw(bytes) => bytes, 353 | Bytecode::LegacyAnalyzed(analyzed) => analyzed.bytecode().clone(), 354 | Bytecode::Eof(eof) => eof.raw().clone(), 355 | Bytecode::Eip7702(eip7702) => eip7702.raw().clone(), 356 | }, 357 | Err(e) => return (Err(e), address), 358 | }; 359 | 360 | (Ok((balance, nonce, code)), address) 361 | }); 362 | return ProviderRequest::Account(fut); 363 | } 364 | Err(_) => {} 365 | } 366 | } 367 | 368 | let provider = self.provider.clone(); 369 | let block_id = self.block_id.unwrap_or_default(); 370 | let fut = Box::pin(async move { 371 | let balance = provider 372 | .get_balance(address) 373 | .block_id(block_id) 374 | .into_future(); 375 | let nonce = provider 376 | .get_transaction_count(address) 377 | .block_id(block_id) 378 | .into_future(); 379 | let code = provider 380 | .get_code_at(address) 381 | .block_id(block_id) 382 | .into_future(); 383 | let resp = tokio::try_join!(balance, nonce, code).map_err(Into::into); 384 | (resp, address) 385 | }); 386 | ProviderRequest::Account(fut) 387 | } 388 | 389 | /// process a request for an account 390 | fn request_account(&mut self, address: Address, listener: AccountInfoSender) { 391 | match self.account_requests.entry(address) { 392 | Entry::Occupied(mut entry) => { 393 | entry.get_mut().push(listener); 394 | } 395 | Entry::Vacant(entry) => { 396 | entry.insert(vec![listener]); 397 | self.pending_requests.push(self.get_account_req(address)); 398 | } 399 | } 400 | } 401 | 402 | /// process a request for an entire block 403 | fn request_full_block(&mut self, number: BlockId, sender: FullBlockSender) { 404 | let provider = self.provider.clone(); 405 | let fut = Box::pin(async move { 406 | let block = provider 407 | .get_block(number, true.into()) 408 | .await 409 | .wrap_err("could not fetch block {number:?}"); 410 | (sender, block, number) 411 | }); 412 | 413 | self.pending_requests.push(ProviderRequest::FullBlock(fut)); 414 | } 415 | 416 | /// process a request for a transactions 417 | fn request_transaction(&mut self, tx: B256, sender: TransactionSender) { 418 | let provider = self.provider.clone(); 419 | let fut = Box::pin(async move { 420 | let block = provider 421 | .get_transaction_by_hash(tx) 422 | .await 423 | .wrap_err_with(|| format!("could not get transaction {tx}")) 424 | .and_then(|maybe| { 425 | maybe.ok_or_else(|| eyre::eyre!("could not get transaction {tx}")) 426 | }); 427 | (sender, block, tx) 428 | }); 429 | 430 | self.pending_requests 431 | .push(ProviderRequest::Transaction(fut)); 432 | } 433 | 434 | /// process a request for a block hash 435 | fn request_hash(&mut self, number: u64, listener: BlockHashSender) { 436 | match self.block_requests.entry(number) { 437 | Entry::Occupied(mut entry) => { 438 | entry.get_mut().push(listener); 439 | } 440 | Entry::Vacant(entry) => { 441 | trace!(target: "backendhandler", number, "preparing block hash request"); 442 | entry.insert(vec![listener]); 443 | let provider = self.provider.clone(); 444 | let fut = Box::pin(async move { 445 | let block = provider 446 | .get_block_by_number( 447 | number.into(), 448 | alloy_rpc_types::BlockTransactionsKind::Hashes, 449 | ) 450 | .await 451 | .wrap_err("failed to get block"); 452 | 453 | let block_hash = match block { 454 | Ok(Some(block)) => Ok(block.header.hash), 455 | Ok(None) => { 456 | warn!(target: "backendhandler", ?number, "block not found"); 457 | // if no block was returned then the block does not exist, in which case 458 | // we return empty hash 459 | Ok(KECCAK_EMPTY) 460 | } 461 | Err(err) => { 462 | error!(target: "backendhandler", %err, ?number, "failed to get block"); 463 | Err(err) 464 | } 465 | }; 466 | (block_hash, number) 467 | }); 468 | self.pending_requests.push(ProviderRequest::BlockHash(fut)); 469 | } 470 | } 471 | } 472 | } 473 | 474 | impl Future for BackendHandler 475 | where 476 | T: Transport + Clone + Unpin, 477 | P: Provider + Clone + Unpin + 'static, 478 | { 479 | type Output = (); 480 | 481 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 482 | let pin = self.get_mut(); 483 | loop { 484 | // Drain queued requests first. 485 | while let Some(req) = pin.queued_requests.pop_front() { 486 | pin.on_request(req) 487 | } 488 | 489 | // receive new requests to delegate to the underlying provider 490 | loop { 491 | match Pin::new(&mut pin.incoming).poll_next(cx) { 492 | Poll::Ready(Some(req)) => { 493 | pin.queued_requests.push_back(req); 494 | } 495 | Poll::Ready(None) => { 496 | trace!(target: "backendhandler", "last sender dropped, ready to drop (&flush cache)"); 497 | return Poll::Ready(()); 498 | } 499 | Poll::Pending => break, 500 | } 501 | } 502 | 503 | // poll all requests in progress 504 | for n in (0..pin.pending_requests.len()).rev() { 505 | let mut request = pin.pending_requests.swap_remove(n); 506 | match &mut request { 507 | ProviderRequest::Account(fut) => { 508 | if let Poll::Ready((resp, addr)) = fut.poll_unpin(cx) { 509 | // get the response 510 | let (balance, nonce, code) = match resp { 511 | Ok(res) => res, 512 | Err(err) => { 513 | let err = Arc::new(err); 514 | if let Some(listeners) = pin.account_requests.remove(&addr) { 515 | listeners.into_iter().for_each(|l| { 516 | let _ = l.send(Err(DatabaseError::GetAccount( 517 | addr, 518 | Arc::clone(&err), 519 | ))); 520 | }) 521 | } 522 | continue; 523 | } 524 | }; 525 | 526 | // convert it to revm-style types 527 | let (code, code_hash) = if !code.is_empty() { 528 | (code.clone(), keccak256(&code)) 529 | } else { 530 | (Bytes::default(), KECCAK_EMPTY) 531 | }; 532 | 533 | // update the cache 534 | let acc = AccountInfo { 535 | nonce, 536 | balance, 537 | code: Some(Bytecode::new_raw(code)), 538 | code_hash, 539 | }; 540 | pin.db.accounts().write().insert(addr, acc.clone()); 541 | 542 | // notify all listeners 543 | if let Some(listeners) = pin.account_requests.remove(&addr) { 544 | listeners.into_iter().for_each(|l| { 545 | let _ = l.send(Ok(acc.clone())); 546 | }) 547 | } 548 | continue; 549 | } 550 | } 551 | ProviderRequest::Storage(fut) => { 552 | if let Poll::Ready((resp, addr, idx)) = fut.poll_unpin(cx) { 553 | let value = match resp { 554 | Ok(value) => value, 555 | Err(err) => { 556 | // notify all listeners 557 | let err = Arc::new(err); 558 | if let Some(listeners) = 559 | pin.storage_requests.remove(&(addr, idx)) 560 | { 561 | listeners.into_iter().for_each(|l| { 562 | let _ = l.send(Err(DatabaseError::GetStorage( 563 | addr, 564 | idx, 565 | Arc::clone(&err), 566 | ))); 567 | }) 568 | } 569 | continue; 570 | } 571 | }; 572 | 573 | // update the cache 574 | pin.db 575 | .storage() 576 | .write() 577 | .entry(addr) 578 | .or_default() 579 | .insert(idx, value); 580 | 581 | // notify all listeners 582 | if let Some(listeners) = pin.storage_requests.remove(&(addr, idx)) { 583 | listeners.into_iter().for_each(|l| { 584 | let _ = l.send(Ok(value)); 585 | }) 586 | } 587 | continue; 588 | } 589 | } 590 | ProviderRequest::BlockHash(fut) => { 591 | if let Poll::Ready((block_hash, number)) = fut.poll_unpin(cx) { 592 | let value = match block_hash { 593 | Ok(value) => value, 594 | Err(err) => { 595 | let err = Arc::new(err); 596 | // notify all listeners 597 | if let Some(listeners) = pin.block_requests.remove(&number) { 598 | listeners.into_iter().for_each(|l| { 599 | let _ = l.send(Err(DatabaseError::GetBlockHash( 600 | number, 601 | Arc::clone(&err), 602 | ))); 603 | }) 604 | } 605 | continue; 606 | } 607 | }; 608 | 609 | // update the cache 610 | pin.db 611 | .block_hashes() 612 | .write() 613 | .insert(U256::from(number), value); 614 | 615 | // notify all listeners 616 | if let Some(listeners) = pin.block_requests.remove(&number) { 617 | listeners.into_iter().for_each(|l| { 618 | let _ = l.send(Ok(value)); 619 | }) 620 | } 621 | continue; 622 | } 623 | } 624 | ProviderRequest::FullBlock(fut) => { 625 | if let Poll::Ready((sender, resp, number)) = fut.poll_unpin(cx) { 626 | let msg = match resp { 627 | Ok(Some(block)) => Ok(block), 628 | Ok(None) => Err(DatabaseError::BlockNotFound(number)), 629 | Err(err) => { 630 | let err = Arc::new(err); 631 | Err(DatabaseError::GetFullBlock(number, err)) 632 | } 633 | }; 634 | let _ = sender.send(msg); 635 | continue; 636 | } 637 | } 638 | ProviderRequest::Transaction(fut) => { 639 | if let Poll::Ready((sender, tx, tx_hash)) = fut.poll_unpin(cx) { 640 | let msg = match tx { 641 | Ok(tx) => Ok(tx), 642 | Err(err) => { 643 | let err = Arc::new(err); 644 | Err(DatabaseError::GetTransaction(tx_hash, err)) 645 | } 646 | }; 647 | let _ = sender.send(msg); 648 | continue; 649 | } 650 | } 651 | ProviderRequest::AnyRequest(fut) => { 652 | if fut.poll_inner(cx).is_ready() { 653 | continue; 654 | } 655 | } 656 | } 657 | // not ready, insert and poll again 658 | pin.pending_requests.push(request); 659 | } 660 | 661 | // If no new requests have been queued, break to 662 | // be polled again later. 663 | if pin.queued_requests.is_empty() { 664 | return Poll::Pending; 665 | } 666 | } 667 | } 668 | } 669 | 670 | /// Mode for the `SharedBackend` how to block in the non-async [`DatabaseRef`] 671 | /// when interacting with [`BackendHandler`]. 672 | #[derive(Default, Clone, Debug, PartialEq)] 673 | pub enum BlockingMode { 674 | /// This mode use `tokio::task::block_in_place()` to block in place. 675 | /// 676 | /// This should be used when blocking on the call site is disallowed. 677 | #[default] 678 | BlockInPlace, 679 | /// The mode blocks the current task 680 | /// 681 | /// This can be used if blocking on the call site is allowed, e.g. on a 682 | /// tokio blocking task. 683 | Block, 684 | } 685 | 686 | impl BlockingMode { 687 | /// run process logic with the blocking mode 688 | pub fn run(&self, f: F) -> R 689 | where 690 | F: FnOnce() -> R, 691 | { 692 | match self { 693 | Self::BlockInPlace => tokio::task::block_in_place(f), 694 | Self::Block => f(), 695 | } 696 | } 697 | } 698 | 699 | /// A cloneable backend type that shares access to the backend data with all its 700 | /// clones. 701 | /// 702 | /// This backend type is connected to the `BackendHandler` via a mpsc unbounded 703 | /// channel. The `BackendHandler` is spawned on a tokio task and listens for 704 | /// incoming commands on the receiver half of the channel. A `SharedBackend` 705 | /// holds a sender for that channel, which is `Clone`, so there can be multiple 706 | /// `SharedBackend`s communicating with the same `BackendHandler`, hence this 707 | /// `Backend` type is thread safe. 708 | /// 709 | /// All `Backend` trait functions are delegated as a `BackendRequest` via the 710 | /// channel to the `BackendHandler`. All `BackendRequest` variants include a 711 | /// sender half of an additional channel that is used by the `BackendHandler` to 712 | /// send the result of an executed `BackendRequest` back to `SharedBackend`. 713 | /// 714 | /// The `BackendHandler` holds a `Provider` to look up missing accounts or 715 | /// storage slots from remote (e.g. infura). It detects duplicate requests from 716 | /// multiple `SharedBackend`s and bundles them together, so that always only one 717 | /// provider request is executed. For example, there are two `SharedBackend`s, 718 | /// `A` and `B`, both request the basic account info of account `0xasd9sa7d...` 719 | /// at the same time. After the `BackendHandler` receives the request from `A`, 720 | /// it sends a new provider request to the provider's endpoint, then it reads 721 | /// the identical request from `B` and simply adds it as an additional listener 722 | /// for the request already in progress, instead of sending another one. So that 723 | /// after the provider returns the response all listeners (`A` and `B`) get 724 | /// notified. 725 | // **Note**: the implementation makes use of [tokio::task::block_in_place()] when interacting with 726 | // the underlying [BackendHandler] which runs on a separate spawned tokio task. 727 | // [tokio::task::block_in_place()] 728 | // > Runs the provided blocking function on the current thread without blocking the executor. 729 | // This prevents issues (hangs) we ran into were the [SharedBackend] itself is called from a spawned 730 | // task. 731 | #[derive(Clone, Debug)] 732 | pub struct SharedBackend { 733 | /// channel used for sending commands related to database operations 734 | backend: UnboundedSender, 735 | /// Ensures that the underlying cache gets flushed once the last 736 | /// `SharedBackend` is dropped. 737 | /// 738 | /// There is only one instance of the type, so as soon as the last 739 | /// `SharedBackend` is deleted, `FlushJsonBlockCacheDB` is also deleted 740 | /// and the cache is flushed. 741 | cache: Arc, 742 | 743 | /// The mode for the `SharedBackend` to block in place or not 744 | blocking_mode: BlockingMode, 745 | } 746 | 747 | impl SharedBackend { 748 | /// _Spawns_ a new `BackendHandler` on a `tokio::task` that listens for 749 | /// requests from any `SharedBackend`. Missing values get inserted in 750 | /// the `db`. 751 | /// 752 | /// The spawned `BackendHandler` finishes once the last `SharedBackend` 753 | /// connected to it is dropped. 754 | /// 755 | /// NOTE: this should be called with `Arc` 756 | pub async fn spawn_backend( 757 | provider: P, 758 | file_db_factory: Option, 759 | db: BlockchainDb, 760 | pin_block: Option, 761 | ) -> Self 762 | where 763 | T: Transport + Clone + Unpin, 764 | P: Provider + Unpin + 'static + Clone, 765 | { 766 | let (shared, handler) = Self::new(provider, file_db_factory, db, pin_block); 767 | // spawn the provider handler to a task 768 | trace!(target: "backendhandler", "spawning Backendhandler task"); 769 | tokio::spawn(handler); 770 | shared 771 | } 772 | 773 | /// Same as `Self::spawn_backend` but spawns the `BackendHandler` on a 774 | /// separate `std::thread` in its own `tokio::Runtime` 775 | pub fn spawn_backend_thread( 776 | provider: P, 777 | file_db_factory: Option, 778 | db: BlockchainDb, 779 | pin_block: Option, 780 | ) -> Self 781 | where 782 | T: Transport + Clone + Unpin, 783 | P: Provider + Unpin + 'static + Clone, 784 | { 785 | let (shared, handler) = Self::new(provider, file_db_factory, db, pin_block); 786 | 787 | // spawn a light-weight thread with a thread-local async runtime just for 788 | // sending and receiving data from the remote client 789 | std::thread::Builder::new() 790 | .name("fork-backend".into()) 791 | .spawn(move || { 792 | let rt = tokio::runtime::Builder::new_current_thread() 793 | .enable_all() 794 | .build() 795 | .expect("failed to build tokio runtime"); 796 | 797 | rt.block_on(handler); 798 | }) 799 | .expect("failed to spawn thread"); 800 | trace!(target: "backendhandler", "spawned Backendhandler thread"); 801 | 802 | shared 803 | } 804 | 805 | /// Returns a new `SharedBackend` and the `BackendHandler` 806 | pub fn new( 807 | provider: P, 808 | file_db_factory: Option, 809 | db: BlockchainDb, 810 | pin_block: Option, 811 | ) -> (Self, BackendHandler) 812 | where 813 | T: Transport + Clone + Unpin, 814 | P: Provider + Unpin + 'static + Clone, 815 | { 816 | let (backend, backend_rx) = unbounded(); 817 | let cache = Arc::new(FlushJsonBlockCacheDB(Arc::clone(db.cache()))); 818 | let handler = BackendHandler::new(provider, file_db_factory, db, backend_rx, pin_block); 819 | (Self { backend, cache, blocking_mode: Default::default() }, handler) 820 | } 821 | 822 | /// Returns a new `SharedBackend` and the `BackendHandler` with a specific 823 | /// blocking mode 824 | pub fn with_blocking_mode(&self, mode: BlockingMode) -> Self { 825 | Self { backend: self.backend.clone(), cache: self.cache.clone(), blocking_mode: mode } 826 | } 827 | 828 | /// Updates the pinned block to fetch data from 829 | pub fn set_pinned_block(&self, block: impl Into) -> eyre::Result<()> { 830 | let req = BackendRequest::SetPinnedBlock(block.into()); 831 | self.backend 832 | .unbounded_send(req) 833 | .map_err(|e| eyre::eyre!("{:?}", e)) 834 | } 835 | 836 | /// Returns the full block for the given block identifier 837 | pub fn get_full_block(&self, block: impl Into) -> DatabaseResult { 838 | self.blocking_mode.run(|| { 839 | let (sender, rx) = oneshot_channel(); 840 | let req = BackendRequest::FullBlock(block.into(), sender); 841 | self.backend.unbounded_send(req)?; 842 | rx.recv()? 843 | }) 844 | } 845 | 846 | /// Returns the transaction for the hash 847 | pub fn get_transaction( 848 | &self, 849 | tx: B256, 850 | ) -> DatabaseResult>> { 851 | self.blocking_mode.run(|| { 852 | let (sender, rx) = oneshot_channel(); 853 | let req = BackendRequest::Transaction(tx, sender); 854 | self.backend.unbounded_send(req)?; 855 | rx.recv()? 856 | }) 857 | } 858 | 859 | fn do_get_basic(&self, address: Address) -> DatabaseResult> { 860 | self.blocking_mode.run(|| { 861 | let (sender, rx) = oneshot_channel(); 862 | let req = BackendRequest::Basic(address, sender); 863 | self.backend.unbounded_send(req)?; 864 | rx.recv()?.map(Some) 865 | }) 866 | } 867 | 868 | fn do_get_storage(&self, address: Address, index: U256) -> DatabaseResult { 869 | self.blocking_mode.run(|| { 870 | let (sender, rx) = oneshot_channel(); 871 | let req = BackendRequest::Storage(address, index, sender); 872 | self.backend.unbounded_send(req)?; 873 | rx.recv()? 874 | }) 875 | } 876 | 877 | fn do_get_block_hash(&self, number: u64) -> DatabaseResult { 878 | self.blocking_mode.run(|| { 879 | let (sender, rx) = oneshot_channel(); 880 | let req = BackendRequest::BlockHash(number, sender); 881 | self.backend.unbounded_send(req)?; 882 | rx.recv()? 883 | }) 884 | } 885 | 886 | /// Inserts or updates data for multiple addresses 887 | pub fn insert_or_update_address(&self, address_data: AddressData) { 888 | let req = BackendRequest::UpdateAddress(address_data); 889 | let err = self.backend.unbounded_send(req); 890 | match err { 891 | Ok(_) => (), 892 | Err(e) => { 893 | error!(target: "sharedbackend", "Failed to send update address request: {:?}", e) 894 | } 895 | } 896 | } 897 | 898 | /// Inserts or updates data for multiple storage slots 899 | pub fn insert_or_update_storage(&self, storage_data: StorageData) { 900 | let req = BackendRequest::UpdateStorage(storage_data); 901 | let err = self.backend.unbounded_send(req); 902 | match err { 903 | Ok(_) => (), 904 | Err(e) => { 905 | error!(target: "sharedbackend", "Failed to send update address request: {:?}", e) 906 | } 907 | } 908 | } 909 | 910 | /// Inserts or updates data for multiple block hashes 911 | pub fn insert_or_update_block_hashes(&self, block_hash_data: BlockHashData) { 912 | let req = BackendRequest::UpdateBlockHash(block_hash_data); 913 | let err = self.backend.unbounded_send(req); 914 | match err { 915 | Ok(_) => (), 916 | Err(e) => { 917 | error!(target: "sharedbackend", "Failed to send update address request: {:?}", e) 918 | } 919 | } 920 | } 921 | 922 | /// Returns any arbitrary request on the provider 923 | pub fn do_any_request(&mut self, fut: F) -> DatabaseResult 924 | where 925 | F: Future> + Send + 'static, 926 | T: fmt::Debug + Send + 'static, 927 | { 928 | self.blocking_mode.run(|| { 929 | let (sender, rx) = oneshot_channel::>(); 930 | let req = BackendRequest::AnyRequest(Box::new(AnyRequestFuture { 931 | sender, 932 | future: Box::pin(fut), 933 | })); 934 | self.backend.unbounded_send(req)?; 935 | rx.recv()? 936 | .map_err(|err| DatabaseError::AnyRequest(Arc::new(err))) 937 | }) 938 | } 939 | 940 | /// Flushes the DB to disk if caching is enabled 941 | pub fn flush_cache(&self) { 942 | self.cache.0.flush(); 943 | } 944 | 945 | /// Flushes the DB to a specific file 946 | pub fn flush_cache_to(&self, cache_path: &Path) { 947 | self.cache.0.flush_to(cache_path); 948 | } 949 | 950 | /// Returns the DB 951 | pub fn data(&self) -> Arc { 952 | self.cache.0.db().clone() 953 | } 954 | 955 | /// Returns the DB accounts 956 | pub fn accounts(&self) -> AddressData { 957 | self.cache.0.db().accounts.read().clone() 958 | } 959 | 960 | /// Returns the DB accounts length 961 | pub fn accounts_len(&self) -> usize { 962 | self.cache.0.db().accounts.read().len() 963 | } 964 | 965 | /// Returns the DB storage 966 | pub fn storage(&self) -> StorageData { 967 | self.cache.0.db().storage.read().clone() 968 | } 969 | 970 | /// Returns the DB storage length 971 | pub fn storage_len(&self) -> usize { 972 | self.cache.0.db().storage.read().len() 973 | } 974 | 975 | /// Returns the DB block_hashes 976 | pub fn block_hashes(&self) -> BlockHashData { 977 | self.cache.0.db().block_hashes.read().clone() 978 | } 979 | 980 | /// Returns the DB block_hashes length 981 | pub fn block_hashes_len(&self) -> usize { 982 | self.cache.0.db().block_hashes.read().len() 983 | } 984 | } 985 | 986 | impl DatabaseRef for SharedBackend { 987 | type Error = DatabaseError; 988 | 989 | fn basic_ref(&self, address: Address) -> Result, Self::Error> { 990 | trace!(target: "sharedbackend", %address, "request basic"); 991 | self.do_get_basic(address).map_err(|err| { 992 | error!(target: "sharedbackend", %err, %address, "Failed to send/recv `basic`"); 993 | if err.is_possibly_non_archive_node_error() { 994 | error!(target: "sharedbackend", "{NON_ARCHIVE_NODE_WARNING}"); 995 | } 996 | err 997 | }) 998 | } 999 | 1000 | fn code_by_hash_ref(&self, hash: B256) -> Result { 1001 | Err(DatabaseError::MissingCode(hash)) 1002 | } 1003 | 1004 | fn storage_ref(&self, address: Address, index: U256) -> Result { 1005 | trace!(target: "sharedbackend", "request storage {:?} at {:?}", address, index); 1006 | self.do_get_storage(address, index).map_err(|err| { 1007 | error!(target: "sharedbackend", %err, %address, %index, "Failed to send/recv `storage`"); 1008 | if err.is_possibly_non_archive_node_error() { 1009 | error!(target: "sharedbackend", "{NON_ARCHIVE_NODE_WARNING}"); 1010 | } 1011 | err 1012 | }) 1013 | } 1014 | 1015 | fn block_hash_ref(&self, number: u64) -> Result { 1016 | trace!(target: "sharedbackend", "request block hash for number {:?}", number); 1017 | self.do_get_block_hash(number).map_err(|err| { 1018 | error!(target: "sharedbackend", %err, %number, "Failed to send/recv `block_hash`"); 1019 | if err.is_possibly_non_archive_node_error() { 1020 | error!(target: "sharedbackend", "{NON_ARCHIVE_NODE_WARNING}"); 1021 | } 1022 | err 1023 | }) 1024 | } 1025 | } 1026 | -------------------------------------------------------------------------------- /crates/evm-fork-db/src/cache.rs: -------------------------------------------------------------------------------- 1 | //! Cache related abstraction 2 | use std::collections::BTreeSet; 3 | use std::fs; 4 | use std::io::{BufWriter, Write}; 5 | use std::path::{Path, PathBuf}; 6 | use std::sync::Arc; 7 | 8 | use alloy_consensus::BlockHeader; 9 | use alloy_primitives::{Address, B256, U256}; 10 | use alloy_provider::network::TransactionResponse; 11 | use parking_lot::RwLock; 12 | use revm::primitives::map::{AddressHashMap, HashMap}; 13 | use revm::primitives::{ 14 | Account, AccountInfo, AccountStatus, BlobExcessGasAndPrice, BlockEnv, CfgEnv, KECCAK_EMPTY, 15 | }; 16 | use revm::DatabaseCommit; 17 | use serde::ser::SerializeMap; 18 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 19 | use url::Url; 20 | 21 | pub type StorageInfo = HashMap; 22 | 23 | /// A shareable Block database 24 | #[derive(Clone, Debug)] 25 | pub struct BlockchainDb { 26 | /// Contains all the data 27 | db: Arc, 28 | /// metadata of the current config 29 | meta: Arc>, 30 | /// the cache that can be flushed 31 | cache: Arc, 32 | } 33 | 34 | impl BlockchainDb { 35 | /// Creates a new instance of the [BlockchainDb]. 36 | /// 37 | /// If a `cache_path` is provided it attempts to load a previously stored 38 | /// [JsonBlockCacheData] and will try to use the cached entries it 39 | /// holds. 40 | /// 41 | /// This will return a new and empty [MemDb] if 42 | /// - `cache_path` is `None` 43 | /// - the file the `cache_path` points to, does not exist 44 | /// - the file contains malformed data, or if it couldn't be read 45 | /// - the provided `meta` differs from [BlockchainDbMeta] that's stored on 46 | /// disk 47 | pub fn new(meta: BlockchainDbMeta, cache_path: Option) -> Self { 48 | Self::new_db(meta, cache_path, false) 49 | } 50 | 51 | /// Creates a new instance of the [BlockchainDb] and skips check when 52 | /// comparing meta This is useful for offline-start mode when we don't 53 | /// want to fetch metadata of `block`. 54 | /// 55 | /// if a `cache_path` is provided it attempts to load a previously stored 56 | /// [JsonBlockCacheData] and will try to use the cached entries it 57 | /// holds. 58 | /// 59 | /// This will return a new and empty [MemDb] if 60 | /// - `cache_path` is `None` 61 | /// - the file the `cache_path` points to, does not exist 62 | /// - the file contains malformed data, or if it couldn't be read 63 | /// - the provided `meta` differs from [BlockchainDbMeta] that's stored on 64 | /// disk 65 | pub fn new_skip_check(meta: BlockchainDbMeta, cache_path: Option) -> Self { 66 | Self::new_db(meta, cache_path, true) 67 | } 68 | 69 | fn new_db(meta: BlockchainDbMeta, cache_path: Option, skip_check: bool) -> Self { 70 | trace!(target: "forge::cache", cache=?cache_path, "initialising blockchain db"); 71 | // read cache and check if metadata matches 72 | let cache = cache_path 73 | .as_ref() 74 | .and_then(|p| { 75 | JsonBlockCacheDB::load(p).ok().filter(|cache| { 76 | if skip_check { 77 | return true; 78 | } 79 | let mut existing = cache.meta().write(); 80 | existing.hosts.extend(meta.hosts.clone()); 81 | if meta != *existing { 82 | warn!(target: "cache", "non-matching block metadata"); 83 | false 84 | } else { 85 | true 86 | } 87 | }) 88 | }) 89 | .unwrap_or_else(|| JsonBlockCacheDB::new(Arc::new(RwLock::new(meta)), cache_path)); 90 | 91 | Self { db: Arc::clone(cache.db()), meta: Arc::clone(cache.meta()), cache: Arc::new(cache) } 92 | } 93 | 94 | /// Returns the map that holds the account related info 95 | pub fn accounts(&self) -> &RwLock> { 96 | &self.db.accounts 97 | } 98 | 99 | /// Returns the map that holds the storage related info 100 | pub fn storage(&self) -> &RwLock> { 101 | &self.db.storage 102 | } 103 | 104 | /// Returns the map that holds all the block hashes 105 | pub fn block_hashes(&self) -> &RwLock> { 106 | &self.db.block_hashes 107 | } 108 | 109 | /// Returns the Env related metadata 110 | pub const fn meta(&self) -> &Arc> { 111 | &self.meta 112 | } 113 | 114 | /// Returns the inner cache 115 | pub const fn cache(&self) -> &Arc { 116 | &self.cache 117 | } 118 | 119 | /// Returns the underlying storage 120 | pub const fn db(&self) -> &Arc { 121 | &self.db 122 | } 123 | } 124 | 125 | /// relevant identifying markers in the context of [BlockchainDb] 126 | #[derive(Clone, Debug, Eq, Serialize, Default)] 127 | pub struct BlockchainDbMeta { 128 | pub cfg_env: CfgEnv, 129 | pub block_env: BlockEnv, 130 | /// all the hosts used to connect to 131 | pub hosts: BTreeSet, 132 | } 133 | 134 | impl BlockchainDbMeta { 135 | /// Creates a new instance 136 | pub fn new(env: revm::primitives::Env, url: String) -> Self { 137 | let host = Url::parse(&url) 138 | .ok() 139 | .and_then(|url| url.host().map(|host| host.to_string())) 140 | .unwrap_or(url); 141 | 142 | Self { cfg_env: env.cfg.clone(), block_env: env.block, hosts: BTreeSet::from([host]) } 143 | } 144 | 145 | /// Sets the chain_id in the [CfgEnv] of this instance. 146 | /// 147 | /// Remaining fields of [CfgEnv] are left unchanged. 148 | pub const fn with_chain_id(mut self, chain_id: u64) -> Self { 149 | self.cfg_env.chain_id = chain_id; 150 | self 151 | } 152 | 153 | /// Sets the [BlockEnv] of this instance using the provided 154 | /// [alloy_rpc_types::Block] 155 | pub fn with_block( 156 | mut self, 157 | block: &alloy_rpc_types::Block, 158 | ) -> Self { 159 | self.block_env = BlockEnv { 160 | number: U256::from(block.header.number()), 161 | coinbase: block.header.beneficiary(), 162 | timestamp: U256::from(block.header.timestamp()), 163 | difficulty: U256::from(block.header.difficulty()), 164 | basefee: block 165 | .header 166 | .base_fee_per_gas() 167 | .map(U256::from) 168 | .unwrap_or_default(), 169 | gas_limit: U256::from(block.header.gas_limit()), 170 | prevrandao: block.header.mix_hash(), 171 | blob_excess_gas_and_price: Some(BlobExcessGasAndPrice::new( 172 | block.header.excess_blob_gas().unwrap_or_default(), 173 | )), 174 | }; 175 | 176 | self 177 | } 178 | 179 | /// Infers the host from the provided url and adds it to the set of hosts 180 | pub fn with_url(mut self, url: &str) -> Self { 181 | let host = Url::parse(url) 182 | .ok() 183 | .and_then(|url| url.host().map(|host| host.to_string())) 184 | .unwrap_or(url.to_string()); 185 | self.hosts.insert(host); 186 | self 187 | } 188 | 189 | /// Sets [CfgEnv] of this instance 190 | pub fn set_cfg_env(mut self, cfg_env: revm::primitives::CfgEnv) { 191 | self.cfg_env = cfg_env; 192 | } 193 | 194 | /// Sets the [BlockEnv] of this instance 195 | pub fn set_block_env(mut self, block_env: revm::primitives::BlockEnv) { 196 | self.block_env = block_env; 197 | } 198 | } 199 | 200 | // ignore hosts to not invalidate the cache when different endpoints are used, 201 | // as it's commonly the case for http vs ws endpoints 202 | impl PartialEq for BlockchainDbMeta { 203 | fn eq(&self, other: &Self) -> bool { 204 | self.cfg_env == other.cfg_env && self.block_env == other.block_env 205 | } 206 | } 207 | 208 | impl<'de> Deserialize<'de> for BlockchainDbMeta { 209 | fn deserialize(deserializer: D) -> Result 210 | where 211 | D: Deserializer<'de>, 212 | { 213 | /// A backwards compatible representation of [revm::primitives::CfgEnv] 214 | /// 215 | /// This prevents deserialization errors of cache files caused by 216 | /// breaking changes to the default [revm::primitives::CfgEnv], 217 | /// for example enabling an optional feature. By hand rolling 218 | /// deserialize impl we can prevent cache file issues 219 | struct CfgEnvBackwardsCompat { 220 | inner: revm::primitives::CfgEnv, 221 | } 222 | 223 | impl<'de> Deserialize<'de> for CfgEnvBackwardsCompat { 224 | fn deserialize(deserializer: D) -> Result 225 | where 226 | D: Deserializer<'de>, 227 | { 228 | let mut value = serde_json::Value::deserialize(deserializer)?; 229 | 230 | // we check for breaking changes here 231 | if let Some(obj) = value.as_object_mut() { 232 | let default_value = 233 | serde_json::to_value(revm::primitives::CfgEnv::default()).unwrap(); 234 | for (key, value) in default_value.as_object().unwrap() { 235 | if !obj.contains_key(key) { 236 | obj.insert(key.to_string(), value.clone()); 237 | } 238 | } 239 | } 240 | 241 | let cfg_env: revm::primitives::CfgEnv = 242 | serde_json::from_value(value).map_err(serde::de::Error::custom)?; 243 | Ok(Self { inner: cfg_env }) 244 | } 245 | } 246 | 247 | /// A backwards compatible representation of 248 | /// [revm::primitives::BlockEnv] 249 | /// 250 | /// This prevents deserialization errors of cache files caused by 251 | /// breaking changes to the 252 | /// default [revm::primitives::BlockEnv], for example enabling an 253 | /// optional feature. By hand rolling deserialize impl we can 254 | /// prevent cache file issues 255 | struct BlockEnvBackwardsCompat { 256 | inner: revm::primitives::BlockEnv, 257 | } 258 | 259 | impl<'de> Deserialize<'de> for BlockEnvBackwardsCompat { 260 | fn deserialize(deserializer: D) -> Result 261 | where 262 | D: Deserializer<'de>, 263 | { 264 | let mut value = serde_json::Value::deserialize(deserializer)?; 265 | 266 | // we check for any missing fields here 267 | if let Some(obj) = value.as_object_mut() { 268 | let default_value = 269 | serde_json::to_value(revm::primitives::BlockEnv::default()).unwrap(); 270 | for (key, value) in default_value.as_object().unwrap() { 271 | if !obj.contains_key(key) { 272 | obj.insert(key.to_string(), value.clone()); 273 | } 274 | } 275 | } 276 | 277 | let cfg_env: revm::primitives::BlockEnv = 278 | serde_json::from_value(value).map_err(serde::de::Error::custom)?; 279 | Ok(Self { inner: cfg_env }) 280 | } 281 | } 282 | 283 | // custom deserialize impl to not break existing cache files 284 | #[derive(Deserialize)] 285 | struct Meta { 286 | cfg_env: CfgEnvBackwardsCompat, 287 | block_env: BlockEnvBackwardsCompat, 288 | /// all the hosts used to connect to 289 | #[serde(alias = "host")] 290 | hosts: Hosts, 291 | } 292 | 293 | #[derive(Deserialize)] 294 | #[serde(untagged)] 295 | enum Hosts { 296 | Multi(BTreeSet), 297 | Single(String), 298 | } 299 | 300 | let Meta { cfg_env, block_env, hosts } = Meta::deserialize(deserializer)?; 301 | Ok(Self { 302 | cfg_env: cfg_env.inner, 303 | block_env: block_env.inner, 304 | hosts: match hosts { 305 | Hosts::Multi(hosts) => hosts, 306 | Hosts::Single(host) => BTreeSet::from([host]), 307 | }, 308 | }) 309 | } 310 | } 311 | 312 | /// In Memory cache containing all fetched accounts and storage slots 313 | /// and their values from RPC 314 | #[derive(Debug, Default)] 315 | pub struct MemDb { 316 | /// Account related data 317 | pub accounts: RwLock>, 318 | /// Storage related data 319 | pub storage: RwLock>, 320 | /// All retrieved block hashes 321 | pub block_hashes: RwLock>, 322 | } 323 | 324 | impl MemDb { 325 | /// Clears all data stored in this db 326 | pub fn clear(&self) { 327 | self.accounts.write().clear(); 328 | self.storage.write().clear(); 329 | self.block_hashes.write().clear(); 330 | } 331 | 332 | // Inserts the account, replacing it if it exists already 333 | pub fn do_insert_account(&self, address: Address, account: AccountInfo) { 334 | self.accounts.write().insert(address, account); 335 | } 336 | 337 | /// The implementation of [DatabaseCommit::commit()] 338 | pub fn do_commit(&self, changes: HashMap) { 339 | let mut storage = self.storage.write(); 340 | let mut accounts = self.accounts.write(); 341 | for (add, mut acc) in changes { 342 | if acc.is_empty() || acc.is_selfdestructed() { 343 | accounts.remove(&add); 344 | storage.remove(&add); 345 | } else { 346 | // insert account 347 | if let Some(code_hash) = acc 348 | .info 349 | .code 350 | .as_ref() 351 | .filter(|code| !code.is_empty()) 352 | .map(|code| code.hash_slow()) 353 | { 354 | acc.info.code_hash = code_hash; 355 | } else if acc.info.code_hash.is_zero() { 356 | acc.info.code_hash = KECCAK_EMPTY; 357 | } 358 | accounts.insert(add, acc.info); 359 | 360 | let acc_storage = storage.entry(add).or_default(); 361 | if acc.status.contains(AccountStatus::Created) { 362 | acc_storage.clear(); 363 | } 364 | for (index, value) in acc.storage { 365 | if value.present_value().is_zero() { 366 | acc_storage.remove(&index); 367 | } else { 368 | acc_storage.insert(index, value.present_value()); 369 | } 370 | } 371 | if acc_storage.is_empty() { 372 | storage.remove(&add); 373 | } 374 | } 375 | } 376 | } 377 | } 378 | 379 | impl Clone for MemDb { 380 | fn clone(&self) -> Self { 381 | Self { 382 | storage: RwLock::new(self.storage.read().clone()), 383 | accounts: RwLock::new(self.accounts.read().clone()), 384 | block_hashes: RwLock::new(self.block_hashes.read().clone()), 385 | } 386 | } 387 | } 388 | 389 | impl DatabaseCommit for MemDb { 390 | fn commit(&mut self, changes: HashMap) { 391 | self.do_commit(changes) 392 | } 393 | } 394 | 395 | /// A DB that stores the cached content in a json file 396 | #[derive(Debug)] 397 | pub struct JsonBlockCacheDB { 398 | /// Where this cache file is stored. 399 | /// 400 | /// If this is a [None] then caching is disabled 401 | cache_path: Option, 402 | /// Object that's stored in a json file 403 | data: JsonBlockCacheData, 404 | } 405 | 406 | impl JsonBlockCacheDB { 407 | /// Creates a new instance. 408 | fn new(meta: Arc>, cache_path: Option) -> Self { 409 | Self { cache_path, data: JsonBlockCacheData { meta, data: Arc::new(Default::default()) } } 410 | } 411 | 412 | /// Loads the contents of the diskmap file and returns the read object 413 | /// 414 | /// # Errors 415 | /// This will fail if 416 | /// - the `path` does not exist 417 | /// - the format does not match [JsonBlockCacheData] 418 | pub fn load(path: impl Into) -> eyre::Result { 419 | let path = path.into(); 420 | trace!(target: "cache", ?path, "reading json cache"); 421 | let contents = std::fs::read_to_string(&path).map_err(|err| { 422 | warn!(?err, ?path, "Failed to read cache file"); 423 | err 424 | })?; 425 | let data = serde_json::from_str(&contents).map_err(|err| { 426 | warn!(target: "cache", ?err, ?path, "Failed to deserialize cache data"); 427 | err 428 | })?; 429 | Ok(Self { cache_path: Some(path), data }) 430 | } 431 | 432 | /// Returns the [MemDb] it holds access to 433 | pub const fn db(&self) -> &Arc { 434 | &self.data.data 435 | } 436 | 437 | /// Metadata stored alongside the data 438 | pub const fn meta(&self) -> &Arc> { 439 | &self.data.meta 440 | } 441 | 442 | /// Returns `true` if this is a transient cache and nothing will be flushed 443 | pub const fn is_transient(&self) -> bool { 444 | self.cache_path.is_none() 445 | } 446 | 447 | /// Flushes the DB to disk if caching is enabled. 448 | #[instrument(level = "warn", skip_all, fields(path = ?self.cache_path))] 449 | pub fn flush(&self) { 450 | let Some(path) = &self.cache_path else { return }; 451 | self.flush_to(path.as_path()); 452 | } 453 | 454 | /// Flushes the DB to a specific file 455 | pub fn flush_to(&self, cache_path: &Path) { 456 | let path: &Path = cache_path; 457 | 458 | trace!(target: "cache", "saving json cache"); 459 | 460 | if let Some(parent) = path.parent() { 461 | let _ = fs::create_dir_all(parent); 462 | } 463 | 464 | let file = match fs::File::create(path) { 465 | Ok(file) => file, 466 | Err(e) => return warn!(target: "cache", %e, "Failed to open json cache for writing"), 467 | }; 468 | 469 | let mut writer = BufWriter::new(file); 470 | if let Err(e) = serde_json::to_writer(&mut writer, &self.data) { 471 | return warn!(target: "cache", %e, "Failed to write to json cache"); 472 | } 473 | if let Err(e) = writer.flush() { 474 | return warn!(target: "cache", %e, "Failed to flush to json cache"); 475 | } 476 | 477 | trace!(target: "cache", "saved json cache"); 478 | } 479 | } 480 | 481 | /// The Data the [JsonBlockCacheDB] can read and flush 482 | /// 483 | /// This will be deserialized in a JSON object with the keys: 484 | /// `["meta", "accounts", "storage", "block_hashes"]` 485 | #[derive(Debug)] 486 | pub struct JsonBlockCacheData { 487 | pub meta: Arc>, 488 | pub data: Arc, 489 | } 490 | 491 | impl Serialize for JsonBlockCacheData { 492 | fn serialize(&self, serializer: S) -> Result 493 | where 494 | S: Serializer, 495 | { 496 | let mut map = serializer.serialize_map(Some(4))?; 497 | 498 | map.serialize_entry("meta", &*self.meta.read())?; 499 | map.serialize_entry("accounts", &*self.data.accounts.read())?; 500 | map.serialize_entry("storage", &*self.data.storage.read())?; 501 | map.serialize_entry("block_hashes", &*self.data.block_hashes.read())?; 502 | 503 | map.end() 504 | } 505 | } 506 | 507 | impl<'de> Deserialize<'de> for JsonBlockCacheData { 508 | fn deserialize(deserializer: D) -> Result 509 | where 510 | D: Deserializer<'de>, 511 | { 512 | #[derive(Deserialize)] 513 | struct Data { 514 | meta: BlockchainDbMeta, 515 | accounts: AddressHashMap, 516 | storage: AddressHashMap>, 517 | block_hashes: HashMap, 518 | } 519 | 520 | let Data { meta, accounts, storage, block_hashes } = Data::deserialize(deserializer)?; 521 | 522 | Ok(Self { 523 | meta: Arc::new(RwLock::new(meta)), 524 | data: Arc::new(MemDb { 525 | accounts: RwLock::new(accounts), 526 | storage: RwLock::new(storage), 527 | block_hashes: RwLock::new(block_hashes), 528 | }), 529 | }) 530 | } 531 | } 532 | 533 | /// A type that flushes a `JsonBlockCacheDB` on drop 534 | /// 535 | /// This type intentionally does not implement `Clone` since it's intended that 536 | /// there's only once instance that will flush the cache. 537 | #[derive(Debug)] 538 | pub struct FlushJsonBlockCacheDB(pub Arc); 539 | 540 | impl Drop for FlushJsonBlockCacheDB { 541 | fn drop(&mut self) { 542 | trace!(target: "fork::cache", "flushing cache"); 543 | self.0.flush(); 544 | trace!(target: "fork::cache", "flushed cache"); 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /crates/evm-fork-db/src/database.rs: -------------------------------------------------------------------------------- 1 | //! A revm database that forks off a remote client 2 | 3 | use std::sync::Arc; 4 | 5 | use alloy_primitives::map::HashMap; 6 | use alloy_primitives::{Address, B256, U256}; 7 | use alloy_rpc_types::BlockId; 8 | use foundry_evm::backend::{RevertStateSnapshotAction, StateSnapshot}; 9 | use foundry_evm_core::state_snapshot::StateSnapshots; 10 | use parking_lot::Mutex; 11 | use revm::db::{CacheDB, DatabaseRef}; 12 | use revm::primitives::{Account, AccountInfo, Bytecode}; 13 | use revm::{Database, DatabaseCommit}; 14 | use tracing::{trace, warn}; 15 | 16 | use crate::backend::SharedBackend; 17 | use crate::cache::BlockchainDb; 18 | use crate::error::DatabaseError; 19 | 20 | /// a [revm::Database] that's forked off another client 21 | /// 22 | /// The `backend` is used to retrieve (missing) data, which is then fetched from 23 | /// the remote endpoint. The inner in-memory database holds this storage and 24 | /// will be used for write operations. This database uses the `backend` for read 25 | /// and the `db` for write operations. But note the `backend` will also write 26 | /// (missing) data to the `db` in the background 27 | #[derive(Clone, Debug)] 28 | pub struct ForkedDatabase { 29 | /// Responsible for fetching missing data. 30 | /// 31 | /// This is responsible for getting data. 32 | backend: SharedBackend, 33 | /// Cached Database layer, ensures that changes are not written to the 34 | /// database that exclusively stores the state of the remote client. 35 | /// 36 | /// This separates Read/Write operations 37 | /// - reads from the `SharedBackend as DatabaseRef` writes to the internal 38 | /// cache storage. 39 | cache_db: CacheDB, 40 | /// Contains all the data already fetched. 41 | /// 42 | /// This exclusively stores the _unchanged_ remote client state. 43 | db: BlockchainDb, 44 | /// Holds the state snapshots of a blockchain. 45 | state_snapshots: Arc>>, 46 | } 47 | 48 | impl ForkedDatabase { 49 | /// Creates a new instance of this DB 50 | pub fn new(backend: SharedBackend, db: BlockchainDb) -> Self { 51 | Self { 52 | cache_db: CacheDB::new(backend.clone()), 53 | backend, 54 | db, 55 | state_snapshots: Arc::new(Mutex::new(Default::default())), 56 | } 57 | } 58 | 59 | pub fn database(&self) -> &CacheDB { 60 | &self.cache_db 61 | } 62 | 63 | pub fn database_mut(&mut self) -> &mut CacheDB { 64 | &mut self.cache_db 65 | } 66 | 67 | pub fn state_snapshots(&self) -> &Arc>> { 68 | &self.state_snapshots 69 | } 70 | 71 | /// Reset the fork to a fresh forked state, and optionally update the fork 72 | /// config 73 | pub fn reset( 74 | &mut self, 75 | _url: Option, 76 | block_number: impl Into, 77 | ) -> Result<(), String> { 78 | self.backend 79 | .set_pinned_block(block_number) 80 | .map_err(|err| err.to_string())?; 81 | 82 | // TODO need to find a way to update generic provider via url 83 | 84 | // wipe the storage retrieved from remote 85 | self.inner().db().clear(); 86 | // create a fresh `CacheDB`, effectively wiping modified state 87 | self.cache_db = CacheDB::new(self.backend.clone()); 88 | trace!(target: "backend::forkdb", "Cleared database"); 89 | Ok(()) 90 | } 91 | 92 | /// Flushes the cache to disk if configured 93 | pub fn flush_cache(&self) { 94 | self.db.cache().flush() 95 | } 96 | 97 | /// Returns the database that holds the remote state 98 | pub fn inner(&self) -> &BlockchainDb { 99 | &self.db 100 | } 101 | 102 | pub fn create_state_snapshot(&self) -> ForkDbStateSnapshot { 103 | let db = self.db.db(); 104 | let state_snapshot = StateSnapshot { 105 | accounts: db.accounts.read().clone(), 106 | storage: db.storage.read().clone(), 107 | block_hashes: db.block_hashes.read().clone(), 108 | }; 109 | ForkDbStateSnapshot { local: self.cache_db.clone(), state_snapshot } 110 | } 111 | 112 | pub fn insert_state_snapshot(&self) -> U256 { 113 | let state_snapshot = self.create_state_snapshot(); 114 | let mut state_snapshots = self.state_snapshots().lock(); 115 | let id = state_snapshots.insert(state_snapshot); 116 | trace!(target: "backend::forkdb", "Created new snapshot {}", id); 117 | id 118 | } 119 | 120 | /// Removes the snapshot from the tracked snapshot and sets it as the 121 | /// current state 122 | pub fn revert_state_snapshot(&mut self, id: U256, action: RevertStateSnapshotAction) -> bool { 123 | let state_snapshot = { self.state_snapshots().lock().remove_at(id) }; 124 | if let Some(state_snapshot) = state_snapshot { 125 | if action.is_keep() { 126 | self.state_snapshots() 127 | .lock() 128 | .insert_at(state_snapshot.clone(), id); 129 | } 130 | let ForkDbStateSnapshot { 131 | local, 132 | state_snapshot: StateSnapshot { accounts, storage, block_hashes }, 133 | } = state_snapshot; 134 | let db = self.inner().db(); 135 | { 136 | let mut accounts_lock = db.accounts.write(); 137 | accounts_lock.clear(); 138 | accounts_lock.extend(accounts); 139 | } 140 | { 141 | let mut storage_lock = db.storage.write(); 142 | storage_lock.clear(); 143 | storage_lock.extend(storage); 144 | } 145 | { 146 | let mut block_hashes_lock = db.block_hashes.write(); 147 | block_hashes_lock.clear(); 148 | block_hashes_lock.extend(block_hashes); 149 | } 150 | 151 | self.cache_db = local; 152 | 153 | trace!(target: "backend::forkdb", "Reverted snapshot {}", id); 154 | true 155 | } else { 156 | warn!(target: "backend::forkdb", "No snapshot to revert for {}", id); 157 | false 158 | } 159 | } 160 | } 161 | 162 | impl Database for ForkedDatabase { 163 | type Error = DatabaseError; 164 | 165 | fn basic(&mut self, address: Address) -> Result, Self::Error> { 166 | // Note: this will always return Some, since the `SharedBackend` will always 167 | // load the account, this differs from `::basic`, 168 | // See also [MemDb::ensure_loaded](crate::backend::MemDb::ensure_loaded) 169 | Database::basic(&mut self.cache_db, address) 170 | } 171 | 172 | fn code_by_hash(&mut self, code_hash: B256) -> Result { 173 | Database::code_by_hash(&mut self.cache_db, code_hash) 174 | } 175 | 176 | fn storage(&mut self, address: Address, index: U256) -> Result { 177 | Database::storage(&mut self.cache_db, address, index) 178 | } 179 | 180 | fn block_hash(&mut self, number: u64) -> Result { 181 | Database::block_hash(&mut self.cache_db, number) 182 | } 183 | } 184 | 185 | impl DatabaseRef for ForkedDatabase { 186 | type Error = DatabaseError; 187 | 188 | fn basic_ref(&self, address: Address) -> Result, Self::Error> { 189 | self.cache_db.basic_ref(address) 190 | } 191 | 192 | fn code_by_hash_ref(&self, code_hash: B256) -> Result { 193 | self.cache_db.code_by_hash_ref(code_hash) 194 | } 195 | 196 | fn storage_ref(&self, address: Address, index: U256) -> Result { 197 | DatabaseRef::storage_ref(&self.cache_db, address, index) 198 | } 199 | 200 | fn block_hash_ref(&self, number: u64) -> Result { 201 | self.cache_db.block_hash_ref(number) 202 | } 203 | } 204 | 205 | impl DatabaseCommit for ForkedDatabase { 206 | fn commit(&mut self, changes: HashMap) { 207 | self.database_mut().commit(changes) 208 | } 209 | } 210 | 211 | /// Represents a snapshot of the database 212 | /// 213 | /// This mimics `revm::CacheDB` 214 | #[derive(Clone, Debug)] 215 | pub struct ForkDbStateSnapshot { 216 | pub local: CacheDB, 217 | pub state_snapshot: StateSnapshot, 218 | } 219 | 220 | impl ForkDbStateSnapshot { 221 | fn get_storage(&self, address: Address, index: U256) -> Option { 222 | self.local 223 | .accounts 224 | .get(&address) 225 | .and_then(|account| account.storage.get(&index)) 226 | .copied() 227 | } 228 | } 229 | 230 | // This `DatabaseRef` implementation works similar to `CacheDB` which 231 | // prioritizes modified elements, and uses another db as fallback 232 | // We prioritize stored changed accounts/storage 233 | impl DatabaseRef for ForkDbStateSnapshot { 234 | type Error = DatabaseError; 235 | 236 | fn basic_ref(&self, address: Address) -> Result, Self::Error> { 237 | match self.local.accounts.get(&address) { 238 | Some(account) => Ok(Some(account.info.clone())), 239 | None => { 240 | let mut acc = self.state_snapshot.accounts.get(&address).cloned(); 241 | 242 | if acc.is_none() { 243 | acc = self.local.basic_ref(address)?; 244 | } 245 | Ok(acc) 246 | } 247 | } 248 | } 249 | 250 | fn code_by_hash_ref(&self, code_hash: B256) -> Result { 251 | self.local.code_by_hash_ref(code_hash) 252 | } 253 | 254 | fn storage_ref(&self, address: Address, index: U256) -> Result { 255 | match self.local.accounts.get(&address) { 256 | Some(account) => match account.storage.get(&index) { 257 | Some(entry) => Ok(*entry), 258 | None => match self.get_storage(address, index) { 259 | None => DatabaseRef::storage_ref(&self.local, address, index), 260 | Some(storage) => Ok(storage), 261 | }, 262 | }, 263 | None => match self.get_storage(address, index) { 264 | None => DatabaseRef::storage_ref(&self.local, address, index), 265 | Some(storage) => Ok(storage), 266 | }, 267 | } 268 | } 269 | 270 | fn block_hash_ref(&self, number: u64) -> Result { 271 | match self 272 | .state_snapshot 273 | .block_hashes 274 | .get(&U256::from(number)) 275 | .copied() 276 | { 277 | None => self.local.block_hash_ref(number), 278 | Some(block_hash) => Ok(block_hash), 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /crates/evm-fork-db/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::sync::mpsc::RecvError; 3 | use std::sync::Arc; 4 | 5 | use alloy_primitives::{Address, B256, U256}; 6 | use alloy_rpc_types::BlockId; 7 | use futures::channel::mpsc::{SendError, TrySendError}; 8 | 9 | /// Result alias with `DatabaseError` as error 10 | pub type DatabaseResult = Result; 11 | 12 | /// Errors that can happen when working with [`revm::Database`] 13 | #[derive(Debug, thiserror::Error)] 14 | #[allow(missing_docs)] 15 | pub enum DatabaseError { 16 | #[error("missing bytecode for code hash {0}")] 17 | MissingCode(B256), 18 | #[error(transparent)] 19 | Recv(#[from] RecvError), 20 | #[error(transparent)] 21 | Send(#[from] SendError), 22 | #[error("failed to get account for {0}: {1}")] 23 | GetAccount(Address, Arc), 24 | #[error("failed to get storage for {0} at {1}: {2}")] 25 | GetStorage(Address, U256, Arc), 26 | #[error("failed to get block hash for {0}: {1}")] 27 | GetBlockHash(u64, Arc), 28 | #[error("failed to get full block for {0:?}: {1}")] 29 | GetFullBlock(BlockId, Arc), 30 | #[error("block {0:?} does not exist")] 31 | BlockNotFound(BlockId), 32 | #[error("failed to get transaction {0}: {1}")] 33 | GetTransaction(B256, Arc), 34 | #[error("failed to process AnyRequest: {0}")] 35 | AnyRequest(Arc), 36 | } 37 | 38 | impl DatabaseError { 39 | fn get_rpc_error(&self) -> Option<&eyre::Error> { 40 | match self { 41 | Self::GetAccount(_, err) => Some(err), 42 | Self::GetStorage(_, _, err) => Some(err), 43 | Self::GetBlockHash(_, err) => Some(err), 44 | Self::GetFullBlock(_, err) => Some(err), 45 | Self::GetTransaction(_, err) => Some(err), 46 | Self::AnyRequest(err) => Some(err), 47 | // Enumerate explicitly to make sure errors are updated if a new one is added. 48 | Self::MissingCode(_) | Self::Recv(_) | Self::Send(_) | Self::BlockNotFound(_) => None, 49 | } 50 | } 51 | 52 | /// Whether the error is potentially caused by the user forking from an 53 | /// older block in a non-archive node. 54 | pub fn is_possibly_non_archive_node_error(&self) -> bool { 55 | static GETH_MESSAGE: &str = "missing trie node"; 56 | 57 | self.get_rpc_error() 58 | .map(|err| err.to_string().to_lowercase().contains(GETH_MESSAGE)) 59 | .unwrap_or(false) 60 | } 61 | } 62 | 63 | impl From> for DatabaseError { 64 | fn from(value: TrySendError) -> Self { 65 | value.into_send_error().into() 66 | } 67 | } 68 | 69 | impl From for DatabaseError { 70 | fn from(value: Infallible) -> Self { 71 | match value {} 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/evm-fork-db/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate tracing; 3 | 4 | pub mod backend; 5 | pub mod cache; 6 | pub mod database; 7 | pub mod error; 8 | pub mod types; 9 | -------------------------------------------------------------------------------- /crates/evm-fork-db/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::sync::Arc; 3 | 4 | use reth_chainspec::ChainSpecBuilder; 5 | use reth_db::{open_db_read_only, DatabaseEnv}; 6 | use reth_node_ethereum::EthereumNode; 7 | use reth_node_types::NodeTypesWithDBAdapter; 8 | use reth_provider::providers::StaticFileProvider; 9 | use reth_provider::ProviderFactory; 10 | 11 | pub type DBFactory = ProviderFactory>>; 12 | 13 | pub fn get_db(db_path: &str) -> DatabaseEnv { 14 | let db_path = Path::new(db_path); 15 | open_db_read_only(&db_path, Default::default()).unwrap() 16 | } 17 | 18 | pub fn get_db_factory(db_path: &str, static_path: &str) -> DBFactory { 19 | let db = get_db(db_path); 20 | let spec = ChainSpecBuilder::mainnet().build(); 21 | 22 | ProviderFactory::>>::new( 23 | db.into(), 24 | spec.into(), 25 | StaticFileProvider::read_only(static_path, true).unwrap(), 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /crates/shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "shared" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | alloy = { workspace = true } 8 | alloy-network = { workspace = true } 9 | alloy-provider = { workspace = true } 10 | alloy-rpc-client = { workspace = true } 11 | alloy-transport-http = { workspace = true } 12 | anyhow = { workspace = true } 13 | const_format = { workspace = true } 14 | csv = { workspace = true } 15 | tracing = { workspace = true } 16 | tracing-appender = { workspace = true } 17 | tracing-subscriber = { workspace = true } 18 | -------------------------------------------------------------------------------- /crates/shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod logging; 2 | pub mod utils; 3 | -------------------------------------------------------------------------------- /crates/shared/src/logging.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use const_format::formatcp; 4 | use tracing::level_filters::LevelFilter; 5 | use tracing_appender::non_blocking::WorkerGuard; 6 | use tracing_subscriber::layer::SubscriberExt; 7 | use tracing_subscriber::util::SubscriberInitExt; 8 | use tracing_subscriber::{EnvFilter, Layer}; 9 | 10 | const DEFAULT_FILE_DIRECTIVE: &str = formatcp!("info,{}=debug", env!("CARGO_PKG_NAME")); 11 | const LOG_FILE_NAME: &str = formatcp!("{}.log", env!("CARGO_PKG_NAME")); 12 | 13 | pub fn setup_tracing( 14 | log_directory: Option<&Path>, 15 | log_file_name: Option<&str>, 16 | ) -> Option { 17 | let stdout_filter = EnvFilter::builder() 18 | .with_env_var("RUST_LOG") 19 | .with_default_directive(LevelFilter::INFO.into()) 20 | .from_env() 21 | .unwrap(); 22 | let stdout_layer = tracing_subscriber::fmt::layer() 23 | .with_writer(std::io::stderr) 24 | .with_filter(stdout_filter); 25 | 26 | let (file_layer, file_guard) = log_directory 27 | .map(|directory| { 28 | let log_file_name = log_file_name.unwrap_or(LOG_FILE_NAME); 29 | let file_appender = tracing_appender::rolling::hourly(directory, log_file_name); 30 | let (file_writer, file_guard) = tracing_appender::non_blocking(file_appender); 31 | 32 | let file_filter = std::env::var("RUST_FILE_LOG").ok(); 33 | let file_filter = file_filter.as_deref().unwrap_or(DEFAULT_FILE_DIRECTIVE); 34 | let file_filter: EnvFilter = file_filter.parse().unwrap(); 35 | 36 | let file_layer = tracing_subscriber::fmt::layer() 37 | .json() 38 | .with_writer(file_writer) 39 | .with_filter(file_filter); 40 | 41 | (Some(file_layer), Some(file_guard)) 42 | }) 43 | .unwrap_or((None, None)); 44 | 45 | tracing_subscriber::registry() 46 | .with(stdout_layer) 47 | .with(file_layer) 48 | .init(); 49 | 50 | file_guard 51 | } 52 | -------------------------------------------------------------------------------- /crates/shared/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use alloy::primitives::Address; 4 | use alloy::pubsub::PubSubFrontend; 5 | use alloy::rpc::types::{Filter, Log}; 6 | use alloy::transports::Transport; 7 | use alloy_network::AnyNetwork; 8 | use alloy_provider::{Provider, ProviderBuilder, RootProvider, WsConnect}; 9 | use alloy_rpc_client::ClientBuilder; 10 | use alloy_transport_http::{Client, Http}; 11 | use anyhow::Result; 12 | 13 | pub fn get_env(key: &str) -> String { 14 | std::env::var(key).unwrap_or_else(|err| panic!("Missing env; key={key}; err={err}")) 15 | } 16 | 17 | pub fn get_http_provider(endpoint: &str) -> RootProvider, AnyNetwork> { 18 | ProviderBuilder::new() 19 | .network::() 20 | .on_client(ClientBuilder::default().http(endpoint.parse().unwrap())) 21 | } 22 | 23 | pub async fn get_ws_provider(endpoint: &str) -> RootProvider { 24 | ProviderBuilder::new() 25 | .on_ws(WsConnect::new(endpoint)) 26 | .await 27 | .unwrap() 28 | } 29 | 30 | pub fn get_block_range(from_block: u64, to_block: u64, chunk: u64) -> Vec<(u64, u64)> { 31 | (from_block..=to_block) 32 | .step_by(chunk as usize) 33 | .map(|start| (start, (start + chunk - 1).min(to_block))) 34 | .collect() 35 | } 36 | 37 | pub async fn get_logs( 38 | provider: Arc

, 39 | from_block: u64, 40 | to_block: u64, 41 | address: Option

, 42 | events: &[&str], 43 | ) -> Result> 44 | where 45 | P: Provider + ?Sized + Send + Sync + 'static, 46 | T: Transport + Clone + Send + Sync + 'static, 47 | { 48 | let mut event_filter = Filter::new() 49 | .from_block(from_block) 50 | .to_block(to_block) 51 | .events(events); 52 | 53 | if let Some(address) = address { 54 | event_filter = event_filter.address(address); 55 | } 56 | 57 | let logs = provider.get_logs(&event_filter).await?; 58 | Ok(logs) 59 | } 60 | -------------------------------------------------------------------------------- /crates/simulator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "simulator" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | alloy = { workspace = true } 8 | alloy-sol-types = { workspace = true } 9 | anyhow = { workspace = true } 10 | derivative = { workspace = true } 11 | evm-fork-db = { workspace = true } 12 | revm = { workspace = true } 13 | shared = { workspace = true } 14 | tracing = { workspace = true } 15 | -------------------------------------------------------------------------------- /crates/simulator/src/abi.rs: -------------------------------------------------------------------------------- 1 | use alloy::sol; 2 | 3 | sol! { 4 | #[derive(Debug, PartialEq, Eq)] 5 | #[sol(rpc)] 6 | contract IERC20 { 7 | event Transfer(address indexed from, address indexed to, uint256 value); 8 | 9 | function name() external view returns (string); 10 | 11 | function symbol() external view returns (string); 12 | 13 | function decimals() external view returns (uint8); 14 | 15 | function totalSupply() external view returns (uint256); 16 | 17 | function balanceOf(address account) external view returns (uint256 balance); 18 | 19 | function transfer(address to, uint value) external returns (bool success); 20 | } 21 | } 22 | 23 | sol! { 24 | #[derive(Debug, PartialEq, Eq)] 25 | #[sol(rpc)] 26 | contract IWETH { 27 | function deposit() external payable; 28 | 29 | function transfer(address to, uint value) external returns (bool success); 30 | 31 | function withdraw(uint amount) external; 32 | 33 | function balanceOf(address account) external view returns (uint256); 34 | } 35 | } 36 | 37 | sol! { 38 | #[derive(Debug, PartialEq, Eq)] 39 | #[sol(rpc)] 40 | contract IERC4626 { 41 | event Deposit( 42 | address indexed caller, 43 | address indexed owner, 44 | uint256 assets, 45 | uint256 shares 46 | ); 47 | 48 | event Withdraw( 49 | address indexed caller, 50 | address indexed receiver, 51 | address indexed owner, 52 | uint256 assets, 53 | uint256 shares 54 | ); 55 | 56 | function convertToShares(uint256 assets) external view returns (uint256); 57 | 58 | function convertToAssets(uint256 shares) external view returns (uint256); 59 | 60 | function asset() external view returns (address); 61 | 62 | function deposit( 63 | uint256 assets, 64 | address receiver 65 | ) external returns (uint256 shares); 66 | 67 | function mint( 68 | uint256 shares, 69 | address receiver 70 | ) external returns (uint256 assets); 71 | 72 | function withdraw( 73 | uint256 assets, 74 | address receiver, 75 | address owner 76 | ) external returns (uint256 shares); 77 | 78 | function redeem( 79 | uint256 shares, 80 | address receiver, 81 | address owner 82 | ) external returns (uint256 assets); 83 | 84 | function previewMint(uint256 shares) external view returns (uint256); 85 | 86 | function previewDeposit(uint256 assets) external view returns (uint256); 87 | } 88 | } 89 | 90 | sol! { 91 | #[derive(Debug, PartialEq, Eq)] 92 | #[sol(rpc)] 93 | contract ICurveV2Pool { 94 | function get_dy( 95 | uint256 i, 96 | uint256 j, 97 | uint256 dx 98 | ) external returns (uint256); 99 | 100 | function exchange( 101 | uint256 i, 102 | uint256 j, 103 | uint256 dx, 104 | uint256 min_dy 105 | ) external; 106 | 107 | function coins(uint256 index) external returns (address); 108 | } 109 | } 110 | 111 | sol! { 112 | #[derive(Debug, PartialEq, Eq)] 113 | #[sol(rpc)] 114 | contract IUniswapV3Pool { 115 | event Swap( 116 | address indexed sender, 117 | address indexed recipient, 118 | int256 amount0, 119 | int256 amount1, 120 | uint160 sqrtPriceX96, 121 | uint128 liquidity, 122 | int24 tick 123 | ); 124 | 125 | event Burn( 126 | address indexed owner, 127 | int24 indexed tickLower, 128 | int24 indexed tickUpper, 129 | uint128 amount, 130 | uint256 amount0, 131 | uint256 amount1 132 | ); 133 | 134 | event Mint( 135 | address sender, 136 | address indexed owner, 137 | int24 indexed tickLower, 138 | int24 indexed tickUpper, 139 | uint128 amount, 140 | uint256 amount0, 141 | uint256 amount1 142 | ); 143 | 144 | function token0() external view returns (address); 145 | 146 | function token1() external view returns (address); 147 | 148 | function fee() external view returns (uint24); 149 | 150 | function tickSpacing() external view returns (int24); 151 | 152 | function liquidity() external view returns (uint128); 153 | 154 | function slot0() 155 | external 156 | view 157 | returns ( 158 | uint160 sqrtPriceX96, 159 | int24 tick, 160 | uint16 observationIndex, 161 | uint16 observationCardinality, 162 | uint16 observationCardinalityNext, 163 | uint8 feeProtocol, 164 | bool unlocked 165 | ); 166 | 167 | function ticks( 168 | int24 tick 169 | ) 170 | external 171 | view 172 | returns ( 173 | uint128 liquidityGross, 174 | int128 liquidityNet, 175 | uint256 feeGrowthOutside0X128, 176 | uint256 feeGrowthOutside1X128, 177 | int56 tickCumulativeOutside, 178 | uint160 secondsPerLiquidityOutsideX128, 179 | uint32 secondsOutside, 180 | bool initialized 181 | ); 182 | 183 | function swap( 184 | address recipient, 185 | bool zeroForOne, 186 | int256 amountSpecified, 187 | uint160 sqrtPriceLimitX96, 188 | bytes calldata data 189 | ) external returns (int256 amount0, int256 amount1); 190 | } 191 | } 192 | 193 | sol! { 194 | #[derive(Debug, PartialEq, Eq)] 195 | #[sol(rpc)] 196 | contract CrocSwapDex { 197 | event CrocSwap( 198 | address indexed base, 199 | address indexed quote, 200 | uint256 poolIdx, 201 | bool isBuy, 202 | bool inBaseQty, 203 | uint128 qty, 204 | uint16 tip, 205 | uint128 limitPrice, 206 | uint128 minOut, 207 | uint8 reserveFlags, 208 | int128 baseFlow, 209 | int128 quoteFlow 210 | ); 211 | } 212 | } 213 | 214 | sol! { 215 | #[derive(Debug, PartialEq, Eq)] 216 | #[sol(rpc)] 217 | contract IUniswapV2Factory { 218 | event PairCreated(address indexed token0, address indexed token1, address pair, uint); 219 | } 220 | } 221 | 222 | sol! { 223 | #[derive(Debug, PartialEq, Eq)] 224 | #[sol(rpc)] 225 | contract IUniswapV2Pair { 226 | event Swap( 227 | address indexed sender, 228 | uint amount0In, 229 | uint amount1In, 230 | uint amount0Out, 231 | uint amount1Out, 232 | address indexed to 233 | ); 234 | 235 | function token0() external view returns (address); 236 | 237 | function token1() external view returns (address); 238 | 239 | function getReserves() external view returns ( 240 | uint112 reserve0, 241 | uint112 reserve1, 242 | uint32 blockTimestampLast 243 | ); 244 | } 245 | } 246 | 247 | sol! { 248 | #[derive(Debug, PartialEq, Eq)] 249 | #[sol(rpc)] 250 | contract IUniswapV3Factory { 251 | event PoolCreated( 252 | address indexed token0, 253 | address indexed token1, 254 | uint24 indexed fee, 255 | int24 tickSpacing, 256 | address pool 257 | ); 258 | } 259 | } 260 | 261 | sol! { 262 | #[derive(Debug, PartialEq, Eq)] 263 | #[sol(rpc)] 264 | contract Simulator { 265 | function flashswapLstArbitrage( 266 | address pool, 267 | bool zeroForOne, 268 | uint256 amountIn 269 | ) external; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /crates/simulator/src/bytecode.rs: -------------------------------------------------------------------------------- 1 | pub const SIMULATOR_BYTECODE: &str = "0x6080604052600436101561001b575b361561001957600080fd5b005b6000803560e01c80630da167c814610c8657806320871dd514610c5857806339f35adf14610ad1578063651bd8d914610aa35780636873b1ae146108a25780638b1c13c514610861578063f04f2707146103775763fa461e331461007f575061000e565b346103745760603660031901126103745760443560043567ffffffffffffffff8211610372573660238301121561037257816004013567ffffffffffffffff81116101a25760248301918184019036602483011161036e578581131561036257604090925b036101a657604090849003126101a2576101459261010f6044610108602095610cca565b9201610cca565b60405163a9059cbb60e01b81526001600160a01b0390921660048301526024820192909252928391908290869082906044820190565b03926001600160a01b03165af1801561019757610161575b5080f35b6020813d60201161018f575b8161017a60209383610d11565b8101031261018b5761015d90611022565b5080fd5b3d915061016d565b6040513d84823e3d90fd5b8380fd5b909190606090849003126101a2576101bd90610cca565b91836101cb60448301610cca565b916001600160a01b03906101e190606401610cca565b166040516370a0823160e01b8152306004820152602081602481855afa90811561032257839161032d575b50604051906363737ac960e11b82526004820152602081602481855afa9081156103225783916102ea575b509060646020926040519485938492635d043b2960e11b845260048401523060248401523060448401525af180156102df576102a5575b5060405163a9059cbb60e01b81526001600160a01b0390931660048401526024830191909152602090829081858160448101610145565b91906020833d6020116102d7575b816102c060209383610d11565b810103126102d257909150602061026e565b600080fd5b3d91506102b3565b6040513d87823e3d90fd5b919250506020813d60201161031a575b8161030760209383610d11565b810103126102d257518591906064610237565b3d91506102fa565b6040513d85823e3d90fd5b9250506020823d60201161035a575b8161034960209383610d11565b810103126102d2578591513861020c565b3d915061033c565b506040602435926100e4565b8580fd5b825b80fd5b5034610374576080366003190112610374576004359067ffffffffffffffff821161037457366023830112156103745781600401356103b581610da0565b926103c36040519485610d11565b8184526024602085019260051b820101903682116101a257602401915b81831061083d5750505060243567ffffffffffffffff811161018b5761040a903690600401610db8565b9160443567ffffffffffffffff81116103725761042b903690600401610db8565b5060643567ffffffffffffffff81116103725761044c903690600401610d49565b9073ba12222222228d8ba445958a75a0704d566bf2c8330361082a57610486906001600160a01b039061047e906112d0565b5116936112d0565b518151916040816020810194810103126101a2576024906104a684611532565b604082015194906020906001600160a01b03166104c4868a83611189565b6040516370a0823160e01b815230600482015294859182905afa92831561081f5786936107eb575b5060409460008061054361055789516105058b82610d11565b600f81526e4c53542062616c616e63653a20257360881b60208201528a51928391632d839cb360e21b60208401528c60248401526064830190610e6d565b89604483015203601f198101835282610d11565b6020815191016a636f6e736f6c652e6c6f675afa508061072b575060808280518101031261036e579061058d6105b59392611532565b506105a6608061059f60608401611532565b9201611022565b906001600160a01b0316610ece565b81516370a0823160e01b8152306004820152602081602481885afa90811561071e5784916106ec575b508181039084831280158284131691831216176106d857600080610643610657865161060a8882610d11565b600a81526970726f6669743a20257360b01b60208201528751928391631e53134760e11b60208401528960248401526064830190610e6d565b86604483015203601f198101835282610d11565b6020815191016a636f6e736f6c652e6c6f675afa508381126106c65750829360446020928451958693849263a9059cbb60e01b845273ba12222222228d8ba445958a75a0704d566bf2c8600485015260248401525af19081156106bd5750610161575080f35b513d84823e3d90fd5b639908aa9d60e01b8452600452602483fd5b634e487b7160e01b84526011600452602484fd5b90506020813d602011610716575b8161070760209383610d11565b810103126102d25751386105de565b3d91506106fa565b50505051903d90823e3d90fd5b6001810361078d575060a08280518101031261036e579061074f6107889392611532565b50606081015161076160808301611532565b916001600160a01b03906107779060a001611532565b16916001600160a01b0316906112f3565b6105b5565b600281036107d9575060a08280518101031261036e57906107b16107889392611532565b506107be60608201611532565b608082015160a09092015191906001600160a01b031661102f565b63e5f7a2d560e01b8752600452602486fd5b9092506020813d602011610817575b8161080760209383610d11565b810103126102d2575191386104ec565b3d91506107fa565b6040513d88823e3d90fd5b63d86ad9cf60e01b835233600452602483fd5b82356001600160a01b038116810361085d578152602092830192016103e0565b8480fd5b50346103745760803660031901126103745761087b610cb4565b604435906001600160a01b03821682036103725761089f91606435916004356112f3565b80f35b5034610374576080366003190112610374576108bc610c9e565b906108c5610cb4565b9160643567ffffffffffffffff8111610372576108e6903690600401610d49565b9061097b6040918251936108fa8486610d11565b6001855261099f6020860192601f19860198893686376001600160a01b0316610922886112d0565b5260208651936109328886610d11565b600185528185019a368c37604435610949866112d0565b5287516001600160a01b0390931682840190815282845261096a8985610d11565b885197889451809285870190610e4a565b830161098f82518093858085019101610e4a565b010103601f198101855284610d11565b73ba12222222228d8ba445958a75a0704d566bf2c83b1561036e578351632e1c224f60e11b81523060048201526080602482015294516084860181905260a486019290875b818110610a84575050506020906003198684030160448701525191828152019590855b818110610a6e5750505090838380610a2c888496600319848303016064850152610e6d565b03818373ba12222222228d8ba445958a75a0704d566bf2c85af1908115610a635750610a555780f35b610a5e91610d11565b818180f35b51913d9150823e3d90fd5b8251885260209788019790920191600101610a07565b82516001600160a01b03168552602094850194909201916001016109e4565b50346103745760603660031901126103745761089f610ac0610c9e565b610ac8610cb4565b60443591611189565b503461037457610ae036610cde565b604051630dfe168160e01b8152929091906001600160a01b0316602084600481845afa9384156102df578594610c37575b5060405163d21220a760e01b815292602084600481855afa93841561081f578694610c06575b50858315610bfc57604094935b8015610be1576401000276a4945b8651602081018690526001600160a01b039889168189015297166060808901919091528752610b82608088610d11565b610ba2865197889687958694630251596160e31b86523060048701610e92565b03925af1801561019757610bb4575080f35b610bd59060403d604011610bda575b610bcd8183610d11565b810190610e34565b505080f35b503d610bc3565b73fffd8963efd1fc6a506488495d951d5263988d2594610b52565b6040949593610b44565b610c2991945060203d602011610c30575b610c218183610d11565b810190610e15565b9238610b37565b503d610c17565b610c5191945060203d602011610c3057610c218183610d11565b9238610b11565b50346103745760803660031901126103745761089f610c75610c9e565b60643590604435906024359061102f565b50346103745761089f610c9836610cde565b91610ece565b600435906001600160a01b03821682036102d257565b602435906001600160a01b03821682036102d257565b35906001600160a01b03821682036102d257565b60609060031901126102d2576004356001600160a01b03811681036102d2579060243580151581036102d2579060443590565b90601f8019910116810190811067ffffffffffffffff821117610d3357604052565b634e487b7160e01b600052604160045260246000fd5b81601f820112156102d25780359067ffffffffffffffff8211610d335760405192610d7e601f8401601f191660200185610d11565b828452602083830101116102d257816000926020809301838601378301015290565b67ffffffffffffffff8111610d335760051b60200190565b9080601f830112156102d2578135610dcf81610da0565b92610ddd6040519485610d11565b81845260208085019260051b8201019283116102d257602001905b828210610e055750505090565b8135815260209182019101610df8565b908160209103126102d257516001600160a01b03811681036102d25790565b91908260409103126102d2576020825192015190565b60005b838110610e5d5750506000910152565b8181015183820152602001610e4d565b90602091610e8681518092818552858086019101610e4a565b601f01601f1916010190565b6001600160a01b039182168152911515602083015260408201929092529116606082015260a060808201819052610ecb92910190610e6d565b90565b6001600160a01b0316908015610fd457604051630dfe168160e01b815291602083600481845afa8015610f8c57604093600091610fb5575b50935b8215610f985760006401000276a4935b8551602081018590526001600160a01b0390971687870152858752610f3f606088610d11565b610f5f865197889687958694630251596160e31b86523060048701610e92565b03925af18015610f8c57610f705750565b610f889060403d604011610bda57610bcd8183610d11565b5050565b6040513d6000823e3d90fd5b600073fffd8963efd1fc6a506488495d951d5263988d2593610f19565b610fce915060203d602011610c3057610c218183610d11565b38610f06565b60405163d21220a760e01b815291602083600481845afa8015610f8c57604093600091611003575b5093610f09565b61101c915060203d602011610c3057610c218183610d11565b38610ffc565b519081151582036102d257565b60405163c661065760e01b8152600481018390526000949392916001600160a01b03811691906020826024818a875af191821561116f576110ae9260209288928a92611150575b5060405163095ea7b360e01b81526001600160a01b0390911660048201526024810192909252909283919082908a9082906044820190565b03926001600160a01b03165af1801561081f57611119575b50803b1561085d57849291836084926040519687958694630b68372160e31b86526004860152602485015260448401528160648401525af180156101975761110c575050565b8161111691610d11565b50565b6020813d602011611148575b8161113260209383610d11565b8101031261036e5761114390611022565b6110c6565b3d9150611125565b611168919250843d8611610c3057610c218183610d11565b9038611076565b6040513d89823e3d90fd5b908160209103126102d2575190565b6040516338d52e0f60e01b81526001600160a01b0382169392602082600481885afa918215610f8c576000926112af575b506001600160a01b0390811691168190036102d25760405163095ea7b360e01b81526001600160a01b0392909216600483015260248201839052602090829060449082906000905af18015610f8c5761126b575b5060009160446020926040519485938492636e553f6560e01b845260048401523060248401525af18015610f8c576112435750565b6111169060203d602011611264575b61125c8183610d11565b81019061117a565b503d611252565b916020833d6020116112a7575b8161128560209383610d11565b810103126102d257604460209261129d600095611022565b509250509161120e565b3d9150611278565b6112c991925060203d602011610c3057610c218183610d11565b90386111ba565b8051156112dd5760200190565b634e487b7160e01b600052603260045260246000fd5b60405163095ea7b360e01b815273ba12222222228d8ba445958a75a0704d566bf2c860048201526bffffffffffffffffffffffff60248201526001600160a01b0390921693909290916020816044816000895af18015610f8c576114fb575b506020936040516113638682610d11565b60008152601f19860136878301376040519460c0860186811067ffffffffffffffff821117610d335760405285528585016000815260408601928352606086019360018060a01b031684526080860194855260a08601918252604051916080830183811067ffffffffffffffff821117610d3357604052308352878301600081526040840191308352606085019360008552604051996352bbbe2960e01b8b5260e060048c01525160e48b0152519060028210156114e5578a988a988998611466946101048b015260018060a01b039051166101248a015260018060a01b03905116610144890152516101648801525160c06101848801526101a4870190610e6d565b93516001600160a01b03908116602487015290511515604486015290511660648401525115156084830152600060a48301819052603360f71b60c484015291900390829073ba12222222228d8ba445958a75a0704d566bf2c85af18015610f8c576114cf575050565b8161111692903d106112645761125c8183610d11565b634e487b7160e01b600052602160045260246000fd5b6020813d60201161152a575b8161151460209383610d11565b810103126102d25761152590611022565b611352565b3d9150611507565b51906001600160a01b03821682036102d25756fea264697066735822122025200669eab8c998e105f400ab97539107be09a1c160726543a5ebb6b162b4dd64736f6c634300081c0033"; 2 | -------------------------------------------------------------------------------- /crates/simulator/src/evm.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use std::sync::Arc; 3 | 4 | use alloy::primitives::Address; 5 | use alloy_sol_types::SolCall; 6 | use anyhow::{anyhow, Result}; 7 | use evm_fork_db::backend::SharedBackend; 8 | use evm_fork_db::cache::{BlockchainDb, BlockchainDbMeta}; 9 | use evm_fork_db::database::ForkedDatabase; 10 | use evm_fork_db::types::get_db_factory; 11 | use revm::db::WrapDatabaseRef; 12 | use revm::primitives::state::AccountInfo; 13 | use revm::primitives::{Account, Bytecode, ExecutionResult, Output, TransactTo, SHANGHAI, U256}; 14 | use revm::{Database, Evm}; 15 | use shared::utils::get_http_provider; 16 | use tracing::error; 17 | 18 | use crate::abi; 19 | use crate::bytecode::SIMULATOR_BYTECODE; 20 | 21 | pub struct EVM<'a> { 22 | backend: SharedBackend, 23 | fork: ForkedDatabase, 24 | pub evm: Evm<'a, (), WrapDatabaseRef>, 25 | 26 | weth: Address, 27 | block_number: u64, 28 | owner: Address, 29 | simulator: Address, 30 | } 31 | 32 | impl<'a> EVM<'a> { 33 | pub async fn new( 34 | rpc_url: &str, 35 | db_path: Option<&str>, 36 | static_path: Option<&str>, 37 | block_number: u64, 38 | weth: Address, 39 | owner: Address, 40 | balance: U256, 41 | ) -> Self { 42 | let http_provider = get_http_provider(rpc_url); 43 | 44 | let file_db_factory = db_path.map(|path| { 45 | get_db_factory(path, static_path.expect("static_path must be provided with db_path")) 46 | }); 47 | 48 | let meta = BlockchainDbMeta { 49 | cfg_env: Default::default(), 50 | block_env: Default::default(), 51 | hosts: BTreeSet::from([rpc_url.to_string()]), 52 | }; 53 | 54 | let db = BlockchainDb::new(meta, None); 55 | 56 | let backend = SharedBackend::spawn_backend( 57 | Arc::new(http_provider.clone()), 58 | file_db_factory, 59 | db.clone(), 60 | None, 61 | ) 62 | .await; 63 | 64 | let fork = ForkedDatabase::new(backend.clone(), db.clone()); 65 | 66 | let evm = Evm::builder() 67 | .with_spec_id(SHANGHAI) 68 | .with_ref_db(fork.clone()) 69 | .build(); 70 | 71 | let mut _self = Self { 72 | backend, 73 | fork, 74 | evm, 75 | weth, 76 | block_number, 77 | owner: Address::default(), 78 | simulator: Address::default(), 79 | }; 80 | 81 | _self.set_block_number(block_number); 82 | _self.setup_owner(owner, balance); 83 | 84 | _self.simulator = _self.deploy_simulator(None); 85 | 86 | _self 87 | } 88 | 89 | pub fn db(&self) -> &ForkedDatabase { 90 | &self.fork 91 | } 92 | 93 | pub fn evm_cloned(&self) -> Evm<'_, (), WrapDatabaseRef> { 94 | Evm::builder() 95 | .with_spec_id(SHANGHAI) 96 | .with_ref_db(self.db().clone()) 97 | .build() 98 | } 99 | 100 | pub fn weth(&self) -> Address { 101 | self.weth 102 | } 103 | 104 | pub fn block_number(&self) -> u64 { 105 | self.block_number 106 | } 107 | 108 | pub fn owner(&self) -> Address { 109 | self.owner 110 | } 111 | 112 | pub fn simulator(&self) -> Address { 113 | self.simulator 114 | } 115 | 116 | pub fn set_block_number(&mut self, block_number: u64) { 117 | if let Err(e) = self.backend.set_pinned_block(block_number) { 118 | error!("failed to set block. error={e:?}"); 119 | } 120 | self.block_number = block_number; 121 | self.set_block_env(); 122 | } 123 | 124 | pub fn set_block_env(&mut self) { 125 | let block_env = self.evm.block_mut(); 126 | block_env.number = U256::from(self.block_number); 127 | } 128 | 129 | pub fn deploy_contract( 130 | &mut self, 131 | contract_addr: Option
, 132 | bytecode_str: &str, 133 | ) -> Address { 134 | let bytes = bytecode_str.parse().unwrap(); 135 | let code = Bytecode::new_legacy(bytes); 136 | let account = AccountInfo::new(U256::ZERO, 0, code.hash_slow(), code); 137 | 138 | let addy = match contract_addr { 139 | Some(addy) => addy, 140 | None => Address::random(), 141 | }; 142 | 143 | let cache_db_mut = self.evm.db_mut().0.database_mut(); 144 | cache_db_mut.insert_account_info(addy, account); 145 | 146 | addy 147 | } 148 | 149 | pub fn deploy_simulator(&mut self, contract_addr: Option
) -> Address { 150 | self.deploy_contract(contract_addr, SIMULATOR_BYTECODE) 151 | } 152 | 153 | pub fn setup_owner(&mut self, owner: Address, balance: U256) { 154 | self.owner = owner; 155 | self.set_eth_balance(owner, balance); 156 | } 157 | 158 | pub fn basic(&mut self, target: Address) -> Result> { 159 | self.evm 160 | .db_mut() 161 | .0 162 | .basic(target) 163 | .map_err(|e| anyhow!("failed to get basic. error={e:?}")) 164 | } 165 | 166 | pub fn get_eth_balance(&mut self, target: Address) -> U256 { 167 | match self.basic(target) { 168 | Ok(basic) => match basic { 169 | Some(account) => account.balance, 170 | None => { 171 | error!("failed to get eth balance. target={}, error=no account", target); 172 | U256::ZERO 173 | } 174 | }, 175 | Err(e) => { 176 | error!("failed to get eth balance. target={}, error={:?}", target, e); 177 | U256::ZERO 178 | } 179 | } 180 | } 181 | 182 | pub fn set_eth_balance(&mut self, target: Address, balance: U256) { 183 | let account = match self.basic(target) { 184 | Ok(Some(mut account)) => { 185 | account.balance = balance; 186 | account 187 | } 188 | Ok(None) | Err(_) => AccountInfo { balance, ..Default::default() }, 189 | }; 190 | 191 | self.evm 192 | .db_mut() 193 | .0 194 | .database_mut() 195 | .insert_account_info(target, account); 196 | } 197 | 198 | pub fn wrap_eth(&mut self, amount: U256) -> Result<()> { 199 | let encoded = abi::IWETH::depositCall::new(()).abi_encode(); 200 | 201 | let tx_env = self.evm.tx_mut(); 202 | tx_env.transact_to = TransactTo::Call(self.weth); 203 | tx_env.data = encoded.into(); 204 | tx_env.caller = self.owner; 205 | tx_env.value = amount; 206 | 207 | let result = self.evm.transact_commit()?; 208 | 209 | match result { 210 | ExecutionResult::Halt { reason, gas_used } => { 211 | error!("wrap_weth halted. gas_used={}, reason={:?}", gas_used, reason); 212 | } 213 | ExecutionResult::Revert { gas_used, output } => { 214 | error!("wrap_weth reverted. gas_used={}, output={}", gas_used, output); 215 | } 216 | _ => {} 217 | } 218 | 219 | Ok(()) 220 | } 221 | 222 | pub fn transfer_token( 223 | &mut self, 224 | token: Address, 225 | from: Address, 226 | to: Address, 227 | amount: U256, 228 | ) -> Result<()> { 229 | let encoded = abi::IERC20::transferCall::new((to, amount)).abi_encode(); 230 | 231 | let tx_env = self.evm.tx_mut(); 232 | tx_env.transact_to = TransactTo::Call(token); 233 | tx_env.data = encoded.into(); 234 | tx_env.caller = from; 235 | tx_env.value = U256::ZERO; 236 | 237 | let result = self.evm.transact_commit()?; 238 | 239 | match result { 240 | ExecutionResult::Halt { reason, gas_used } => { 241 | error!("transfer_token halted. gas_used={}, reason={:?}", gas_used, reason); 242 | } 243 | ExecutionResult::Revert { gas_used, output } => { 244 | error!("transfer_token reverted. gas_used={}, output={}", gas_used, output); 245 | } 246 | _ => {} 247 | } 248 | 249 | Ok(()) 250 | } 251 | 252 | pub fn fund_simulator(&mut self, amount: U256) -> Result<()> { 253 | self.wrap_eth(amount)?; 254 | self.transfer_token(self.weth, self.owner, self.simulator, amount) 255 | } 256 | 257 | pub fn get_token_balance( 258 | &mut self, 259 | token: Address, 260 | account: Address, 261 | ) -> Result<(U256, Account)> { 262 | let encoded = abi::IERC20::balanceOfCall::new((account,)).abi_encode(); 263 | 264 | let tx_env = self.evm.tx_mut(); 265 | tx_env.transact_to = TransactTo::Call(token); 266 | tx_env.data = encoded.into(); 267 | tx_env.caller = Address::ZERO; 268 | tx_env.value = U256::ZERO; 269 | 270 | let ref_tx = self.evm.transact()?; 271 | let result = ref_tx.result; 272 | 273 | let value = match result { 274 | ExecutionResult::Success { output: Output::Call(value), .. } => Ok(value), 275 | _ => Err(anyhow!("failed to get token balance. token={}", token)), 276 | }?; 277 | 278 | let result = abi::IERC20::balanceOfCall::abi_decode_returns(&value, false)?; 279 | 280 | let tx_state = ref_tx.state; 281 | let touched_account = match tx_state.get(&token) { 282 | Some(state) => state, 283 | None => &Account::default(), 284 | }; 285 | 286 | Ok((result.balance, touched_account.to_owned())) 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /crates/simulator/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod abi; 2 | pub mod bytecode; 3 | pub mod evm; 4 | pub mod traits; 5 | -------------------------------------------------------------------------------- /crates/simulator/src/traits/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod simulator; 2 | pub mod uniswap_v3; 3 | 4 | pub use simulator::SimulatorContract; 5 | pub use uniswap_v3::UniswapV3PoolContract; 6 | -------------------------------------------------------------------------------- /crates/simulator/src/traits/simulator.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Address; 2 | use alloy_sol_types::SolCall; 3 | use anyhow::Result; 4 | use revm::primitives::{ExecutionResult, TransactTo, U256}; 5 | use tracing::error; 6 | 7 | use crate::abi; 8 | use crate::evm::EVM; 9 | 10 | pub trait SimulatorContract { 11 | fn flashswap_lst_arbitrage(&mut self, pool: Address, zfo: bool, amount_in: U256) -> Result<()>; 12 | } 13 | 14 | impl SimulatorContract for EVM<'_> { 15 | fn flashswap_lst_arbitrage(&mut self, pool: Address, zfo: bool, amount_in: U256) -> Result<()> { 16 | let owner = self.owner(); 17 | let simulator = self.simulator(); 18 | 19 | let encoded = 20 | abi::Simulator::flashswapLstArbitrageCall::new((pool, zfo, amount_in)).abi_encode(); 21 | 22 | let evm = &mut self.evm; 23 | 24 | let tx_env = evm.tx_mut(); 25 | tx_env.transact_to = TransactTo::Call(simulator); 26 | tx_env.data = encoded.into(); 27 | tx_env.caller = owner; 28 | tx_env.value = U256::ZERO; 29 | 30 | let result = evm.transact_commit()?; 31 | 32 | match result { 33 | ExecutionResult::Halt { reason, gas_used } => { 34 | error!("transfer_token halted. gas_used={}, reason={:?}", gas_used, reason); 35 | } 36 | ExecutionResult::Revert { gas_used, output } => { 37 | error!("transfer_token reverted. gas_used={}, output={}", gas_used, output); 38 | } 39 | _ => {} 40 | } 41 | 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/simulator/src/traits/uniswap_v3.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Address; 2 | use alloy_sol_types::SolCall; 3 | use anyhow::{anyhow, Result}; 4 | use revm::primitives::{ExecutionResult, Output, TransactTo, U256}; 5 | 6 | use crate::abi; 7 | use crate::evm::EVM; 8 | 9 | pub trait UniswapV3PoolContract { 10 | fn token0(&mut self, contract_address: Address) -> Result
; 11 | 12 | fn token1(&mut self, contract_address: Address) -> Result
; 13 | } 14 | 15 | impl UniswapV3PoolContract for EVM<'_> { 16 | fn token0(&mut self, contract_address: Address) -> Result
{ 17 | let owner = self.owner(); 18 | 19 | let encoded = abi::IUniswapV3Pool::token0Call::new(()).abi_encode(); 20 | 21 | let evm = &mut self.evm; 22 | 23 | let tx_env = evm.tx_mut(); 24 | tx_env.transact_to = TransactTo::Call(contract_address); 25 | tx_env.data = encoded.into(); 26 | tx_env.caller = owner; 27 | tx_env.value = U256::ZERO; 28 | 29 | let ref_tx = evm.transact()?; 30 | let result = ref_tx.result; 31 | 32 | let value = match result { 33 | ExecutionResult::Success { output: Output::Call(value), .. } => Ok(value), 34 | _ => Err(anyhow!("failed to get token0. pool={}", contract_address)), 35 | }?; 36 | 37 | let result = abi::IUniswapV3Pool::token0Call::abi_decode_returns(&value, false)?; 38 | 39 | Ok(result._0) 40 | } 41 | 42 | fn token1(&mut self, contract_address: Address) -> Result
{ 43 | let owner = self.owner(); 44 | 45 | let encoded = abi::IUniswapV3Pool::token1Call::new(()).abi_encode(); 46 | 47 | let evm = &mut self.evm; 48 | 49 | let tx_env = evm.tx_mut(); 50 | tx_env.transact_to = TransactTo::Call(contract_address); 51 | tx_env.data = encoded.into(); 52 | tx_env.caller = owner; 53 | tx_env.value = U256::ZERO; 54 | 55 | let ref_tx = evm.transact()?; 56 | let result = ref_tx.result; 57 | 58 | let value = match result { 59 | ExecutionResult::Success { output: Output::Call(value), .. } => Ok(value), 60 | _ => Err(anyhow!("failed to get token0. pool={}", contract_address)), 61 | }?; 62 | 63 | let result = abi::IUniswapV3Pool::token1Call::abi_decode_returns(&value, false)?; 64 | 65 | Ok(result._0) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.83.0" 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | error_on_line_overflow = true 3 | wrap_comments = true 4 | use_field_init_shorthand = true 5 | imports_granularity = "Module" 6 | condense_wildcard_suffixes = true 7 | format_strings = true 8 | group_imports = "StdExternalCrate" 9 | use_small_heuristics = "Max" 10 | chain_width = 60 11 | -------------------------------------------------------------------------------- /taplo.toml: -------------------------------------------------------------------------------- 1 | [formatting] 2 | column_width = 120 3 | array_auto_expand = true 4 | allowed_blank_lines = 1 5 | 6 | [[rule]] 7 | keys = [ 8 | "dependencies", 9 | "dev-dependencies", 10 | "build-dependencies", 11 | "toolchain", 12 | "workspace.dependencies", 13 | ] 14 | 15 | [rule.formatting] 16 | reorder_keys = true 17 | --------------------------------------------------------------------------------