Arbitrum Stylus logo

Stylus by Example

Contract Calls in Stylus

The Stylus SDK provides Solidity ABI-equivalent contract calls, allowing you to interact with contracts without needing to know their internal implementations. By defining Solidity-like interfaces using the sol_interface! macro, you can easily invoke contracts in Stylus, whether you are using Rust or any other supported programming language.

For more info in sol_interface! macro, and how to use it on Rust Stylus contracts, please refer to interface page. Also you can find more info on how to extract interface from Stylus contracts in this page

For instance, this code defines the IService and ITree interfaces, allowing you to invoke their functions from a contract in Stylus.

1sol_interface! {
2    interface IService {
3        function makePayment(address user) payable external returns (string);
4        function getConstant() pure external returns (bytes32);
5    }
6
7    interface ITree {
8        // Define more interface methods here
9    }
10}
1sol_interface! {
2    interface IService {
3        function makePayment(address user) payable external returns (string);
4        function getConstant() pure external returns (bytes32);
5    }
6
7    interface ITree {
8        // Define more interface methods here
9    }
10}

Invoking Contracts Using the Interface

Once an interface is defined, you can call a contract's functions by using a Rust-style snake_case format. Here's how you can use the makePayment method from the IService interface:

1pub fn do_call(&mut self, account: IService, user: Address) -> Result<String, Vec<u8>> {
2    account.make_payment(self, user)  // Calls the method in snake_case
3}
1pub fn do_call(&mut self, account: IService, user: Address) -> Result<String, Vec<u8>> {
2    account.make_payment(self, user)  // Calls the method in snake_case
3}

Explanation: The make_payment method in Solidity is written in CamelCase but gets converted to snake_case when used in Rust. This allows you to call the function directly via the interface without worrying about the internal details of the target contract.


Configuring Gas and Value for Contract Calls

You can easily adjust gas and Ether value for contract calls by configuring the Call object in Stylus. This is similar to how you would configure a file or other resource in Rust by specifying parameters before executing the call.

1pub fn call_with_gas_value(&mut self, account: IService, user: Address) -> Result<String, Vec<u8>> {
2    let config = Call::new_in(self)
3        .gas(evm::gas_left() / 2)  // Assign half the available gas
4        .value(msg::value());      // Set the Ether value for the transaction
5
6    account.make_payment(config, user)
7}
1pub fn call_with_gas_value(&mut self, account: IService, user: Address) -> Result<String, Vec<u8>> {
2    let config = Call::new_in(self)
3        .gas(evm::gas_left() / 2)  // Assign half the available gas
4        .value(msg::value());      // Set the Ether value for the transaction
5
6    account.make_payment(config, user)
7}

Explanation: This function uses Call::new_in(self) to configure gas and value before making the call. In this case, half of the remaining gas is used, and the amount of Ether transferred in the transaction is passed through.


Handling Reentrancy and Managing Storage with TopLevelStorage

In Stylus, external contract calls can be made using:

  • Interface-based calls: These do not require explicit storage management and use &self or &mut self depending on the nature of the call (pure, view, or write).
  • Low-level calls: These involve Call::new() or Call::new_in() for more control over the call configuration. Call::new() can only be used when the contract is non-reentrant, while Call::new_in() is required for reentrant contracts.

Interface-Based Calls: pure, view, and write

When making calls using a contract interface, Stylus automatically handles storage based on whether the function is pure, view, or write. These calls use &self for read-only methods (pure and view) and &mut self for methods that modify the state (write).

Example:

