sol_interface!() { /* proc-macro */ }
Expand description
Facilitates calls to other contracts.
This macro defines a struct
for each of the Solidity interfaces provided.
sol_interface! {
interface IService {
function makePayment(address user) external payable returns (string);
function getConstant() external pure returns (bytes32);
}
interface ITree {
// other interface methods
}
}
The above will define IService
and ITree
for calling the methods of the two contracts.
For example, IService
will have a make_payment
method that accepts an Address
and returns a B256
.
Currently only functions are supported, and any other items in the interface will cause an
error. Additionally, each function must be marked external
. Inheritance is not supported.
use stylus_sdk::call::{Call, Error};
use alloy_primitives::Address;
pub fn do_call(account: IService, user: Address) -> Result<String, Error> {
let config = Call::new()
.gas(evm::gas_left() / 2) // limit to half the gas left
.value(msg::value()); // set the callvalue
account.make_payment(config, user) // note the snake case
}
Observe the casing change. sol_interface!
computes the selector based on the exact name passed in,
which should almost always be camelCase
. For aesthetics, the rust functions will instead use snake_case
.
Note that structs may be used, as return types for example. Trying to reference structs using
the Solidity path separator (module.MyStruct
) is supported and paths will be converted to
Rust syntax (module::MyStruct
).
§Reentrant calls
Contracts that opt into reentrancy via the reentrant
feature flag require extra care.
When enabled, cross-contract calls must flush
or clear
the StorageCache
to safeguard state.
This happens automatically via the type system.
sol_interface! {
interface IMethods {
function pureFoo() external pure;
function viewFoo() external view;
function writeFoo() external;
function payableFoo() external payable;
}
}
#[entrypoint] #[storage] struct Contract {}
#[public]
impl Contract {
pub fn call_pure(&self, methods: IMethods) -> Result<(), Vec<u8>> {
Ok(methods.pure_foo(self)?) // `pure` methods might lie about not being `view`
}
pub fn call_view(&self, methods: IMethods) -> Result<(), Vec<u8>> {
Ok(methods.view_foo(self)?)
}
pub fn call_write(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
methods.view_foo(&mut *self)?; // allows `pure` and `view` methods too
Ok(methods.write_foo(self)?)
}
#[payable]
pub fn call_payable(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
methods.write_foo(Call::new_in(self))?; // these are the same
Ok(methods.payable_foo(self)?) // ------------------
}
}
In the above, we’re able to pass &self
and &mut self
because Contract
implements
TopLevelStorage
, which means that a reference to it entails access to the entirety of
the contract’s state. This is the reason it is sound to make a call, since it ensures all
cached values are invalidated and/or persisted to state at the right time.
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 is the usual solution.
use stylus_sdk::{call::{Call, Error}};
use stylus_sdk::stylus_core::storage::TopLevelStorage;
use alloy_primitives::Address;
pub fn do_call(
storage: &mut impl TopLevelStorage, // can be generic, but often just &mut self
account: IService, // serializes as an Address
user: Address,
) -> Result<String, Error> {
let config = Call::new_in(storage)
.gas(evm::gas_left() / 2) // limit to half the gas left
.value(msg::value()); // set the callvalue
account.make_payment(config, user) // note the snake case
}
Note that in the context of a #[public]
call, the &mut impl
argument will correctly
distinguish the method as being write
or payable
. This means you can write library code that will
work regardless of whether the reentrant
feature flag is enabled.