1use crate::errors::RunnerError;
2use crate::runner::db_error;
3use crate::spec::{AccountValue, ExpectedSlot, ExpectedValue};
4use alloy_primitives::{U256, hex};
5use foundry_evm_core::decode::RevertDecoder;
6use huff_neo_utils::prelude::Contract;
7use revm::DatabaseRef;
8use serde::Serialize;
9
10const CHEATCODE_ERROR_SELECTOR: [u8; 4] = hex!("eeaa9e6f");
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16#[serde(tag = "kind", rename_all = "snake_case")]
17pub enum Outcome {
18 Return { data: String },
20 Revert { data: String },
22 Halt { reason: String },
24 Skip { reason: Option<String> },
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
30pub struct Failure {
31 pub expectation: String,
33 pub message: String,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum ExpectedOutcome {
40 Return(Option<Vec<u8>>),
42 Revert(Option<Vec<u8>>),
44 PartialRevert(Vec<u8>),
46 Halt(Option<String>),
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Expectations {
53 pub outcome: ExpectedOutcome,
55 pub outcome_flag: Option<String>,
57 pub min_gas: Option<u64>,
59 pub max_gas: Option<u64>,
61 pub storage: Vec<ExpectedSlot>,
63 pub balances: Vec<AccountValue<ExpectedValue>>,
65}
66
67impl Default for Expectations {
68 fn default() -> Self {
69 Self {
70 outcome: ExpectedOutcome::Return(None),
71 outcome_flag: None,
72 min_gas: None,
73 max_gas: None,
74 storage: Vec::new(),
75 balances: Vec::new(),
76 }
77 }
78}
79
80impl Expectations {
81 pub fn check(&self, outcome: &Outcome, gas: u64, contract: &Contract) -> Vec<Failure> {
83 let mut failures = Vec::new();
84 if matches!(outcome, Outcome::Skip { .. }) {
86 return failures;
87 }
88
89 if let Outcome::Revert { data } = outcome
91 && let data = decode_hex(data)
92 && data.starts_with(&CHEATCODE_ERROR_SELECTOR)
93 {
94 failures.push(Failure { expectation: String::from("cheatcode"), message: RevertDecoder::new().decode(&data, None) });
95 } else if let Some(message) = self.outcome_mismatch(outcome, contract) {
96 let expectation = self.outcome_flag.as_deref().unwrap_or("outcome");
97 failures.push(Failure { expectation: expectation.to_string(), message });
98 }
99 if let Some(min) = self.min_gas
100 && gas < min
101 {
102 failures.push(Failure { expectation: String::from("expect_gas"), message: format!("used {gas} gas, less than min {min}") });
103 }
104 if let Some(max) = self.max_gas
105 && gas > max
106 {
107 failures.push(Failure { expectation: String::from("expect_gas"), message: format!("used {gas} gas, more than max {max}") });
108 }
109
110 failures
111 }
112
113 pub fn check_post_state<DB>(&self, db: &DB) -> Result<Vec<Failure>, RunnerError>
115 where
116 DB: DatabaseRef,
117 <DB as DatabaseRef>::Error: std::fmt::Debug,
118 {
119 let mut failures = Vec::new();
120
121 for ExpectedSlot { address, slot, expected } in &self.storage {
122 let actual = db.storage_ref(*address, *slot).map_err(db_error)?;
123 if !expected.matches(actual) {
124 failures.push(Failure {
125 expectation: String::from("expect_storage"),
126 message: format!("slot {slot:#x} at {address}: expected {}, got {actual:#x}", expected.describe(|v| format!("{v:#x}"))),
127 });
128 }
129 }
130 for AccountValue { address, value: expected } in &self.balances {
131 let actual = db.basic_ref(*address).map_err(db_error)?.map_or(U256::ZERO, |account| account.balance);
132 if !expected.matches(actual) {
133 failures.push(Failure {
134 expectation: String::from("expect_balance"),
135 message: format!("balance of {address}: expected {}, got {actual}", expected.describe(|v| v.to_string())),
136 });
137 }
138 }
139
140 Ok(failures)
141 }
142
143 fn outcome_mismatch(&self, outcome: &Outcome, contract: &Contract) -> Option<String> {
145 match (&self.outcome, outcome) {
146 (ExpectedOutcome::Return(expected), Outcome::Return { data }) => {
148 let data = decode_hex(data);
149 match expected {
150 Some(expected) if *expected != data => {
151 Some(format!("expected return data {}, got {}", display_hex(expected), display_hex(&data)))
152 }
153 _ => None,
154 }
155 }
156 (ExpectedOutcome::Revert(expected), Outcome::Revert { data }) => {
157 let data = decode_hex(data);
158 match expected {
159 Some(expected) if *expected != data => Some(format!(
160 "expected revert data {}, got {}",
161 describe_revert(expected, contract),
162 describe_revert(&data, contract)
163 )),
164 _ => None,
165 }
166 }
167 (ExpectedOutcome::PartialRevert(prefix), Outcome::Revert { data }) => {
168 let data = decode_hex(data);
169 (!data.starts_with(prefix)).then(|| {
170 format!(
171 "expected revert data starting with {}, got {}",
172 describe_revert(prefix, contract),
173 describe_revert(&data, contract)
174 )
175 })
176 }
177 (ExpectedOutcome::Halt(expected), Outcome::Halt { reason }) => match expected {
178 Some(expected) if !halt_reason_matches(expected, reason) => {
179 Some(format!("expected the test to halt with {expected}, but it halted with {reason}"))
180 }
181 _ => None,
182 },
183
184 (ExpectedOutcome::Return(_), _) => Some(format!("expected the test to return, but it {}", describe_outcome(outcome, contract))),
186 (ExpectedOutcome::Revert(_) | ExpectedOutcome::PartialRevert(_), _) => {
187 Some(format!("expected a revert, but the test {}", describe_outcome(outcome, contract)))
188 }
189 (ExpectedOutcome::Halt(_), _) => Some(format!("expected the test to halt, but it {}", describe_outcome(outcome, contract))),
190 }
191 }
192}
193
194fn halt_reason_matches(expected: &str, actual: &str) -> bool {
197 actual == expected || actual.strip_prefix(expected).is_some_and(|rest| rest.starts_with('('))
198}
199
200fn describe_outcome(outcome: &Outcome, contract: &Contract) -> String {
202 match outcome {
203 Outcome::Return { data } => format!("returned {}", display_hex(&decode_hex(data))),
204 Outcome::Revert { data } => format!("reverted with {}", describe_revert(&decode_hex(data), contract)),
205 Outcome::Halt { reason } => format!("halted with {reason}"),
206 Outcome::Skip { .. } => String::from("was skipped"),
207 }
208}
209
210fn describe_revert(data: &[u8], contract: &Contract) -> String {
213 let error = data.get(..4).and_then(|selector| contract.errors.iter().find(|e| e.selector == selector));
214 match error {
215 Some(error) => format!("{} ({})", display_hex(data), error.name),
216 None => match RevertDecoder::new().maybe_decode_known(data) {
217 Some(message) => format!("{} ({message})", display_hex(data)),
218 None => display_hex(data),
219 },
220 }
221}
222
223fn decode_hex(data: &str) -> Vec<u8> {
225 hex::decode(data).unwrap_or_default()
226}
227
228fn display_hex(data: &[u8]) -> String {
230 if data.is_empty() { String::from("0x (empty)") } else { format!("0x{}", hex::encode(data)) }
231}