1#[public]
2impl ExampleContract {
3    pub fn call_pure(&self, methods: IMethods) -> Result<(), Vec<u8>> {
4        Ok(methods.pure_foo(self)?)  // Read-only method using `&self`
5    }
6
7    pub fn call_view(&self, methods: IMethods) -> Result<(), Vec<u8>> {
8        Ok(methods.view_foo(self)?)  // Another read-only method using `&self`
9    }
10
11    pub fn call_write(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
12        methods.view_foo(&mut *self)?;  // Re-borrow self for reading
13        Ok(methods.write_foo(self)?)    // Modify state using `&mut self`
14    }
15}
1#[public]
2impl ExampleContract {
3    pub fn call_pure(&self, methods: IMethods) -> Result<(), Vec<u8>> {
4        Ok(methods.pure_foo(self)?)  // Read-only method using `&self`
5    }
6
7    pub fn call_view(&self, methods: IMethods) -> Result<(), Vec<u8>> {
8        Ok(methods.view_foo(self)?)  // Another read-only method using `&self`
9    }
10
11    pub fn call_write(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
12        methods.view_foo(&mut *self)?;  // Re-borrow self for reading
13        Ok(methods.write_foo(self)?)    // Modify state using `&mut self`
14    }
15}

Explanation:

  • call_pure and call_view are read-only operations and use &self to access the contract's state.
  • call_write uses &mut self to modify the contract's state since it requires mutable access to storage.

These calls are handled automatically by Stylus through the interface, so you don't need to worry about explicit storage management or the reentrancy state of the contract.

Low-Level Calls with Call::new_in() and Call::new()

When using low-level calls to interact with external contracts, Stylus provides two options:

  • Call::new_in(): Used when the reentrancy feature is enabled or when you need to pass storage explicitly for more control.
  • Call::new(): A simpler method for non-reentrant calls that do not require explicit storage management.

Using Call::new_in()

If reentrancy is enabled, you must use Call::new_in(), which requires passing a reference to storage. This ensures that the contract's state is properly managed during cross-contract calls.

1pub fn make_generic_call(
2    storage: &mut impl TopLevelStorage,  // Pass storage explicitly
3    account: IService,
4    user: Address,
5) -> Result<String, Vec<u8>> {
6    let config = Call::new_in(storage)  // Use explicit storage reference
7        .gas(evm::gas_left() / 2)      // Set gas limit
8        .value(msg::value());          // Set Ether value
9
10    account.make_payment(config, user)  // Execute the call with config
11}
1pub fn make_generic_call(
2    storage: &mut impl TopLevelStorage,  // Pass storage explicitly
3    account: IService,
4    user: Address,
5) -> Result<String, Vec<u8>> {
6    let config = Call::new_in(storage)  // Use explicit storage reference
7        .gas(evm::gas_left() / 2)      // Set gas limit
8        .value(msg::value());          // Set Ether value
9
10    account.make_payment(config, user)  // Execute the call with config
11}

Explanation: Call::new_in() is required for contracts with reentrancy enabled. You pass storage explicitly (storage: &mut impl TopLevelStorage), which gives more control over the call's configuration, including gas and Ether value.

Using Call::new() (When Reentrancy Is Disabled)

If the reentrancy feature is disabled, you can use Call::new() to simplify contract calls. This method does not require storage to be passed explicitly, making it easier to configure the call when reentrancy is not a concern. Note that you need to be sure that in Cargo.toml file, the Stylus SDK doesn't have reentrant flag.

1#![cfg_attr(not(feature = "export-abi"), no_main)]
2extern crate alloc;
3
4use stylus_sdk::{
5    alloy_primitives::Address,
6    call::{Call, Error},
7    evm, msg,
8    prelude::*,
9};
10
11sol_interface! {
12    interface IService {
13        function makePayment(address user) payable external returns (string);
14        function getConstant() pure external returns (bytes32);
15    }
16}
17
18#[storage]
19#[entrypoint]
20
21pub struct ExampleContract;
22
23#[public]
24impl ExampleContract {
25    pub fn do_call(account: IService, user: Address) -> Result<String, Error> {
26        let config = Call::new()
27            .gas(evm::gas_left() / 2)       // limit to half the gas left
28            .value(msg::value());           // set the callvalue
29    
30        account.make_payment(config, user) 
31    }
32}
33}
1#![cfg_attr(not(feature = "export-abi"), no_main)]
2extern crate alloc;
3
4use stylus_sdk::{
5    alloy_primitives::Address,
6    call::{Call, Error},
7    evm, msg,
8    prelude::*,
9};
10
11sol_interface! {
12    interface IService {
13        function makePayment(address user) payable external returns (string);
14        function getConstant() pure external returns (bytes32);
15    }
16}
17
18#[storage]
19#[entrypoint]
20
21pub struct ExampleContract;
22
23#[public]
24impl ExampleContract {
25    pub fn do_call(account: IService, user: Address) -> Result<String, Error> {
26        let config = Call::new()
27            .gas(evm::gas_left() / 2)       // limit to half the gas left
28            .value(msg::value());           // set the callvalue
29    
30        account.make_payment(config, user) 
31    }
32}
33}

