Arbitrum Stylus logo

Stylus by Example

Stylus Constructors

Starting in SDK 0.9.0 Stylus supports constructors—an atomic way to deploy → activate → initialize a contract in a single transaction. Without a constructor the contract’s storage might be touched before your init logic runs, which can lead to subtle bugs. This page explains:

  • How to declare a constructor in Rust Stylus.
  • How cargo stylus packages constructor arguments and calls the on-chain StylusDeployer.
  • A full, runnable example you can clone today.

1. Declaring a Constructor

A constructor is just a normal external function annotated with #[constructor]. Constructor can be defined as below similar to Solidity:

1pub fn constructor(&mut self, initial_number: U256) {
2        // Use tx_origin instead of msg_sender because we use a factory contract in deployment.
3        let owner = self.vm().tx_origin();
4        self.owner.set(owner);
5        self.number.set(initial_number);
6    }
1pub fn constructor(&mut self, initial_number: U256) {
2        // Use tx_origin instead of msg_sender because we use a factory contract in deployment.
3        let owner = self.vm().tx_origin();
4        self.owner.set(owner);
5        self.number.set(initial_number);
6    }

Rules & guarantees

RuleWhy it exists
Exactly 0 or 1 constructor per contractMimics Solidity and avoids ambiguity

Function must be annotated with #[constructor]
(name can be anything → external signature is always constructor())

Guarantees the deployer calls the correct init method; the SDK throws an error if a function named constructor lacks the macro

Function must take &mut selfWrites to storage during deployment
Returns () or Result<(), Vec<u8>>Reverting aborts deployment
Use tx_origin() instead of msg_sender() to access deployer address in constructorA factory contract and StylusDeployer are used in the deployment process, so msg_sender() would return the intermediate contract address
SDK writes sentinel at keccak256("stylus_constructor")Guarantees the constructor can never run twice

If your contract inherits another contract that defines a constructor you must call the parent’s constructor manually—Stylus will not inject it for you.

2. Deploying with cargo stylus

Stylus constructors are deployed through the on-chain StylusDeployer contract. cargo stylus deploy hides the details:

2.1 Pre-deployment Setup: CREATE2 Factory & StylusDeployer

Behind the scenes, cargo stylus deploy uses a CREATE2 factory to upload your Wasm bytecode deterministically and the StylusDeployer to fund and call your constructor. On Arbitrum One, Arbitrum Nova, and the Nitro Dev Node these two contracts are already deployed at the same hardcoded addresses that Cargo Stylus expects. If you’re on a custom chain or local network without them, deploy them first:

1# 1. Deploy CREATE2 factory (for deterministic bytecode upload)
2echo "Deploying the CREATE2 factory"
3cast send --rpc-url $RPC --private-key $KEY_PATH --value "1 ether" \
4     0x3fab184622dc19b6109349b94811493bf2a45362
5cast publish --rpc-url $RPC \
6  0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222
7
8# Verify factory deployment
9if [ "$(cast code -r $RPC $CREATE2_FACTORY)" == "0x" ]; then
10  echo "Failed to deploy CREATE2 factory"
11  exit 1
12fi
13
14# 2. Deploy StylusDeployer (handles constructor calls & funding)
15deployer_code=$(cat ./stylus-deployer-bytecode.txt)
16deployer_address=$(cast create2 --salt $SALT --init-code $deployer_code)
17cast send --private-key $KEY_PATH --rpc-url $RPC \
18    $CREATE2_FACTORY "$SALT$deployer_code"
19
20# Verify deployer deployment
21if [ "$(cast code -r $RPC $deployer_address)" == "0x" ]; then
22  echo "Failed to deploy StylusDeployer"
23  exit 1
24fi
25echo "StylusDeployer deployed at address: $deployer_address"
1# 1. Deploy CREATE2 factory (for deterministic bytecode upload)
2echo "Deploying the CREATE2 factory"
3cast send --rpc-url $RPC --private-key $KEY_PATH --value "1 ether" \
4     0x3fab184622dc19b6109349b94811493bf2a45362
5cast publish --rpc-url $RPC \
6  0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222
7
8# Verify factory deployment
9if [ "$(cast code -r $RPC $CREATE2_FACTORY)" == "0x" ]; then
10  echo "Failed to deploy CREATE2 factory"
11  exit 1
12fi
13
14# 2. Deploy StylusDeployer (handles constructor calls & funding)
15deployer_code=$(cat ./stylus-deployer-bytecode.txt)
16deployer_address=$(cast create2 --salt $SALT --init-code $deployer_code)
17cast send --private-key $KEY_PATH --rpc-url $RPC \
18    $CREATE2_FACTORY "$SALT$deployer_code"
19
20# Verify deployer deployment
21if [ "$(cast code -r $RPC $deployer_address)" == "0x" ]; then
22  echo "Failed to deploy StylusDeployer"
23  exit 1
24fi
25echo "StylusDeployer deployed at address: $deployer_address"

