1use alloy_network::{Network, TransactionBuilder};
10use alloy_primitives::{Bytes, U256};
11use alloy_provider::Provider;
12use alloy_rpc_types::BlockId;
13use foundry_compilers::artifacts::EvmVersion;
14use revm::bytecode::opcode::{
15 AND, CALL, CLZ, CODECOPY, CREATE, DUP6, EQ, GAS, INVALID, ISZERO, JUMPDEST, JUMPI, MCOPY,
16 MSTORE, MUL, OR, POP, PUSH0, PUSH1, PUSH2, PUSH3, RETURN, RETURNDATASIZE, SLOTNUM, STATICCALL,
17 STOP, SWAP1,
18};
19
20#[rustfmt::skip]
23const PROBES: [(EvmVersion, &[u8]); 5] = [
24 (EvmVersion::Shanghai, &[PUSH0, POP, STOP]),
25 (EvmVersion::Cancun, &[PUSH1, 0, PUSH1, 0, PUSH1, 0, MCOPY, STOP]),
26 (EvmVersion::Prague, &[
29 PUSH1, 0, PUSH1, 0, PUSH2, 0x01, 0x00, PUSH1, 0, PUSH1, 0x0b, GAS, STATICCALL,
31 RETURNDATASIZE, PUSH1, 0x80, EQ, AND, PUSH1, 22, JUMPI, INVALID, JUMPDEST, STOP,
33 ]),
34 (EvmVersion::Osaka, &[PUSH1, 0, CLZ, POP, STOP]),
35 (EvmVersion::Amsterdam, &[SLOTNUM, POP, STOP]),
36];
37
38const PROBE_GAS: u32 = 20_000;
40
41const PROBE_CALL_GAS: u64 = 3_000_000;
44
45pub async fn probe_evm_version<N: Network, P: Provider<N>>(
50 provider: &P,
51 block: BlockId,
52) -> Option<EvmVersion> {
53 let request = N::TransactionRequest::default()
54 .with_deploy_code(probe_code())
55 .with_gas_limit(PROBE_CALL_GAS);
56 let output = match provider.call(request).block(block).await {
57 Ok(output) => output,
58 Err(err) => {
59 trace!(%err, "EVM version probe failed");
60 return None;
61 }
62 };
63 let Ok(word) = <[u8; 32]>::try_from(output.as_ref()) else {
64 trace!(%output, "unexpected EVM version probe output");
65 return None;
66 };
67 let mask = U256::from_be_bytes(word);
68 if !(mask >> PROBES.len()).is_zero() {
69 trace!(%mask, "unexpected EVM version probe mask");
70 return None;
71 }
72 let version = evm_version_from_mask(mask);
73 trace!(?version, %mask, "probed EVM version");
74 Some(version)
75}
76
77fn evm_version_from_mask(mask: U256) -> EvmVersion {
82 PROBES
83 .iter()
84 .enumerate()
85 .take_while(|(bit, _)| mask.bit(*bit))
86 .last()
87 .map_or(EvmVersion::Paris, |(_, (version, _))| *version)
88}
89
90fn probe_code() -> Bytes {
96 const PER_PROBE: usize = 39;
97 const HEADER_AND_FOOTER: usize = 10;
98
99 let [_, gas @ ..] = PROBE_GAS.to_be_bytes();
100 let mut code = vec![PUSH1, 0];
102 let mut data = Vec::new();
103 let mut offset = HEADER_AND_FOOTER + PER_PROBE * PROBES.len();
104 for (bit, (_, runtime)) in PROBES.iter().enumerate() {
105 let initcode = deploy_code(runtime);
106 let len = u8::try_from(initcode.len()).expect("probe initcode length fits in PUSH1");
107 let [start_hi, start_lo] =
108 u16::try_from(offset).expect("probe initcode offset fits in PUSH2").to_be_bytes();
109 code.extend([PUSH1, len, PUSH2, start_hi, start_lo, PUSH1, 0, CODECOPY]);
111 code.extend([PUSH1, len, PUSH1, 0, PUSH1, 0, CREATE]);
113 code.extend([PUSH1, 0].repeat(5));
116 code.extend([DUP6, PUSH3, gas[0], gas[1], gas[2], CALL]);
117 code.extend([SWAP1, ISZERO, ISZERO, AND]);
119 code.extend([PUSH1, 1 << bit, MUL, OR]);
121 offset += initcode.len();
122 data.extend(initcode);
123 }
124 code.extend([PUSH1, 0, MSTORE, PUSH1, 32, PUSH1, 0, RETURN]);
126 debug_assert_eq!(code.len(), HEADER_AND_FOOTER + PER_PROBE * PROBES.len());
127 code.extend(data);
128 code.into()
129}
130
131fn deploy_code(runtime: &[u8]) -> Vec<u8> {
133 const PREFIX_LEN: u8 = 12;
134 let len = u8::try_from(runtime.len()).expect("probe runtime length fits in PUSH1");
135 let mut code =
137 vec![PUSH1, len, PUSH1, PREFIX_LEN, PUSH1, 0, CODECOPY, PUSH1, len, PUSH1, 0, RETURN];
138 code.extend_from_slice(runtime);
139 code
140}
141
142#[cfg(test)]
143mod tests {
144
145 use super::*;
146 use alloy_primitives::TxKind;
147 use revm::{
148 Context, ExecuteEvm, MainBuilder, MainContext, context::TxEnv, database::InMemoryDB,
149 primitives::hardfork::SpecId,
150 };
151
152 #[test]
153 fn detects_executed_spec() {
154 for (spec, expected) in [
155 (SpecId::LONDON, EvmVersion::Paris),
156 (SpecId::MERGE, EvmVersion::Paris),
157 (SpecId::SHANGHAI, EvmVersion::Shanghai),
158 (SpecId::CANCUN, EvmVersion::Cancun),
159 (SpecId::PRAGUE, EvmVersion::Prague),
160 (SpecId::OSAKA, EvmVersion::Osaka),
161 (SpecId::AMSTERDAM, EvmVersion::Amsterdam),
162 ] {
163 let result = Context::mainnet()
164 .modify_cfg_chained(|cfg| cfg.set_spec_and_mainnet_gas_params(spec))
165 .with_db(InMemoryDB::default())
166 .build_mainnet()
167 .transact(
168 TxEnv::builder()
169 .kind(TxKind::Create)
170 .data(probe_code())
171 .gas_limit(PROBE_CALL_GAS)
172 .build()
173 .unwrap(),
174 )
175 .unwrap();
176 let output = result.result.into_output().unwrap();
177 assert_eq!(evm_version_from_mask(U256::from_be_slice(&output)), expected, "{spec:?}");
178 }
179 }
180
181 #[test]
182 fn ignores_probes_after_a_failed_one() {
183 assert_eq!(evm_version_from_mask(U256::from(0b1011)), EvmVersion::Cancun);
184 }
185}