Arbitrum Stylus logo

Stylus by Example

Contract Calls in Stylus

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.

Example interfaces

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}

Invoking Contracts Using the Interface

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}

Configuring Gas and Value for Contract Calls

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 context
  • Call::new_payable(self, value) → payable context with a value

Interface Calls: pure, view, write, payable

Stylus 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}

Generic Calls with TopLevelStorage

When 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}

Low-Level Calls: call and static_call

Drop 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}

Unsafe RawCall

Maximum 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.


Full Example code:

src/lib.rs

1Loading...
1Loading...

Cargo.toml

1Loading...
1Loading...