In Rust Stylus contracts, error handling is a crucial aspect of writing robust and reliable smart contracts. Rust differentiates between recoverable and unrecoverable errors. Recoverable errors are represented using the Result
type, which can either be Ok
, indicating success, or Err
, indicating failure. This allows developers to manage errors gracefully and maintain control over the flow of execution. Unrecoverable errors are handled with the panic!
macro, which stops execution, unwinds the stack, and returns a dataless error.
In Stylus contracts, error types are often explicitly defined, providing clear and structured ways to handle different failure scenarios. This structured approach promotes better error management, ensuring that contracts are secure, maintainable, and behave predictably under various conditions. Similar to Solidity and EVM, errors in Stylus will undo all changes made to the state during a transaction by reverting the transaction. Thus, there are two main types of errors in Rust Stylus contracts:
#[derive(SolidityError)]
alloy_sol_types::SolError
Error handling: Rust book
Recoverable errors are represented using the Result
type, which can either be Ok
, indicating success, or Err
, indicating failure. The Stylus SDK provides tools to define custom error types and manage recoverable errors effectively.
Here's a simplified Rust Stylus contract demonstrating how to define and handle recoverable errors:
1#![cfg_attr(not(feature = "export-abi"), no_main)]
2extern crate alloc;
3
4
5use alloy_sol_types::sol;
6use stylus_sdk::{abi::Bytes, alloy_primitives::{Address, U256}, call::RawCall, prelude::*};
7
8#[storage]
9#[entrypoint]
10pub struct MultiCall;
11
12// Declare events and Solidity error types
13sol! {
14 error ArraySizeNotMatch();
15 error CallFailed(uint256 call_index);
16}
17
18#[derive(SolidityError)]
19pub enum MultiCallErrors {
20 ArraySizeNotMatch(ArraySizeNotMatch),
21 CallFailed(CallFailed),
22}
23
24#[public]
25impl MultiCall {
26 pub fn multicall(
27 &self,
28 addresses: Vec<Address>,
29 data: Vec<Bytes>,
30 ) -> Result<Vec<Bytes>, MultiCallErrors> {
31 let addr_len = addresses.len();
32 let data_len = data.len();
33 let mut results: Vec<Bytes> = Vec::new();
34 if addr_len != data_len {
35 return Err(MultiCallErrors::ArraySizeNotMatch(ArraySizeNotMatch {}));
36 }
37 for i in 0..addr_len {
38 let result: Result<Vec<u8>, Vec<u8>> =
39 RawCall::new().call(addresses[i], data[i].to_vec().as_slice());
40 let data = match result {
41 Ok(data) => data,
42 Err(_data) => return Err(MultiCallErrors::CallFailed(CallFailed { call_index: U256::from(i) })),
43 };
44 results.push(data.into())
45 }
46 Ok(results)
47 }
48}
1#![cfg_attr(not(feature = "export-abi"), no_main)]
2extern crate alloc;
3
4
5use alloy_sol_types::sol;
6use stylus_sdk::{abi::Bytes, alloy_primitives::{Address, U256}, call::RawCall, prelude::*};
7
8#[storage]
9#[entrypoint]
10pub struct MultiCall;
11
12// Declare events and Solidity error types
13sol! {
14 error ArraySizeNotMatch();
15 error CallFailed(uint256 call_index);
16}
17
18#[derive(SolidityError)]
19pub enum MultiCallErrors {
20 ArraySizeNotMatch(ArraySizeNotMatch),
21 CallFailed(CallFailed),
22}
23
24#[public]
25impl MultiCall {
26 pub fn multicall(
27 &self,
28 addresses: Vec<Address>,
29 data: Vec<Bytes>,
30 ) -> Result<Vec<Bytes>, MultiCallErrors> {
31 let addr_len = addresses.len();
32 let data_len = data.len();
33 let mut results: Vec<Bytes> = Vec::new();
34 if addr_len != data_len {
35 return Err(MultiCallErrors::ArraySizeNotMatch(ArraySizeNotMatch {}));
36 }
37 for i in 0..addr_len {
38 let result: Result<Vec<u8>, Vec<u8>> =
39 RawCall::new().call(addresses[i], data[i].to_vec().as_slice());
40 let data = match result {
41 Ok(data) => data,
42 Err(_data) => return Err(MultiCallErrors::CallFailed(CallFailed { call_index: U256::from(i) })),
43 };
44 results.push(data.into())
45 }
46 Ok(results)
47 }
48}
SolidityError
Derive Macro: The #[derive(SolidityError)]
attribute is used for the MultiCallErrors
enum, automatically implementing the necessary traits for error handling.ArraySizeNotMatch
and CallFailed
is declared in MultiCallErrors
enum. CallFailed
error includes a call_index
parameter to indicate which call failed.multicall
function returns ArraySizeNotMatch
if the size of addresses and data vectors are not equal.multicall
function returns a CallFailed
error with the index of the failed call if any call fails. Note that we're using match to check if the result of the call is an error or a return data. We'll describe match pattern in the further sections.Here are various ways to handle such errors in the multicall
function, which calls multiple addresses and panics in different scenarios:
panic!
Directly panics if the call fails, including the index of the failed call.
1for i in 0..addr_len {
2 let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice());
3 let data = match result {
4 Ok(data) => data,
5 Err(_data) => panic!("Call to address {:?} failed at index {}", addresses[i], i),
6 };
7 results.push(data.into());
8}
1for i in 0..addr_len {
2 let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice());
3 let data = match result {
4 Ok(data) => data,
5 Err(_data) => panic!("Call to address {:?} failed at index {}", addresses[i], i),
6 };
7 results.push(data.into());
8}
Handling Call Failure with panic!
: The function panics if any call fails and the transaction will be reverted without any data.
unwrap
Uses unwrap
to handle the result, panicking if the call fails.
1for i in 0..addr_len {
2 let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice()).unwrap();
3 results.push(result.into());
4}
1for i in 0..addr_len {
2 let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice()).unwrap();
3 results.push(result.into());
4}
Handling Call Failure with unwrap
: The function uses unwrap
to panic if any call fails, including the index of the failed call.
match
Uses a match
statement to handle the result of call
, panicking if the call fails.
1for i in 0..addr_len {
2 let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice());
3 let data = match result {
4 Ok(data) => data,
5 Err(_data) => return Err(MultiCallErrors::CallFailed(CallFailed { call_index: U256::from(i) })),
6 };
7 results.push(data.into());
8}
1for i in 0..addr_len {
2 let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice());
3 let data = match result {
4 Ok(data) => data,
5 Err(_data) => return Err(MultiCallErrors::CallFailed(CallFailed { call_index: U256::from(i) })),
6 };
7 results.push(data.into());
8}
Handling Call Failure with match
: The function uses a match
statement to handle the result of call
, returning error if any call fails.
?
OperatorUses the ?
operator to propagate the error if the call fails, including the index of the failed call.
1for i in 0..addr_len {
2 let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice())
3 .map_err(|_| MultiCallErrors::CallFailed(CallFailed { call_index: U256::from(i) }))?;
4 results.push(result.into());
5}
1for i in 0..addr_len {
2 let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice())
3 .map_err(|_| MultiCallErrors::CallFailed(CallFailed { call_index: U256::from(i) }))?;
4 results.push(result.into());
5}
Handling Call Failure with ?
Operator: The function uses the ?
operator to propagate the error if any call fails, including the index of the failed call.
Each method demonstrates a different way to handle unrecoverable errors in the multicall
function of a Rust Stylus contract, providing a comprehensive approach to error management.
Note that as mentioned above, it is strongly recommended to use custom error handling instead of unrecoverable error handling.
The lib.rs code can be found at the top of the page in the recoverable error example section.
1[package]
2name = "stylus-multicall-contract"
3version = "0.1.7"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "=0.7.6"
8alloy-sol-types = "=0.7.6"
9stylus-sdk = "0.6.0"
10hex = "0.4.3"
11
12[dev-dependencies]
13tokio = { version = "1.12.0", features = ["full"] }
14ethers = "2.0"
15eyre = "0.6.8"
16
17[features]
18export-abi = ["stylus-sdk/export-abi"]
19
20[[bin]]
21name = "stylus-multicall-contract"
22path = "src/main.rs"
23
24[lib]
25crate-type = ["lib", "cdylib"]
1[package]
2name = "stylus-multicall-contract"
3version = "0.1.7"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "=0.7.6"
8alloy-sol-types = "=0.7.6"
9stylus-sdk = "0.6.0"
10hex = "0.4.3"
11
12[dev-dependencies]
13tokio = { version = "1.12.0", features = ["full"] }
14ethers = "2.0"
15eyre = "0.6.8"
16
17[features]
18export-abi = ["stylus-sdk/export-abi"]
19
20[[bin]]
21name = "stylus-multicall-contract"
22path = "src/main.rs"
23
24[lib]
25crate-type = ["lib", "cdylib"]