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.
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:
| Rule | Why it exists |
|---|---|
| Exactly 0 or 1 constructor per contract | Mimics Solidity behavior and avoids ambiguity |
Must be annotated with #[constructor] | Guarantees the deployer calls the correct initialization method |
Must take &mut self | Allows writing to contract storage during deployment |
Returns () or Result<(), Error> | Enables error handling; reverting aborts deployment |
Use tx_origin() for deployer address | Factory contracts are used in deployment, so msg_sender() returns the factory address |
| Constructor runs exactly once | The SDK uses a sentinel system to prevent re-execution |
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
- Parameter validation: Always validate constructor parameters before proceeding with initialization
- Error handling: Use
Result<(), Error>to handle initialization failures gracefully - Payable constructors: Add
#[payable]to receive ETH during deployment - 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(())
}
}
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(¶ms)?;
// 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(¶ms)?;
// 2. Set basic contract metadata
self.name.set_str(¶ms.name);
self.symbol.set_str(¶ms.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 selfparameter - 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