Once both contracts are live, you can use cargo stylus deploy exactly as shown below.

1# dev-node ---------------------------------------------------
2git clone https://github.com/OffchainLabs/nitro-devnode.git
3cd nitro-devnode && ./run-dev-node.sh
4
5# example repo ----------------------------------------------
6git clone -b gligneul/constructors \
7  https://github.com/OffchainLabs/stylus-hello-world.git
8cd stylus-hello-world
9
10export RPC=http://localhost:8547
11export KEY_PATH=$HOME/.key.devnode.txt   # contains the dev-node private key
12
13cargo stylus check -e $RPC               # optional static checks
14cargo stylus deploy \
15  --no-verify \
16  -e $RPC \
17  --private-key-path $KEY_PATH \
18  --constructor-args 0xdeadbeef
1# dev-node ---------------------------------------------------
2git clone https://github.com/OffchainLabs/nitro-devnode.git
3cd nitro-devnode && ./run-dev-node.sh
4
5# example repo ----------------------------------------------
6git clone -b gligneul/constructors \
7  https://github.com/OffchainLabs/stylus-hello-world.git
8cd stylus-hello-world
9
10export RPC=http://localhost:8547
11export KEY_PATH=$HOME/.key.devnode.txt   # contains the dev-node private key
12
13cargo stylus check -e $RPC               # optional static checks
14cargo stylus deploy \
15  --no-verify \
16  -e $RPC \
17  --private-key-path $KEY_PATH \
18  --constructor-args 0xdeadbeef

cargo stylus:

  • Uploads the Wasm byte-code with CREATE2 (salt = hash of the code).
  • Sends enough ETH to activate the contract.
  • Encodes your constructor args (0xdeadbeef above) and calls the constructor.
  • Emits the deployed address.

Verifying the result

1cast call -r $RPC <DEPLOYED_ADDRESS> 'number()'
2# 0x…deadbeef ✅
1cast call -r $RPC <DEPLOYED_ADDRESS> 'number()'
2# 0x…deadbeef ✅

3. Full Example

src/lib.rs

