Skip to main content

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}