Arbitrum Stylus logo

Stylus by Example

Functions

Functions are a fundamental part of any programming language, including Stylus, enabling you to encapsulate logic into reusable components.

This guide covers the syntax and usage of functions, including internal and external functions, and how to return multiple values.

Learn More

Overview

A function in Stylus consists of a name, a set of parameters, an optional return type, and a body.

Just as with storage, Stylus methods are Solidity ABI equivalent. This means that contracts written in different programming languages are fully interoperable.

Functions are declared with the fn keyword. Parameters allow the function to accept inputs, and the return type specifies the output of the function. If no return type is specified, the function returns void.

Following is an example of a function add that takes two uint256 values and returns their sum.

1fn add(a: uint256, b: uint256) -> uint256 {
2    return a + b;
3}
1fn add(a: uint256, b: uint256) -> uint256 {
2    return a + b;
3}

Function Parameters

Function parameters are the inputs to a function. They are specified as a list of IDENTIFIER: Type pairs, separated by commas.

In this example, the function add_numbers takes two u32 parameters, a and b and returns the sum of the two numbers.

1fn add_numbers(a: u32, b: u32) -> u32 {
2    a + b
3}
1fn add_numbers(a: u32, b: u32) -> u32 {
2    a + b
3}

Return Types

Return types in functions are an essential part of defining the behavior and expected outcomes of your smart contract methods.

Here, we explain the syntax and usage of return types in Stylus with general examples.

Basic Syntax

A function with a return type in Stylus follows this basic structure. The return type is specified after the -> arrow. Values are returned using the return keyword or implicitly as the last expression of the function. In Rust and Stylus, the last expression in a function is implicitly returned, so the return keyword is often omitted.

1pub fn function_name(&self) -> ReturnType {
2    // Function body
3}
1pub fn function_name(&self) -> ReturnType {
2    // Function body
3}

Examples

Function returning a String: This get_greeting function returns a String. The return type is specified as String after the -> arrow.

1pub fn get_greeting() -> String {
2    "Hello, Stylus!".into()
3}
1pub fn get_greeting() -> String {
2    "Hello, Stylus!".into()
3}

Function returning an Integer: This get_number function returns an unsigned 32-bit integer (u32).

1pub fn get_number() -> u32 {
2    42
3}
1pub fn get_number() -> u32 {
2    42
3}

Function returning a Result with Ok and Err variants: The perform_operation function returns a Result<u32, CustomError>. The Result type is used for functions that can return either a success value (Ok) or an error (Err). In this case, it returns Ok(value) on success and an error variant of CustomError on failure.

1pub enum CustomError {
2    ErrorVariant,
3}
4
5pub fn perform_operation(value: u32) -> Result<u32, CustomError> {
6    if value > 0 {
7        Ok(value)
8    } else {
9        Err(CustomError::ErrorVariant)
10    }
11}
1pub enum CustomError {
2    ErrorVariant,
3}
4
5pub fn perform_operation(value: u32) -> Result<u32, CustomError> {
6    if value > 0 {
7        Ok(value)
8    } else {
9        Err(CustomError::ErrorVariant)
10    }
11}

Public Functions

Public functions are those that can be called by other contracts.

To define a public function in a Stylus contract, you use the #[public] macro. This macro ensures that the function is accessible from outside the contract.

Previously, all public methods were required to return a Result type with Vec<u8> as the error type. This is now optional. Specifically, if a method is "infallible" (i.e., it cannot produce an error), it does not need to return a Result type. Here's what this means:

  • Infallible methods: Methods that are guaranteed not to fail (no errors possible) do not need to use the Result type. They can return their result directly without wrapping it in Result.

  • Optional error handling: The Result type with Vec<u8> as the error type is now optional for methods that cannot produce an error.

In the following example, owner is a public function that returns the contract owner's address. Since this function is infallible (i.e., it cannot produce an error), it does not need to return a Result type.

1#[external]
2impl Contract {
3    // Define an external function to get the owner of the contract
4    pub fn owner(&self) -> Address {
5        self.owner.get()
6    }
7}
1#[external]
2impl Contract {
3    // Define an external function to get the owner of the contract
4    pub fn owner(&self) -> Address {
5        self.owner.get()
6    }
7}

Internal Functions

Internal functions are those that can only be called within the contract itself. These functions are not exposed to external calls.

To define an internal function, you simply include it within your contract's implementation without the #[public] macro.

The choice between public and internal functions depends on the desired level of accessibility and interaction within and across contracts.

In the followinge example, set_owner is an internal function that sets a new owner for the contract. It is only callable within the contract itself.