Explanation: Call::new() is ideal for non-reentrant contracts, allowing you to configure gas and value without needing to manage storage explicitly. This method simplifies contract calls when there's no risk of reentrant attacks.

Reentrancy Considerations

  • Reentrancy Enabled: When the contract is reentrant (i.e., it can be called recursively), you must use Call::new_in() to explicitly manage storage. This prevents vulnerabilities by ensuring that contract state is handled safely during cross-contract calls.
  • Reentrancy Disabled: If the contract is non-reentrant, you can simplify the call process by using Call::new(), which does not require storage to be passed. This is suitable for straightforward external calls that do not involve recursive state changes.

Low-Level call and static_call

In addition to using sol_interface!, you can make low-level calls directly using the call and static_call functions. These methods provide raw access to contract interaction, allowing you to send calldata in the form of a Vec<u8>.

  • Low-Level call:
1pub fn execute_call(
2    &mut self,
3    contract: Address,
4    calldata: Vec<u8>,  // Raw calldata
5) -> Result<Vec<u8>, Vec<u8>> {
6    let return_data = call(
7        Call::new_in(self)  // Configure gas and value
8            .gas(evm::gas_left() / 2),  // Use half the available gas
9        contract,  // Address of the target contract
10        &calldata,  // Calldata for the function call
11    )?;
12    Ok(return_data)
13}
1pub fn execute_call(
2    &mut self,
3    contract: Address,
4    calldata: Vec<u8>,  // Raw calldata
5) -> Result<Vec<u8>, Vec<u8>> {
6    let return_data = call(
7        Call::new_in(self)  // Configure gas and value
8            .gas(evm::gas_left() / 2),  // Use half the available gas
9        contract,  // Address of the target contract
10        &calldata,  // Calldata for the function call
11    )?;
12    Ok(return_data)
13}
  • Low-Level static_call:
1pub fn execute_static_call(
2    &mut self,
3    contract: Address,
4    calldata: Vec<u8>,
5) -> Result<Vec<u8>, Vec<u8>> {
6    let return_data = static_call(
7        Call::new_in(self),  // Configure the static call
8        contract,  // Target contract address
9        &calldata,  // Raw calldata for the function
10    )?;
11    Ok(return_data)
12}
1pub fn execute_static_call(
2    &mut self,
3    contract: Address,
4    calldata: Vec<u8>,
5) -> Result<Vec<u8>, Vec<u8>> {
6    let return_data = static_call(
7        Call::new_in(self),  // Configure the static call
8        contract,  // Target contract address
9        &calldata,  // Raw calldata for the function
10    )?;
11    Ok(return_data)
12}

Explanation: These functions perform low-level contract interactions using raw calldata (Vec<u8>). call is used for regular contract interactions, while static_call is used for view or pure functions that do not modify state.


Using Unsafe RawCall

RawCall allows for more advanced, low-level control over contract interactions. It should be used with caution, especially in reentrant contracts, as it provides direct access to storage and calldata without type safety.

