Activation
Stylus contracts undergo a two-step process to become executable on Arbitrum chains: deployment and activation. This guide explains both steps, the distinction between them, and how to manage the activation process.
Overview
Unlike traditional EVM contracts that become immediately executable after deployment, Stylus contracts require an additional activation step:
- Deployment: Stores the compressed WASM bytecode onchain at a contract address
- Activation: Converts the bytecode into an executable Stylus program by registering it with the ArbWasm precompile
Why two steps?
- Gas optimization: Activation involves one-time processing and caching that would be expensive to repeat on every call
- Code reuse: Multiple contracts can share the same activated codehash, reducing activation costs
- Version management: Allows the chain to track which Stylus protocol version a contract targets
Deployment vs activation
| Aspect | Deployment | Activation |
|---|---|---|
| Purpose | Store compressed WASM onchain | Register program with ArbWasm |
| Transaction count | one transaction | one transaction (separate) |
| Cost type | Standard EVM deployment gas | Data fee (WASM-specific cost) |
| When required | Always - stores the code | Always - makes code executable |
| Reversible | No | No (but can expire) |
| Who can call | Anyone with funds | Anyone (after deployment) |
| Can be skipped | No | No (unless already activated) |
Contract state
A Stylus contract can be in one of these states:
pub enum ContractStatus {
/// Contract already exists onchain and is activated
Active { code: Vec<u8> },
/// Contract is deployed but not yet activated
/// Ready to activate with the given data fee
Ready { code: Vec<u8>, fee: U256 },
}
The Activation process
Step 1: Build and process WASM
Before deployment, your Rust contract is compiled and processed:
cargo stylus check
This performs:
- Compile Rust to WASM: Using
wasm32-unknown-unknowntarget - Process WASM binary:
- Remove dangling references
- Add project hash metadata
- Strip unnecessary custom sections
- Brotli compression: Maximum compression (level 11)
- Add EOF prefix:
EFF00000(identifies Stylus programs) - Size validation: The decompressed WASM must fit within the chain's
MaxWasmSizeparameter (default 128 KB, raised to 256 KB at ArbOS 60+). This is a chain-configurable ArbOS parameter, not the 24 KB EVM contract-code limit that applies to Solidity. At ArbOS 60+, programs larger than the limit can be split into a root plus fragments.
WASM Processing Pipeline:
Figure 1: WASM binary processing pipeline showing transformation from raw binary to deployment-ready compressed code.
Step 2: Deploy the contract
Deployment creates a transaction that stores your processed WASM onchain:
cargo stylus deploy \
--private-key-path=key.txt \
--endpoint="https://sepolia-rollup.arbitrum.io/rpc"
What happens during deployment:
- Generate deployment bytecode: Create EVM initcode with embedded compressed WASM
- Estimate gas: Calculate deployment transaction gas cost
- Send deployment transaction: To the Stylus deployer contract
- Extract contract address: From the transaction receipt
Deployment Bytecode Structure:
EVM Initcode Prelude (43 bytes):
┌─────────────────────────────────────┐
│ 0x7f PUSH32 <code_len> │ Push code length
│ 0x80 DUP1 │ Duplicate length
│ 0x60 PUSH1 <prelude_length> │ Push prelude length
│ 0x60 PUSH1 0x00 │ Push 0
│ 0x39 CODECOPY │ Copy code to memory
│ 0x60 PUSH1 0x00 │ Push 0
│ 0xf3 RETURN │ Return code
│ 0x00 <version_byte> │ Stylus version
└─────────────────────────────────────┘
↓
<compressed_wasm_code>
Step 3: Calculate activation fee
Before activating, the data fee must be calculated. Conceptually (illustrative pseudocode, not a runnable API):
// Simulated via state overrides (no transaction sent)
let data_fee = calculate_activation_fee(contract_address);
// Apply bump percentage for safety (default: 20%)
let final_fee = data_fee * (1 + bump_percent / 100);
Data fee calculation:
- Uses state override simulation to estimate the fee
- No actual transaction sent during estimation
- Configurable bump percentage protects against variance (default: 20%)
- The fee is paid in ETH when activating
Step 4: Activate the contract
Activation registers your contract with the ArbWasm precompile:
# Automatic activation (default)
cargo stylus deploy --private-key-path=key.txt
# Or manual activation
cargo stylus activate \
--address=0x1234... \
--private-key-path=key.txt
What happens during activation:
- Call ArbWasm precompile: At address
0x0000000000000000000000000000000000000071 - Send activation transaction:
ArbWasm.activateProgram{value: dataFee}(contractAddress)
- ArbWasm processes the code:
- Validates WASM formatting
- Checks against the protocol version
- Stores the activation metadata
- Emits a
ProgramActivatedevent
- Returns activation info:
returns (uint16 version, uint256 actualDataFee)
Using cargo-stylus
The cargo-stylus CLI maps directly onto the two steps above:
cargo stylus deployhandles both deployment and activation in one command (the default).cargo stylus deploy --no-activatedeploys without activating, so the program can be inspected or activated later.cargo stylus activate --address=<ADDRESS>activates a previously deployed contract.cargo stylus checkvalidates the contract and reports whether matching code is already activated, the estimated data fee, or any validation errors.
For the full command syntax and options, see the check and deploy guide and the cargo-stylus command reference.
Deployment with constructors
If your contract has a constructor, provide arguments during deployment:
#[public]
impl MyContract {
#[constructor]
pub fn constructor(&mut self, initial_value: U256, owner: Address) {
self.value.set(initial_value);
self.owner.set(owner);
}
}
Deploy with constructor arguments:
cargo stylus deploy \
--private-key-path=wallet.txt \
--constructor-args 42 0x1234567890abcdef1234567890abcdef12345678
With payable constructor:
#[constructor]
#[payable]
pub fn constructor(&mut self) {
let value = self.vm().msg_value();
self.initial_balance.set(value);
}
cargo stylus deploy \
--private-key-path=wallet.txt \
--constructor-value=1000000000000000000 # 1 ETH in wei
The ArbWasm precompile
Activation is handled by the ArbWasm precompile at address 0x0000000000000000000000000000000000000071.
Key functions
activateProgram
Activates a deployed Stylus contract:
function activateProgram(
address program
) external payable returns (uint16 version, uint256 dataFee);
Parameters:
program: Contract address containing WASM bytecode
Payment:
- Must send
valueequal to the calculated data fee (in wei)
Returns:
version: Stylus protocol version the program was activated againstdataFee: Actual fee paid for activation
Example (via cast):
cast send 0x0000000000000000000000000000000000000071 \
"activateProgram(address)" \
0x1234567890abcdef \
--value 100000000000000000 \
--private-key=$PRIVATE_KEY
codehashVersion
Check if a codehash is activated and get its version:
function codehashVersion(bytes32 codehash) external view returns (uint16 version);
Reverts if:
- The code is not activated
- The program needs an upgrade
- The program has expired
programTimeLeft
Get the remaining time before a program expires:
function programTimeLeft(address program) external view returns (uint64 timeLeft);
Returns the number of seconds until expiration (default: ~1 year from activation).
codehashKeepalive
Extend a program's expiration time:
function codehashKeepalive(bytes32 codehash) external payable;
Resets the expiration timer to prevent program deactivation. The call pays the activation data fee in value and returns no value. To read the time remaining before expiry, use the separate programTimeLeft getter (above), which returns the seconds left.
ArbWasm errors
Activation can fail with these errors:
error ProgramNotWasm();
// The deployed bytecode is not valid WASM
error ProgramNotActivated();
// Contract exists but hasn't been activated
error ProgramNeedsUpgrade(uint16 version, uint16 stylusVersion);
// Program version incompatible with current Stylus version
error ProgramExpired(uint64 ageInSeconds);
// Program has expired and must be reactivated
error ProgramInsufficientValue(uint256 have, uint256 want);
// Sent data fee is less than required
Gas and Fee Optimization
Estimating costs
Before deploying, cargo stylus deploy --estimate-gas reports the deployment gas and cargo stylus check reports the estimated activation data fee. See the check and deploy guide for details.
Fee bump configuration
Protect against fee variance with configurable bump percentage:
# Default: 20% bump
cargo stylus deploy --private-key-path=wallet.txt
# Custom bump percentage
# (Note: Use programmatically via stylus-tools library)
In code (illustrative, using the stylus-tools library):
// Illustrative — types and field names are not a stable public API.
let config = ActivationConfig {
data_fee_bump_percent: 25, // 25% safety margin
};
Code reuse optimization
If your contract's codehash matches an already-activated contract:
cargo stylus check
Output if already activated:
Checking contract...
✓ Contract with this codehash is already activated!
Version: 1
No activation needed - you can deploy without activating.
You can deploy the contract normally, and it will automatically use the existing activation.
Contract caching
After activation, contracts can be cached for cheaper calls:
// ArbWasmCache precompile (0x0000000000000000000000000000000000000072)
function cacheProgram(address program) external payable returns (uint256);
Benefits:
- Reduces gas costs for subsequent contract calls
- One-time caching fee
- Shared across all contracts with the same codehash
Advanced activation patterns
Multi-contract deployment
When deploying multiple instances of the same contract:
# First deployment: full deploy + activate
cargo stylus deploy --private-key-path=wallet.txt
# Contract 1: 0xaaaa... (activated)
# Subsequent deployments: deploy only (reuses activation)
cargo stylus deploy --private-key-path=wallet.txt --no-activate
# Contract 2: 0xbbbb... (uses existing activation)
cargo stylus deploy --private-key-path=wallet.txt --no-activate
# Contract 3: 0xcccc... (uses existing activation)
All three contracts share the same codehash and activation, saving on data fees.
Programmatic deployment
Using the stylus-tools library directly. The following is illustrative pseudocode that sketches the deploy/activate flow — it is not a compilable example, and the exact types and signatures vary by stylus-tools version:
use stylus_tools::core::{
deployment::{deploy, DeploymentConfig},
activation::{activate_contract, ActivationConfig, data_fee},
check::{check_contract, ContractStatus},
};
use alloy::providers::{Provider, WalletProvider};
async fn deploy_and_activate(
provider: &impl Provider + WalletProvider,
) -> Result<Address, Box<dyn std::error::Error>> {
let contract = /* build contract */;
// Step 1: Check if already activated
let config = CheckConfig::default();
match check_contract(&contract, None, &config, provider).await? {
ContractStatus::Active { .. } => {
println!("Already activated!");
// Deploy without activation
}
ContractStatus::Ready { code, fee } => {
println!("Ready to activate. Data fee: {}", fee);
// Continue with deployment + activation
}
}
// Step 2: Deploy
let deploy_config = DeploymentConfig {
no_activate: false,
..Default::default()
};
deploy(&contract, &deploy_config, provider).await?;
// Contract address returned from deployment
Ok(contract_address)
}
Custom deployer contracts
Use a custom deployer contract instead of the default:
cargo stylus deploy \
--private-key-path=wallet.txt \
--deployer-address=0x... \
--deployer-salt=0x0000000000000000000000000000000000000000000000000000000000000001
This is useful for:
- CREATE2 deterministic addresses
- Custom deployment logic
- Factory patterns
Contract lifecycle
Activation lifecycle
Deployed → Activated → [Active] → [Keepalive] → [Expired]
↑ ↓
└──────────┘
(Periodic keepalive)
Expiration and Keepalive
Programs expire after the chain's ExpiryDays parameter (default 365 days). Both the expiry period and the minimum age before a program can be kept alive (KeepaliveDays, default 31 days) are configurable ArbOS parameters:
// Check time remaining
uint64 timeLeft = ArbWasm.programTimeLeft(contractAddress);
// Extend expiration (pays the data fee in value; the program must be at least KeepaliveDays old)
ArbWasm.codehashKeepalive{value: keepaliveFee}(codehash);
Why expiration?
- Prevents abandoned contracts from consuming ArbOS resources
- Encourages active maintenance
- Allows protocol upgrades
Keepalive strategy:
- Monitor
programTimeLeft()periodically - Call
codehashKeepalive()before expiration - Automated scripts can handle this
Reactivation after expiry
If a program expires:
# Reactivate the existing deployment
cargo stylus activate --address=0x...
The contract code remains onchain; only the activation state is cleared.
Troubleshooting
Common activation errors
"Program not activated"
Cause: Trying to call a deployed but not activated contract
Solution:
cargo stylus activate --address=0x...
"Insufficient value"
Cause: The data fee sent is less than required
Solution:
- Check current data fee:
cargo stylus check - Increase fee bump percentage
- Ensure sufficient ETH balance
"Program not WASM"
Cause: Deployed bytecode is not valid Stylus WASM
Solution:
- Verify you deployed the correct contract
- Rebuild and redeploy:
cargo stylus deploy
"Program needs upgrade"
Cause: Contract was activated against an old Stylus version
Solution:
- Recompile with the latest SDK
- Redeploy and reactivate
"Program expired"
Cause: The contract hasn’t been kept alive and has expired
Solution:
# Reactivate the contract
cargo stylus activate --address=0x...
Debugging and verifying activation
To inspect status or debug a failed activation, cargo stylus check validates a contract (add --verbose for detail), and cargo stylus check --address=0x... reports whether an existing deployment is activated. For verbose deploy logging and cast-based status checks against the ArbWasm precompile, see the debugging transactions guide and the check and deploy guide.
Best practices
-
Check before deploying.
cargo stylus checkcatches validation errors and reports whether matching code is already activated, avoiding duplicate activations and wasted gas. -
Use automatic activation. Unless you have a reason to split the steps,
cargo stylus deploydeploys and activates in one command. -
Test on Arbitrum Sepolia first, then deploy to mainnet.
-
Monitor expiration of production contracts with
programTimeLeftand callcodehashKeepalivebefore the program expires. -
Keep the SDK updated — older versions may become incompatible with chain upgrades:
[dependencies]stylus-sdk = "0.10.7"
For the full deployment walkthrough — project setup, gas estimation, reproducible builds, and verification — see the check and deploy guide.
Summary
- Two-step process: Deployment stores code, activation makes it executable
- cargo-stylus handles both: Use
deployfor automatic activation - Data fee required: Activation costs ETH (separate from deployment gas)
- Code reuse: Identical contracts share activation, saving costs
- Expiration: Programs expire after the chain's
ExpiryDaysparameter (default 365 days) unless kept alive - ArbWasm precompile: All activation goes through address
0x71 - Check first: Use the
cargo stylus checkto avoid duplicate activations
See also
- Contracts: Writing Stylus contracts
- Global Variables and Functions: VM interface methods
- Check and deploy: Full
cargo stylusdeployment walkthrough cargo-styluscommand reference: Complete CLI option reference