Skip to main content

Using constructors with Stylus

Constructors allow you to initialize your Stylus smart contracts with specific parameters when deploying them. This guide will show you how to implement constructors in Rust, understand their behavior, and deploy contracts using them.

What you'll accomplish

By the end of this guide, you'll be able to:

  • Implement constructor functions in Stylus contracts
  • Understand the constructor rules and limitations
  • Deploy contracts with constructor parameters
  • Test the constructor functionality
  • Handle constructor errors and validation

Prerequisites

Before implementing constructors, ensure you have:

Rust toolchain

Follow the instructions on Rust Lang's installation page to install a complete Rust toolchain (v1.88 or newer) on your system. After installation, ensure you can access the programs rustup, rustc, and cargo from your preferred terminal application.

cargo stylus

In your terminal, run:

cargo install --force cargo-stylus

Add WASM (WebAssembly) as a build target for the specific Rust toolchain you are using. The below example sets your default Rust toolchain to 1.88 as well as adding the WASM build target:

rustup default 1.88
rustup target add wasm32-unknown-unknown --toolchain 1.88

You can verify that cargo stylus is installed by running cargo stylus --help in your terminal, which will return a list of helpful commands.

A local Arbitrum test node

Instructions on how to set up a local Arbitrum test node can be found in the Nitro-devnode repository.

Understanding Stylus constructors

Stylus constructors provide an atomic way to deploy, activate, and initialize a contract in a single transaction. If your contract lacks a constructor, it may allow access to the contract's storage before the initialization logic runs, leading to unexpected behavior.

Constructors and composition

Stylus uses trait-based composition instead of traditional inheritance. When building contracts that compose multiple traits, constructors help initialize all components properly. See the Constructor with trait-based composition section for examples.

Constructor rules and guarantees

Stylus constructors follow these important rules:

RuleWhy it exists
Exactly 0 or 1 constructor per contractMimics Solidity behavior and avoids ambiguity
Must be annotated with #[constructor]Guarantees the deployer calls the correct initialization method
Must take &mut selfAllows writing to contract storage during deployment
Returns () or Result<(), Error>Enables error handling; reverting aborts deployment
Use tx_origin() for deployer addressFactory contracts are used in deployment, so msg_sender() returns the factory address
Constructor runs exactly onceThe SDK uses a sentinel system to prevent re-execution
Factory pattern in deployment

Stylus uses a factory pattern for deployment, which means msg_sender() in a constructor returns the factory contract address, not the deployer. Always use tx_origin() to get the actual deployer address.

Basic constructor implementation

Here's a simple example of a constructor in a Stylus contract:

#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]

extern crate alloc;

use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
use stylus_sdk::prelude::*;

sol! {
#[derive(Debug)]
error InvalidAmount();
}

sol_storage! {
#[entrypoint]
pub struct SimpleToken {
address owner;
uint256 total_supply;
string name;
string symbol;
mapping(address => uint256) balances;
}
}

#[derive(SolidityError, Debug)]
pub enum SimpleTokenError {
InvalidAmount(InvalidAmount),
}

#[public]
impl SimpleToken {
/// Constructor initializes the token with a name, symbol, and initial supply
#[constructor]
#[payable]
pub fn constructor(
&mut self,
name: String,
symbol: String,
initial_supply: U256,
) -> Result<(), SimpleTokenError> {
// Validate input parameters
if initial_supply == U256::ZERO {
return Err(SimpleTokenError::InvalidAmount(InvalidAmount {}));
}

// Get the deployer address using tx_origin()
let deployer = self.vm().tx_origin();

// Initialize contract state
self.owner.set(deployer);
self.name.set_str(&name);
self.symbol.set_str(&symbol);
self.total_supply.set(initial_supply);

// Mint initial supply to deployer
self.balances.setter(deployer).set(initial_supply);

Ok(())
}

// Additional contract methods...
pub fn balance_of(&self, account: Address) -> U256 {
self.balances.get(account)
}

pub fn total_supply(&self) -> U256 {
self.total_supply.get()
}
}

Key implementation details

  1. Parameter validation: Always validate constructor parameters before proceeding with initialization
  2. Error handling: Use Result<(), Error> to handle initialization failures gracefully
  3. Payable constructors: Add #[payable] to receive ETH during deployment
  4. State initialization: Set all necessary contract state in the constructor

Advanced constructor patterns

Constructor with complex validation

