Arbitrum Stylus logo

Stylus by Example

Inheritance (Trait-Based Composition)

Stylus doesn’t use classical inheritance. Instead, you compose behavior with Rust traits and expose them through the router using #[implements(...)]. This yields Solidity-compatible ABIs with strong type safety.

For Solidity developers

Think of traits like interfaces, and #[implements(...)] as wiring those interfaces into your contract’s externally callable surface (ABI). There’s no virtual/override; you model reuse via composition and default methods.

Overview

  • Define traits (interfaces) per capability (e.g., ERC-20, Ownable).
  • Define storage components per capability.
  • Build an entrypoint contract that contains those components.
  • Use one #[public] inherent impl with #[implements(...)] to export the traits.
  • Implement each trait for the entrypoint type with #[public] impl Trait for Contract.

ABI export considerations (selector precision)

Rust method names are snake_case; Solidity selectors are camelCase and may overlap across traits. When two methods would collide in the ABI, add:

1#[selector(name = "ActualSolidityName")]
1#[selector(name = "ActualSolidityName")]

In the example below, ERC-20’s name() remains standard, while an extra branding method is exported as displayName() to avoid colliding with name().

Method search order

When a call hits your contract, the router checks:

  1. Methods defined directly on the entrypoint (the #[public] impl Contract { ... } block)
  2. Methods from traits listed in #[implements(...)], in order
  3. If not found, the call reverts

Full Example

This single example shows:

  • Trait-based composition (ERC-20 + Ownable)
  • A second trait (IBranding) exposing a “name-like” method with a custom selector (displayName) to avoid ABI collisions with ERC-20’s name()
  • A single #[public] inherent block wired with #[implements(...)] (no router conflicts)

src/lib.rs

1// Copyright 2025, Offchain Labs, Inc.
2// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3
4#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
5
6extern crate alloc;
7
8use stylus_sdk::{
9    alloy_primitives::{Address, U256},
10    prelude::*,
11    storage::{StorageAddress, StorageMap, StorageU256},
12};
13
14// ──────────────────────────────────────────────────────────────────────────────
15// Traits (interfaces)
16// ──────────────────────────────────────────────────────────────────────────────
17
18trait IErc20 {
19    fn name(&self) -> String;
20    fn symbol(&self) -> String;
21    fn decimals(&self) -> U256;
22    fn total_supply(&self) -> U256;
23    fn balance_of(&self, account: Address) -> U256;
24    fn transfer(&mut self, to: Address, value: U256) -> bool;
25}
26
27trait IOwnable {
28    fn owner(&self) -> Address;
29    fn transfer_ownership(&mut self, new_owner: Address) -> bool;
30    fn renounce_ownership(&mut self) -> bool;
31}
32
33// Extra trait with a “name-like” concept we also want to export.
34// We'll give it a distinct Solidity-visible selector to avoid clobbering ERC-20's `name()`.
35trait IBranding {
36    fn brand_name(&self) -> String;
37}
38
39// ──────────────────────────────────────────────────────────────────────────────
40/* Storage components */
41// ──────────────────────────────────────────────────────────────────────────────
42
43#[storage]
44struct Erc20 {
45    balances: StorageMap<Address, StorageU256>,
46    total_supply: StorageU256,
47}
48
49#[storage]
50struct Ownable {
51    owner: StorageAddress,
52}
53
54// ──────────────────────────────────────────────────────────────────────────────
55/* Entrypoint contract */
56// ──────────────────────────────────────────────────────────────────────────────
57
58#[storage]
59#[entrypoint]
60struct Contract {
61    erc20: Erc20,
62    ownable: Ownable,
63}
64
65// One (and only one) public inherent impl with the router wiring.
66// Add traits here to export them in the ABI.
67#[public]
68#[implements(IErc20, IOwnable, IBranding)]
69impl Contract {}
70
71// ──────────────────────────────────────────────────────────────────────────────
72/* Trait implementations */
73// ──────────────────────────────────────────────────────────────────────────────
74
75#[public]
76impl IErc20 for Contract {
77    fn name(&self) -> String {
78        "MyToken".to_string()
79    }
80
81    fn symbol(&self) -> String {
82        "MTK".to_string()
83    }
84
85    fn decimals(&self) -> U256 {
86        U256::from(18)
87    }
88
89    fn total_supply(&self) -> U256 {
90        self.erc20.total_supply.get()
91    }
92
93    fn balance_of(&self, account: Address) -> U256 {
94        self.erc20.balances.get(account)
95    }
96
97    fn transfer(&mut self, to: Address, value: U256) -> bool {
98        // Example-only: fill in real checks/moves as needed
99        let from = self.vm().msg_sender();
100        if from == Address::ZERO || to == Address::ZERO {
101            return false;
102        }
103        let from_bal = self.erc20.balances.get(from);
104        if from_bal < value {
105            return false;
106        }
107        self.erc20.balances.setter(from).set(from_bal - value);
108        let to_bal = self.erc20.balances.get(to);
109        self.erc20.balances.setter(to).set(to_bal + value);
110        true
111    }
112}
113
114#[public]
115impl IOwnable for Contract {
116    fn owner(&self) -> Address {
117        self.ownable.owner.get()
118    }
119
120    fn transfer_ownership(&mut self, new_owner: Address) -> bool {
121        let caller = self.vm().msg_sender();
122        if caller != self.ownable.owner.get() || new_owner == Address::ZERO {
123            return false;
124        }
125        self.ownable.owner.set(new_owner);
126        true
127    }
128
129    fn renounce_ownership(&mut self) -> bool {
130        let caller = self.vm().msg_sender();
131        if caller != self.ownable.owner.get() {
132            return false;
133        }
134        self.ownable.owner.set(Address::ZERO);
135        true
136    }
137}
138
139// Important part: give the extra name-like method a DISTINCT selector.
140// This avoids colliding with ERC-20's `name()` in the ABI.
141#[public]
142impl IBranding for Contract {
143    #[selector(name = "displayName")]
144    fn brand_name(&self) -> String {
145        "MyToken".to_string()
146    }
147}
1// Copyright 2025, Offchain Labs, Inc.
2// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3
4#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
5
6extern crate alloc;
7
8use stylus_sdk::{
9    alloy_primitives::{Address, U256},
10    prelude::*,
11    storage::{StorageAddress, StorageMap, StorageU256},
12};
13
14// ──────────────────────────────────────────────────────────────────────────────
15// Traits (interfaces)
16// ──────────────────────────────────────────────────────────────────────────────
17
18trait IErc20 {
19    fn name(&self) -> String;
20    fn symbol(&self) -> String;
21    fn decimals(&self) -> U256;
22    fn total_supply(&self) -> U256;
23    fn balance_of(&self, account: Address) -> U256;
24    fn transfer(&mut self, to: Address, value: U256) -> bool;
25}
26
27trait IOwnable {
28    fn owner(&self) -> Address;
29    fn transfer_ownership(&mut self, new_owner: Address) -> bool;
30    fn renounce_ownership(&mut self) -> bool;
31}
32
33// Extra trait with a “name-like” concept we also want to export.
34// We'll give it a distinct Solidity-visible selector to avoid clobbering ERC-20's `name()`.
35trait IBranding {
36    fn brand_name(&self) -> String;
37}
38
39// ──────────────────────────────────────────────────────────────────────────────
40/* Storage components */
41// ──────────────────────────────────────────────────────────────────────────────
42
43#[storage]
44struct Erc20 {
45    balances: StorageMap<Address, StorageU256>,
46    total_supply: StorageU256,
47}
48
49#[storage]
50struct Ownable {
51    owner: StorageAddress,
52}
53
54// ──────────────────────────────────────────────────────────────────────────────
55/* Entrypoint contract */
56// ──────────────────────────────────────────────────────────────────────────────
57
58#[storage]
59#[entrypoint]
60struct Contract {
61    erc20: Erc20,
62    ownable: Ownable,
63}
64
65// One (and only one) public inherent impl with the router wiring.
66// Add traits here to export them in the ABI.
67#[public]
68#[implements(IErc20, IOwnable, IBranding)]
69impl Contract {}
70
71// ──────────────────────────────────────────────────────────────────────────────
72/* Trait implementations */
73// ──────────────────────────────────────────────────────────────────────────────
74
75#[public]
76impl IErc20 for Contract {
77    fn name(&self) -> String {
78        "MyToken".to_string()
79    }
80
81    fn symbol(&self) -> String {
82        "MTK".to_string()
83    }
84
85    fn decimals(&self) -> U256 {
86        U256::from(18)
87    }
88
89    fn total_supply(&self) -> U256 {
90        self.erc20.total_supply.get()
91    }
92
93    fn balance_of(&self, account: Address) -> U256 {
94        self.erc20.balances.get(account)
95    }
96
97    fn transfer(&mut self, to: Address, value: U256) -> bool {
98        // Example-only: fill in real checks/moves as needed
99        let from = self.vm().msg_sender();
100        if from == Address::ZERO || to == Address::ZERO {
101            return false;
102        }
103        let from_bal = self.erc20.balances.get(from);
104        if from_bal < value {
105            return false;
106        }
107        self.erc20.balances.setter(from).set(from_bal - value);
108        let to_bal = self.erc20.balances.get(to);
109        self.erc20.balances.setter(to).set(to_bal + value);
110        true
111    }
112}
113
114#[public]
115impl IOwnable for Contract {
116    fn owner(&self) -> Address {
117        self.ownable.owner.get()
118    }
119
120    fn transfer_ownership(&mut self, new_owner: Address) -> bool {
121        let caller = self.vm().msg_sender();
122        if caller != self.ownable.owner.get() || new_owner == Address::ZERO {
123            return false;
124        }
125        self.ownable.owner.set(new_owner);
126        true
127    }
128
129    fn renounce_ownership(&mut self) -> bool {
130        let caller = self.vm().msg_sender();
131        if caller != self.ownable.owner.get() {
132            return false;
133        }
134        self.ownable.owner.set(Address::ZERO);
135        true
136    }
137}
138
139// Important part: give the extra name-like method a DISTINCT selector.
140// This avoids colliding with ERC-20's `name()` in the ABI.
141#[public]
142impl IBranding for Contract {
143    #[selector(name = "displayName")]
144    fn brand_name(&self) -> String {
145        "MyToken".to_string()
146    }
147}

Cargo.toml

1[package]
2name = "stylus-inheritance-selectors"
3version = "0.1.0"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6
7[dependencies]
8alloy-primitives = "1.0"
9alloy-sol-types = "1.0"
10stylus-sdk = "0.10.0-beta.0"
11
12[dev-dependencies]
13alloy = { version = "1.0", features = ["contract", "signer-local", "rpc-types", "sha3-keccak"] }
14eyre = "0.6"
15tokio = "1.44"
16stylus-sdk = { version = "0.10.0-beta.0", features = ["stylus-test"] }
17
18[features]
19export-abi = ["stylus-sdk/export-abi"]
20
21[lib]
22crate-type = ["lib", "cdylib"]
23
24[profile.release]
25codegen-units = 1
26strip = true
27lto = true
28panic = "abort"
29opt-level = "s"
30
31[workspace]
1[package]
2name = "stylus-inheritance-selectors"
3version = "0.1.0"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6
7[dependencies]
8alloy-primitives = "1.0"
9alloy-sol-types = "1.0"
10stylus-sdk = "0.10.0-beta.0"
11
12[dev-dependencies]
13alloy = { version = "1.0", features = ["contract", "signer-local", "rpc-types", "sha3-keccak"] }
14eyre = "0.6"
15tokio = "1.44"
16stylus-sdk = { version = "0.10.0-beta.0", features = ["stylus-test"] }
17
18[features]
19export-abi = ["stylus-sdk/export-abi"]
20
21[lib]
22crate-type = ["lib", "cdylib"]
23
24[profile.release]
25codegen-units = 1
26strip = true
27lto = true
28panic = "abort"
29opt-level = "s"
30
31[workspace]

src/main.rs

1// Copyright 2025, Offchain Labs, Inc.
2// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3
4#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
5
6#[cfg(not(any(test, feature = "export-abi")))]
7#[no_mangle]
8pub extern "C" fn main() {}
9
10#[cfg(feature = "export-abi")]
11fn main() {
12    stylus_inheritance_selectors::print_from_args();
13}
1// Copyright 2025, Offchain Labs, Inc.
2// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3
4#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
5
6#[cfg(not(any(test, feature = "export-abi")))]
7#[no_mangle]
8pub extern "C" fn main() {}
9
10#[cfg(feature = "export-abi")]
11fn main() {
12    stylus_inheritance_selectors::print_from_args();
13}

Key takeaways

  • Use traits to model “inheritance”; wire them into the ABI with #[implements(...)].
  • Keep one #[public] inherent block per entrypoint to avoid router conflicts.
  • Prevent ABI collisions by assigning explicit selectors with #[selector(name = "...")] when another trait exposes a similar concept (e.g., displayName vs ERC-20 name).