The Stylus SDK provides Solidity-ABI-equivalent contract calls, letting you interact with other contracts without knowing their internals. Define Solidity-like interfaces with the sol_interface! macro and invoke them from Stylus (Rust) in a type-safe way.
For more on sol_interface! and how to use it in Rust Stylus contracts, see the interface page. You can also learn how to extract interfaces from Stylus contracts on the interface extraction page.
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 IMethods {
8 function pureFoo() external pure;
9 function viewFoo() external view;
10 function writeFoo() external;
11 function payableFoo() external payable;
12 }
13}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 IMethods {
8 function pureFoo() external pure;
9 function viewFoo() external view;
10 function writeFoo() external;
11 function payableFoo() external payable;
12 }
13}Solidity methods like makePayment are exposed in snake_case in Rust. You also pass a call context that specifies gas/value and whether the call is mutating.
1// Simple payable call via interface
2pub fn simple_call(
3 &mut self,
4 account: IService,
5 user: Address,
6) -> Result<String, Vec<u8>> {
7 let config = Call::new_mutating(self); // write/payable context
8 Ok(account.make_payment(self.vm(), config, user)?) // CamelCase -> snake_case
9}1// Simple payable call via interface
2pub fn simple_call(
3 &mut self,
4 account: IService,
5 user: Address,
6) -> Result<String, Vec<u8>> {
7 let config = Call::new_mutating(self); // write/payable context
8 Ok(account.make_payment(self.vm(), config, user)?) // CamelCase -> snake_case
9}Use the Call builder to set gas and the transferred Ether amount:
1#[payable]
2pub fn call_with_gas_value(
3 &mut self,
4 account: IService,
5 user: Address,
6) -> Result<String, Vec<u8>> {
7 let config = Call::new_payable(self, self.vm().msg_value()) // forward msg.value
8 .gas(self.vm().evm_gas_left() / 2); // use half of remaining gas
9 Ok(account.make_payment(self.vm(), config, user)?)
10}1#[payable]
2pub fn call_with_gas_value(
3 &mut self,
4 account: IService,
5 user: Address,
6) -> Result<String, Vec<u8>> {
7 let config = Call::new_payable(self, self.vm().msg_value()) // forward msg.value
8 .gas(self.vm().evm_gas_left() / 2); // use half of remaining gas
9 Ok(account.make_payment(self.vm(), config, user)?)
10}Call::new() → read-only context (pure/view)Call::new_mutating(self) → write contextCall::new_payable(self, value) → payable context with a valuepure, view, write, payableStylus handles the right storage access mode based on the call context you build:
1pub fn call_pure(&self, methods: IMethods) -> Result<(), Vec<u8>> {
2 Ok(methods.pure_foo(self.vm(), Call::new())?)
3}
4
5pub fn call_view(&self, methods: IMethods) -> Result<(), Vec<u8>> {
6 Ok(methods.view_foo(self.vm(), Call::new())?)
7}
8
9pub fn call_write(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
10 // call a view first with a non-mutating context
11 methods.view_foo(self.vm(), Call::new())?;
12 // then call a write with a mutating context
13 let config = Call::new_mutating(self);
14 Ok(methods.write_foo(self.vm(), config)?)
15}
16
17#[payable]
18pub fn call_payable(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
19 // do a write
20 let config = Call::new_mutating(self);
21 methods.write_foo(self.vm(), config)?;
22 // then payable with zero value (example)
23 let config = Call::new_payable(self, U256::ZERO);
24 Ok(methods.payable_foo(self.vm(), config)?)
25}1pub fn call_pure(&self, methods: IMethods) -> Result<(), Vec<u8>> {
2 Ok(methods.pure_foo(self.vm(), Call::new())?)
3}
4
5pub fn call_view(&self, methods: IMethods) -> Result<(), Vec<u8>> {
6 Ok(methods.view_foo(self.vm(), Call::new())?)
7}
8
9pub fn call_write(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
10 // call a view first with a non-mutating context
11 methods.view_foo(self.vm(), Call::new())?;
12 // then call a write with a mutating context
13 let config = Call::new_mutating(self);
14 Ok(methods.write_foo(self.vm(), config)?)
15}
16
17#[payable]
18pub fn call_payable(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
19 // do a write
20 let config = Call::new_mutating(self);
21 methods.write_foo(self.vm(), config)?;
22 // then payable with zero value (example)
23 let config = Call::new_payable(self, U256::ZERO);
24 Ok(methods.payable_foo(self.vm(), config)?)
25}TopLevelStorageWhen writing libraries, you may not have &self. Build the call context from a generic TopLevelStorage:
1pub fn make_generic_call<T: TopLevelStorage + core::borrow::Borrow<Self>>(
2 storage: &mut T, // could be &mut self or any TopLevelStorage
3 account: IService, // interface for the target contract
4 user: Address,
5) -> Result<String, Vec<u8>> {
6 let vm = storage.borrow().vm();
7 let msg_value = vm.msg_value();
8 let gas = vm.evm_gas_left() / 2;
9
10 let config = Call::new_payable(storage, msg_value).gas(gas); // build context from generic storage
11 Ok(account.make_payment(storage.borrow().vm(), config, user)?)
12}1pub fn make_generic_call<T: TopLevelStorage + core::borrow::Borrow<Self>>(
2 storage: &mut T, // could be &mut self or any TopLevelStorage
3 account: IService, // interface for the target contract
4 user: Address,
5) -> Result<String, Vec<u8>> {
6 let vm = storage.borrow().vm();
7 let msg_value = vm.msg_value();
8 let gas = vm.evm_gas_left() / 2;
9
10 let config = Call::new_payable(storage, msg_value).gas(gas); // build context from generic storage
11 Ok(account.make_payment(storage.borrow().vm(), config, user)?)
12}call and static_callDrop down to raw calldata when you need it:
1// bytes-in/bytes-out state-changing call
2pub fn execute_call(
3 &mut self,
4 contract: Address,
5 calldata: Vec<u8>,
6) -> Result<Vec<u8>, Vec<u8>> {
7 let config = Call::new_mutating(self)
8 .gas(self.vm().evm_gas_left() / 2); // use half gas
9 let return_data = call(self.vm(), config, contract, &calldata)?;
10 Ok(return_data)
11}
12
13// bytes-in/bytes-out static (view) call
14pub fn execute_static_call(
15 &mut self,
16 contract: Address,
17 calldata: Vec<u8>,
18) -> Result<Vec<u8>, Vec<u8>> {
19 let return_data = static_call(self.vm(), Call::new(), contract, &calldata)?;
20 Ok(return_data)
21}1// bytes-in/bytes-out state-changing call
2pub fn execute_call(
3 &mut self,
4 contract: Address,
5 calldata: Vec<u8>,
6) -> Result<Vec<u8>, Vec<u8>> {
7 let config = Call::new_mutating(self)
8 .gas(self.vm().evm_gas_left() / 2); // use half gas
9 let return_data = call(self.vm(), config, contract, &calldata)?;
10 Ok(return_data)
11}
12
13// bytes-in/bytes-out static (view) call
14pub fn execute_static_call(
15 &mut self,
16 contract: Address,
17 calldata: Vec<u8>,
18) -> Result<Vec<u8>, Vec<u8>> {
19 let return_data = static_call(self.vm(), Call::new(), contract, &calldata)?;
20 Ok(return_data)
21}RawCallMaximum control, minimal safety. Use cautiously.
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(self.vm())
8 .gas(2100)
9 .limit_return_data(0, 32)
10 .flush_storage_cache()
11 .call(contract, &calldata)?;
12 Ok(data)
13 }
14}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(self.vm())
8 .gas(2100)
9 .limit_return_data(0, 32)
10 .flush_storage_cache()
11 .call(contract, &calldata)?;
12 Ok(data)
13 }
14}Tip: for a plain “pass calldata through” helper you can also use a safe wrapper, but here we show the raw delegate pattern.
1Loading...1Loading...1Loading...1Loading...