1pub fn raw_call_example(
2    &mut self,
3    contract: Address,
4    calldata: Vec<u8>,
5) -> Result<Vec<u8>, Vec<u8>> {
6    unsafe {
7        let data = RawCall::new_delegate()
8            .gas(2100)  // Set gas to 2100
9            .limit_return_data(0, 32)  // Limit return data size to 32 bytes
10            .flush_storage_cache()  // Flush the storage cache before the call
11            .call(contract, &calldata)?;  // Execute the raw delegate call
12
13        Ok(data)  // Return the result data
14    }
15}
1pub fn raw_call_example(
2    &mut self,
3    contract: Address,
4    calldata: Vec<u8>,
5) -> Result<Vec<u8>, Vec<u8>> {
6    unsafe {
7        let data = RawCall::new_delegate()
8            .gas(2100)  // Set gas to 2100
9            .limit_return_data(0, 32)  // Limit return data size to 32 bytes
10            .flush_storage_cache()  // Flush the storage cache before the call
11            .call(contract, &calldata)?;  // Execute the raw delegate call
12
13        Ok(data)  // Return the result data
14    }
15}

Explanation: This function performs an unsafe RawCall, allowing you to set detailed call parameters such as gas limits, return data size, and flushing the storage cache. It provides direct access to the contract interaction but should be handled carefully due to its unsafe nature.


Summary:

  • Interface-Based Calls (pure, view, write): Use &self for read-only operations (pure and view methods) and &mut self for state-modifying operations (write or payable methods). These interface-based calls do not require explicit storage management.
  • Low-Level Calls with Call::new_in(): Use Call::new_in() when reentrancy is enabled or when you need more control over the call's gas and value settings by explicitly passing storage to manage state during cross-contract calls.
  • Simpler Low-Level Calls with Call::new(): Use Call::new() for non-reentrant contracts. It simplifies contract calls by not requiring storage to be passed explicitly, making it ideal for straightforward external calls without the risk of reentrant attacks.
  • Reentrancy Considerations: Always use Call::new_in() if the contract allows reentrancy (recursively calling itself or other contracts). For non-reentrant contracts, use Call::new() for easier, direct contract interactions that don't involve explicit storage handling.
  • Low-Level Direct and Unsafe Calls: For advanced contract interaction, low-level call, static_call, and RawCall provide raw access to the contract's state and calldata. These should be used with caution, particularly RawCall, which allows unsafe delegate calls without type safety.

Full Example code:

src/lib.rs

