Arbitrum Stylus logo

Stylus by Example

Import interfaces

Interfaces let your Stylus contract call other contracts (Solidity, Stylus/Rust, etc.) using their Solidity ABI. You declare them with sol_interface!, then invoke methods from Rust with a VM handle (self.vm()) and a call context (Call).


Declaring interfaces

Use sol_interface! with Solidity syntax (keep CamelCase names; Stylus computes selectors from these exact names):

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        // other interface methods
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        // other interface methods
9    }
10}

This generates Rust types IService and ITree that you can construct from an address (e.g., IService::new(addr)) or accept as parameters.

Note on naming: Solidity makePayment (CamelCase) becomes make_payment when called from Rust. The ABI selector uses the Solidity name; the Rust method is snake_case for ergonomics.


Calling via an interface instance (address → client)

Create an interface bound to a target address, build the right Call context, and pass self.vm():

1use stylus_sdk::{
2    alloy_primitives::Address,
3    call::Call,
4    prelude::*,
5};
6
7sol_interface! {
8    interface IService {
9        function makePayment(address user) payable external returns (string);
10    }
11}
12
13#[public]
14impl MyContract {
15    #[payable]
16    pub fn pay_user(&mut self, service_addr: Address, user: Address) -> Result<String, Vec<u8>> {
17        let service = IService::new(service_addr);
18        let ctx = Call::new_payable(self, self.vm().msg_value()); // set value; add .gas(...) if needed
19        Ok(service.make_payment(self.vm(), ctx, user)?)
20    }
21}
1use stylus_sdk::{
2    alloy_primitives::Address,
3    call::Call,
4    prelude::*,
5};
6
7sol_interface! {
8    interface IService {
9        function makePayment(address user) payable external returns (string);
10    }
11}
12
13#[public]
14impl MyContract {
15    #[payable]
16    pub fn pay_user(&mut self, service_addr: Address, user: Address) -> Result<String, Vec<u8>> {
17        let service = IService::new(service_addr);
18        let ctx = Call::new_payable(self, self.vm().msg_value()); // set value; add .gas(...) if needed
19        Ok(service.make_payment(self.vm(), ctx, user)?)
20    }
21}
  • view/pureCall::new()
  • writeCall::new_mutating(self)
  • payableCall::new_payable(self, value) (optionally .gas(limit))

Calling when the interface is passed in (no address creation)

If your function receives the interface value directly, just build the context and call:

1use stylus_sdk::{call::Call, prelude::*};
2use stylus_sdk::alloy_primitives::Address;
3
4sol_interface! {
5    interface IService {
6        function makePayment(address user) payable external returns (string);
7    }
8}
9
10#[public]
11impl MyContract {
12    #[payable]
13    pub fn do_call(&mut self, account: IService, user: Address) -> Result<String, Vec<u8>> {
14        let ctx = Call::new_payable(self, self.vm().msg_value());
15        Ok(account.make_payment(self.vm(), ctx, user)?)
16    }
17}
1use stylus_sdk::{call::Call, prelude::*};
2use stylus_sdk::alloy_primitives::Address;
3
4sol_interface! {
5    interface IService {
6        function makePayment(address user) payable external returns (string);
7    }
8}
9
10#[public]
11impl MyContract {
12    #[payable]
13    pub fn do_call(&mut self, account: IService, user: Address) -> Result<String, Vec<u8>> {
14        let ctx = Call::new_payable(self, self.vm().msg_value());
15        Ok(account.make_payment(self.vm(), ctx, user)?)
16    }
17}

View vs write examples

For a dedicated “methods” interface, use a non-mutating context for view/pure and a mutating context for write:

1use stylus_sdk::{call::Call, prelude::*};
2
3sol_interface! {
4    interface IMethods {
5        function viewFoo() external view;
6        function writeFoo() external;
7    }
8}
9
10#[public]
11impl Contract {
12    pub fn call_view(&self, addr: Address) -> Result<(), Vec<u8>> {
13        let ext = IMethods::new(addr);
14        Ok(ext.view_foo(self.vm(), Call::new())?)
15    }
16
17    pub fn call_write(&mut self, addr: Address) -> Result<(), Vec<u8>> {
18        let ext = IMethods::new(addr);
19        let ctx = Call::new_mutating(self);
20        Ok(ext.write_foo(self.vm(), ctx)?)
21    }
22}
1use stylus_sdk::{call::Call, prelude::*};
2
3sol_interface! {
4    interface IMethods {
5        function viewFoo() external view;
6        function writeFoo() external;
7    }
8}
9
10#[public]
11impl Contract {
12    pub fn call_view(&self, addr: Address) -> Result<(), Vec<u8>> {
13        let ext = IMethods::new(addr);
14        Ok(ext.view_foo(self.vm(), Call::new())?)
15    }
16
17    pub fn call_write(&mut self, addr: Address) -> Result<(), Vec<u8>> {
18        let ext = IMethods::new(addr);
19        let ctx = Call::new_mutating(self);
20        Ok(ext.write_foo(self.vm(), ctx)?)
21    }
22}

Gas & value configuration (quick reference)

  • Add value: Call::new_payable(self, value)
  • Limit gas: .gas(limit)
  • Read-only: Call::new()
  • State change: Call::new_mutating(self)

Example:

1let ctx = Call::new_payable(self, self.vm().msg_value())
2    .gas(self.vm().evm_gas_left() / 2);
1let ctx = Call::new_payable(self, self.vm().msg_value())
2    .gas(self.vm().evm_gas_left() / 2);