#[constructor]
#[payable]
pub fn constructor(
&mut self,
name: String,
symbol: String,
initial_supply: U256,
max_supply: U256,
) -> Result<(), TokenContractError> {
// Multiple validation checks
if initial_supply == U256::ZERO {
return Err(TokenContractError::InvalidAmount(InvalidAmount {}));
}

if initial_supply > max_supply {
return Err(TokenContractError::TooManyTokens(TooManyTokens {}));
}

if name.is_empty() || symbol.is_empty() {
return Err(TokenContractError::InvalidAmount(InvalidAmount {}));
}

let deployer = self.vm().tx_origin();

// Initialize with timestamp tracking
self.owner.set(deployer);
self.name.set_str(&name);
self.symbol.set_str(&symbol);
self.total_supply.set(initial_supply);
self.max_supply.set(max_supply);
self.created_at.set(U256::from(self.vm().block_timestamp()));

// Mint tokens to deployer
self.balances.setter(deployer).set(initial_supply);

// Emit initialization event
log(self.vm(), TokenCreated {
creator: deployer,
name: name.clone(),
symbol: symbol.clone(),
initial_supply,
});

Ok(())
}

Constructor with trait-based composition

Stylus uses trait-based composition instead of traditional inheritance. When implementing constructors with traits, each component typically has its own initialization logic:

// Define traits for different functionality
trait IErc20 {
fn balance_of(&self, account: Address) -> U256;
fn transfer(&mut self, to: Address, value: U256) -> bool;
}

trait IOwnable {
fn owner(&self) -> Address;
fn transfer_ownership(&mut self, new_owner: Address) -> bool;
}

// Define storage for each component
#[storage]
struct Erc20Component {
balances: StorageMap<Address, StorageU256>,
total_supply: StorageU256,
}

#[storage]
struct OwnableComponent {
owner: StorageAddress,
}

// Main contract that composes functionality
#[storage]
#[entrypoint]
struct MyToken {
erc20: Erc20Component,
ownable: OwnableComponent,
name: StorageString,
symbol: StorageString,
}

#[public]
#[implements(IErc20, IOwnable)]
impl MyToken {
#[constructor]
pub fn constructor(
&mut self,
name: String,
symbol: String,
initial_supply: U256,
) -> Result<(), TokenError> {
// Initialize each component
self.initialize_ownable()?;
self.initialize_erc20(initial_supply)?;

// Initialize contract-specific state
self.name.set_str(&name);
self.symbol.set_str(&symbol);

Ok(())
}

fn initialize_ownable(&mut self) -> Result<(), TokenError> {
let deployer = self.vm().tx_origin();
self.ownable.owner.set(deployer);
Ok(())
}

fn initialize_erc20(&mut self, initial_supply: U256) -> Result<(), TokenError> {
if initial_supply == U256::ZERO {
return Err(TokenError::InvalidSupply);
}

let deployer = self.vm().tx_origin();
self.erc20.total_supply.set(initial_supply);
self.erc20.balances.setter(deployer).set(initial_supply);
Ok(())
}
}
Trait-based composition in Stylus

Unlike Solidity's inheritance, Stylus uses Rust's trait system for composition. Each component is initialized explicitly in the constructor.

Testing constructors

The Stylus SDK provides comprehensive testing tools for constructor functionality:

#[cfg(test)]
mod tests {
use super::*;
use stylus_sdk::testing::*;

#[test]
fn test_constructor_success() {
let vm = TestVMBuilder::new()
.sender(Address::from([0x01; 20]))
.build();

let mut contract = SimpleToken::from(&vm);

let result = contract.constructor(
"Test Token".to_string(),
"TEST".to_string(),
U256::from(1000000),
);

assert!(result.is_ok());
assert_eq!(contract.name.get_string(), "Test Token");
assert_eq!(contract.symbol.get_string(), "TEST");
assert_eq!(contract.total_supply.get(), U256::from(1000000));
assert_eq!(
contract.balance_of(Address::from([0x01; 20])),
U256::from(1000000)
);
}

#[test]
fn test_constructor_validation() {
let vm = TestVMBuilder::new()
.sender(Address::from([0x01; 20]))
.build();

let mut contract = SimpleToken::from(&vm);

// Test zero supply rejection
let result = contract.constructor(
"Test Token".to_string(),
"TEST".to_string(),
U256::ZERO,
);

assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SimpleTokenError::InvalidAmount(_)
));
}
}

