Skip to main content

Exporting ABIs

Stylus contracts written in Rust can automatically generate Solidity Application Binary Interfaces (ABIs) that enable interoperability with existing Ethereum tools, front-end libraries, and other smart contracts.

What is an ABI?

An Application Binary Interface (ABI) defines how to interact with a smart contract:

  • Function signatures: Names, parameters, and return types
  • Events: Event definitions and indexed parameters
  • Errors: Custom error types and parameters
  • Constructor: Initialization parameters

ABIs enable:

  • Front-end libraries (ethers.js, web3.js, viem) to interact with contracts
  • Solidity contracts to call Rust contracts
  • Rust contracts to call Solidity contracts
  • Block explorers to decode transactions
  • Development tools to provide type-safe interfaces

Overview: ABI generation

Stylus contracts generate ABIs through:

  1. #[public] macro: Annotates public functions
  2. export-abi feature: Enables ABI generation code
  3. cargo stylus export-abi: CLI command to generate output
  4. Solidity interface: Generated Solidity interface file
  5. JSON format: Optional JSON ABI for tool integration

The process is automatic—annotate your functions with #[public] and run the export command.

Basic usage

Export Solidity interface

Generate a Solidity interface for your contract:

cargo stylus export-abi

Output:

/**
* This file was automatically generated by Stylus and represents a Rust program.
* For more information, please see [The Stylus SDK](https://github.com/OffchainLabs/stylus-sdk-rs).
*/

// SPDX-License-Identifier: MIT-OR-APACHE-2.0
pragma solidity ^0.8.23;

interface IMyContract {
function getValue() external view returns (uint256);

function setValue(uint256 new_value) external;

error Unauthorized(address);
}

Export to file

Save the interface to a file:

cargo stylus export-abi > IMyContract.sol

Or specify output path:

cargo stylus export-abi --output=./interfaces/IMyContract.sol

Export JSON ABI

Generate JSON format ABI (requires solc installed):

cargo stylus export-abi --json > abi.json

The JSON output is produced by solc, so it includes solc's header lines before the ABI array:

======= <stdin>:IMyContract =======
Contract JSON ABI
[{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"Unauthorized","type":"error"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"new_value","type":"uint256"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]

Strip the two header lines before feeding the array to front-end tooling.

Writing ABI-compatible contracts

Basic contract structure

Contracts must use the #[public] macro to generate ABIs:

use stylus_sdk::{alloy_primitives::U256, prelude::*};

sol_storage! {
#[entrypoint]
pub struct Counter {
uint256 count;
}
}

#[public]
impl Counter {
// This function will be included in the ABI
pub fn get_count(&self) -> U256 {
self.count.get()
}

// This function will also be included
pub fn increment(&mut self) {
let count = self.count.get() + U256::from(1);
self.count.set(count);
}
}

Generated interface:

interface ICounter {
function getCount() external view returns (uint256);

function increment() external;
}

Function visibility mapping

Rust function signatures map to Solidity visibility:

#[public]
impl MyContract {
// Immutable reference → view function
pub fn read_value(&self) -> U256 {
self.value.get()
}

// Mutable reference → non-view function
pub fn write_value(&mut self, new_value: U256) {
self.value.set(new_value);
}

// Pure computation (no self) → pure function
pub fn compute(a: U256, b: U256) -> U256 {
a + b
}
}

Generated Solidity:

interface IMyContract {
function readValue() external view returns (uint256);

function writeValue(uint256 new_value) external;

function compute(uint256 a, uint256 b) external pure returns (uint256);
}

Type mapping

Rust types map to Solidity types automatically:

Rust TypeSolidity TypeExample
U256uint256Token amounts
U128, u128uint128Medium integers
u64, u32, u16, u8uint64, uint32, uint16, uint8Small integers
I256int256Signed integers
I128, i128int128Medium signed integers
i64, i32, i16, i8int64, int32, int16, int8Small signed integers
AddressaddressAccount addresses
boolboolBoolean values
FixedBytes<N>bytesNFixed-size byte arrays
BytesbytesDynamic byte arrays
StringstringUTF-8 strings
Vec<T>T[]Dynamic arrays
[T; N]T[N]Fixed-size arrays

Example:

#[public]
impl MyContract {
pub fn process(
owner: Address,
amount: U256,
data: Bytes,
flags: Vec<bool>,
) -> Result<String, MyError> {
// Implementation
}
}

Generates:

interface IMyContract {
function process(
address owner,
uint256 amount,
bytes calldata data,
bool[] calldata flags
) external returns (string memory);
}

Custom errors

Define custom errors with parameters:

use stylus_sdk::alloy_sol_types::sol;
use stylus_sdk::prelude::*;

sol! {
error InsufficientBalance(address account, uint256 requested, uint256 available);
error Unauthorized(address caller);
error InvalidAmount();
}

#[derive(SolidityError)]
pub enum TokenError {
InsufficientBalance(InsufficientBalance),
Unauthorized(Unauthorized),
InvalidAmount(InvalidAmount),
}

#[public]
impl Token {
pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), TokenError> {
let sender = self.vm().msg_sender();
let balance = self.balances.get(sender);
if balance < amount {
return Err(TokenError::InsufficientBalance(InsufficientBalance {
account: sender,
requested: amount,
available: balance,
}));
}
// Transfer logic
Ok(())
}
}

