1use std::collections::BTreeMap;
2
3use engine_model::{ExecutionEvent, MemorySnapshot, StorageChange, Value};
4
5use wasmtime::*;
15
16pub struct StylusVm {
17 engine: Engine,
18 store: Store<VmState>,
19 module: Option<Module>,
20 instance: Option<Instance>,
21 trace: Vec<ExecutionEvent>,
23 pc: u64,
25 reverted: bool,
27 revert_reason: Option<String>,
28 scenario_instructions: Vec<Instruction>,
30 scenario_ptr: usize,
31}
32
33struct VmState {
34 storage: BTreeMap<String, String>,
35 stack: Vec<String>,
36 memory: Vec<u8>,
37}
38
39#[derive(Debug, Clone)]
42pub enum Instruction {
43 Push(String),
45 Store { slot: String },
47 Load { slot: String },
49 Call { target: String },
51 Log { topic: String },
53 Add,
55 Sub,
57 RequireGte { reason: String },
59 Transfer { to: String },
61 Revert { reason: String },
63}
64
65struct GasCost;
67impl GasCost {
68 const PUSH: u64 = 3;
69 const SSTORE_COLD: u64 = 20_000;
70 const SSTORE_WARM: u64 = 5_000;
71 const SLOAD_COLD: u64 = 2_100;
72 const SLOAD_WARM: u64 = 100;
73 const CALL: u64 = 700;
74 const CALL_WITH_VALUE: u64 = 9_000;
75 const LOG: u64 = 375;
76 const ADD: u64 = 3;
77 const SUB: u64 = 3;
78 const REVERT: u64 = 0;
79}
80
81impl StylusVm {
82 pub fn new() -> Self {
83 let mut config = Config::new();
84 config.epoch_interruption(true);
85 config.consume_fuel(true);
86 let engine = Engine::new(&config).expect("failed to create wasmtime engine");
87 let mut store = Store::new(&engine, VmState {
88 storage: BTreeMap::new(),
89 stack: Vec::new(),
90 memory: Vec::new(),
91 });
92 store.add_fuel(1_000_000_000).expect("failed to set initial fuel");
93
94 Self {
95 engine,
96 store,
97 module: None,
98 instance: None,
99 trace: Vec::new(),
100 pc: 0,
101 reverted: false,
102 revert_reason: None,
103 scenario_instructions: Vec::new(),
104 scenario_ptr: 0,
105 }
106 }
107
108 pub fn load_contract(&mut self, bytes: &[u8]) -> anyhow::Result<()> {
110 let module = Module::new(&self.engine, bytes)?;
111 let mut linker = Linker::new(&self.engine);
112
113 linker.func_wrap("env", "storage_load", |_caller: Caller<'_, VmState>, _slot: i32| {
115 })?;
117
118 self.instance = Some(linker.instantiate(&mut self.store, &module)?);
119 self.module = Some(module);
120 Ok(())
121 }
122
123 pub fn step(&mut self) -> bool {
127 if self.reverted {
128 return false;
129 }
130
131 let fuel_before = self.store.fuel_consumed().unwrap_or(0);
132
133 if !self.scenario_instructions.is_empty() {
134 if self.scenario_ptr >= self.scenario_instructions.len() {
135 return false;
136 }
137 let inst = self.scenario_instructions[self.scenario_ptr].clone();
138 self.dispatch(&inst);
139 self.scenario_ptr += 1;
140 return true;
141 }
142
143 if self.instance.is_none() {
144 return false;
145 }
146
147 self.pc += 1;
149 let fuel_after = self.store.fuel_consumed().unwrap_or(0);
150 let gas_used = fuel_after.saturating_sub(fuel_before);
151
152 self.emit("WASM_STEP", gas_used, vec![], None);
154
155 true
156 }
157
158 pub fn load_instructions(&mut self, instructions: Vec<Instruction>) {
159 self.scenario_instructions = instructions;
160 self.scenario_ptr = 0;
161 }
162
163 pub fn execute(&mut self, instructions: &[Instruction]) -> VmResult {
164 self.load_instructions(instructions.to_vec());
165 while self.step() {}
166
167 let state = self.store.data();
168 VmResult {
169 trace: self.trace.clone(),
170 final_storage: state.storage.clone(),
171 reverted: self.reverted,
172 revert_reason: self.revert_reason.clone(),
173 }
174 }
175
176 fn dispatch(&mut self, inst: &Instruction) {
177 let source = match inst {
178 Instruction::Push(_) => Some("let amount = msg::value();".to_string()),
179 Instruction::Store { .. } => Some("balance += amount;".to_string()),
180 Instruction::Load { .. } => Some("let current = balance;".to_string()),
181 Instruction::Call { target } => Some(format!("contract.{target}();")),
182 Instruction::Log { .. } => Some("emit Deposited(msg::sender(), amount);".to_string()),
183 Instruction::Add => Some("a + b".to_string()),
184 Instruction::Sub => Some("a - b".to_string()),
185 Instruction::RequireGte { reason } => Some(format!("require!(a >= b, \"{reason}\");")),
186 Instruction::Transfer { .. } => Some("msg::sender().transfer(amount);".to_string()),
187 Instruction::Revert { reason } => Some(format!("revert(\"{reason}\");")),
188 };
189
190 match inst {
191 Instruction::Push(val) => {
192 self.store.data_mut().stack.push(val.clone());
193 self.emit("PUSH", GasCost::PUSH, vec![], source);
194 }
195
196 Instruction::Store { slot } => {
197 let value = self.store.data_mut().stack.pop().unwrap_or_else(|| "0x00".into());
198 let old = self.store.data().storage.get(slot).cloned();
199 let gas = if old.is_some() {
200 GasCost::SSTORE_WARM
201 } else {
202 GasCost::SSTORE_COLD
203 };
204 self.store.data_mut().storage.insert(slot.clone(), value.clone());
205 self.emit(
206 "SSTORE",
207 gas,
208 vec![StorageChange {
209 key: slot.clone(),
210 old,
211 new: Some(value),
212 }],
213 source,
214 );
215 }
216
217 Instruction::Load { slot } => {
218 let value = self.store.data().storage.get(slot).cloned().unwrap_or("0x00".into());
219 let gas = if self.trace.iter().any(|e| {
220 e.opcode == "SLOAD"
221 && e.storage_diff.is_empty()
222 && e.stack.last().map(|v| &v.hex) == Some(&value)
223 }) {
224 GasCost::SLOAD_WARM
225 } else {
226 GasCost::SLOAD_COLD
227 };
228 self.store.data_mut().stack.push(value);
229 self.emit("SLOAD", gas, vec![], source);
230 }
231 Instruction::Call { target } => {
232 self.emit("CALL", GasCost::CALL, vec![], source);
233 let frame = format!("call:{target}");
234 self.store.data_mut().memory.extend_from_slice(frame.as_bytes());
235 }
236 Instruction::Log { topic } => {
237 self.store.data_mut().memory.extend_from_slice(topic.as_bytes());
238 self.emit("LOG", GasCost::LOG, vec![], source);
239 }
240
241 Instruction::Add => {
242 let b = self.pop_u128();
243 let a = self.pop_u128();
244 self.store.data_mut().stack.push(format!("0x{:X}", a.wrapping_add(b)));
245 self.emit("ADD", GasCost::ADD, vec![], source);
246 }
247
248 Instruction::Sub => {
249 let b = self.pop_u128();
250 let a = self.pop_u128();
251 self.store.data_mut().stack.push(format!("0x{:X}", a.wrapping_sub(b)));
252 self.emit("SUB", GasCost::SUB, vec![], source);
253 }
254
255 Instruction::RequireGte { reason } => {
256 let b = self.pop_u128();
257 let a = self.pop_u128();
258 if a < b {
259 self.revert_reason = Some(reason.clone());
260 self.reverted = true;
261 self.store.data_mut().memory = reason.as_bytes().to_vec();
262 self.emit("REVERT", GasCost::REVERT, vec![], source);
263 } else {
264 self.store.data_mut().stack.push(format!("0x{:X}", a));
265 self.store.data_mut().stack.push(format!("0x{:X}", b));
266 }
267 }
268 Instruction::Transfer { to } => {
269 let _amount = self.store.data_mut().stack.pop().unwrap_or("0x00".into());
270 self.store.data_mut().memory.extend_from_slice(format!("transfer:{to}").as_bytes());
271 self.emit("CALL", GasCost::CALL_WITH_VALUE, vec![], source);
272 }
273
274 Instruction::Revert { reason } => {
275 self.revert_reason = Some(reason.clone());
276 self.reverted = true;
277 self.store.data_mut().memory = reason.as_bytes().to_vec();
278 self.emit("REVERT", GasCost::REVERT, vec![], source);
279 }
280 }
281 }
282
283 fn emit(&mut self, opcode: &str, gas_used: u64, storage_diff: Vec<StorageChange>, source_line: Option<String>) {
284 let state = self.store.data();
285 let stack_snapshot: Vec<Value> =
286 state.stack.iter().map(|v| Value { hex: v.clone() }).collect();
287
288 self.trace.push(ExecutionEvent {
289 step: self.pc,
290 opcode: opcode.into(),
291 gas_used,
292 stack: stack_snapshot,
293 memory: MemorySnapshot {
294 bytes: state.memory.clone(),
295 },
296 storage_diff,
297 source_line,
298 });
299 self.pc += 1;
300 }
301
302 fn pop_u128(&mut self) -> u128 {
303 let hex = self.store.data_mut().stack.pop().unwrap_or("0x00".into());
304 let stripped = hex.trim_start_matches("0x").trim_start_matches("0X");
305 u128::from_str_radix(stripped, 16).unwrap_or(0)
306 }
307
308 pub fn stack(&self) -> &[String] {
309 &self.store.data().stack
310 }
311
312 pub fn storage(&self) -> &BTreeMap<String, String> {
313 &self.store.data().storage
314 }
315
316 pub fn trace(&self) -> &[ExecutionEvent] {
317 &self.trace
318 }
319
320 pub fn current_ptr(&self) -> usize {
321 0 }
323
324 pub fn is_reverted(&self) -> bool {
325 self.reverted
326 }
327}
328
329#[derive(Debug, Clone, serde::Serialize)]
331pub struct VmResult {
332 pub trace: Vec<ExecutionEvent>,
333 pub final_storage: BTreeMap<String, String>,
334 pub reverted: bool,
335 pub revert_reason: Option<String>,
336}
337
338pub struct ScenarioCompiler;
343
344impl ScenarioCompiler {
345 pub fn compile(scenario: &str, params: &ScenarioParams) -> Result<Vec<Instruction>, String> {
347 match scenario {
348 "deposit_and_withdraw" => Ok(Self::deposit_and_withdraw(params)),
349 "overflow_withdraw" => Ok(Self::overflow_withdraw(params)),
350 "double_deposit" => Ok(Self::double_deposit(params)),
351 _ => Err(format!("unknown scenario: {scenario}")),
352 }
353 }
354
355 fn deposit_and_withdraw(params: &ScenarioParams) -> Vec<Instruction> {
356 let deposit_amt = params.deposit_amount.unwrap_or(100);
357 let withdraw_amt = params.withdraw_amount.unwrap_or(deposit_amt / 2);
358
359 vec![
360 Instruction::Push(format!("0x{:X}", deposit_amt)),
362 Instruction::Call {
363 target: "deposit".into(),
364 },
365 Instruction::Load {
367 slot: "balance".into(),
368 },
369 Instruction::Push(format!("0x{:X}", deposit_amt)),
370 Instruction::Add,
371 Instruction::Store {
372 slot: "balance".into(),
373 },
374 Instruction::Log {
375 topic: "Deposited".into(),
376 },
377 Instruction::Push(format!("0x{:X}", withdraw_amt)),
379 Instruction::Call {
380 target: "withdraw".into(),
381 },
382 Instruction::Load {
384 slot: "balance".into(),
385 },
386 Instruction::Push(format!("0x{:X}", withdraw_amt)),
387 Instruction::RequireGte {
388 reason: "insufficient balance".into(),
389 },
390 Instruction::Load {
392 slot: "balance".into(),
393 },
394 Instruction::Push(format!("0x{:X}", withdraw_amt)),
395 Instruction::Sub,
396 Instruction::Store {
397 slot: "balance".into(),
398 },
399 Instruction::Push(format!("0x{:X}", withdraw_amt)),
401 Instruction::Transfer {
402 to: "msg.sender".into(),
403 },
404 Instruction::Log {
405 topic: "Withdrawn".into(),
406 },
407 ]
408 }
409
410 fn overflow_withdraw(params: &ScenarioParams) -> Vec<Instruction> {
411 let deposit_amt = params.deposit_amount.unwrap_or(10);
412 let withdraw_amt = params.withdraw_amount.unwrap_or(255);
413
414 vec![
415 Instruction::Push(format!("0x{:X}", deposit_amt)),
417 Instruction::Call {
418 target: "deposit".into(),
419 },
420 Instruction::Load {
421 slot: "balance".into(),
422 },
423 Instruction::Push(format!("0x{:X}", deposit_amt)),
424 Instruction::Add,
425 Instruction::Store {
426 slot: "balance".into(),
427 },
428 Instruction::Push(format!("0x{:X}", withdraw_amt)),
430 Instruction::Call {
431 target: "withdraw".into(),
432 },
433 Instruction::Load {
434 slot: "balance".into(),
435 },
436 Instruction::Push(format!("0x{:X}", withdraw_amt)),
437 Instruction::RequireGte {
439 reason: "insufficient balance".into(),
440 },
441 ]
442 }
443
444 fn double_deposit(params: &ScenarioParams) -> Vec<Instruction> {
445 let first = params.deposit_amount.unwrap_or(50);
446 let second = params.withdraw_amount.unwrap_or(75); vec![
449 Instruction::Push(format!("0x{:X}", first)),
451 Instruction::Call {
452 target: "deposit".into(),
453 },
454 Instruction::Load {
455 slot: "balance".into(),
456 },
457 Instruction::Push(format!("0x{:X}", first)),
458 Instruction::Add,
459 Instruction::Store {
460 slot: "balance".into(),
461 },
462 Instruction::Log {
463 topic: "Deposited".into(),
464 },
465 Instruction::Push(format!("0x{:X}", second)),
467 Instruction::Call {
468 target: "deposit".into(),
469 },
470 Instruction::Load {
471 slot: "balance".into(),
472 },
473 Instruction::Push(format!("0x{:X}", second)),
474 Instruction::Add,
475 Instruction::Store {
476 slot: "balance".into(),
477 },
478 Instruction::Log {
479 topic: "Deposited".into(),
480 },
481 ]
482 }
483}
484
485#[derive(Debug, Clone, Default)]
487pub struct ScenarioParams {
488 pub deposit_amount: Option<u128>,
489 pub withdraw_amount: Option<u128>,
490}