Sui Bella Ciao — Inside the New Move VM

Sui rewrote their entire Move VM. Here's how the new execution layer works, what changed from the old one, and where the interesting attack surface lives for security researchers.

✍ 0xTheBlackPanther 📅 March 28, 2026 ⏱ 8 min read 🏷 Sui, Move, VM, Security

What Is Bella Ciao?

Bella Ciao is the internal codename for Sui's complete rewrite of the Move VM — the engine that executes every Move smart contract on the Sui network. It shipped as execution version 4, enabled at protocol version 118, behind a feature flag called new_vm_enabled.

This isn't a patch or an optimization pass. It's a new VM with a different architecture, new caching layer, new package resolution, and a new programmable transaction block (PTB) execution pipeline.

The 4-Layer Architecture

The new VM is structured as four layers, each with a clear responsibility:

┌─────────────────────────────────────────────────┐ │ 1. ROUTER (sui-execution/src/lib.rs) │ │ Protocol version → execution version │ ├─────────────────────────────────────────────────┤ │ 2. ADAPTER (sui-execution/latest/sui-adapter) │ │ Sui transactions → Move VM calls │ ├─────────────────────────────────────────────────┤ │ 3. NATIVES (sui-move-natives/) │ │ Rust implementations Move code calls │ ├─────────────────────────────────────────────────┤ │ 4. MOVE VM (external-crates/move/) │ │ Bytecode execution engine │ └─────────────────────────────────────────────────┘

Layer 1: The Router

A single file (sui-execution/src/lib.rs) that reads the protocol config and dispatches to the correct VM version. This is auto-generated code with a simple match statement:

Execution VersionModuleVM TypeProtocol Versions
0v0MoveVM (old)< 18
1v1MoveVM (old)18 — 30
2v2MoveVM (old)31 — 37
3v3MoveVM (old)38 — 117
4latestMoveRuntime (new)>= 118

Old versions (v0–v3) are kept frozen as snapshots so nodes syncing historical state produce identical results. This is how you ship a VM rewrite on a live network without breaking consensus.

There's also an encapsulation test that verifies sui-node and sui-replay can only access execution layer crates through sui-execution. If any crate bypasses this routing, the test fails — because bypassing could mean the wrong VM version gets used, causing a consensus fork.

Layer 2: The Adapter

The adapter (sui-execution/latest/sui-adapter/) is the bridge between Sui's transaction model and the Move VM. It has several key components:

execution_engine.rs — the main orchestrator. Takes a Sui transaction, sets up the execution environment, runs it, settles gas, and produces TransactionEffects.

static_programmable_transactions/ — the new PTB execution pipeline. This is where bella ciao fundamentally differs from the old VM. PTBs now go through a 4-stage pipeline:

1. linkage/ — resolve which packages are needed, build a LinkageContext
2. loading/ — parse raw PTB commands into a typed AST
3. typing/ — type-check and verify the AST
4. execution/ — run each command through the MoveVM interpreter

The SPT module is hardened with #![deny(clippy::arithmetic_side_effects)] and #![deny(clippy::indexing_slicing)] — compile-time guards against overflow and out-of-bounds panics.

execution_mode.rs — defines 4 execution modes that control what's allowed:

ModeArbitrary CallsArbitrary ValuesSkip ConservationUsed For
NormalNoNoNoUser transactions
GenesisYesYesNoChain bootstrap
SystemYesYesNoEpoch changes
DevInspectIf SKIPIf SKIPIf SKIPDry-run simulation

Normal is the only mode users can trigger. The others bypass visibility and type constraints for system operations.

temporary_store.rs — the transactional object store (57KB, the largest file). Tracks every object create, mutate, and delete during execution.

gas_charger.rs / gas_meter.rs — manages gas payment (coins vs address-balance) and per-instruction gas metering.

Layer 3: The Natives

When Move code calls a native function like sui::transfer::transfer(), it crosses into Rust. The sui-move-natives/ crate implements all of Sui's native functions:

Object lifecycleobject.rs handles new_id, delete_id, borrow_uid. These are the primitives for creating and destroying object identity.

Transfer operationstransfer.rs implements transfer_internal, freeze_object, share_object, receive_object_internal, and the new party_transfer_internal.

Dynamic fieldsdynamic_field.rs handles child object CRUD (add, borrow, remove, has).

Crypto — 14 files covering ed25519, ecdsa (k1/r1), bls12381, groth16 (ZK-SNARKs), poseidon, zklogin, VDF, and nitro attestation.

