Skip to main content

Exporting ABIs

Stylus contracts written in Rust can automatically generate Solidity Application Binary Interfaces (ABIs) that enable seamless 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 caller);
}

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

Output:

[
{
"type": "function",
"name": "getValue",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "setValue",
"inputs": [
{
"name": "new_value",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
}
]

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::prelude::*;

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

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

Generated interface includes errors:

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

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

Events

Events are automatically included in the ABI:

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
evm::log(Transfer {
from: 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(owner: Address, initial_value: U256) {
self.owner.set(owner);
self.initial_value.set(initial_value);
}

// Other methods...
}

Export constructor signature:

cargo stylus export-abi 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);
// msg::value() is available
}
}

Output:

constructor(address owner) payable

Export configuration

Custom license

Specify a custom SPDX license identifier:

cargo stylus export-abi --license=GPL-3.0

Output includes:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;

Custom pragma

Specify a custom Solidity version pragma:

cargo stylus export-abi --pragma="pragma solidity ^0.8.20;"

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 export-abi 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 implement AbiType:

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

// ❌ Custom types need AbiType implementation
pub fn process(&self, amount: MyCustomType) -> MyCustomType { }

For custom types, implement AbiType:

use stylus_sdk::abi::AbiType;

#[derive(Clone)]
struct MyType(U256);

impl AbiType for MyType {
type SolType = alloy_sol_types::sol_data::Uint<256>;

fn encode(&self) -> Vec<u8> {
self.0.encode()
}

fn decode(data: &[u8]) -> Result<Self, alloy_sol_types::Error> {
U256::decode(data).map(MyType)
}
}

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:

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

# Export all contracts
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