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}
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.
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.
TopLevelStorage
In Stylus, external contract calls can be made using:
&self
or &mut self
depending on the nature of the call (pure
, view
, or write
).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.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.
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.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.
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.
Call::new_in()
to explicitly manage storage. This prevents vulnerabilities by ensuring that contract state is handled safely during cross-contract calls.Call::new()
, which does not require storage to be passed. This is suitable for straightforward external calls that do not involve recursive state changes.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>
.
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}
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.
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.
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.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.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.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.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.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}
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"]