EVM and WASM VM differences
Arbitrum Nitro supports two execution environments: the traditional Ethereum Virtual Machine (EVM) for Solidity contracts and a WebAssembly (WASM) VM for Stylus contracts. While both environments are fully interoperable and share the same state, they differ significantly in their execution models, performance characteristics, and developer experience.
Execution model
EVM: Stack-based architecture
The EVM uses a stack-based execution model:
- Operations: Work with values on a stack (PUSH, POP, ADD, etc.)
- Opcodes: 256 predefined opcodes with fixed gas costs
- Memory: Linear, byte-addressable memory that grows dynamically
- Storage: 256-bit word-based key-value store
- Call depth: Limited to 1024 levels
Example EVM execution:
PUSH1 0x02 // Push 2 onto stack
PUSH1 0x03 // Push 3 onto stack
ADD // Pop 2 values, push sum (5)
WASM: Register-based architecture
The Stylus WASM VM uses a register-based execution model:
- Operations: Work with virtual registers and local variables
- Instructions: Thousands of WASM instructions with fine-grained metering
- Memory: Linear memory with explicit grow operations
- Storage: Same 256-bit storage as EVM (shared state)
- Call depth: Same 1024 limit for compatibility
Example WASM execution:
(local.get 0) ;; Read from local variable 0
(local.get 1) ;; Read from local variable 1
(i32.add) ;; Add and store result
(local.set 2) ;; Store in local variable 2
Memory model
EVM memory
- Dynamic expansion: Memory grows in 32-byte chunks
- Gas cost: Quadratic growth (memory expansion gets expensive)
- Access pattern: Byte-level addressing
- Limit: Practical limit around 15 MB due to gas costs
WASM memory
- Page-based: Memory grows in 64 KB pages (WASM standard)
- Gas cost: Linear cost per page through
pay_for_memory_grow - Access pattern: Direct memory load/store instructions
- Limit: Can grow much larger efficiently
Memory growth in Stylus:
// The entrypoint macro automatically handles pay_for_memory_grow
#[entrypoint]
pub struct MyContract {
// Large data structures are more practical in WASM
data: StorageVec<StorageU256>,
}
// Nitro automatically inserts pay_for_memory_grow calls
// when allocating new pages
let large_vector = vec![0u8; 100_000]; // Efficient in WASM
The Stylus SDK's entrypoint! macro includes a no-op call to pay_for_memory_grow to ensure the function is referenced. Nitro then automatically inserts actual calls when memory allocation occurs.
Gas metering: Ink and gas
EVM gas metering
- Unit: Gas (standard Ethereum unit)
- Granularity: Per opcode (e.g., ADD = 3 gas, SSTORE = 20,000 gas)
- Measurement: Coarse-grained
- Refunds: Available for storage deletions
Stylus ink metering
Stylus introduces "ink" as a fine-grained metering unit:
- Unit: Ink (Stylus-specific, converted to gas)
- Granularity: Per WASM instruction (more fine-grained)
- Measurement: Precise tracking of WASM execution costs
- Conversion: Ink → Gas conversion happens automatically
Ink to gas conversion:
// Check remaining ink
let ink_left = evm_ink_left();
// Check remaining gas
let gas_left = evm_gas_left();
// Get ink price (in gas basis points)
let ink_price = tx_ink_price();
// Conversion formula:
// gas = ink * ink_price / 10000
Why ink?
- Precision: WASM instructions have varying costs that don't map cleanly to EVM gas
- Efficiency: Fine-grained metering allows for more accurate pricing
- Performance: Enables cheaper execution for compute-heavy operations
- Flexibility: Ink prices can be adjusted without changing contract code
Gas cost comparison:
| Operation | EVM Gas | Stylus Gas | Improvement |
|---|---|---|---|
| Basic arithmetic | 3-5 | ~1-2 | 2-3x cheaper |
| Memory operations | Variable | Efficient | 10-100x cheaper |
| Complex computation | Expensive | Cheap | 10-100x cheaper |
| Storage operations | Same | Same | Equal |
| External calls | Same | Same | Equal |
Instruction sets
EVM opcodes
- Count: ~140 opcodes
- Categories: Arithmetic, logic, storage, flow control, system
- Size: 1 byte per opcode
- Examples:
ADD,MUL,SUB,DIV(arithmetic)SLOAD,SSTORE(storage)CALL,DELEGATECALL(calls)SHA3(hashing)
WASM instructions
- Count: Hundreds of instructions
- Categories: Numeric, memory, control flow, function calls
- Size: Variable encoding (1-5 bytes)
- Examples:
i32.add,i64.mul,f64.div(numeric)memory.grow,memory.size(memory)call,call_indirect(functions)- Hostio imports (system operations)
WASM advantages:
- More expressive instruction set
- Better compiler optimization targets
- Efficient handling of complex data structures
- Native support for 32-bit and 64-bit operations
Size limits
EVM contracts
- Maximum size: 24,576 bytes (24 KB) of deployed bytecode
- Limit reason: Block gas limit and deployment costs
- Workaround: Contract splitting, proxies
Stylus contracts
- Initial limit: 24 KB (same as EVM for compatibility)
- Compressed size: Can be larger before compression
- Future: Limit may be increased as WASM tooling improves
- Practical size: Stylus programs are often smaller due to efficient compilation
Size optimization:
// Stylus contracts benefit from:
// 1. Rust's zero-cost abstractions
// 2. Dead code elimination by wasm-opt
// 3. Efficient WASM encoding
#[no_std] // Opt out of standard library for smaller binaries
extern crate alloc;
// Only the code actually used is included
use stylus_sdk::prelude::*;
Storage model
Both EVM and WASM contracts use the same storage system:
- Format: 256-bit key-value store
- Compatibility: EVM and WASM contracts can share storage
- Costs: SLOAD and SSTORE costs are identical
- Caching: Stylus VM implements storage caching for efficiency
Storage caching in Stylus
use stylus_sdk::prelude::*;
#[storage]
pub struct Counter {
count: StorageU256,
}
#[public]
impl Counter {
pub fn increment(&mut self) {
// First read: full SLOAD cost
let current = self.count.get();
// Write is cached
self.count.set(current + U256::from(1));
// Additional reads in same call are cheaper (cached)
let new_value = self.count.get();
// Cache is automatically flushed at call boundary
}
}
Cache benefits:
- Reduced gas costs: Repeated reads are cheaper
- Better performance: Fewer state trie accesses
- Automatic management: SDK handles cache flushing
- Compatibility: Refund logic matches EVM exactly
Performance characteristics
Compute operations
| Category | EVM | Stylus WASM | Winner |
|---|---|---|---|
| Integer arithmetic | Moderate | Fast | WASM (10x+) |
| Loops | Expensive | Cheap | WASM (100x+) |
| Memory copying | Expensive | Cheap | WASM (10x+) |
| Hashing (keccak256) | Native | Native | Equal |
| Cryptography | Limited | Efficient | WASM |
| String operations | Expensive | Cheap | WASM (100x+) |
Storage operations
| Operation | EVM | Stylus WASM | Winner |
|---|---|---|---|
| SLOAD | 2,100 gas | 2,100 gas | Equal |
| SSTORE (new) | 20,000 gas | 20,000 gas | Equal |
| SSTORE (update) | 5,000 gas | 5,000 gas | Equal |
| Storage refunds | Standard | Standard | Equal |
| Cached reads | No | Yes | WASM |
External interactions
| Operation | EVM | Stylus WASM | Winner |
|---|---|---|---|
| Contract calls | ~700 gas base | ~700 gas base | Equal |
| Cross-language calls | N/A | Efficient | WASM |
| Event emission | Same cost | Same cost | Equal |
| Value transfers | Same cost | Same cost | Equal |
Call semantics
Interoperability
Both environments support seamless interoperability:
// Stylus calling Solidity
sol_interface! {
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
}
#[public]
impl MyContract {
pub fn call_evm_contract(&self, token: Address) -> Result<bool, Vec<u8>> {
let erc20 = IERC20::new(token);
let result = erc20.transfer(self.vm(), recipient, amount)?;
Ok(result)
}
}
// Solidity calling Stylus
interface IStylusContract {
function computeHash(bytes calldata data) external view returns (bytes32);
}
contract EvmContract {
function useStylus(address stylusAddr, bytes calldata data) public view returns (bytes32) {
return IStylusContract(stylusAddr).computeHash(data);
}
}
Call costs
- Same call overhead: Both directions have similar base costs
- ABI encoding: Identical for both
- Gas forwarding: Follows 63/64 rule in both cases
- Return data: Handled consistently
Contract lifecycle
Deployment
EVM contracts:
- Submit init code (constructor bytecode)
- EVM executes init code
- Returns runtime bytecode
- Bytecode stored onchain
Stylus contracts:
- Compile Rust → WASM
- Submit WASM code
- Activation step: One-time compilation to native code
- Activated programs cached for efficiency
- WASM code stored onchain
Activation benefits:
# Deploy and activate a Stylus program
cargo stylus deploy --private-key $PRIVATE_KEY
# Activation happens once
# Subsequent calls use cached native code
- One-time cost: Pay activation gas once
- Future savings: All executions use optimized native code
- Upgradeability: Re-activation needed for upgrades
Execution flow
EVM contracts:
Transaction → EVM → Opcode interpretation → State changes
Stylus contracts:
Transaction → WASM VM → Native code execution → State changes
↓
Hostio calls for state access
Developer experience
EVM development
Languages: Solidity, Vyper, Huff
Tools:
- Hardhat, Foundry for testing
- Remix for quick development
- Ethers.js/Web3.js for interaction
Debugging:
- Revert messages
- Events for tracing
- Stack traces limited
Stylus development
Languages: Rust, C, C++ (any WASM-compatible language)
Tools:
cargo stylusfor deployment- Standard Rust tooling (cargo, rustc)
TestVMfor unit testing- Rust analyzer for IDE support
Debugging:
- Full Rust error messages
- Compile-time safety checks
console!macro for debug builds- Stack traces in development
Development comparison:
| Aspect | EVM | Stylus | Notes |
|---|---|---|---|
| Type safety | Runtime | Compile-time | Rust catches errors before deployment |
| Memory safety | Manual | Automatic | Rust's borrow checker |
| Testing | External tools | Built-in Rust tests | #[test] functions work natively |
| Iteration speed | Slower | Faster | No need to redeploy for tests |
| Learning curve | Moderate | Steeper | Rust has more concepts |
| Maturity | Very mature | Growing | Solidity has more resources |
Feature compatibility
Supported features
Both EVM and Stylus support:
✅ Contract calls and delegate calls ✅ Value transfers ✅ Event emission ✅ Storage operations ✅ Block and transaction properties ✅ Cryptographic functions (keccak256) ✅ Contract creation (CREATE, CREATE2) ✅ Revert and error handling ✅ Reentrancy guards
EVM-specific features not in WASM
❌ Inline assembly (use hostio or Rust instead)
❌ selfdestruct (deprecated in Ethereum anyway)
❌ Solidity modifiers (use Rust functions)
❌ Multiple inheritance (use traits and composition)
Stylus-specific features not in EVM
✅ Access to Rust ecosystem (crates) ✅ Efficient memory management ✅ Zero-cost abstractions ✅ Compile-time guarantees ✅ Native testing support ✅ Better optimization opportunities
State sharing
EVM and WASM contracts share the same blockchain state:
// Stylus contract can read EVM contract storage
#[storage]
pub struct Bridge {
evm_contract: StorageAddress,
}
#[public]
impl Bridge {
pub fn read_evm_storage(&self, key: U256) -> U256 {
// Both VMs use the same storage layout
// Can read storage written by EVM contracts
storage_load_bytes32(key)
}
}
Shared state:
- Account balances
- Contract storage
- Contract code
- Transaction history
- Block data
Gas economics
Cost structure
EVM contract execution:
Total cost = Base transaction cost (21,000 gas)
+ Input data cost (~16 gas/byte)
+ Execution cost (opcode gas)
+ Storage cost (SLOAD/SSTORE)
Stylus contract execution:
Total cost = Base transaction cost (21,000 gas)
+ Input data cost (~16 gas/byte)
+ Execution cost (ink → gas conversion)
+ Storage cost (same as EVM)
When to use each
Use EVM (Solidity) when:
- Quick prototyping needed
- Simple contracts with minimal computation
- Team expertise in Solidity
- Extensive storage operations (cost is equal)
- Maximum ecosystem compatibility
Use Stylus (Rust) when:
- Compute-intensive operations
- Complex algorithms or data structures
- Need for memory safety guarantees
- Existing Rust codebase to port
- Optimizing for gas efficiency
- Cryptographic operations
- String/byte manipulation
Best practices
For EVM contracts
- Minimize storage operations: Use memory when possible
- Optimize loops: Keep iterations minimal
- Pack storage: Use smaller types when possible
- Avoid complex math: Basic operations only
- Use libraries: Leverage audited code
For Stylus contracts
- Leverage Rust's safety: Let the compiler catch bugs
- Use iterators: More efficient than manual loops
- Profile before optimizing: Use cargo-stylus tools
- Test thoroughly: Use Rust's built-in test framework
- Consider binary size: Use
#[no_std]if needed - Batch operations: Take advantage of cheap compute
Hybrid approach
Many projects can benefit from both:
// Compute-heavy logic in Stylus
#[public]
impl ComputeEngine {
pub fn complex_calculation(&self, data: Vec<u256>) -> Vec<U256> {
// Efficient loops and data processing
data.iter()
.map(|x| expensive_computation(*x))
.collect()
}
}
// Coordination and state management in Solidity
contract Coordinator {
IComputeEngine public engine; // Stylus contract
function process(uint256[] calldata data) public {
uint256[] memory results = engine.complex_calculation(data);
// Store results, emit events, etc.
}
}
Migration considerations
From Solidity to Stylus
What stays the same:
- Contract addresses
- Storage layout
- ABIs and interfaces
- Gas for storage operations
- Event signatures
What changes:
- Programming language (Solidity → Rust)
- Execution engine (EVM → WASM)
- Gas costs for compute (usually cheaper)
- Development workflow
- Testing approach
Migration strategy:
- Start with compute-heavy functions
- Maintain same ABI for compatibility
- Test extensively with existing contracts
- Monitor gas costs in production
- Gradually migrate more functionality
Future developments
EVM evolution
- EIP improvements
- New opcodes
- Gas repricing
- EOF (EVM Object Format)
Stylus evolution
- Support for more languages
- SIMD instructions
- Floating point operations
- Larger contract size limits
- Further gas optimizations
- Enhanced debugging tools
Resources
- Stylus documentation
- Ink and gas metering
- WASM specification
- EVM opcodes reference
- Stylus SDK repository
Summary
The WASM VM in Arbitrum Nitro represents a significant evolution in smart contract execution:
Key advantages of WASM:
- 10-100x cheaper for compute operations
- More expressive programming languages
- Better memory management
- Compile-time safety guarantees
- Access to mature language ecosystems
Key advantages of EVM:
- Mature tooling and ecosystem
- Familiar to existing developers
- No activation cost
- Decades of collective knowledge
Both execution environments coexist harmoniously on Arbitrum, allowing developers to choose the best tool for each use case while maintaining full interoperability.