Generated interface includes errors. Note that the exported Solidity drops the error parameter names, keeping only their types:

interface IToken {
function transfer(address to, uint256 amount) external;

error InsufficientBalance(address, uint256, uint256);
error Unauthorized(address);
error InvalidAmount();
}

Events

Events are automatically included in the ABI:

use stylus_sdk::alloy_sol_types::sol;
use stylus_sdk::prelude::*;

sol! {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}

#[public]
impl Token {
pub fn transfer(&mut self, to: Address, value: U256) -> bool {
// Transfer logic
self.vm().log(Transfer {
from: self.vm().msg_sender(),
to,
value,
});
true
}
}

Generated interface:

interface IToken {
function transfer(address to, uint256 value) external returns (bool);

event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}

Trait implementation

Export ABIs for trait implementations:

Define a trait

// ierc20.rs
use stylus_sdk::prelude::*;

#[public]
pub trait IErc20 {
fn name(&self) -> String;
fn symbol(&self) -> String;
fn decimals(&self) -> u8;
fn total_supply(&self) -> U256;
fn balance_of(&self, owner: Address) -> U256;
fn transfer(&mut self, to: Address, value: U256) -> Result<bool, Erc20Error>;
}

Implement the trait

// lib.rs
use stylus_sdk::prelude::*;

sol_storage! {
#[entrypoint]
struct MyToken {
// Storage fields
}
}

#[public]
#[implements(IErc20)]
impl MyToken {
// Additional functions beyond the trait
pub fn mint(&mut self, to: Address, value: U256) {
// Mint logic
}
}

#[public]
impl IErc20 for MyToken {
fn name(&self) -> String {
"My Token".to_string()
}

fn symbol(&self) -> String {
"MTK".to_string()
}

fn decimals(&self) -> u8 {
18
}

fn total_supply(&self) -> U256 {
self.total_supply.get()
}

fn balance_of(&self, owner: Address) -> U256 {
self.balances.get(owner)
}

fn transfer(&mut self, to: Address, value: U256) -> Result<bool, Erc20Error> {
// Transfer logic
Ok(true)
}
}

Generated interface with inheritance:

interface IMyToken is IIErc20 {
function mint(address to, uint256 value) external;
}

interface IIErc20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address owner) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
}

Constructor signatures

Export constructor signatures for deployment:

sol_storage! {
#[entrypoint]
struct MyContract {
address owner;
uint256 initial_value;
}
}

#[public]
impl MyContract {
#[constructor]
pub fn new(&mut self, owner: Address, initial_value: U256) {
self.owner.set(owner);
self.initial_value.set(initial_value);
}

// Other methods...
}

Export constructor signature with the top-level constructor command (the constructor is not part of export-abi output):

cargo stylus constructor

Output:

constructor(address owner, uint256 initial_value)

For payable constructors:

#[public]
impl MyContract {
#[constructor]
#[payable]
pub fn new(owner: Address) {
self.owner.set(owner);
// self.vm().msg_value() is available
}
}

Output:

constructor(address owner) payable

Export configuration

Rust features

Export ABI with specific Rust features enabled:

cargo stylus export-abi --rust-features=feature1,feature2

This is useful when your contract has conditional compilation:

#[cfg(feature = "advanced")]
#[public]
impl MyContract {
pub fn advanced_function(&self) -> U256 {
// Advanced logic
}
}

Integration with front-end

Using ethers.js

import { ethers } from 'ethers';
import MyContractABI from './abi.json';

const provider = new ethers.JsonRpcProvider('https://arb1.arbitrum.io/rpc');
const contract = new ethers.Contract('0x1234567890123456789012345678901234567890', MyContractABI, provider);

// Call view function
const value = await contract.getValue();
console.log('Value:', value.toString());

