risc0_steel/
contract.rs

1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::{state::WrapStateDb, EvmFactory, GuestEvmEnv};
16use alloy_evm::Evm;
17use alloy_primitives::Address;
18use alloy_sol_types::{SolCall, SolType};
19use anyhow::anyhow;
20use revm::context::result::{ExecutionResult, ResultAndState, SuccessReason};
21use std::{fmt::Debug, marker::PhantomData};
22
23/// Represents a contract instance for interacting with EVM environments.
24///
25/// This struct provides a way to interact with a deployed smart contract
26/// at a specific `address` within a given EVM environment `E`.
27///
28/// **Note:** This contract interaction is not type-safe regarding the ABI.
29/// Ensure the deployed contract at `address` matches the ABI used for calls (`S: SolCall`).
30///
31/// ### Usage Scenarios
32///
33/// 1. **Host (Preflight):** Use [Contract::preflight] to set up calls on the host environment. The
34///    environment can be initialized using the [EthEvmEnv::builder] or [EvmEnv::builder]. This
35///    fetches necessary state and prepares proofs for guest execution.
36///     - Consider [CallBuilder::call_with_prefetch] for calls with many storage accesses to
37///       potentially optimize preflight time by reducing RPC calls.
38/// 2. **Guest:** Use [Contract::new] within the guest environment, typically initialized from
39///    [EvmInput::into_env].
40///
41///
42/// ### Making Contract Calls (Host Preflight or Guest Execution)
43///
44/// To interact with the contract's functions, you use the [Contract::call_builder] method to
45/// prepare a call.
46/// This follows a specific workflow:
47///
48/// 1. **Create Builder:** Call [Contract::call_builder] with a specific Solidity function call
49///    object (e.g., `MyCall { arg1: ..., arg2: ... }` derived using `alloy_sol_types::sol!`). This
50///    returns a [CallBuilder] instance, initializing its internal transaction environment (`tx`)
51///    with the contract address and call data.
52///
53/// 2.  **Configure Transaction:** Because the underlying transaction type (`EvmFactory::Tx`) is
54///     generic, configuration parameters (like caller address, value, gas limit, nonce)
55///     are set by **directly modifying the public `.tx` field** of the returned [CallBuilder]
56///     instance.
57///     ```rust,no_run
58///     # use risc0_steel::{ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}, Contract};
59///     # use alloy_primitives::{Address, U256};
60///     # use alloy_sol_types::sol;
61///     # sol! { interface Test { function test() external view returns (uint); } }
62///     # #[tokio::main(flavor = "current_thread")]
63///     # async fn main() -> anyhow::Result<()> {
64///     # let rpc_url = "https://ethereum-rpc.publicnode.com".parse()?;
65///     # let mut env = EthEvmEnv::builder().rpc(rpc_url).chain_spec(&ETH_MAINNET_CHAIN_SPEC).build().await?;
66///     # let mut contract = Contract::preflight(Address::ZERO, &mut env);
67///     # let my_call = Test::testCall {};
68///     let mut builder = contract.call_builder(&my_call);
69///     builder.tx.caller = Address::ZERO;
70///     builder.tx.value = U256::from(0); // Set value if payable
71///     builder.tx.gas_limit = 100_000;
72///     // ... set other fields like gas_price, nonce as needed
73///     # Ok(())
74///     # }
75///     ```
76///     **Note:** Fluent configuration methods like `.from(address)` or `.value(amount)` are
77///     **not available** directly on the `CallBuilder` due to this generic design. You must
78///     use direct field access on `.tx`. Consult the documentation of the specific `Tx`
79///     type provided by your chosen [`EvmFactory`] for available fields (e.g., `revm::primitives::TxEnv`).
80///
81/// 3. **Execute Call:** Once configured, execute the call using the appropriate method on the
82///    [`CallBuilder`] instance. Common methods include:
83///     - `.call()`: Executes in the guest, panicking on EVM errors.
84///     - `.try_call()`: Executes in the guest, returning a `Result` for error handling.
85///     - `.call().await`: Executes preflight on the host (requires `host` feature).
86///     - `.call_with_prefetch().await`: Executes preflight on the host, potentially optimizing
87///       state loading (requires `host` feature).
88///
89/// See the [`CallBuilder`] documentation for more details on execution methods.
90///
91/// ### Examples
92///
93/// ```rust,no_run
94/// # use risc0_steel::{ethereum::{EthEvmInput, EthEvmEnv, ETH_MAINNET_CHAIN_SPEC}, Contract, host::BlockNumberOrTag};
95/// # use alloy_primitives::{Address, address};
96/// # use alloy_sol_types::sol;
97/// # use url::Url;
98/// # #[tokio::main(flavor = "current_thread")]
99/// # async fn main() -> anyhow::Result<()> {
100///  const CONTRACT_ADDRESS: Address = address!("dAC17F958D2ee523a2206206994597C13D831ec7"); // USDT
101///  const ACCOUNT_TO_QUERY: Address = address!("F977814e90dA44bFA03b6295A0616a897441aceC"); // Binance
102///  sol! {
103///     interface IERC20 {
104///         function balanceOf(address account) external view returns (uint);
105///     }
106/// }
107/// const CALL: IERC20::balanceOfCall = IERC20::balanceOfCall { account: ACCOUNT_TO_QUERY };
108///
109/// // === Host Setup ===
110/// let rpc_url = "https://ethereum-rpc.publicnode.com".parse()?;
111/// let mut host_env = EthEvmEnv::builder().rpc(rpc_url).chain_spec(&ETH_MAINNET_CHAIN_SPEC).build().await?;
112///
113/// // Preflight the call on the host
114/// let mut contract_host = Contract::preflight(CONTRACT_ADDRESS, &mut host_env);
115/// let mut builder = contract_host.call_builder(&CALL);
116/// // Configure via builder.tx
117/// builder.tx.caller = Address::default();
118/// builder.tx.gas_limit = 10_000;
119/// // Execute
120/// let balance_result = builder.call().await?;
121/// println!("Host preflight balance: {}", balance_result);
122///
123/// // Generate input for the guest
124/// let evm_input = host_env.into_input().await?;
125///
126/// // === Guest Setup & Execution ===
127/// // (Inside the RISC Zero guest)
128/// # {
129/// let guest_env = evm_input.into_env(&ETH_MAINNET_CHAIN_SPEC);
130/// let contract_guest = Contract::new(CONTRACT_ADDRESS, &guest_env);
131///
132/// // Execute the same call in the guest
133/// let mut builder = contract_guest.call_builder(&CALL);
134/// builder.tx.caller = Address::default();
135/// builder.tx.gas_limit = 10_000;
136/// let guest_balance_result = builder.call();
137/// println!("Guest execution balance: {}", guest_balance_result);
138/// # }
139/// # Ok(())
140/// # }
141/// ```
142///
143/// [EthEvmEnv::builder]: crate::ethereum::EthEvmEnv
144/// [EvmEnv::builder]: crate::EvmEnv
145/// [EvmInput::into_env]: crate::EvmInput::into_env
146pub struct Contract<E> {
147    address: Address,
148    env: E,
149}
150
151impl<'a, F: EvmFactory> Contract<&'a GuestEvmEnv<F>> {
152    /// Creates a `Contract` instance for use within the guest environment.
153    ///
154    /// The `env` should typically be obtained via [EvmInput::into_env].
155    ///
156    /// [EvmInput::into_env]: crate::EvmInput::into_env
157    pub fn new(address: Address, env: &'a GuestEvmEnv<F>) -> Self {
158        Self { address, env }
159    }
160
161    /// Initializes a builder for executing a specific contract call (`S`) in the guest.
162    pub fn call_builder<S: SolCall>(&self, call: &S) -> CallBuilder<F::Tx, S, &GuestEvmEnv<F>> {
163        CallBuilder::new(F::new_tx(self.address, call.abi_encode().into()), self.env)
164    }
165}
166
167/// Represents a prepared EVM contract call, ready for configuration and execution.
168///
169/// Instances are created via [Contract::call_builder]. The primary interaction
170/// involves configuring the transaction parameters via the public [CallBuilder::tx] field,
171/// followed by invoking an execution method like `.call()`.
172///
173/// See the documentation on the [Contract] struct for a detailed explanation of the
174/// configuration workflow and examples.
175#[derive(Debug, Clone)]
176#[must_use = "CallBuilder does nothing unless an execution method like `.call()` is called"]
177pub struct CallBuilder<T, S, E> {
178    /// The transaction environment (`EvmFactory::Tx`) containing call parameters.
179    ///
180    /// **Configuration:** This field holds the transaction details (caller, value, gas, etc.).
181    /// It **must be configured directly** by modifying its members *before* calling an
182    /// execution method.
183    ///
184    /// Example: `builder.tx.caller = MY_ADDRESS; builder.tx.gas_limit = 100_000;`
185    pub tx: T,
186    /// The EVM environment (either host or guest).
187    env: E,
188    /// Phantom data for the `SolCall` type `S`.
189    phantom: PhantomData<S>,
190}
191
192impl<T, S: SolCall, E> CallBuilder<T, S, E> {
193    /// Compile-time assertion that the call has a return value.
194    const RETURNS: () = assert!(
195        std::mem::size_of::<S::Return>() > 0,
196        "Function call must have a return value"
197    );
198
199    fn new(tx: T, env: E) -> Self {
200        #[allow(clippy::let_unit_value)]
201        let _ = Self::RETURNS;
202
203        Self {
204            tx,
205            env,
206            phantom: PhantomData,
207        }
208    }
209}
210
211#[cfg(feature = "host")]
212mod host {
213    use super::*;
214    use crate::{
215        ethereum::EthEvmFactory,
216        host::{db::ProviderDb, HostEvmEnv},
217    };
218    use alloy::{
219        eips::eip2930::AccessList,
220        network::{Ethereum, Network, TransactionBuilder},
221        providers::Provider,
222    };
223    use anyhow::{anyhow, Context, Result};
224
225    impl<'a, F, D, C> Contract<&'a mut HostEvmEnv<D, F, C>>
226    where
227        F: EvmFactory,
228    {
229        /// Creates a `Contract` instance for use on the host for preflighting calls.
230        ///
231        /// This prepares the environment for simulating the call, fetching necessary
232        /// state via the `Provider` within `env`, and enabling proof generation
233        /// via [HostEvmEnv::into_input].
234        pub fn preflight(address: Address, env: &'a mut HostEvmEnv<D, F, C>) -> Self {
235            Self { address, env }
236        }
237
238        /// Initializes a builder for preflighting a specific contract call (`S`) on the host.
239        pub fn call_builder<S: SolCall>(
240            &mut self,
241            call: &S,
242        ) -> CallBuilder<F::Tx, S, &mut HostEvmEnv<D, F, C>> {
243            CallBuilder::new(F::new_tx(self.address, call.abi_encode().into()), self.env)
244        }
245    }
246
247    // Methods applicable when using ProviderDb on the host
248    impl<S, F, N, P, C> CallBuilder<F::Tx, S, &mut HostEvmEnv<ProviderDb<N, P>, F, C>>
249    where
250        N: Network,
251        P: Provider<N> + Send + Sync + 'static,
252        S: SolCall + Send + Sync + 'static,
253        <S as SolCall>::Return: Send,
254        F: EvmFactory,
255    {
256        /// Prefetches state for a given EIP-2930 `AccessList` on the host.
257        ///
258        /// Fetches EIP-1186 storage proofs for the items
259        /// in the `access_list`. This can reduce the number of individual RPC calls
260        /// (`eth_getStorageAt`) needed during subsequent execution simulation if the
261        /// accessed slots are known beforehand.
262        ///
263        /// This method *only* fetches data; it does *not* set the access list field
264        /// on the transaction itself (EIP-2930).
265        ///
266        /// ### Usage
267        /// Useful when an access list is already available. For automatic generation
268        /// and prefetching, see [`CallBuilder::call_with_prefetch`].
269        ///
270        /// ### Example
271        /// ```rust,no_run
272        /// # use risc0_steel::{ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}, Contract};
273        /// # use alloy_primitives::address;
274        /// # use alloy_sol_types::sol;
275        /// # use alloy::eips::eip2930::AccessList;
276        /// # use url::Url;
277        /// # sol! { interface Test { function test() external view returns (uint); } }
278        /// # #[tokio::main(flavor = "current_thread")]
279        /// # async fn main() -> anyhow::Result<()> {
280        /// # let rpc_url = "https://ethereum-rpc.publicnode.com".parse()?;
281        /// # let mut env = EthEvmEnv::builder().rpc(rpc_url).chain_spec(&ETH_MAINNET_CHAIN_SPEC).build().await?;
282        /// # let contract_address = address!("0x0000000000000000000000000000000000000000");
283        /// # let call = Test::testCall {};
284        /// # let access_list = AccessList::default();
285        /// let mut contract = Contract::preflight(contract_address, &mut env);
286        /// let builder = contract.call_builder(&call).prefetch_access_list(access_list).await?;
287        /// let result = builder.call().await?;
288        /// # Ok(())
289        /// # }
290        /// ```
291        pub async fn prefetch_access_list(self, access_list: AccessList) -> Result<Self> {
292            let db = self.env.db_mut();
293            db.add_access_list(access_list).await?;
294
295            Ok(self)
296        }
297
298        /// Executes the configured call during host preflight.
299        ///
300        /// This simulates the transaction execution using `revm` within a blocking thread
301        /// (via [`tokio::task::spawn_blocking`]) to avoid blocking the async runtime.
302        /// It uses the state fetched (and potentially prefetched) into the `ProviderDb`.
303        ///
304        /// Returns the decoded return value of the call or an error if execution fails.
305        pub async fn call(self) -> Result<S::Return> {
306            log::info!("Executing preflight calling '{}'", S::SIGNATURE);
307
308            // as mutable references are not possible, the DB must be moved in and out of the task
309            let mut db = self.env.db.take().unwrap();
310
311            let chain_id = self.env.chain_id;
312            let spec = self.env.spec;
313            let header = self.env.header.inner().clone();
314            let (result, db) = tokio::task::spawn_blocking(move || {
315                let exec_result = {
316                    let mut evm = F::create_evm(&mut db, chain_id, spec, &header);
317                    transact::<_, F, S>(self.tx, &mut evm)
318                };
319                (exec_result, db)
320            })
321            .await
322            .expect("EVM execution panicked");
323
324            // restore the DB before handling errors, so that we never return an env without a DB
325            self.env.db = Some(db);
326
327            result.map_err(|err| anyhow!("call '{}' failed: {}", S::SIGNATURE, err))
328        }
329    }
330
331    // Methods specific to Ethereum network + EthEvmFactory (e.g., eth_createAccessList)
332    impl<S, P, C>
333        CallBuilder<
334            <EthEvmFactory as EvmFactory>::Tx,
335            S,
336            &mut HostEvmEnv<ProviderDb<Ethereum, P>, EthEvmFactory, C>,
337        >
338    where
339        S: SolCall + Send + Sync + 'static,
340        <S as SolCall>::Return: Send,
341        P: Provider<Ethereum> + Send + Sync + 'static,
342    {
343        /// Automatically creates and prefetches an EIP-2930 access list, then executes the call.
344        ///
345        /// This method aims to optimize host preflight time for calls involving numerous
346        /// storage reads (`SLOAD`). It performs the following steps:
347        /// 1. Calls `eth_createAccessList` RPC to determine the storage slots and accounts the
348        ///    transaction is likely to access.
349        /// 2. Calls [CallBuilder::prefetch_access_list] with the generated list to fetch the
350        ///    required state efficiently (often in a single batch RPC).
351        /// 3. Executes the call simulation using [CallBuilder::call].
352        ///
353        /// ### Trade-offs
354        /// - **Node Compatibility:** Relies on the `eth_createAccessList` RPC, which might not be
355        ///   available or fully supported on all Ethereum node software or chains.
356        /// - **Gas Estimation Issues:** Some node implementations might perform gas checks or
357        ///   require sufficient balance in the `from` account for `eth_createAccessList`, even for
358        ///   view calls. Setting a relevant `from` address  might be necessary.
359        ///
360        /// ### Example
361        /// ```rust,no_run
362        /// # use risc0_steel::{ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}, Contract};
363        /// # use alloy_primitives::address;
364        /// # use alloy_sol_types::sol;
365        /// # use url::Url;
366        /// # sol! { interface Test { function test() external view returns (uint); } }
367        /// # #[tokio::main(flavor = "current_thread")]
368        /// # async fn main() -> anyhow::Result<()> {
369        /// # let rpc_url = "https://ethereum-rpc.publicnode.com".parse()?;
370        /// # let mut env = EthEvmEnv::builder().rpc(rpc_url).chain_spec(&ETH_MAINNET_CHAIN_SPEC).build().await?;
371        /// # let contract_address = address!("0x0000000000000000000000000000000000000000");
372        /// # let call = Test::testCall {};
373        /// let mut contract = Contract::preflight(contract_address, &mut env);
374        /// // Automatically generates access list, fetches state, and executes
375        /// let result = contract.call_builder(&call).call_with_prefetch().await?;
376        /// # Ok(())
377        /// # }
378        /// ```
379        pub async fn call_with_prefetch(self) -> Result<S::Return> {
380            let access_list = {
381                let tx_request = <Ethereum as Network>::TransactionRequest::default()
382                    .with_from(self.tx.caller)
383                    .with_gas_limit(self.tx.gas_limit)
384                    .with_gas_price(self.tx.gas_price)
385                    .with_kind(self.tx.kind)
386                    .with_value(self.tx.value)
387                    .with_input(self.tx.data.clone());
388
389                let db = self.env.db_mut();
390                let provider = db.inner().provider();
391                let hash = db.inner().block();
392
393                let access_list_result = provider
394                    .create_access_list(&tx_request)
395                    .hash(hash)
396                    .await
397                    .context("eth_createAccessList failed")?;
398
399                access_list_result.access_list
400            };
401
402            // Add the generated access list to the DB for prefetching
403            self.env
404                .db_mut()
405                .add_access_list(access_list)
406                .await
407                .context("failed to add generated access list")?;
408
409            self.call().await
410        }
411    }
412}
413
414impl<S, F> CallBuilder<F::Tx, S, &GuestEvmEnv<F>>
415where
416    S: SolCall,
417    F: EvmFactory,
418{
419    /// Executes the call within the guest environment, returning a `Result`.
420    ///
421    /// Use this if you need to handle potential EVM execution errors explicitly
422    /// (e.g., reverts, halts) within the guest. The error type is `String` for simplicity
423    /// in the guest context.
424    ///
425    /// For straightforward calls where failure should halt guest execution, prefer
426    /// [CallBuilder::call].
427    pub fn try_call(self) -> Result<S::Return, String> {
428        // create a temporary EVM instance for this call
429        let mut evm = F::create_evm(
430            // wrap the database and header for guest state access
431            WrapStateDb::new(self.env.db(), &self.env.header),
432            self.env.chain_id,
433            self.env.spec,
434            self.env.header.inner(),
435        );
436        // execute the transaction
437        transact::<_, F, S>(self.tx, &mut evm)
438    }
439
440    /// Executes the call within the guest environment, panicking on failure.
441    ///
442    /// This is a convenience wrapper around [CallBuilder::try_call]. It unwraps
443    /// the result, causing the guest to panic if the EVM call reverts, halts, or
444    /// encounters an error. Use this when a successful call is expected.
445    #[track_caller] // Improve panic message location
446    pub fn call(self) -> S::Return {
447        match self.try_call() {
448            Ok(value) => value,
449            Err(e) => panic!("Executing call '{}' failed: {}", S::SIGNATURE, e),
450        }
451    }
452}
453
454/// Executes a transaction using the provided EVM instance and decodes the result.
455/// Returns `Result<S::Return, String>` where `String` contains the error reason.
456fn transact<DB, F, S>(tx: F::Tx, evm: &mut F::Evm<DB>) -> Result<S::Return, String>
457where
458    DB: alloy_evm::Database,
459    F: EvmFactory,
460    S: SolCall,
461{
462    let ResultAndState { result, .. } = evm
463        .transact_raw(tx)
464        .map_err(|err| format!("EVM error: {:#}", anyhow!(err)))?;
465    let output_bytes = match result {
466        ExecutionResult::Success { reason, output, .. } => {
467            // ensure the transaction returned, not stopped or other success reason
468            if reason == SuccessReason::Return {
469                Ok(output)
470            } else {
471                Err(format!("succeeded but did not return (reason: {reason:?})"))
472            }
473        }
474        ExecutionResult::Revert { output, .. } => Err(format!("reverted with output: {output}")),
475        ExecutionResult::Halt { reason, .. } => Err(format!("halted: {reason:?}")),
476    }?;
477
478    // decode the successful return output
479    S::abi_decode_returns(&output_bytes.into_data()).map_err(|err| {
480        format!(
481            "Failed to decode return data, expected type '{}': {}",
482            <S::ReturnTuple<'_> as SolType>::SOL_NAME,
483            err
484        )
485    })
486}