Skip to main content

Using inheritance with trait-based composition model

Inheritance allows you to build upon existing smart contract functionality without duplicating code. In Stylus, the Rust SDK provides tools to implement inheritance patterns similar to Solidity, but with some important differences. This guide walks you through implementing trait-based composition in your Stylus smart contracts.

Overview

The Stylus SDK offers trait-based composition using traits and the #[implements] annotation. This approach follows Rust's composition patterns and provides stronger type safety.

Warning

Stylus doesn't currently support contract multi-inheritance yet, so you should design your contracts accordingly.

Getting started

Before implementing trait-based composition, ensure you have:

Rust toolchain

Follow the instructions on Rust Lang's installation page to install a complete Rust toolchain (v1.81 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 below example sets your default Rust toolchain to 1.81 as well as adding the WASM build target:

rustup default 1.81
rustup target add wasm32-unknown-unknown --toolchain 1.81

You can verify that cargo stylus is installed by running cargo stylus --help in your terminal, which will return a list of helpful commands.

Trait-based composition model

The recommended approach to inheritance in Stylus uses traits and the #[implements] annotation, which follows Rust's standard composition patterns:

Basic example of trait-based composition

Trait-Based Inheritance Example: ERC-20
use stylus_sdk::{
alloy_primitives::{Address, U256},
prelude::*,
storage::{StorageAddress, StorageMap, StorageU256},
};

// Define traits for different functionality
trait IErc20 {
fn name(&self) -> String;
fn symbol(&self) -> String;
fn decimals(&self) -> U256;
fn total_supply(&self) -> U256;
fn balance_of(&self, account: Address) -> U256;
fn transfer(&mut self, to: Address, value: U256) -> bool;
}

trait IOwnable {
fn owner(&self) -> Address;
fn transfer_ownership(&mut self, new_owner: Address) -> bool;
fn renounce_ownership(&mut self) -> bool;
}

// Define storage for each component
#[storage]
struct Erc20 {
balances: StorageMap<Address, StorageU256>,
total_supply: StorageU256,
}

#[storage]
struct Ownable {
owner: StorageAddress,
}

// Define the main contract that composes different functionality
#[storage]
#[entrypoint]
struct Contract {
erc20: Erc20,
ownable: Ownable,
}

// The #[implements] attribute connects the contract to the traits it implements
#[public]
#[implements(IErc20, IOwnable)]
impl Contract {}

// Implement the ERC20 interface for the contract
#[public]
impl IErc20 for Contract {
fn name(&self) -> String {
"MyToken".to_string()
}

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

fn decimals(&self) -> U256 {
U256::from(18)
}

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

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

fn transfer(&mut self, to: Address, value: U256) -> bool {
// Implementation here
true
}
}

// Implement the Ownable interface for the contract
#[public]
impl IOwnable for Contract {
fn owner(&self) -> Address {
self.ownable.owner.get()
}

fn transfer_ownership(&mut self, new_owner: Address) -> bool {
// Implementation here
true
}

fn renounce_ownership(&mut self) -> bool {
// Implementation here
true
}
}

How trait-based composition works

The trait-based composition model follows these principles:

  1. Define traits that represent interfaces (similar to Solidity interfaces)
  2. Implement these traits for your contract
  3. Use the #[implements(...)] attribute to tell the Stylus SDK which traits your contract implements
  4. The router will connect incoming calls to the appropriate implementation

This approach is aligned with Rust's composition patterns and offers better type safety.

Method overriding

If both parent and child implement the same method, the one in the child will override the one in the parent. This allows for customizing inherited functionality.

No explicit override keywords

Stylus does not currently contain explicit override or virtual keywords for marking override functions. It is important to carefully ensure that contracts are only overriding the functions you intend to override.

ABI export considerations with trait-based composition

When using trait-based composition, you need to be careful about function selectors to ensure correct ABI generation. Due to how Rust handles traits, you may need to explicitly set selectors for methods to match Solidity's expected function signatures.

Selector precision

When implementing traits with methods that have matching names, you must manually use the #[selector(name = "ActualName")] attribute to avoid method selector collisions. This is particularly important when implementing standard interfaces like ERC-20 or ERC-721.

Selector issue example: ERC-721
// In Solidity, both these functions would have different selectors:
// function safeTransferFrom(address from, address to, uint256 tokenId)
// function safeTransferFrom(address from, address to, uint256 tokenId, bytes data)

// In Rust, we need to use different method names, but want the same selectors: #[public]
impl<T: Erc721Params> Erc721<T> {
// Use the #[selector] attribute to specify the correct Solidity-compatible name #[selector(name = "safeTransferFrom")]
pub fn safe_transfer_from_with_data<S: TopLevelStorage + BorrowMut<Self>>(
storage: &mut S,
from: Address,
to: Address,
token_id: U256,
data: Bytes,
) -> Result<(), Erc721Error> {
// Implementation
}

// This method also needs the same selector name
#[selector(name = "safeTransferFrom")]
pub fn safe_transfer_from<S: TopLevelStorage + BorrowMut<Self>>(
storage: &mut S,
from: Address,
to: Address,
token_id: U256,
) -> Result<(), Erc721Error> {
// Implementation
}

}

ABI generation and inheritance

The Stylus SDK generates ABIs based on the methods that are available at the entrypoint contract. When using trait-based composition, make sure that all methods you want exposed in the ABI are properly included through the #[implements] attribute.

Methods search order

When using trait-based composition, it's important to understand the order in which methods are searched:

  1. The search starts in the type that uses the #[entrypoint] macro
  2. If the method is not found, the search continues in the implemented traits, in the order specified in the #[implements] annotation
  3. If the method is not found in any implemented trait, the call reverts

In a typical composition chain:

  • Calling a method first searches in the contract itself
  • If not found there, it looks in the first trait specified in the inheritance list
  • If still not found, it searches in the next trait in the list
  • This continues until the method is found or all possibilities are exhausted