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:
cargo stylus
packages constructor arguments and calls the on-chain StylusDeployer.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 }
Rule | Why it exists |
---|---|
Exactly 0 or 1 constructor per contract | Mimics Solidity and avoids ambiguity |
Function must be annotated with | Guarantees the deployer calls the correct init method; the SDK throws an
error if a function named |
Function must take &mut self | Writes to storage during deployment |
Returns () or Result<(), Vec<u8>> | Reverting aborts deployment |
Use tx_origin() instead of msg_sender() to access deployer address in constructor | A 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.
cargo stylus
Stylus constructors are deployed through the on-chain StylusDeployer contract. cargo stylus deploy
hides the details:
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
:
0xdeadbeef
above) and calls the constructor.1cast call -r $RPC <DEPLOYED_ADDRESS> 'number()'
2# 0x…deadbeef ✅
1cast call -r $RPC <DEPLOYED_ADDRESS> 'number()'
2# 0x…deadbeef ✅
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