Import and call external contract interfaces
Interfaces enable your Stylus contract to interact with other contracts on the blockchain, regardless of whether they're written in Solidity, Rust, or another language. This guide shows you how to import and use external contract interfaces in your Stylus smart contracts.
Why use interfaces
Contract interfaces provide a type-safe way to communicate with other contracts on the blockchain. Common use cases include:
- Interacting with existing protocols: Call methods on deployed Solidity contracts like
ERC-20tokens, oracles, or DeFi protocols - Composing functionality: Build contracts that leverage other contracts' capabilities
- Cross-language interoperability: Stylus contracts can call Solidity contracts and vice versa
- Upgradeability patterns: Use interfaces to interact with proxy contracts
Since interfaces operate at the ABI level, they work identically whether the target contract is written in Solidity, Rust, or any other language that compiles to EVM bytecode.
Prerequisites
Before implementing interfaces, ensure you have:
Rust toolchain
Follow the instructions on Rust Lang's installation page to install a complete Rust toolchain (v1.88 or newer) on your system. After installation, ensure you can access the programs rustup, rustc, and cargo from your preferred terminal application.
cargo stylus
In your terminal, run:
cargo install --force cargo-stylus
Add WASM (WebAssembly) as a build target for the specific Rust toolchain you are using. The example below sets your default Rust toolchain to 1.88, as well as adding the WASM build target:
rustup default 1.88
rustup target add wasm32-unknown-unknown --toolchain 1.88
You can verify that cargo stylus is installed by running cargo stylus --help in your terminal, which will return a list of helpful commands.
Declaring interfaces with sol_interface!
The sol_interface! macro allows you to declare interfaces using Solidity syntax. It generates Rust structs that represent external contracts and provides type-safe methods for calling them.
Basic interface declaration
use stylus_sdk::prelude::*;
sol_interface! {
interface IToken {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
}
}
This macro generates an IToken struct that you can use to call methods on any deployed contract that implements this interface.
Declaring multiple interfaces
You can declare multiple interfaces in a single sol_interface! block:
sol_interface! {
interface IPaymentService {
function makePayment(address user) payable returns (string);
function getBalance(address user) view returns (uint256);
}
interface IOracle {
function getPrice(bytes32 feedId) external view returns (uint256);
function getLastUpdate() external view returns (uint256);
}
interface IVault {
function deposit() external payable;
function withdraw(uint256 amount) external;
}
}
Interface declarations use standard Solidity syntax. The SDK computes the correct 4-byte function selectors based on the exact names and parameter types you provide.
Calling external contract methods
Once you've declared an interface, you can call methods on external contracts using instances of the generated struct.
Creating interface instances
Use the ::new(address) constructor to create an interface instance pointing to a deployed contract:
use alloy_primitives::Address;
// Create an instance pointing to a deployed token contract
let token_address = Address::from([0x12; 20]); // Replace with actual address
let token = IToken::new(token_address);
CamelCase to snake_case conversion
The sol_interface! macro converts Solidity's CamelCase method names to Rust's snake_case convention:
| Solidity method | Rust method |
|---|---|
balanceOf | balance_of |
makePayment | make_payment |
getPrice | get_price |
transferFrom | transfer_from |
The macro preserves the original CamelCase name for computing the correct function selector, so your calls reach the right method on the target contract.
Basic method calls
Here's how to call methods on an external contract:
use stylus_sdk::{call::Call, prelude::*};
use alloy_primitives::{Address, U256};
sol_interface! {
interface IToken {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
}
#[public]
impl MyContract {
pub fn check_balance(&self, token_address: Address, account: Address) -> U256 {
let token = IToken::new(token_address);
let config = Call::new();
token.balance_of(self.vm(), config, account).unwrap()
}
}
Configuring your calls
The Stylus SDK provides three Call constructors for different types of external calls. Choosing the correct one is essential for your contract to work properly.
Use this decision tree to choose the correct Call constructor:

Figure 2: Decision tree for selecting the appropriate Call constructor based on state modification and payment requirements.
View calls with Call::new()
Use Call::new() for read-only calls that don't modify state:
use stylus_sdk::call::Call;
#[public]
impl MyContract {
pub fn get_token_balance(&self, token: Address, account: Address) -> U256 {
let token_contract = IToken::new(token);
let config = Call::new();
token_contract.balance_of(self.vm(), config, account).unwrap()
}
pub fn get_oracle_price(&self, oracle: Address, feed_id: [u8; 32]) -> U256 {
let oracle_contract = IOracle::new(oracle);
let config = Call::new();
oracle_contract.get_price(self.vm(), config, feed_id.into()).unwrap()
}
}
State-changing calls with Call::new_mutating(self)
Use Call::new_mutating(self) for calls that modify state on the target contract:
#[public]
impl MyContract {
pub fn transfer_tokens(
&mut self,
token: Address,
to: Address,
amount: U256,
) -> bool {
let token_contract = IToken::new(token);
let config = Call::new_mutating(self);
token_contract.transfer(self.vm(), config, to, amount).unwrap()
}
pub fn approve_spender(
&mut self,
token: Address,
spender: Address,
amount: U256,
) -> bool {
let token_contract = IToken::new(token);
let config = Call::new_mutating(self);
token_contract.approve(self.vm(), config, spender, amount).unwrap()
}
}
When using Call::new_mutating(self), your method must take &mut self as its first parameter.
This ensures the Stylus runtime properly handles state changes and reentrancy protection.
Payable calls with Call::new_payable(self, value)
Use Call::new_payable(self, value) to send ETH along with your call:
use alloy_primitives::U256;
sol_interface! {
interface IVault {
function deposit() external payable;
}
}
#[public]
impl MyContract {
#[payable]
pub fn deposit_to_vault(&mut self, vault: Address) -> Result<(), Vec<u8>> {
let vault_contract = IVault::new(vault);
let value = self.vm().msg_value();
let config = Call::new_payable(self, value);
vault_contract.deposit(self.vm(), config)?;
Ok(())
}
pub fn deposit_specific_amount(
&mut self,
vault: Address,
amount: U256,
) -> Result<(), Vec<u8>> {
let vault_contract = IVault::new(vault);
let config = Call::new_payable(self, amount);
vault_contract.deposit(self.vm(), config)?;
Ok(())
}
}