Name of your project: Benchmarking Stylus: Common Contract Primitives
Link to your Questbook application: Benchmarking Stylus: Common Contract Primitives
Link to your website/socials, others: GitHub — GitHub - cryptohumano/StylusCommonContracts · GitHub
Previous Stylus-EVM benchmarks confirmed Stylus’s gas efficiency for isolated, computationally intensive tasks. This project extends that analysis in the opposite direction: it measures Stylus against the everyday contract primitives that most Arbitrum applications are actually built from — access control, fungible and multi-token standards, royalties, proxies and upgradeability, typed-data signing, signature recovery, and vesting. The goal is to answer the question a developer choosing between Solidity and Stylus has to answer first: for ordinary, storage-bound contract code, what does Stylus actually cost?
This code repository benchmarks gas, execution time, and code size between EVM and Stylus implementations of 12 standard contract primitives. Each primitive is implemented twice — once in Solidity following OpenZeppelin patterns, once in Rust on the Stylus SDK — with matching interfaces, events, and error selectors, then deployed and exercised on-chain for a like-for-like comparison.
Performance Against KPIs
Deliverables — all 6 milestones complete
- Milestone 1 — Ownable, AccessControl — Solidity
/ Stylus
/ on-chain benchmark ![]()
- Milestone 2 — ERC-20, ERC-721 — Solidity
/ Stylus
/ on-chain benchmark ![]()
- Milestone 3 — ERC-1155, ERC-6909 — Solidity
/ Stylus
/ on-chain benchmark ![]()
- Milestone 4 — ERC-2981, ERC-1967 Proxy (+ CounterLogic) — Solidity
/ Stylus
/ on-chain benchmark ![]()
- Milestone 5 — UpgradeableBeacon + BeaconProxy, EIP-712 — Solidity
/ Stylus
/ on-chain benchmark ![]()
- Milestone 6 — ECDSA, VestingWallet — Solidity
/ Stylus
/ on-chain benchmark ![]()
Test coverage: 119 Solidity correctness tests (with vm.expectEmit assertions on every emitted event) + 63 isolated benchmark tests + 51 Stylus host-side unit tests — all passing. Every Stylus contract was additionally deployed, activated, and called end-to-end on the Nitro devnode with no deploy or call failures.
Reproducibility: all three metrics are regenerable from the repository — forge test --gas-report and forge build --sizes for Solidity, cargo stylus deploy + cast send/cast estimate for Stylus, and ./scripts/capture-timings.sh (median of 5 runs, committed verbatim to reports/timings/forge-bench-timings.json).
Headline result: the fixed per-call baseline is the whole story
Across every contract measured on the activated deployment path (M3–M6), Stylus gas decomposes cleanly into a fixed ~55–80k WASM-instantiation baseline plus the real work, where the real work is priced almost identically on both VMs (SLOAD/SSTORE and precompile gas are VM-independent).
That single fact explains the entire data set. Gas comparison, ordered worst ratio to best — operation, Solidity gas → Stylus gas (ratio):
- Immutable read, VestingWallet.start() — 223 → 62,442 (280x)
- Cached-constant read, EIP712.domainSeparatorV4() — 326 → 55,410 (170x)
- Pure-compute view, vestingSchedule() — 939 → 63,244 (67x)
- Simple storage view, ERC1155.balanceOf() — 2,634 → 80,086 (30x)
- Storage view, VestingWallet.released() — 2,280 → 62,449 (27x)
- Precompile-bound view, ECDSA.recover() — 4,057 → 61,840 (15x)
- Proxy-forwarded write, 3 hops, increment() via BeaconProxy — 21,518 → 134,898 (6.3x)
- Proxy-forwarded write, 1 hop, increment() via ERC-1967 — 32,159 → 96,462 (3.0x)
- Storage clear, deleteDefaultRoyalty() — 21,490 → 58,000 (2.7x)
- Royalty setter, setTokenRoyalty() — 44,400 → 103,114 (2.3x)
- Owner-gated write, upgradeTo() — 32,690 → 64,589 (2.0x)
- Token mint, ERC1155.mint() — 50,146 → 101,198 (2.0x)
- Storage write + event, VestingWallet.release() — 45,129 → 85,613 (1.9x)
- Write with allowance check, ERC6909.transferFrom() — 57,266 → 79,167 (1.4x)
- Batch write over 3 ids, ERC1155.mintBatch() — 100,724 → 127,582 (1.3x)
The ratio is inversely proportional to how much work the call does. Cheap reads look catastrophic (15x–280x) because a fixed 60k baseline is compared against a 200–4,000 gas EVM operation. Substantial writes converge toward parity (1.3x–2.0x) because the SSTORE gas that dominates them is identical on both VMs. Batching amortizes the baseline and narrows the gap further — mintBatch over 3 ids is only +27%.
Two structural findings hold independent of the baseline:
1. Proxy indirection costs less on Stylus than on Solidity, in absolute terms. Isolating the delegate-call hop for increment() (through-proxy minus direct-on-logic): 21,395 gas on Stylus vs 25,917 on Solidity — 0.83x. Stylus’s penalty lives in the per-call VM entry, not in the call machinery. The beacon pattern, by contrast, adds a full extra baseline per hop (implementation() via proxy = 2 hops = 96k; increment() via proxy = 3 hops = 135k), making hop count the dominant design variable for upgradeable Stylus systems.
2. ECDSA is the strongest parity candidate in the suite. ecrecover runs as the 0x01 precompile on both VMs, so the expensive cryptography is priced identically; the entire +57.5k delta is VM entry plus the static-call hop. It has the lowest view-ratio of any contract measured (15.2x on a non-cached devnode) and would be the first to reach parity once cached.
Code size
Stylus binaries are consistently 10–30x larger than the equivalent Solidity runtime — the SDK bundles a router and ABI decoder into every contract, so even a trivial contract carries a floor of ~6 KB activated. Contract — Solidity runtime / Stylus WASM raw / Stylus activated:
- ERC-1967 Proxy — 155 B / 26,447 B / ~7 KB
- CounterLogic — 280 B / 21,631 B / ~6 KB
- BeaconProxy — 426 B / 27,327 B / 9,173 B
- Ownable — 461 B / 21,581 B / ~7.1 KB
- UpgradeableBeacon — 708 B / 25,279 B / 8,091 B
- EIP-712 — 890 B / 27,204 B / 9,069 B
- AccessControl — 977 B / 32,830 B / ~10.2 KB
- ERC-2981 — 1,124 B / 43,419 B / ~13 KB
- ECDSA — 1,233 B / 31,992 B / 10,314 B
- VestingWallet — 1,394 B / 45,118 B / 13,923 B
- ERC-20 — 2,113 B / 65,395 B / ~19.0 KB
- ERC-6909 — 2,246 B / 63,210 B / ~17 KB
- ERC-721 — 3,421 B / 87,078 B / ~25.7 KB
- ERC-1155 — 5,889 B / 105,894 B / ~30 KB
This is a real deployment constraint, not a cosmetic one. ERC-1155 compiled to 131.6 KB of raw WASM on default settings — over the Stylus 128 KB activation cap. Tightening the workspace release profile (opt-level = “z”, lto = “fat”, codegen-units = 1, strip, panic = “abort”, overflow-checks = false) brought it to 105.9 KB and shrank every other crate in the suite by 7–31%. Any team porting a large contract to Stylus should treat the release profile as a required first step, not an optimization.
Execution time
Solidity wall-clock times (median of 5 forge runs) range from ~5 µs for a simple view to ~28 µs for ecrecover — the only EVM-side operation where measurable compute shows up. Stylus wall-clock on the non-cached devnode is ~50–100 ms per call regardless of operation, entirely dominated by WASM instantiation; it is reported as an environment characteristic rather than a comparison, because at that magnitude it measures the devnode, not the contract.
Stated limitation — devnode gas is an upper bound, not production
Every Stylus figure here was measured on a local Nitro devnode (v3.7.1-926f1ab, chain 412346) with no CacheManager entry and no JIT compilation. On Arbitrum mainnet, a contract cached via cargo stylus cache bid is compiled to native code and the instantiation baseline largely disappears. The ratios above are therefore worst-case bounds: the storage-bound writes (already 1.3x–2.0x) would move toward or past parity, and the compute-bound cases (EIP-712 hashing, ECDSA, vesting math) — where the baseline is ~98% of the measured cost — are where the largest improvement is expected.
One methodological caveat is stated openly: the M1 and M2 figures (~1.2–2.0M gas per call) reflect a different, un-activated execution path than M3–M6 (~55–80k). From M3 onward every contract was deployed via cargo stylus deploy --no-verify and activated before measurement. M1/M2 numbers are retained as-measured and flagged in their own reports; the cross-suite analysis above is drawn exclusively from the consistent M3–M6 methodology. Re-running M1/M2 under the activated path, and re-running the whole suite on Arbitrum Sepolia with cached contracts, are the two clear next measurements.
Qualitative Impact & Community Feedback
The project demonstrates that the entire standard contract toolkit — access control, ERC-20/721/1155/6909, ERC-2981 royalties, ERC-1967 and beacon proxies, EIP-712 typed data, ECDSA recovery, and linear vesting — can be implemented faithfully in Rust on Stylus, with matching interfaces, events, and error selectors, and deployed and exercised end-to-end on-chain.
More importantly, it produces the counter-weight data set the ecosystem was missing. Prior benchmarks showed Stylus winning decisively on compute-heavy workloads. This suite deliberately measured the opposite regime — ordinary, storage-bound application code — and found that Stylus’s cost there is a fixed per-call VM entry, not a per-operation inefficiency. That reframes the porting decision into a concrete rule of thumb a developer can apply immediately:
- Port to Stylus when calls do substantial work (compute, batching, cryptography beyond precompiles), where the fixed entry amortizes.
- Stay on Solidity for contracts dominated by cheap, high-frequency view calls, where a 60k entry cost dwarfs a 2k SLOAD.
- Mind the hop count in upgradeable architectures — each Stylus contract boundary is a full VM entry, so a beacon proxy costs materially more per call than a self-slot ERC-1967 proxy.
Several practical findings surfaced that are useful to any team attempting a port, and are documented in the milestone reports:
- The 128 KB activation cap is reachable by an ordinary ERC-1155. The release profile is not optional.
- Rust’s Vec maps to Solidity’s uint8[], not bytes. Using stylus_sdk::abi::Bytes is required for init(address,bytes) and ERC-1155’s data argument. This class of bug is invisible under typed test bindings and only appears when raw cast calls hit the contract — which is exactly why every contract here was benchmarked on-chain rather than in a harness.
- Stylus-on-Stylus delegatecall works and preserves the standard’s storage semantics — state lands in the proxy’s slots, the EIP-1967 slot is readable via vm().storage_load_bytes32, and upgrades propagate correctly through a beacon to its proxies.
The result is a reproducible, open-source reference — 12 primitives, dual implementations, 233 tests, and per-milestone data — that a developer evaluating Stylus can run themselves rather than take on trust.