Skip to main content

How To Implement Inheritance Patterns in Stylus

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 inheritance in your Stylus smart contracts.

Overview

The inheritance model in Stylus aims to replicate the composition pattern found in Solidity. Types that implement the Router trait (provided by the #[public] macro) can be connected via inheritance.

Warning

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

Getting started

Before implementing inheritance, ensure you have:

  1. Installed the Rust toolchain
  2. Installed cargo-stylus CLI tool: cargo install --force cargo-stylus
  3. Set up a Stylus project: cargo stylus new your_project_name

Understanding the inheritance model in stylus

The inheritance pattern in Stylus requires two components:

  1. A storage structure using the #[borrow] annotation
  2. An implementation block using the #[inherit] annotation

When you use these annotations properly, the child contract will be able to inherit the public methods from the parent contract.

Basic inheritance pattern

Let's walk through a practical example of implementing inheritance in Stylus.

Step 1: Define the base contract

First, define your base contract that will be inherited:

Base Contract Example
use stylus_sdk::{alloy_primitives::U256, prelude::*};

sol_storage! {
pub struct BaseContract {
uint256 value;
}
}

#[public]
impl BaseContract {
pub fn get_value(&self) -> Result<U256, Vec<u8>> {
Ok(self.value.get())
}

pub fn set_value(&mut self, new_value: U256) -> Result<(), Vec<u8>> {
self.value.set(new_value);
Ok(())
}
}

In this example, we've created a simple base contract with a single state variable and two methods to get and set its value.

Step 2: Define the child contract with inheritance

Next, create your child contract that inherits from the base contract:

Child Contract Example
sol_storage! {
#[entrypoint]
pub struct ChildContract {
#[borrow]
BaseContract base_contract;
uint256 additional_value;
}
}

#[public]
#[inherit(BaseContract)]
impl ChildContract {
pub fn get_additional_value(&self) -> Result<U256, Vec<u8>> {
Ok(self.additional_value.get())
}

pub fn set_additional_value(&mut self, new_value: U256) -> Result<(), Vec<u8>> {
self.additional_value.set(new_value);
Ok(())
}
}

How it works

In the above code, when someone calls the ChildContract on a function defined in BaseContract, like get_value(), the function from BaseContract will be executed.

Here's the step-by-step process of how inheritance works in Stylus:

  1. The #[entrypoint] macro on ChildContract marks it as the entry point for Stylus execution
  2. The #[borrow] annotation on the BaseContract field implements the Borrow<BaseContract> trait, allowing the child to access the parent's storage
  3. The #[inherit(BaseContract)] annotation on the implementation connects the child to the parent's methods through the Router trait

When a method is called on ChildContract, it first checks if the requested method exists within ChildContract. If a matching function is not found, it will then try the BaseContract. Only after trying everything ChildContract inherits will the call revert.

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.

For example:

Method Overriding Example
#[public]
#[inherit(BaseContract)]
impl ChildContract {
// This overrides the base_contract.set_value method
pub fn set_value(&mut self, new_value: U256) -> Result<(), Vec<u8>> {
// Custom implementation with validation
if new_value > U256::from(100) {
return Err("Value too large".as_bytes().to_vec());
}
self.base_contract.set_value(new_value)?;
Ok(())
}
}
No Explicit Override Keywords

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

Methods search order

When using inheritance, 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 inherited types, in the order specified in the #[inherit] annotation
  3. If the method is not found in any inherited type, the call reverts

In a typical inheritance chain:

  • Calling a method first searches in the child contract
  • If not found there, it looks in the first parent specified in the #[inherit] list
  • If still not found, it searches in the next parent in the list
  • This continues until the method is found or all possibilities are exhausted

Advanced inheritance patterns

Chained inheritance

Inheritance can be chained. When using #[inherit(A, B, C)], the contract will inherit all three types, checking for methods in that order. Types A, B, and C may also inherit other types themselves. Method resolution follows a Depth First Search pattern.

Chained Inheritance Example
#[public]
#[inherit(A, B, C)]
impl MyContract {
// Custom implementations here
}

When using chained inheritance, remember that method resolution follows the order specified in the #[inherit] annotation, from left to right, with depth-first search.

Generics and inheritance

Stylus also supports using generics with inheritance, which is particularly useful for creating configurable base contracts:

Generics with Inheritance Example
pub trait Erc20Params {
const NAME: &'static str;
const SYMBOL: &'static str;
const DECIMALS: u8;
}

sol_storage! {
pub struct Erc20<T> {
mapping(address => uint256) balances;
PhantomData<T> phantom; // Zero-cost generic parameter
}
}

// Implementation for the generic base contract
#[public]
impl<T: Erc20Params> Erc20<T> {
// Methods here
}

// Usage in a child contract
struct MyTokenParams;
impl Erc20Params for MyTokenParams {
const NAME: &'static str = "MyToken";
const SYMBOL: &'static str = "MTK";
const DECIMALS: u8 = 18;
}

sol_storage! {
#[entrypoint]
pub struct MyToken {
#[borrow]
Erc20<MyTokenParams> erc20;
}
}

#[public]
#[inherit(Erc20<MyTokenParams>)]
impl MyToken {
// Custom implementations here
}

This pattern allows consumers of generic base contracts like Erc20 to choose immutable constants via specialization.

Storage layout considerations

Storage Layout in Inherited Contracts

Note that one exception to Stylus's storage layout guarantee is contracts which utilize inheritance. The current solution in Stylus using #[borrow] and #[inherit(...)] packs nested (inherited) structs into their own slots. This is consistent with regular struct nesting in solidity, but not inherited structs.

This has important implications when upgrading from Solidity to Rust, as storage slots may not align the same way. The Stylus team plans to revisit this behavior in an upcoming release.

Working example: ERC-20 token with inheritance

A practical example of inheritance in Stylus is implementing an ERC-20 token with custom functionality. Here's how it works:

ERC-20 Implementation with Inheritance
// Define the base ERC-20 functionality parameters
struct StylusTokenParams;
impl Erc20Params for StylusTokenParams {
const NAME: &'static str = "StylusToken";
const SYMBOL: &'static str = "STK";
const DECIMALS: u8 = 18;
}

// Storage definition with inheritance
sol_storage! {
#[entrypoint]
struct StylusToken {
#[borrow]
Erc20<StylusTokenParams> erc20;
}
}

// Implementation with inheritance
#[public]
#[inherit(Erc20<StylusTokenParams>)]
impl StylusToken {
// Add custom functionality
pub fn mint(&mut self, value: U256) -> Result<(), Erc20Error> {
self.erc20.mint(msg::sender(), value)?;
Ok(())
}

// Add more custom methods as needed
pub fn mint_to(&mut self, to: Address, value: U256) -> Result<(), Erc20Error> {
self.erc20.mint(to, value)?;
Ok(())
}

// Override parent methods if needed
pub fn burn(&mut self, value: U256) -> Result<(), Erc20Error> {
self.erc20.burn(msg::sender(), value)?;
Ok(())
}
}

This example shows how to inherit from a generic ERC-20 implementation and add custom functionality like minting. The pattern is very useful for token contracts where you need all the standard ERC-20 functionality but want to add custom features.

Current limitations and best practices

Limitations

  1. Stylus doesn't support contract multi-inheritance yet
  2. The storage layout for inherited contracts differs from Solidity's inheritance model
  3. There's a risk of undetected selector collisions with functions from inherited contracts

Best practices

  1. Use cargo expand to examine the expanded code and verify inheritance is working as expected
  2. Be cautious with method overriding since there are no explicit override keywords
  3. Design your contracts with single inheritance in mind
  4. Test thoroughly to ensure all inherited methods work correctly
  5. Be aware of potential storage layout differences when migrating from Solidity
  6. Consider using OpenZeppelin's Rust contracts for standardized implementations

Debugging inheritance issues

If you encounter issues with inheritance in your Stylus contracts, try these approaches:

  1. Verify the #[borrow] annotation is correctly applied to the parent contract field
  2. Ensure the #[inherit] annotation includes the correct parent contract type
  3. Check for method name conflicts between parent and child contracts
  4. Use the cargo stylus check command to verify your contract compiles correctly
  5. Test individual methods from both the parent and child contracts to isolate issues

For more information, refer to the Stylus SDK documentation and Stylus by Example.