The ObjectRuntime (object_runtime/mod.rs) is the most critical piece. It's the in-memory ledger that tracks all state changes during a transaction — new IDs, deleted IDs, transfers, events, dynamic field mutations. At the end of execution, it produces RuntimeResults which become the transaction's effects.

Layer 4: The Move VM

This is the core of bella ciao — the actual bytecode execution engine in external-crates/move/crates/.

The biggest architectural change is the split between MoveRuntime and MoveVM:

OLD VM (v3) MoveVM (monolithic)
- single long-lived instance
- per-module loading
- direct execution

struct Executor(Arc<MoveVM>)
NEW VM (v4 / bella ciao) MoveRuntime → MoveVM (factory)
- MoveRuntime: long-lived cache
- MoveVM: ephemeral per-execution
- package-level VTable caching

struct Executor(Arc<MoveRuntime>)

MoveRuntime is created once per epoch. It holds a MoveCache (for loaded packages), NativeFunctions, and VMConfig. When a transaction needs to execute Move code, it calls make_vm() which creates a short-lived MoveVM instance bound to a specific LinkageContext (the set of exact package versions to use).

The MoveVM instance gets VMDispatchTables — pre-computed virtual function dispatch tables for all packages in the linkage context. These tables are cached by LinkageHash, so the same dependency graph doesn't need to be rebuilt for every transaction.

The Verification Pipeline

When someone publishes a new Move package, it goes through two verification layers:

Move bytecode verifier — the standard Move verifier checks type safety, reference safety, resource safety, etc. Runs with a meter that enforces a time budget per module. If verification times out, the publish is rejected.

Sui-specific verifier (sui-verifier/) — runs 7 additional passes:

  1. struct_with_key_verifier — structs with key must have id: UID as first field
  2. global_storage_access_verifier — blocks move_to, move_from, borrow_global (Sui uses object model)
  3. id_leak_verifier — prevents leaking/unpacking UID
  4. private_generics_verifier — certain generic types can only be instantiated by their defining module
  5. entry_points_verifier — validates init() and entry function signatures
  6. one_time_witness_verifier — validates the OTW pattern

What Changed — Old vs New

AspectOld (v3)New (bella ciao)
VM coreMoveVM (monolithic)MoveRuntime + ephemeral MoveVM
Package loadingPer-modulePackage-level with MoveCache
PTB executionDirect4-stage: linkage → load → type → execute
DispatchDirect resolutionVTable caching by LinkageHash
ArithmeticStandardchecked_arithmetic macro + clippy deny
New featuresparty_transfer, accumulators, settlement

Where the Attack Surface Lives

For security researchers looking at the bella ciao bounty, here are the areas worth focusing on:

VTable cache keying. Dispatch tables are cached by LinkageHash. If two different linkage contexts produce the same hash, a cached VTable from one context could be reused for another. There's a sanity check that logs an error and reloads — but it recovers rather than hard-failing.

debug_assert-only invariants. Critical state invariants in ObjectRuntime::finish() — like transfer/delete exclusivity and child object ownership consistency — are enforced via debug_assert!. In release builds, these are no-ops. If an invariant is violated, execution proceeds silently.

Verifier timeout classification. The function check_for_verifier_timeout() treats REFERENCE_SAFETY_INCONSISTENT as a timeout error. This could also be a real reference safety violation — it gets logged as a debug_fatal! but is still classified as a timeout, not a hard verification failure.

The v3-to-v4 behavioral delta. Same trait interface, different implementations. Any semantic divergence in how the old and new VM execute the same transaction is potentially exploitable. The interesting diff lives between external-crates/move/move-execution/v3/ (old) and external-crates/move/crates/ (new).

The SPT pipeline. The static_programmable_transactions/ module is entirely new code. Its own AST, typing system, linkage resolution, and interpreter. Fresh code means fresh bugs.

party_transfer_internal. A new native function that currently restricts to exactly 1 party member with ALL permissions. The permission bitmask (READ=1, WRITE=2, DELETE=4, TRANSFER=8) suggests future multi-party ownership. Any bypass of the single-party restriction before the feature is fully ready could be dangerous.

Note: This post is based on reading the bella-ciao branch of the Sui repository. If anything here is inaccurate or outdated, reach out to me on X @thepantherplus and I'll fix it.
Codebase: Sui (bella-ciao branch)
Scope: New Move VM — execution version 4, protocol version 118+
Key Directories: sui-execution/latest/, external-crates/move/crates/
Feature Flag: new_vm_enabled
Follow: @thepantherplus