1impl Contract {
2    // Define an internal function to set a new owner
3    pub fn set_owner(&mut self, new_owner: Address) {
4        self.owner.set(new_owner);
5    }
6}
1impl Contract {
2    // Define an internal function to set a new owner
3    pub fn set_owner(&mut self, new_owner: Address) {
4        self.owner.set(new_owner);
5    }
6}

To mix public and internal functions within the same contract, you should use two separate impl blocks with the same contract name. Public functions are defined within an impl block annotated with the #[public] attribute, signifying that these functions are part of the contract's public interface and can be invoked from outside the contract. In contrast, internal functions are placed within a separate impl block that does not have the #[public] attribute, making them internal to the contract and inaccessible to external entities.

src/lib.rs

1// Only run this as a WASM if the export-abi feature is not set.
2#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
3extern crate alloc;
4
5use alloc::vec;
6use stylus_sdk::alloy_primitives::Address;
7use stylus_sdk::prelude::*;
8use stylus_sdk::storage::StorageAddress;
9
10use stylus_sdk::alloy_primitives::U256;
11use stylus_sdk::storage::StorageU256;
12use stylus_sdk::console;
13
14
15#[storage]
16#[entrypoint]
17pub struct ExampleContract {
18    owner: StorageAddress,
19    data: StorageU256,
20}
21
22#[public]
23impl ExampleContract {
24    // External function to set the data
25    pub fn set_data(&mut self, value: U256) {
26        self.data.set(value);
27    }
28
29    // External function to get the data
30    pub fn get_data(&self) -> U256 {
31        self.data.get()
32    }
33
34    // External function to get the contract owner
35    pub fn get_owner(&self) -> Address {
36        self.owner.get()
37    }
38}
39
40impl ExampleContract {
41    // Internal function to set a new owner
42    pub fn set_owner(&mut self, new_owner: Address) {
43        self.owner.set(new_owner);
44    }
45
46    // Internal function to log data
47    pub fn log_data(&self) {
48        let _data = self.data.get();
49        console!("Current data is: {:?}", _data);
50    }
51}
1// Only run this as a WASM if the export-abi feature is not set.
2#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
3extern crate alloc;
4
5use alloc::vec;
6use stylus_sdk::alloy_primitives::Address;
7use stylus_sdk::prelude::*;
8use stylus_sdk::storage::StorageAddress;
9
10use stylus_sdk::alloy_primitives::U256;
11use stylus_sdk::storage::StorageU256;
12use stylus_sdk::console;
13
14
15#[storage]
16#[entrypoint]
17pub struct ExampleContract {
18    owner: StorageAddress,
19    data: StorageU256,
20}
21
22#[public]
23impl ExampleContract {
24    // External function to set the data
25    pub fn set_data(&mut self, value: U256) {
26        self.data.set(value);
27    }
28
29    // External function to get the data
30    pub fn get_data(&self) -> U256 {
31        self.data.get()
32    }
33
34    // External function to get the contract owner
35    pub fn get_owner(&self) -> Address {
36        self.owner.get()
37    }
38}
39
40impl ExampleContract {
41    // Internal function to set a new owner
42    pub fn set_owner(&mut self, new_owner: Address) {
43        self.owner.set(new_owner);
44    }
45
46    // Internal function to log data
47    pub fn log_data(&self) {
48        let _data = self.data.get();
49        console!("Current data is: {:?}", _data);
50    }
51}

Cargo.toml

1[package]
2name = "stylus-functions"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "=0.7.6"
8alloy-sol-types = "=0.7.6"
9mini-alloc = "0.4.2"
10stylus-sdk = "0.6.0"
11hex = "0.4.3"
12sha3 = "0.10.8"
13
14[features]
15export-abi = ["stylus-sdk/export-abi"]
16debug = ["stylus-sdk/debug"]
17
18[lib]
19crate-type = ["lib", "cdylib"]
20
21[profile.release]
22codegen-units = 1
23strip = true
24lto = true
25panic = "abort"
26opt-level = "s"
1[package]
2name = "stylus-functions"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "=0.7.6"
8alloy-sol-types = "=0.7.6"
9mini-alloc = "0.4.2"
10stylus-sdk = "0.6.0"
11hex = "0.4.3"
12sha3 = "0.10.8"
13
14[features]
15export-abi = ["stylus-sdk/export-abi"]
16debug = ["stylus-sdk/debug"]
17
18[lib]
19crate-type = ["lib", "cdylib"]
20
21[profile.release]
22codegen-units = 1
23strip = true
24lto = true
25panic = "abort"
26opt-level = "s"