// Call state-changing function (requires signer)
const signer = provider.getSigner();
const contractWithSigner = contract.connect(signer);
const tx = await contractWithSigner.setValue(42);
await tx.wait();

Using viem

import { createPublicClient, http } from 'viem';
import { arbitrum } from 'viem/chains';
import MyContractABI from './abi.json';

const client = createPublicClient({
chain: arbitrum,
transport: http(),
});

// Read contract
const value = await client.readContract({
address: '0x1234567890123456789012345678901234567890',
abi: MyContractABI,
functionName: 'getValue',
});

// Write contract
const hash = await client.writeContract({
address: '0x1234567890123456789012345678901234567890',
abi: MyContractABI,
functionName: 'setValue',
args: [42n],
});

Using wagmi/RainbowKit

import { useContractRead, useContractWrite } from 'wagmi';
import MyContractABI from './abi.json';

function MyComponent() {
// Read contract
const { data: value } = useContractRead({
address: '0x1234567890123456789012345678901234567890',
abi: MyContractABI,
functionName: 'getValue',
});

// Write contract
const { write } = useContractWrite({
address: '0x1234567890123456789012345678901234567890',
abi: MyContractABI,
functionName: 'setValue',
});

return (
<div>
<p>Current value: {value?.toString()}</p>
<button onClick={() => write({ args: [42n] })}>Set Value to 42</button>
</div>
);
}

Solidity integration

Use exported interfaces in Solidity contracts:

Import the interface

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "./IMyContract.sol";

contract SolidityContract {
IMyContract public stylusContract;

constructor(address _stylusContract) {
stylusContract = IMyContract(_stylusContract);
}

function interactWithStylus() external {
// Read from Stylus contract
uint256 value = stylusContract.getValue();

// Write to Stylus contract
stylusContract.setValue(value + 1);
}
}

Cross-language composition

Combine Solidity and Rust contracts:

contract Router {
IToken public token;
IStaking public staking;

constructor(address _token, address _staking) {
token = IToken(_token); // Rust contract
staking = IStaking(_staking); // Rust contract
}

function stakeTokens(uint256 amount) external {
// Transfer tokens (Rust contract)
require(
token.transferFrom(msg.sender, address(this), amount),
"Transfer failed"
);

// Stake tokens (Rust contract)
token.approve(address(staking), amount);
staking.stake(msg.sender, amount);
}
}

How it works

The export-abi feature

The export-abi feature enables ABI generation:

# Cargo.toml
[features]
export-abi = ["stylus-sdk/export-abi"]

[lib]
crate-type = ["lib", "cdylib"]

When enabled, the SDK generates:

  1. A GenerateAbi trait implementation
  2. A CLI entry point for running ABI export
  3. Formatting logic for Solidity interface generation

Main function

Your contract needs a main function for ABI export:

// main.rs
#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]

#[cfg(not(any(test, feature = "export-abi")))]
#[no_mangle]
pub extern "C" fn main() {}

#[cfg(feature = "export-abi")]
fn main() {
my_contract::print_from_args();
}

This is the main function:

  • Runs only when the export-abi feature is enabled
  • Executes the ABI generation logic
  • Outputs the Solidity interface to stdout

The #[public] macro

The #[public] macro generates ABI code:

// From stylus-proc/src/macros/public/export_abi.rs
impl GenerateAbi for MyContract {
const NAME: &'static str = "MyContract";

fn fmt_abi(f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "interface I{} {{", Self::NAME)?;
// Generate function signatures
write!(f, "\n function getValue() external view returns (uint256);")?;
writeln!(f, "}}")?;
Ok(())
}
}

Key transformations:

  • snake_casecamelCase function names
  • Rust types → Solidity types
  • &selfview, &mut self → non-view
  • Result<T, E> → return type T, error E

Best practices

1. Always export ABIs for integration

# ✅ Good: Generate and version control ABIs
cargo stylus export-abi > interfaces/IMyContract.sol
git add interfaces/IMyContract.sol
git commit -m "Update contract ABI"

# ❌ Bad: Rely on manual interface definitions

2. Use semantic function names

// ✅ Good: Clear, descriptive names
#[public]
impl Token {
pub fn get_balance(&self, account: Address) -> U256 { }
pub fn transfer_from(&mut self, from: Address, to: Address, amount: U256) { }
}

// ❌ Bad: Unclear abbreviations
#[public]
impl Token {
pub fn bal(&self, acc: Address) -> U256 { }
pub fn xfer(&mut self, f: Address, t: Address, amt: U256) { }
}

