Arbitrum Stylus logo

Stylus by Example

Inheritance

The Stylus Rust SDK replicates the composition pattern of Solidity. The #[public] macro provides the Router trait, which can be used to connect types via inheritance, via the #[inherit] macro.

Let's see an example:

1#[public]
2#[inherit(Erc20)]
3impl Token {
4    pub fn mint(&mut self, amount: U256) -> Result<(), Vec<u8>> {
5        ...
6    }
7}
8
9#[public]
10impl Erc20 {
11    pub fn balance_of() -> Result<U256> {
12        ...
13    }
14}
1#[public]
2#[inherit(Erc20)]
3impl Token {
4    pub fn mint(&mut self, amount: U256) -> Result<(), Vec<u8>> {
5        ...
6    }
7}
8
9#[public]
10impl Erc20 {
11    pub fn balance_of() -> Result<U256> {
12        ...
13    }
14}

In the above code, we can see how Token inherits from Erc20, meaning that it will inherit the public methods available in Erc20. If someone called the Token contract on the function balanceOf, the function Erc20.balance_of() would be executed.

Additionally, the inheriting type must implement the Borrow trait for borrowing data from the inherited type. In the case above, Token should implement Borrow<Erc20>. For simplicity, #[storage] and sol_storage! provide a #[borrow] annotation that can be used instead of manually implementing the trait:

1sol_storage! {
2    #[entrypoint]
3    pub struct Token {
4        #[borrow]
5        Erc20 erc20;
6        ...
7    }
8
9    pub struct Erc20 {
10        ...
11    }
12}
1sol_storage! {
2    #[entrypoint]
3    pub struct Token {
4        #[borrow]
5        Erc20 erc20;
6        ...
7    }
8
9    pub struct Erc20 {
10        ...
11    }
12}

Methods search order

A type can inherit multiple other types (as long as they use the #[public] macro). Since execution begins in the type that uses the #[entrypoint] macro, that type will be first checked when searching a specific method. If the method is not found in that type, the search will continue in the inherited types, in order of inheritance. If the method is not found in any of the inherited methods, the call will revert.

Let's see an example:

1#[public]
2#[inherit(B, C)]
3impl A {
4    pub fn foo() -> Result<(), Vec<u8>> {
5        ...
6    }
7}
8
9#[public]
10impl B {
11    pub fn bar() -> Result<(), Vec<u8>> {
12        ...
13    }
14}
15
16#[public]
17impl C {
18    pub fn bar() -> Result<(), Vec<u8>> {
19        ...
20    }
21
22    pub fn baz() -> Result<(), Vec<u8>> {
23        ...
24    }
25}
1#[public]
2#[inherit(B, C)]
3impl A {
4    pub fn foo() -> Result<(), Vec<u8>> {
5        ...
6    }
7}
8
9#[public]
10impl B {
11    pub fn bar() -> Result<(), Vec<u8>> {
12        ...
13    }
14}
15
16#[public]
17impl C {
18    pub fn bar() -> Result<(), Vec<u8>> {
19        ...
20    }
21
22    pub fn baz() -> Result<(), Vec<u8>> {
23        ...
24    }
25}

In the code above:

  • calling foo() will search the method in A, find it, and execute A.foo()
  • calling bar() will search the method in A first, then in B, find it, and execute B.bar()
  • calling baz() will search the method in A, B and finally C, so it will execute C.baz()

Notice that C.bar() won't ever be reached, since the inheritance goes through B first, which has a method named bar() too.

Finally, since the inherited types can also inherit other types themselves, keep in mind that method resolution finds the first matching method by Depth First Search.

Overriding methods

Because methods are checked in the inherited order, if two types implement the same method, the one in the higher level in the hierarchy will override the one in the lower levels, which won’t be callable. This allows for patterns where the developer imports a crate implementing a standard, like ERC-20, and then adds or overrides just the methods they want to without modifying the imported ERC-20 type.

Important warning: The Stylus Rust SDK 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.

Let's see an example:

1#[public]
2#[inherit(B, C)]
3impl A {
4    pub fn foo() -> Result<(), Vec<u8>> {
5        ...
6    }
7}
8
9#[public]
10impl B {
11    pub fn foo() -> Result<(), Vec<u8>> {
12        ...
13    }
14
15    pub fn bar() -> Result<(), Vec<u8>> {
16        ...
17    }
18}
1#[public]
2#[inherit(B, C)]
3impl A {
4    pub fn foo() -> Result<(), Vec<u8>> {
5        ...
6    }
7}
8
9#[public]
10impl B {
11    pub fn foo() -> Result<(), Vec<u8>> {
12        ...
13    }
14
15    pub fn bar() -> Result<(), Vec<u8>> {
16        ...
17    }
18}

In the example above, even though B has an implementation for foo(), calling foo() will execute A.foo() since the method is searched first in A.

Learn more