An example that uses the ERC20 interface's balanceOf
method.
To get started, you need to have Rust installed. If you haven't done so, follow the instructions here.
Next, install the cargo risczero
tool. We'll use cargo binstall
to facilitate this. Detailed instructions can be found at cargo-binstall.
cargo install cargo-binstall
cargo binstall cargo-risczero
Finally, install the risc0
toolchain with the following command:
cargo risczero install
You'll also need access to an Ethereum RPC node, such as through Alchemy.
To run the example, which queries the USDT balance of 0x9737100D2F42a196DE56ED0d1f6fF598a250E7E4
on Sepolia, execute the following command:
RPC_URL=https://eth-sepolia.g.alchemy.com/v2/<API_KEY> RUST_LOG=info cargo run --release
The output should resemble the following:
2024-06-04T09:32:22.119650Z INFO risc0_steel::contract: Executing preflight for 'balanceOf(address)' on contract 0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0
For block 6037045 `balanceOf(address)` returns: 399534748753251
Running the guest with the constructed input:
View call result: 399534748753251
2024-06-04T09:32:24.152552Z INFO executor: risc0_zkvm::host::server::exec::executor: execution time: 218.576666ms
2024-06-04T09:32:24.152578Z INFO executor: risc0_zkvm::host::server::session: number of segments: 5
2024-06-04T09:32:24.152581Z INFO executor: risc0_zkvm::host::server::session: total cycles: 5242880
2024-06-04T09:32:24.152583Z INFO executor: risc0_zkvm::host::server::session: user cycles: 4126244
Here is a snippet of the relevant code of the guest:
/// Specify the function to call using the [`sol!`] macro.
/// This parses the Solidity syntax to generate a struct that implements the `SolCall` trait.
sol! {
/// ERC-20 balance function signature.
interface IERC20 {
function balanceOf(address account) external view returns (uint);
}
}
/// Function to call, implements the `SolCall` trait.
const CALL: IERC20::balanceOfCall = IERC20::balanceOfCall {
account: address!("9737100D2F42a196DE56ED0d1f6fF598a250E7E4"),
};
/// Address of the deployed contract to call the function on (USDT contract on Sepolia).
const CONTRACT: Address = address!("aA8E23Fb1079EA71e0a56F48a2aA51851D8433D0");
/// Address of the caller. If not provided, the caller will be the [CONTRACT].
const CALLER: Address = address!("f08A50178dfcDe18524640EA6618a1f965821715");
fn main() {
// Read the input from the guest environment.
let input: EthEvmInput = env::read();
// Converts the input into a `EvmEnv` for execution. The `with_chain_spec` method is used
// to specify the chain configuration. It checks that the state matches the state root in the
// header provided in the input.
let env = input.into_env().with_chain_spec(Ð_SEPOLIA_CHAIN_SPEC);
// Commit the block hash and number used when deriving `EvmEnv` to the journal.
env::commit_slice(&env.block_commitment().abi_encode());
// Execute the view call; it returns the result in the type generated by the `sol!` macro.
let contract = Contract::new(CONTRACT, &env);
let returns = contract.call_builder(&CALL).from(CALLER).call();
println!("View call result: {}", returns._0);
}
Here is a snippet to the relevant code on the host, it requires the same arguments as the guest:
// Create an EVM environment from an RPC endpoint and a block number. If no block number is
// provided, the latest block is used.
let mut env = EthEvmEnv::from_rpc(&args.rpc_url, None)?;
// The `with_chain_spec` method is used to specify the chain configuration.
env = env.with_chain_spec(Ð_SEPOLIA_CHAIN_SPEC);
// Preflight the call to prepare the input that is required to execute the function in
// the guest without RPC access. It also returns the result of the call.
let mut contract = Contract::preflight(CONTRACT, &mut env);
let returns = contract.call_builder(&CALL).from(CALLER).call()?;
// Finally, construct the input from the environment.
let input = env.into_input()?;