3. Document complex functions

#[public]
impl Staking {
/// Stakes tokens for a specified duration
///
/// # Arguments
/// * `amount` - Amount of tokens to stake
/// * `duration` - Lock duration in seconds
///
/// # Returns
/// The unique stake ID
pub fn stake(&mut self, amount: U256, duration: u64) -> U256 {
// Implementation
}
}

4. Export JSON for tooling

# ✅ Good: Generate both formats
cargo stylus export-abi > IMyContract.sol
cargo stylus export-abi --json > abi.json

# Share with front-end team
cp abi.json ../frontend/src/abis/

5. Version control constructor changes

When adding or modifying constructors, regenerate and commit:

cargo stylus constructor > CONSTRUCTOR.txt
git add CONSTRUCTOR.txt
git commit -m "Update constructor signature"

6. Test ABI compatibility

// test/abi.test.ts
import { expect } from 'chai';
import { ethers } from 'hardhat';
import MyContractABI from '../abi.json';

describe('ABI Compatibility', () => {
it('should match deployed contract', async () => {
const contract = await ethers.getContractAt(MyContractABI, deployedAddress);

// Verify functions exist
expect(contract.getValue).to.exist;
expect(contract.setValue).to.exist;

// Call and verify
const value = await contract.getValue();
expect(value).to.be.a('bigint');
});
});

7. Keep interfaces synchronized

Use CI/CD to verify ABI is up to date:

# .github/workflows/check-abi.yml
name: Check ABI

on: [pull_request]

jobs:
check-abi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
- name: Generate ABI
run: cargo stylus export-abi > /tmp/abi.sol
- name: Check for changes
run: diff /tmp/abi.sol interfaces/IMyContract.sol

Troubleshooting

solc not found

Error: failed to run solc: No such file or directory

Solution: Install Solidity compiler:

# macOS
brew install solidity

# Ubuntu/Debian
sudo add-apt-repository ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install solc

# Or use solc-select
pip install solc-select
solc-select install 0.8.23
solc-select use 0.8.23

Feature not enabled

Error: no main function

Solution: Ensure export-abi feature is defined and main.rs exists:

# Cargo.toml
[features]
export-abi = ["stylus-sdk/export-abi"]
// main.rs
#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]

#[cfg(feature = "export-abi")]
fn main() {
my_contract::print_from_args();
}

Type not supported

Error: the trait AbiType is not implemented for MyType

Solution: Use supported types, or derive AbiType for a custom struct:

// ✅ Use supported types
pub fn process(&self, amount: U256) -> U256 { }

// ❌ Arbitrary Rust types have no ABI representation
pub fn process(&self, amount: MyCustomType) -> MyCustomType { }

For custom struct types, derive AbiType inside a sol! block. The struct can then be used in public method parameters and return values:

use stylus_sdk::alloy_sol_types::sol;
use stylus_sdk::prelude::*;

sol! {
#[derive(AbiType)]
struct Point {
uint256 x;
uint256 y;
}
}

#[public]
impl MyContract {
pub fn echo_point(&self, p: Point) -> Point {
p
}
}

Missing function in the ABI

Error: Function doesn't appear in exported ABI

Solutions:

  1. Ensure function is in #[public] impl block:

    #[public]
    impl MyContract {
    pub fn my_function(&self) -> U256 { } // ✅ Exported
    }

    impl MyContract {
    pub fn helper(&self) -> U256 { } // ❌ Not exported
    }
  2. Check function visibility is pub:

    #[public]
    impl MyContract {
    pub fn exported(&self) -> U256 { } // ✅ Exported
    fn not_exported(&self) -> U256 { } // ❌ Not exported
    }

Advanced: Multiple contracts

Export ABIs for all contracts in a workspace:

The --contract value is a cargo package name. Repeat the flag to select several contracts; with no --contract, the workspace's default contracts are exported.

# Export specific contract by package name
cargo stylus export-abi --contract=my-token

# Select several contracts in one invocation
cargo stylus export-abi --contract=token --contract=staking

# Export each to its own file
for contract in token staking governance; do
cargo stylus export-abi --contract=$contract > interfaces/I${contract^}.sol
done

Or create a script:

#!/bin/bash
# export-all-abis.sh

contracts=("token" "staking" "governance")

for contract in "${contracts[@]}"; do
echo "Exporting ABI for $contract..."
cargo stylus export-abi --contract=$contract > "interfaces/I${contract^}.sol"
cargo stylus export-abi --contract=$contract --json > "abis/${contract}.json"
done

echo "✅ All ABIs exported"

Resources