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:
#[public]macro: Annotates public functionsexport-abifeature: Enables ABI generation codecargo stylus export-abi: CLI command to generate output- Solidity interface: Generated Solidity interface file
- 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 Type | Solidity Type | Example |
|---|---|---|
U256 | uint256 | Token amounts |
U128, u128 | uint128 | Medium integers |
u64, u32, u16, u8 | uint64, uint32, uint16, uint8 | Small integers |
I256 | int256 | Signed integers |
Address | address | Account addresses |
bool | bool | Boolean values |
FixedBytes<N> | bytesN | Fixed-size byte arrays |
Bytes | bytes | Dynamic byte arrays |
String | string | UTF-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:
- A
GenerateAbitrait implementation - A CLI entry point for running ABI export
- 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-abifeature 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_case→camelCasefunction names- Rust types → Solidity types
&self→view,&mut self→ non-viewResult<T, E>→ return typeT, errorE
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:
-
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
} -
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"