Designing an Immutable Stylus Contract for Arbitrum Nitro
Deploying a Stylus contract on Arbitrum Nitro is safest with a single immutable contract: narrow public surface, explicit storage, bounded loops. This note covers requirements, design, trust boundaries, operational checks, failure modes, and CI verification.
14 Jul 2026, 00:12 UTC

Problem and Takeaway
Deploying a Stylus contract on Arbitrum Nitro introduces a new set of constraints: the contract must be deterministic, gas‑bounded, and non‑upgradeable by default unless a proxy is explicitly added. The simplest, safest architecture is a single immutable contract with a narrow public surface, explicit storage layout, and bounded loops. This article explains why that design is preferred, how to implement it in Rust, and what checks you must run to guarantee correctness.
Requirements
- Determinism: All execution paths must produce the same state given the same inputs.
- Gas boundedness: No unbounded loops or allocations that could exceed the block gas limit.
- Immutable logic: No admin or upgrade path unless explicitly required.
- Explicit storage: All state variables must be declared with fixed types; no dynamic storage layout changes.
- Strict ABI: The exported ABI must be reviewed as a diff before each release.
- Reentrancy safety: External calls to EVM contracts are considered reentrant; guard them.
Smallest Suitable Design
The minimal contract looks like this (Rust syntax for Stylus):
#![no_std]
use stylus::prelude::*;
#[derive(Default)]
struct Storage {
counter: u64,
owner: Address,
}
#[stylus]
impl Contract for Counter {
type Storage = Storage;
fn init(&mut self, owner: Address) {
self.owner = owner;
}
#[payable]
fn increment(&mut self, by: u64) {
// bounded loop: at most 1 iteration
self.counter = self.counter.checked_add(by).expect("overflow");
}
fn get(&self) -> u64 {
self.counter
}
}
Key points:
- The contract has no admin or upgrade functions.
- All arithmetic uses checked operations to avoid overflow.
- Loops are bounded (here, a single iteration). If you need a loop, limit it to a compile‑time constant.
- The public API is tiny:
init,increment,get.
Trust & Data Boundaries
On‑Chain Storage
On‑chain storage is the source of truth. All state changes happen here; off‑chain components (indexers, RPC clients) consume snapshots but cannot alter state.
WASM Sandbox
The Stylus runtime meters every byte of execution. External host functions are the only way the contract interacts with the outside world, and each call is a re‑entrant boundary that must be treated as untrusted.
Off‑Chain Indexers & Signatures
Indexers parse logs and state to build a view for UI or analytics. They are not part of the trust model; any tampering with signatures or payloads will be rejected by the contract during execution.
External EVM Calls
When the contract calls an EVM contract (e.g., for ERC‑20 transfer), the call is re‑entrant. Use the checks‑effects‑interactions pattern and a reentrancy guard even if the callee is “trusted”.
Operational Checks
Every CI pipeline should gate on the following steps. All commands are run from the project root and require the Stylus toolchain and an Arbitrum Nitro dev node for simulation.
- Compile & Check
stylus compile --out contract.wasm stylus checkExpected: no warnings,
contract.wasmsize < 32 KiB (arbitrary limit). - ABI Export & Diff
stylus abi --out abi.json git diff --name-only HEAD~1 abi.jsonReview the diff; any change to function signatures must be justified.
- Local Deployment & Replay
arbitrum dev-node start & stylus deploy --node http://localhost:8545 --address 0xContract # Run a predefined transaction set stylus test run --node http://localhost:8545 --contract 0xContractCheck that balances, storage slots, and emitted events match expectations.
- Gas Ceiling Test
stylus fuzz --max-gas 5_000_000Fail the build if any path exceeds the ceiling.
- Activation Rules Check
stylus activate --chain arbitrum-nitro --checkVerify that the contract’s activation address and block number align with the current Nitro release.
Failure Modes & Conditions to Re‑Design
Out‑of‑Gas
Unbounded loops or allocations can exhaust gas. If you need more complex logic, consider an off‑chain worker that submits signed intent batches.
Non‑Determinism
Floating‑point math, system time, or randomness break determinism. Stick to fixed‑width integers and deterministic pseudo‑random generators if needed.
Storage‑Layout Drift
Adding a proxy or factory later changes the storage layout. If upgradeability is required, start with a proxy that delegates to immutable logic from the beginning.
Reentrancy via External Calls
Even a trusted ERC‑20 transfer can re‑enter the contract. Use a reentrancy guard or the checks‑effects‑interactions pattern.
Deployment/Activation Variance
Different Arbitrum Nitro releases may change activation addresses or gas constants. Always run the activation check CI step on the target chain before a release.
When to Add a Factory or Proxy
- Many identical instances are needed (e.g., token contracts).
- You anticipate future upgrades or parameter changes.
- You need to audit each instance separately.
In those cases, design a thin Solidity façade that forwards to a shared Stylus logic contract, or deploy a proxy that delegates to the immutable logic.
Practical Verification Checklist
- WASM Size:
stylus compile --sizeshould be < 32 KiB. - ABI Integrity: Git diff shows no unexpected changes.
- Replay Result: State after replay matches the expected snapshot.
- Gas Usage: No path exceeds 5 M gas.
- Activation:
stylus activate --checkpasses.
If any check fails, review the code for the corresponding failure mode and adjust the design before merging.
Conclusion
For most use cases on Arbitrum Nitro, a single immutable Stylus contract with a narrow public surface is the safest and most efficient design. It eliminates the complexity of upgrade paths, reduces attack surface, and keeps the on‑chain state simple. Only add a proxy or factory when you have a clear, repeatable need for multiple identical instances or future upgradeability. Follow the operational checks above to ensure every release is deterministic, gas‑bounded, and verifiable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.