Macro stylus_proc::sol_interface
source · 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) payable returns (string);
function getConstant() 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.
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.
§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() pure;
function viewFoo() view;
function writeFoo();
function payableFoo() payable;
}
}
#[external]
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(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.
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 an #[external] 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.