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.
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.
#[public] inherent impl with #[implements(...)] to export the traits.#[public] impl Trait for Contract.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().
When a call hits your contract, the router checks:
#[public] impl Contract { ... } block)#[implements(...)], in orderThis single example shows:
displayName) to avoid ABI collisions with ERC-20’s name()#[public] inherent block wired with #[implements(...)] (no router conflicts)src/lib.rs1// 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.toml1[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.rs1// 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}#[implements(...)].#[public] inherent block per entrypoint to avoid router conflicts.#[selector(name = "...")] when another trait exposes a similar concept (e.g., displayName vs ERC-20 name).