Skip to main content

Exporting Solidity 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
  • 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—just 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
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 main function:

  • Runs only when 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 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