1#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
2
3extern crate alloc;
4
5use alloy_primitives::{Address, U256};
6use alloy_sol_types::sol;
7use stylus_sdk::prelude::*;
8
9sol! {
10    error Unauthorized();
11}
12
13sol_storage! {
14    #[entrypoint]
15    pub struct Contract {
16        address owner;
17        uint256 number;
18    }
19}
20
21#[derive(SolidityError)]
22pub enum ContractErrors {
23    Unauthorized(Unauthorized),
24}
25
26#[public]
27impl Contract {
28    /// The constructor sets the owner as the EOA that deployed the contract.
29    #[constructor]
30    #[payable]
31    pub fn constructor(&mut self, initial_number: U256) {
32        // Use tx_origin instead of msg_sender because we use a factory contract in deployment.
33        let owner = self.vm().tx_origin();
34        self.owner.set(owner);
35        self.number.set(initial_number);
36    }
37
38    /// Only the owner can set the number in the contract.
39    pub fn set_number(&mut self, number: U256) -> Result<(), ContractErrors> {
40        if self.owner.get() != self.vm().msg_sender() {
41            return Err(ContractErrors::Unauthorized(Unauthorized {}));
42        }
43        self.number.set(number);
44        Ok(())
45    }
46
47    pub fn number(&self) -> U256 {
48        self.number.get()
49    }
50
51    pub fn owner(&self) -> Address {
52        self.owner.get()
53    }
54}
1#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
2
3extern crate alloc;
4
5use alloy_primitives::{Address, U256};
6use alloy_sol_types::sol;
7use stylus_sdk::prelude::*;
8
9sol! {
10    error Unauthorized();
11}
12
13sol_storage! {
14    #[entrypoint]
15    pub struct Contract {
16        address owner;
17        uint256 number;
18    }
19}
20
21#[derive(SolidityError)]
22pub enum ContractErrors {
23    Unauthorized(Unauthorized),
24}
25
26#[public]
27impl Contract {
28    /// The constructor sets the owner as the EOA that deployed the contract.
29    #[constructor]
30    #[payable]
31    pub fn constructor(&mut self, initial_number: U256) {
32        // Use tx_origin instead of msg_sender because we use a factory contract in deployment.
33        let owner = self.vm().tx_origin();
34        self.owner.set(owner);
35        self.number.set(initial_number);
36    }
37
38    /// Only the owner can set the number in the contract.
39    pub fn set_number(&mut self, number: U256) -> Result<(), ContractErrors> {
40        if self.owner.get() != self.vm().msg_sender() {
41            return Err(ContractErrors::Unauthorized(Unauthorized {}));
42        }
43        self.number.set(number);
44        Ok(())
45    }
46
47    pub fn number(&self) -> U256 {
48        self.number.get()
49    }
50
51    pub fn owner(&self) -> Address {
52        self.owner.get()
53    }
54}

Cargo.toml

1[package]
2name = "stylus_constructor_example"
3version = "0.1.11"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6homepage = "https://github.com/OffchainLabs/stylus-hello-world"
7repository = "https://github.com/OffchainLabs/stylus-hello-world"
8keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
9description = "Stylus hello world example"
10[dependencies]
11alloy-primitives = "=0.8.20"
12alloy-sol-types = "=0.8.20"
13mini-alloc = "0.9.0"
14stylus-sdk = "0.9.0"
15hex = "0.4.3"
16dotenv = "0.15.0"
17
18[dev-dependencies]
19tokio = { version = "1.12.0", features = ["full"] }
20ethers = "2.0"
21eyre = "0.6.8"
22stylus-sdk = { version = "0.9.0", features = ["stylus-test"] }
23
24[features]
25export-abi = ["stylus-sdk/export-abi"]
26debug = ["stylus-sdk/debug"]
27[[bin]]
28name = "stylus-hello-world"
29path = "src/main.rs"
30[lib]
31crate-type = ["lib", "cdylib"]
32[profile.release]
33codegen-units = 1
34strip = true
35lto = true
36panic = "abort"
37# If you need to reduce the binary size, it is advisable to try other
38# optimization levels, such as "s" and "z"
39opt-level = 3
1[package]
2name = "stylus_constructor_example"
3version = "0.1.11"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6homepage = "https://github.com/OffchainLabs/stylus-hello-world"
7repository = "https://github.com/OffchainLabs/stylus-hello-world"
8keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
9description = "Stylus hello world example"
10[dependencies]
11alloy-primitives = "=0.8.20"
12alloy-sol-types = "=0.8.20"
13mini-alloc = "0.9.0"
14stylus-sdk = "0.9.0"
15hex = "0.4.3"
16dotenv = "0.15.0"
17
18[dev-dependencies]
19tokio = { version = "1.12.0", features = ["full"] }
20ethers = "2.0"
21eyre = "0.6.8"
22stylus-sdk = { version = "0.9.0", features = ["stylus-test"] }
23
24[features]
25export-abi = ["stylus-sdk/export-abi"]
26debug = ["stylus-sdk/debug"]
27[[bin]]
28name = "stylus-hello-world"
29path = "src/main.rs"
30[lib]
31crate-type = ["lib", "cdylib"]
32[profile.release]
33codegen-units = 1
34strip = true
35lto = true
36panic = "abort"
37# If you need to reduce the binary size, it is advisable to try other
38# optimization levels, such as "s" and "z"
39opt-level = 3