Deploying contracts with constructors

Using cargo stylus

Deploy your contract with constructor arguments using cargo stylus deploy:

# Deploy with constructor parameters
cargo stylus deploy \
--private-key-path ~/.arbitrum/key \
--endpoint https://sepolia-rollup.arbitrum.io/rpc \
--constructor-args "MyToken" "MTK" 1000000

Constructor argument encoding

cargo stylus automatically encodes the constructor arguments. The arguments should be provided in the same order as defined in your constructor function.

For complex types:

  • Strings: Provide as quoted strings
  • Numbers: Provide as decimal or hex (0x prefix)
  • Addresses: Provide as hex strings with 0x prefix
  • Arrays: Use JSON array syntax
# Example with multiple argument types
cargo stylus deploy \
--constructor-args "TokenName" "TKN" 1000000 "0x742d35Cc6635C0532925a3b8D95B5C1b0ea3C28F"

Best practices

Constructor parameter validation

Always validate constructor parameters to prevent deployment of misconfigured contracts:

#[constructor]
pub fn constructor(&mut self, params: ConstructorParams) -> Result<(), Error> {
// Validate all parameters before any state changes
self.validate_parameters(&params)?;

// Initialize state only after validation passes
self.initialize_state(params)?;

Ok(())
}

fn validate_parameters(&self, params: &ConstructorParams) -> Result<(), Error> {
if params.name.is_empty() {
return Err(Error::InvalidName);
}
// Additional validation...
Ok(())
}

Error handling patterns

Use descriptive error types and provide meaningful error messages:

sol! {
#[derive(Debug)]
error InvalidName(string reason);
#[derive(Debug)]
error InvalidSupply(uint256 provided, uint256 max_allowed);
#[derive(Debug)]
error Unauthorized(address caller);
}

#[derive(SolidityError, Debug)]
pub enum ConstructorError {
InvalidName(InvalidName),
InvalidSupply(InvalidSupply),
Unauthorized(Unauthorized),
}

State initialization order

Initialize contract state in a logical order to avoid dependency issues:

#[constructor]
pub fn constructor(&mut self, params: ConstructorParams) -> Result<(), Error> {
// 1. Validate parameters first
self.validate_parameters(&params)?;

// 2. Set basic contract metadata
self.name.set_str(&params.name);
self.symbol.set_str(&params.symbol);

// 3. Set ownership and permissions
let deployer = self.vm().tx_origin();
self.owner.set(deployer);

// 4. Initialize token economics
self.total_supply.set(params.initial_supply);
self.max_supply.set(params.max_supply);

// 5. Set up initial balances
self.balances.setter(deployer).set(params.initial_supply);

// 6. Emit events last
log(self.vm(), ContractInitialized { /* ... */ });

Ok(())
}

Common pitfalls and solutions

Using msg_sender() instead of tx_origin()

Problem: Using msg_sender() in constructors returns the factory contract address, not the deployer.

// ❌ Wrong - returns factory address
let deployer = self.vm().msg_sender();

// ✅ Correct - returns actual deployer
let deployer = self.vm().tx_origin();

Missing parameter validation

Problem: Not validating constructor parameters can lead to unusable contracts.

// ❌ Wrong - no validation
#[constructor]
pub fn constructor(&mut self, supply: U256) {
self.total_supply.set(supply); // Could be zero!
}

// ✅ Correct - validate first
#[constructor]
pub fn constructor(&mut self, supply: U256) -> Result<(), Error> {
if supply == U256::ZERO {
return Err(Error::InvalidSupply);
}
self.total_supply.set(supply);
Ok(())
}

Forgetting the #[constructor] annotation

Problem: Functions named "constructor" without the annotation won't be recognized.

// ❌ Wrong - missing annotation
pub fn constructor(&mut self, value: U256) {
// This won't be called during deployment
}

// ✅ Correct - properly annotated
#[constructor]
pub fn constructor(&mut self, value: U256) {
// This will be called during deployment
}

Summary

Constructors in Stylus provide a powerful way to initialize your smart contracts during deployment. Key takeaways:

  • Use #[constructor] annotation and &mut self parameter
  • Always use tx_origin() to get the deployer address
  • Validate all parameters before initializing state
  • Handle errors gracefully with Result<(), Error> return type
  • Test the constructor behavior thoroughly
  • Deploy with cargo stylus deploy --constructor-args