1#![cfg_attr(not(feature = "export-abi"), no_main)]
2extern crate alloc;
3
4use stylus_sdk::{
5    alloy_primitives::Address,
6    call::{call, static_call, Call, RawCall},
7    evm, msg,
8    prelude::*,
9};
10
11sol_interface! {
12    interface IService {
13        function makePayment(address user) payable external returns (string);
14        function getConstant() pure external returns (bytes32);
15    }
16
17    interface IMethods {
18        function pureFoo() external pure;
19        function viewFoo() external view;
20        function writeFoo() external;
21        function payableFoo() external payable;
22    }
23}
24
25#[storage]
26#[entrypoint]
27
28pub struct ExampleContract;
29
30#[public]
31impl ExampleContract {
32    // simple call to contract using interface
33    pub fn simple_call(&mut self, account: IService, user: Address) -> Result<String, Vec<u8>> {
34        // Calls the make_payment method
35        Ok(account.make_payment(self, user)?)
36    }
37    #[payable]
38    // configuring gas and value with Call
39    pub fn call_with_gas_value(
40        &mut self,
41        account: IService,
42        user: Address,
43    ) -> Result<String, Vec<u8>> {
44        let config = Call::new_in(self)
45            .gas(evm::gas_left() / 2) // Use half the remaining gas
46            .value(msg::value()); // Use the transferred value
47
48        Ok(account.make_payment(config, user)?)
49    }
50
51    pub fn call_pure(&self, methods: IMethods) -> Result<(), Vec<u8>> {
52        Ok(methods.pure_foo(self)?) // `pure` methods might lie about not being `view`
53    }
54
55    pub fn call_view(&self, methods: IMethods) -> Result<(), Vec<u8>> {
56        Ok(methods.view_foo(self)?)
57    }
58
59    pub fn call_write(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
60        methods.view_foo(&mut *self)?; // Re-borrow `self` to avoid moving it
61        Ok(methods.write_foo(self)?) // Safely use `self` again for write_foo
62    }
63
64    #[payable]
65    pub fn call_payable(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
66        methods.write_foo(Call::new_in(self))?; // these are the same
67        Ok(methods.payable_foo(self)?) // ------------------
68    }
69
70    // When writing Stylus libraries, a type might not be TopLevelStorage and therefore &self or &mut self won't work. Building a Call from a generic parameter via new_in is the usual solution.
71    pub fn make_generic_call(
72        storage: &mut impl TopLevelStorage, // This could be `&mut self`, or another type implementing `TopLevelStorage`
73        account: IService,                  // Interface for calling the target contract
74        user: Address,
75    ) -> Result<String, Vec<u8>> {
76        let config = Call::new_in(storage) // Take exclusive access to all contract storage
77            .gas(evm::gas_left() / 2) // Use half the remaining gas
78            .value(msg::value()); // Use the transferred value
79
80        Ok(account.make_payment(config, user)?) // Call using the configured parameters
81    }
82
83    // Low level Call
84    pub fn execute_call(
85        &mut self,
86        contract: Address,
87        calldata: Vec<u8>, // Calldata is supplied as a Vec<u8>
88    ) -> Result<Vec<u8>, Vec<u8>> {
89        // Perform a low-level `call`
90        let return_data = call(
91            Call::new_in(self) // Configuration for gas, value, etc.
92                .gas(evm::gas_left() / 2), // Use half the remaining gas
93            contract,  // The target contract address
94            &calldata, // Raw calldata to be sent
95        )?;
96
97        // Return the raw return data from the contract call
98        Ok(return_data)
99    }
100
101    // Low level Static Call
102    pub fn execute_static_call(
103        &mut self,
104        contract: Address,
105        calldata: Vec<u8>,
106    ) -> Result<Vec<u8>, Vec<u8>> {
107        // Perform a low-level `static_call`, which does not modify state
108        let return_data = static_call(
109            Call::new_in(self), // Configuration for the call
110            contract,           // Target contract
111            &calldata,          // Raw calldata
112        )?;
113
114        // Return the raw result data
115        Ok(return_data)
116    }
117
118    // Using Unsafe RawCall
119    pub fn raw_call_example(
120        &mut self,
121        contract: Address,
122        calldata: Vec<u8>,
123    ) -> Result<Vec<u8>, Vec<u8>> {
124        unsafe {
125            let data = RawCall::new_delegate()
126                .gas(2100) // Set gas to 2100
127                .limit_return_data(0, 32) // Limit return data to 32 bytes
128                .flush_storage_cache() // flush the storage cache before the call
129                .call(contract, &calldata)?; // Execute the call
130            Ok(data) // Return the raw result
131        }
132    }
133}
1#![cfg_attr(not(feature = "export-abi"), no_main)]
2extern crate alloc;
3
4use stylus_sdk::{
5    alloy_primitives::Address,
6    call::{call, static_call, Call, RawCall},
7    evm, msg,
8    prelude::*,
9};
10
11sol_interface! {
12    interface IService {
13        function makePayment(address user) payable external returns (string);
14        function getConstant() pure external returns (bytes32);
15    }
16
17    interface IMethods {
18        function pureFoo() external pure;
19        function viewFoo() external view;
20        function writeFoo() external;
21        function payableFoo() external payable;
22    }
23}
24
25#[storage]
26#[entrypoint]
27
28pub struct ExampleContract;
29
30#[public]
31impl ExampleContract {
32    // simple call to contract using interface
33    pub fn simple_call(&mut self, account: IService, user: Address) -> Result<String, Vec<u8>> {
34        // Calls the make_payment method
35        Ok(account.make_payment(self, user)?)
36    }
37    #[payable]
38    // configuring gas and value with Call
39    pub fn call_with_gas_value(
40        &mut self,
41        account: IService,
42        user: Address,
43    ) -> Result<String, Vec<u8>> {
44        let config = Call::new_in(self)
45            .gas(evm::gas_left() / 2) // Use half the remaining gas
46            .value(msg::value()); // Use the transferred value
47
48        Ok(account.make_payment(config, user)?)
49    }
50
51    pub fn call_pure(&self, methods: IMethods) -> Result<(), Vec<u8>> {
52        Ok(methods.pure_foo(self)?) // `pure` methods might lie about not being `view`
53    }
54
55    pub fn call_view(&self, methods: IMethods) -> Result<(), Vec<u8>> {
56        Ok(methods.view_foo(self)?)
57    }
58
59    pub fn call_write(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
60        methods.view_foo(&mut *self)?; // Re-borrow `self` to avoid moving it
61        Ok(methods.write_foo(self)?) // Safely use `self` again for write_foo
62    }
63
64    #[payable]
65    pub fn call_payable(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
66        methods.write_foo(Call::new_in(self))?; // these are the same
67        Ok(methods.payable_foo(self)?) // ------------------
68    }
69
70    // When writing Stylus libraries, a type might not be TopLevelStorage and therefore &self or &mut self won't work. Building a Call from a generic parameter via new_in is the usual solution.
71    pub fn make_generic_call(
72        storage: &mut impl TopLevelStorage, // This could be `&mut self`, or another type implementing `TopLevelStorage`
73        account: IService,                  // Interface for calling the target contract
74        user: Address,
75    ) -> Result<String, Vec<u8>> {
76        let config = Call::new_in(storage) // Take exclusive access to all contract storage
77            .gas(evm::gas_left() / 2) // Use half the remaining gas
78            .value(msg::value()); // Use the transferred value
79
80        Ok(account.make_payment(config, user)?) // Call using the configured parameters
81    }
82
83    // Low level Call
84    pub fn execute_call(
85        &mut self,
86        contract: Address,
87        calldata: Vec<u8>, // Calldata is supplied as a Vec<u8>
88    ) -> Result<Vec<u8>, Vec<u8>> {
89        // Perform a low-level `call`
90        let return_data = call(
91            Call::new_in(self) // Configuration for gas, value, etc.
92                .gas(evm::gas_left() / 2), // Use half the remaining gas
93            contract,  // The target contract address
94            &calldata, // Raw calldata to be sent
95        )?;
96
97        // Return the raw return data from the contract call
98        Ok(return_data)
99    }
100
101    // Low level Static Call
102    pub fn execute_static_call(
103        &mut self,
104        contract: Address,
105        calldata: Vec<u8>,
106    ) -> Result<Vec<u8>, Vec<u8>> {
107        // Perform a low-level `static_call`, which does not modify state
108        let return_data = static_call(
109            Call::new_in(self), // Configuration for the call
110            contract,           // Target contract
111            &calldata,          // Raw calldata
112        )?;
113
114        // Return the raw result data
115        Ok(return_data)
116    }
117
118    // Using Unsafe RawCall
119    pub fn raw_call_example(
120        &mut self,
121        contract: Address,
122        calldata: Vec<u8>,
123    ) -> Result<Vec<u8>, Vec<u8>> {
124        unsafe {
125            let data = RawCall::new_delegate()
126                .gas(2100) // Set gas to 2100
127                .limit_return_data(0, 32) // Limit return data to 32 bytes
128                .flush_storage_cache() // flush the storage cache before the call
129                .call(contract, &calldata)?; // Execute the call
130            Ok(data) // Return the raw result
131        }
132    }
133}

Cargo.toml

1[package]
2name = "stylus-call-example"
3version = "0.1.7"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "0.7.6"
8alloy-sol-types = "0.7.6"
9stylus-sdk = { version = "0.6.0", features = ["reentrant"] }
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[lib]
21crate-type = ["lib", "cdylib"]
1[package]
2name = "stylus-call-example"
3version = "0.1.7"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "0.7.6"
8alloy-sol-types = "0.7.6"
9stylus-sdk = { version = "0.6.0", features = ["reentrant"] }
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[lib]
21crate-type = ["lib", "cdylib"]