In one sentence. Building a blockchain runtime from scratch eliminates entire categories of famous attacks and creates one new, narrower danger zone: the seam where trusted system code meets untrusted user code.
Picture it like this. Building your own airport rather than leasing a terminal. You inherit nobody's known flaws — and nobody's twenty years of hard-won procedures either. Every door is one you designed, which means every door is one you're responsible for.
Why it matters. Most security writing is a list of things that went right. This is a list of specific attacks, how each is structurally prevented, and — in two places — a plain statement of where the prevention is thinner than the threat model calls for.
The architecturally interesting part is the Go–WASM boundary.
Smart contract security on a custom blockchain runtime is both harder and easier than on Ethereum.
Harder, because there's no ten-year corpus of battle-tested patterns, no automated scanner like Slither or Mythril that understands the runtime's semantics, and no community of auditors who can spot a reentrancy bug from across the room.
Easier, because the runtime isn't bound to EVM's execution model. Execution semantics, transaction ordering, and the storage model can all be decided from first principles.
ComputeFlux's security posture follows directly from those choices. Its execution model eliminates entire classes of EVM-native attack — reentrancy, front-running, gas griefing — and opens new surface at the boundary where the native Go runtime meets the WASM contracts it hosts.
This article is the second half of a pair. Article 18: Model Report Audit covered oversight of the models and providers running on top of ComputeFlux — a social and economic layer of quality enforcement. This article covers oversight of the substrate itself: the contract runtime that Article 5: WASM Contract Upgrades described as ComputeFlux's mechanism for zero-downtime code evolution. Together, the two form a complete picture of trust enforcement — one watching the applications, the other watching the platform those applications run on.
Nonce Replay: Per-(caller, callerType) Granularity
Transaction replay — submitting the same signed transaction twice and having it execute twice — is the oldest blockchain attack. The standard defense is a per-account nonce: each account maintains a counter that increments with every transaction, and the transaction includes a nonce that must equal the account's current nonce + 1. A replayed transaction has a stale nonce and is rejected.
ComputeFlux adds a callerType dimension, because the runtime treats transaction origins with different trust models differently. callerType records the authentication method. Type 0 marks system-generated transactions from the TEE/DKG-signed side, exempt from nonce checking as described below. Type 1 marks transactions authenticated by a secp256k1 (EVM-style) wallet signature — the key scheme Article 16 derives for users; ComputeFlux's own wallets never use Polkadot's sr25519 scheme. Additional numeric types cover other signing schemes, such as ed25519. Each (caller, callerType) pair keeps its own nonce sequence, independent of the rest.
The rationale: if a single nonce counter served all caller types, a user transaction (type 1) would advance the nonce, potentially invalidating a pending system transaction (type 0) that was generated before the user's transaction but not yet included in a block. The system cannot control when users submit transactions, and forcing all caller types to share a nonce would make the system transaction pipeline fragile to user activity. Separate nonce sequences per type allow each type to progress independently.
However, type 0 transactions skip nonce validation entirely (callerType != 0 is the check in tx_finalize.go). This is because type 0 transactions are generated by the DKG governance process — the configured threshold signature (t-of-n validators, per the DKG configuration described in Article 3) on the governance proposal serves as the replay protection. A governance proposal that is submitted twice would have the same DKG signature; the runtime detects the duplicate proposal ID (stored in on-chain state) and rejects the second submission. The DKG signature is the nonce-equivalent for system transactions: it binds a specific proposal to a specific block height and governance cycle.
The dual nonce-checking mechanism — check against the DB's stored nonce, and also against the in-memory state of nonces advanced by previous transactions in the same block — prevents a subtle intra-block replay. Within a single CometBFT block, multiple transactions from the same caller may be included. If the validation only checked the DB nonce (which hasn't been updated yet because the block hasn't been finalized), the second transaction would see the same DB nonce as the first and pass validation. By maintaining an in-memory map of (caller, callerType) → currentNonce that is updated after each successful nonce check within the block's FinalizeBlock execution, the second transaction sees the incremented nonce and its nonce must be currentNonce + 1. This is a standard pattern in account-based blockchains (Ethereum does the same in its state transition function), but forgetting the in-memory check is a common implementation bug.
Three-Tier Access Control: CallbySystem, ensureSudo, and Owner Check
The runtime has three distinct authorization levels, each for a different security context, and conflating them is a source of privilege escalation vulnerabilities.
CallbySystem is the lowest-level primitive. It checks whether the current transaction's caller equals the configured sudo identity: bytes.Equal(r.sudo.V, r.caller.V) && r.sudo.T == r.caller.T. The sudo identity is set during the runtime's initialization (from genesis configuration, or transferred early via governance) — and once DisableSudo runs, no code path can re-create it: the one-way door of Article 17. CallbySystem is a check, not a guard — it returns a boolean that the calling code uses to decide whether to proceed. The contract mutation engine checks CallbySystem before allowing operations that modify the runtime itself: upgrading the WASM contract code, modifying the validator set, changing the sudo identity. If CallbySystem returns false, these operations are rejected.
CallbySystem is deliberately simple because it sits on the critical path of every privileged operation. Any complexity — role-based access, multisig checks, timelock enforcement — would introduce potential bypass paths. The principle is: the sudo check must be a single boolean condition that is trivially auditable. More complex authorization logic belongs in the governance module, which can implement multi-signature requirements, voting periods, and timelocks before eventually submitting a transaction that passes the simple CallbySystem check.
ensureSudo is the governance module's error-returning check, used alongside CallbySystem. CallbySystem returns a boolean; ensureSudo does the equivalent identity comparison against the governance module's tracked sudo account and returns ErrNotSudo when the caller doesn't match. It also covers two cases a bare boolean can't: ErrSudoNotSet when no sudo account is configured, and ErrSudoDisabled when sudo has been permanently switched off.
Governance operations like CallAsSudo and DisableSudo use it, since those arrive as external transactions from community-passed proposals rather than from the runtime itself. The pattern exists to stop a developer from writing if !CallbySystem() { proceed anyway } when they meant if !CallbySystem() { return error } — while folding in the sudo-disablement check that a bare CallbySystem would miss.
Owner check is the application-level authorization for contract-specific operations. Each WASM contract stores an owner address in its state, and operations that change the contract's configuration — renaming it, updating metadata, pausing it — verify caller == owner.
That check lives in the contract's WASM code rather than the Go runtime, because ownership is contract-specific. Some contracts have multiple owners in a multisig. Some allow ownership transfer. Some have no owner at all, fully autonomous and governed only by their code.
The three tiers form a hierarchy: CallbySystem is the base layer that authorizes runtime-level operations; ensureSudo extends CallbySystem for governance-initiated operations; the owner check is the application layer for contract-specific operations. A contract cannot call CallbySystem (it's a host function not exported to WASM), cannot escalate to sudo (the WASM runtime's caller identity is the contract ID, not the original transaction sender), and cannot bypass the owner check (it's enforced by the contract's own code). The security property is that no WASM contract, regardless of its code, can perform any operation that requires sudo or system-level authorization — including the very upgrade mechanism described in Article 5, which is itself gated behind CallbySystem/ensureSudo checks.
Integer Overflow and Underflow in Token Arithmetic
Token contracts perform arithmetic on balances: adding deposited amounts, subtracting withdrawn amounts, multiplying by exchange rates. In languages with checked arithmetic (Rust's overflow_checks, Solidity 0.8+), an overflow or underflow causes a panic or revert. In Go, integer overflow wraps around silently: math.MaxUint64 + 1 == 0. Whatever code path performs balance arithmetic — host function, native Go mutation logic, or eventually WASM contract code — must use checked arithmetic to prevent overflow-based attacks.
The specific attack is subtraction underflow: a user with balance 0 withdraws 1 token, and with unchecked arithmetic newBalance = oldBalance - amount computes 0 - 1 and wraps to math.MaxUint64, letting the user withdraw funds that don't exist. (Addition overflow is the wrong example — it needs a balance near 2^64, which is unreachable in practice; the real risk is the withdrawal direction.)
In ComputeFlux today, balance-affecting operations live in native Go rather than WASM contract code — see Article 22 on the Go-native/WASM hybrid dispatch model — so the checked-arithmetic burden sits on that native path. The gateway module's DeductBalance shows the pattern: it checks if m.Balance < amount before subtracting and returns ok=false rather than letting the subtraction underflow. The guard is an explicit comparison inside the mutation logic, not a generic checked-arithmetic host function exposed to contracts.
As gateway logic migrates into WASM under the roadmap's full-WASM plan, the same discipline has to hold at whatever boundary ends up mediating storage writes — host functions or contract-side validation. Validate before mutating. Never let a subtraction proceed after its check failed.
The principle holds wherever the check lives, because Go's integer subtraction wraps silently: 0 - 1 on a uint64 yields math.MaxUint64. Rust and Solidity 0.8+ hand you a runtime panic for free. Here, every balance-mutating path has to guard against underflow explicitly.
The serialization layer (ScaleEncode/ScaleDecode) provides a second line of defense against integer-related memory corruption. The ScaleDecode function checks bounds before every read: if off+N > len(data) { return error }. This prevents a malformed or maliciously crafted byte slice from causing an out-of-bounds read that could leak memory or crash the runtime. The check is per-field, not per-message, ensuring that even deeply nested structures cannot cause reads beyond the allocated buffer.
Denial of Service via Large Arrays in Contract Calls
WASM contracts accept byte arrays as inputs to their exported functions. A malicious caller could submit a contract call with a 10MB array — far larger than any legitimate input — hoping to exhaust the runtime's memory or CPU. Without input size limits, a single transaction could degrade performance for all subsequent transactions in the block, or in the worst case, crash the runtime.
Unbounded input size is a real concern on any contract call path accepting caller-supplied byte arguments. The mitigation is the one used elsewhere in the system: reject oversized payloads before they reach contract execution, rather than trusting the contract to handle them gracefully. The enclave's memory budget sets the hard ceiling — the WASM sub-enclave's heap is configured at 512MB (the 2GB heap belongs to the main dsecret enclave), see Article 21 — and whatever threshold the transaction-decoding stage enforces should sit well below it. That number is an implementation detail, not a constant worth memorizing.
A related, more subtle DoS vector is the number of PebbleDB keys a contract call can iterate over. A prefix scan with an overly broad (or empty) prefix could in principle walk over every key in the database — potentially millions of entries across all contracts and modules. ComputeFlux's storage layer addresses this with cursor/offset-based pagination (ListByPrefixLimit takes an explicit offset and limit) rather than an unbounded scan, so a caller controls — and can bound — how much of the keyspace a single query walks. The general principle is the same as the argument-size case: bound the query, don't trust the caller to ask for a reasonable amount of data.
The Security Boundary Between Native Go and WASM Contracts
The Go-WASM boundary is the most architecturally significant security interface in the system — it is precisely the boundary that Article 5 introduced when describing the Native/WASM dual dispatch model for contract upgrades. On one side: the Go runtime, with full access to PebbleDB, the network stack, the DKG signing module, and the system's cryptographic keys. On the other side: untrusted WASM bytecode uploaded by users through governance proposals. The boundary must ensure that no WASM contract — regardless of what it does with its CPU budget and memory inside its sandbox — can read or modify data outside its designated storage namespace, call unauthorized host functions, or escape the WASM sandbox.
Import whitelisting is the first layer. WASM modules declare their imports in a section of the binary format. The runtime's WASM engine (wazero, a pure-Go, zero-CGO WASM runtime — a deliberate choice for EGo/SGX compatibility, since CGO inside EGo is only experimental and would pull a separately-auditable C runtime into the enclave's TCB) only resolves imports that are explicitly registered as host functions. ComputeFlux registers a small, deliberately narrow set: storage access (storage_get, storage_set, storage_delete, storage_scan_prefix, storage_delete_prefix), block/call context (get_height, get_block_time, get_version), and caller identity (get_caller, get_system_account, is_system). Notably, there are no host functions for direct network I/O or filesystem access — a WASM contract has no way to reach either, by construction, not merely by convention. A module that imports something outside this registered set will fail to instantiate. The whitelist is exhaustive: every host function available to contracts is explicitly listed in the runtime's import resolver, and adding a new host function requires a code change to the runtime (a governance-gated upgrade).
Memory isolation is handled by the WASM engine. WASM's memory model is a linear memory — a contiguous byte array that the WASM module can read and write. The engine enforces that all memory accesses (load and store instructions) fall within the allocated memory bounds. An out-of-bounds access traps (the WASM equivalent of a segfault) and the runtime terminates the contract call with an error. The WASM module cannot access the Go runtime's memory — the linear memory is a separate allocation, and the WASM engine enforces this separation at the instruction level. Even if the WASM module corrupts its own memory (a buffer overflow within the linear memory), it cannot corrupt the Go runtime's data structures.
Resource limits are the concern every shared, consensus-critical execution environment has to face. One contract call must not be able to burn unbounded CPU and stall every other transaction in the block, whether through an infinite loop or a pathologically expensive computation.
wazero runs in interpreter mode here, chosen over compiler mode because EGo/SGX forbids executable memory pages inside the enclave — compiler mode would crash the enclave outright. Interpreting bytecode without native code generation is itself a meaningful throughput ceiling. But a dedicated instruction-metering or per-call timeout mechanism is hardening this threat model calls for, not something to claim is already built.
Determinism is enforced through host function design. All host functions that read state return deterministic results — the state at the beginning of the block. The WASM engine does not expose non-deterministic sources (random number generation, system time, network I/O) to contracts. This ensures that a contract's execution is fully determined by its inputs and the blockchain state, and that all validators executing the contract produce identical results. Non-deterministic host functions would break consensus: two validators executing the same contract with the same inputs might produce different results if one validator's system clock returned a different timestamp.
The most subtle security concern at the boundary is the handling of panics. Go's recover() mechanism can catch panics that occur in host functions, but a panic in WASM code (a trap) is caught by the WASM engine and translated into a Go error. The runtime must ensure that a WASM trap does not leave PebbleDB in an inconsistent state — e.g., a host function that has written to PebbleDB but not yet completed its logical operation when the trap occurs. The solution is to batch all PebbleDB writes for a contract call in a transaction object that is committed atomically at the end of the call. If the call traps, the transaction is discarded, and no partial writes persist. This is the same ACID (Atomicity, Consistency, Isolation, Durability minus the "I" from ACID — CometBFT provides isolation through sequential execution) guarantee that databases provide, applied at the contract execution granularity.
Key Takeaways
- Whole classes of infamous attacks simply don't exist here. Reentrancy, front-running, and gas griefing are artifacts of a particular execution model, and this runtime doesn't use it. That's the genuine upside of not inheriting a platform.
- And the genuine downside: nothing is battle-tested. No mature scanners understand this runtime, there's no decade of established patterns, and no auditor community recognizes its bug shapes at a glance. Novelty is a cost as well as a benefit.
- Replay is checked twice — against stored state and against the current block in memory. The second check catches a duplicate inside a single block, where stored state hasn't updated yet. Omitting it is a classic and very easy mistake.
- The privilege check is deliberately trivial. One boolean comparison, because anything more elaborate creates bypass paths. All the complexity — voting, timelocks, multi-signature — lives in governance above the check, never inside it.
- The sandbox is closed by absence, not by policy. Untrusted contracts have no function available to reach the network or the filesystem. Nothing is blocked, because nothing was ever offered.
- An honest gap: there's no per-call instruction metering or timeout, so a pathologically expensive contract call is bounded only by the interpreter's own slowness. Named here as hardening the threat model calls for, not claimed as finished work.
Closing the Trust Thread
Together, Article 18's model audits and this article's contract security give ComputeFlux a two-layer trust story. Reputation economics police the applications running on the network. A runtime design that structurally forecloses replay, privilege escalation, overflow, and sandbox escape polices the platform they run on. This is the last of the series' security-and-oversight articles. Part 5 turns to a different concern entirely: not whether ComputeFlux can be trusted, but whether it can be run.
Next — Article 20: OTel Distributed Tracing: everything above is only debuggable if you can see it happening. Here's how.