cratefield_module_guardrails/simulate.rs
1//! The `Simulator` port: what a request would actually do, before it is
2//! allowed to do it.
3//!
4//! Simulation is not optional. A policy that reasons only over the request
5//! sees the transaction a caller wrote; a policy that reasons over a
6//! simulation sees what the chain would do — the approvals a flash-loan
7//! callback would take, the delegatecalls a proxy would make, the balances
8//! that would actually move. An adapter wraps `eth_simulateV1` (state-overridden
9//! to the acting address, with the trace enabled) for EVM chains and
10//! `simulateTransaction` for Solana; this crate ships the port and the
11//! engine, no RPC client.
12
13use async_trait::async_trait;
14use serde::Serialize;
15
16use crate::action::{Request, U256};
17
18/// The kind of a call frame in the simulation trace.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
20pub enum CallKind {
21 /// A plain call.
22 Call,
23 /// A delegatecall: runs foreign code as the caller. Only ever safe into
24 /// an allowlisted contract.
25 DelegateCall,
26 /// A read-only call.
27 StaticCall,
28 /// Contract creation.
29 Create,
30}
31
32/// One balance change the simulation observed: `delta` in the token's
33/// smallest unit, negative for a debit.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35pub struct BalanceChange {
36 /// Whose balance moved.
37 pub owner: String,
38 /// Which token, or `native`.
39 pub token: String,
40 /// The signed delta.
41 pub delta: i128,
42}
43
44/// One approval the simulation observed being taken.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
46pub struct ApprovalChange {
47 /// The token, lowercase hex or `native`.
48 pub token: String,
49 /// The approving owner.
50 pub owner: String,
51 /// The spender that received the allowance.
52 pub spender: String,
53 /// The allowance amount.
54 pub amount: U256,
55}
56
57impl ApprovalChange {
58 /// Whether the amount crosses the unlimited line (2^128).
59 #[must_use]
60 pub fn is_unlimited(&self) -> bool {
61 crate::action::u256_is_unlimited(&self.amount)
62 }
63}
64
65/// One frame of the simulation's call trace.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
67pub struct CallFrame {
68 /// How the frame was entered.
69 pub kind: CallKind,
70 /// The target, lowercase hex.
71 pub to: String,
72}
73
74/// What the simulation observed. `success == false` is a revert: denied, not
75/// an error — a revert is an answer about the request, an unavailable
76/// simulator is a control-plane fault.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
78pub struct SimulationReport {
79 /// Whether the execution succeeded.
80 pub success: bool,
81 /// Every balance change observed.
82 pub balance_changes: Vec<BalanceChange>,
83 /// Every approval observed.
84 pub approvals: Vec<ApprovalChange>,
85 /// The call trace.
86 pub calls: Vec<CallFrame>,
87}
88
89impl SimulationReport {
90 /// A clean report: success, nothing observed.
91 #[must_use]
92 pub fn clean() -> Self {
93 Self {
94 success: true,
95 balance_changes: Vec::new(),
96 approvals: Vec::new(),
97 calls: Vec::new(),
98 }
99 }
100
101 /// A revert.
102 #[must_use]
103 pub fn reverted() -> Self {
104 Self {
105 success: false,
106 ..Self::clean()
107 }
108 }
109}
110
111/// Why the simulation could not be produced. A reverted execution is *not*
112/// an error — it is [`SimulationReport::reverted`].
113#[derive(Debug, Clone, thiserror::Error)]
114pub enum SimError {
115 /// The endpoint could not be reached or refused the call.
116 #[error("simulator unavailable: {0}")]
117 Unavailable(String),
118 /// The endpoint's reply did not decode into a report.
119 #[error("simulator reply malformed: {0}")]
120 Malformed(String),
121}
122
123/// The port the engine requires an implementation of. Adapters wrap the
124/// provider's simulation endpoints; the fakes in this crate script the
125/// reports tests need.
126#[async_trait]
127pub trait Simulator: Send + Sync {
128 /// Simulates the request against current state.
129 ///
130 /// # Errors
131 ///
132 /// When the simulation cannot be produced at all (unreachable endpoint,
133 /// undecodable reply). The engine denies on any error: fail closed.
134 async fn simulate(&self, request: &Request) -> Result<SimulationReport, SimError>;
135}