Critical Findings

Every Critical-severity finding raised against the Covenant compiler by the internal OMEGA adversarial review run by Kairos Lab. Thirteen Criticals across four cycles. Twelve are fully closed; CRT-005 is closed in the compiler and the synthesized contract but still open in the deployed helper contract, which is stated in full below.

These are internal self-reviews, not a third-party audit. No external firm has audited Covenant. An external audit is the gate for V1.0. Finding IDs, fix commits and release tags below are quoted from the covenant-security-reviews archive.


OMEGA V4, v0.6 (2026-04-22): 5 Critical

These five share one pattern: the source looked correct, the tests passed, the contracts deployed, but the emitted bytecode did not enforce the guarantee the source promised. Silent codegen failures are the worst class of bug. Fixed across tags v0.6.1-rc1 through v0.6.1-rc4.

KSR-CVN-011: IrGuard never codegenned, access control was fiction

Component: IR builder and every backend. Fix commit: bf89e8c.

Covenant lets you write only, when and given clauses on an action. The IR builder accepted and type-checked the clause, but no backend ever emitted the check. Every deployed Covenant contract with an access-control clause silently accepted calls from any address.

Fix: emit_only_assert in covenant-ir now emits LoadCaller, LoadPrincipal, Eq, Assert, and the EVM backend lowers Assert to ISZERO + JUMPI into the revert path. A regression test asserts that every only in source produces the CALLER + EQ + ISZERO + JUMPI pattern in the emitted bytes.

KSR-CVN-012: proxy initializer had no re-initialization guard

Component: produced contracts (UUPS proxy pattern). Fix commit: f285368.

An initializer action is supposed to be callable exactly once. The emitted bytecode carried no re-init guard, so initialize() could be called repeatedly, overwriting owner and treasury with attacker-controlled addresses. This is the classic proxy-initializer hijack.

Fix: emit_initializer_guard wraps every initializer with a slot-based flag at keccak256("covenant.proxy.initializer.<ModuleName>"), EIP-7201 namespaced and unique per module. The bytecode reads the slot, reverts if it is already set, then sets it before executing the body.

KSR-CVN-013: precompile STATICCALL success flag discarded

Component: EVM backend, precompile call boundary. Fix commit: d5cbcd3.

Cryptographic precompiles are invoked via STATICCALL. The success flag was popped and discarded, so a precompile that reverted (invalid input, out of gas) was treated as a zero-returning success.

KSR-CVN-014: stale memory read as a forged “verified” result

Component: EVM backend, precompile call boundary. Fix commit: d5cbcd3.

Before the STATICCALL, the return memory slot was not zeroed. A failed call left stale memory that the following MLOAD read as a nonzero value, interpreted as “signature verified”.

Combined with KSR-CVN-013 this was a full authentication bypass. Submit a forged signature, the precompile reverts, the success flag is dropped, the MLOAD picks up a nonzero word left by a prior operation, and verification succeeds. One transaction, deterministic in any deployment with prior operations.

Fix (both findings): three layers at every precompile call site. Zero the return slot before the call, check the success flag, and check that RETURNDATASIZE is 32. CI asserts the ISZERO opcode count as a lower bound so the checks cannot silently disappear again.

KSR-CVN-001: ceremony phase transitions unchecked

Component: amnesia ceremony (ERC-8228) codegen. Fix commit: a2207b4.

A ceremony has a strict four-phase state machine: idle, gathering, finalized, destroyed. Only phase reads were checked; phase transitions had no guards. An attacker could call destroy() at phase 0, jump straight to phase 3, and have is_destroyed() return true without a key ever having existed. Anything relying on is_destroyed() as a destruction proof was relying on a decorative state machine.

Fix: every ceremony function now asserts its required precondition phase at bytecode level via SLOAD + EQ + ISZERO + JUMPI. Phase 3 is terminal: no function accepts it as a valid input state.


Ad-hoc adversarial sweep, v0.9.2 (2026-06-09): 1 Critical

KSR-CVN-ERC721-AUTH: synthesized ERC-721 transferFrom had no caller authorization

Component: stdlib ERC-721 synthesis. Fix commit: 5dcbb12.

The auto-synthesized ERC-721 transferFrom performed no caller authorization at all. Any account could steal any NFT by calling transferFrom(victim, attacker, id).

Fix: a shared emit_is_authorized check (owner, approved, or operator), which also gates burn.


OMEGA V6, v0.9.2 (2026-07-05): 6 Critical

A breadth sweep that found six Criticals none of the four prior cycles had caught, including two that silently broke basic, common patterns in the language. All six were addressed the same day, each with a regression test, and shipped in v0.9.3. Two of the six were fixed by refusal rather than by implementation, and one is only partly closed. Both facts are stated on the findings below.

CRT-002: if without else silently deleted the code that followed it

Any if cond { ... } with no else, followed by further statements, compiled to bytecode where the implicit empty else-branch dead-ended instead of continuing into the rest of the function. Dead-code elimination then removed everything downstream of the guard: state writes, event emissions, the return. check, build, build --release and lint all reported clean. This was not a synthetic corner case. It broke the project's own shipped examples/audit/07_revert_paths.cov, where a withdrawal that should obviously succeed reverted every time.

Fix: removed the Terminator::Unreachable placeholder that let dead-code elimination delete the success path.

CRT-003: for each loops did not iterate

A for each loop executed its body exactly once, with no back-edge and an unbound loop variable, regardless of collection length. The behaviour was undisclosed anywhere outside a single source comment.

Fix: real list<Struct> storage end to end. for each now has header, body and merge blocks with a real back-edge, and append and list[idx].field reads and writes persist via keccak-derived element addresses, following the Solidity dynamic-array convention.

CRT-004: only <builtin_predicate> guards compiled to unconditional true

Roughly sixteen built-in guard predicates beyond owner, admin, deployer and address (registered_key, first_time_caller, party, validator_majority and others) lowered to a literal push of 1. Every guard written with one of them was a complete authorization bypass.

Fix: these predicates now hard-fail compilation with E518 instead of compiling to an allow-all. Refusing to compile is the correct answer until they have a real lowering.

CRT-005: ceremony submit_share and finalize had zero authorization and zero threshold enforcement

The whole purpose of the ceremony construct is to require threshold-of-N guardian consensus before an irreversible destruction proof is produced. That control was absent at every layer: the synthesized submit_share had no caller authorization and no per-guardian dedup, and neither the deployed Solidity helper nor the local mock precompile enforced the threshold either. A single address could drive a ceremony to destruction alone.

Fix, and the one residual on this page: finalize() now asserts a real distinct-submitter count against the threshold before trusting the precompile, and submit_share dedupes by caller. The guardians and threshold values are read from the module metadata instead of being hardcoded. That closes the compiler and synthesized-contract layers. The third layer is still open: the deployed Solidity helper CeremonyHelper.sol was not changed, still has no per-guardian dedup, and still computes thresholdMet off a dedup-free array. The defense therefore lives in the calling contract, so a hand-written Solidity caller, or a ceremony compiled with v0.9.2 or earlier and already deployed, inherits the original weakness in full. This is the one finding in the archive whose published attack recipe still works against shipped code.

CRT-006: builtin identifiers silently shadowed same-named user fields

A field named caller, now, block, msg or current_block was silently and completely shadowed by the compiler's built-in identifier of the same name, inside every action, view, reveal, migrate and on_destroy body, with no diagnostic. Reads and writes to the field went to the builtin instead.

Fix: the behavior-scope seeding pass now checks for an existing binding in the parent scope before seeding a builtin identifier, so a real user field wins.

CRT-007: ERC-8231 key registry ABI and codegen disagreed, corrupting post-quantum keys

The ERC-8231 post-quantum key registry emits action register(pk: pq_key) and view key_of(account) returns pq_key. The ABI correctly declared pq_key as the dynamic Solidity type bytes, but the EVM backend classified it as a static 32-byte word. The result was silent corruption of a security-relevant key registry, on the write side, not only on reads.

Fix: the registry construct now hard-fails compilation with E505 rather than silently corrupting a pq_key, and stays that way until real dynamic-bytes storage and ABI encoding land. The consequence is worth stating plainly: the ERC-8231 key registry is unusable in v0.9.5. This fix is a refusal, not an implementation, as is CRT-004 above.


OMEGA adversarial bounty, v0.9.4 (2026-07-23): 1 Critical

F07: reveal <field> to <target> emitted no access-control gate

Fixed in v0.9.5, commit 2f89488.

reveal <field> to <target> compiled with zero caller check. The owner-only disclosure restriction was silently unenforced because the target was dropped at IR lowering, so the reveal reached the backend guardless. A confidentiality primitive that looked enforced compiled to a public disclosure.

Fix: the reveal now emits the msg.sender == owner gate, reusing the same codegen as only <principal>. to owner resolves to the owner field or the deployer, to caller is public, and collection or unresolved targets fail closed. Verified on a local anvil node: a non-owner reveal reverts, the owner's succeeds.

The finding is currently latent, because the FHE layer on testnet is a mock with no confidentiality to lose. It would have been directly exploitable at the V2.0 real-cryptography release.


How to read this page

  • Twelve of the thirteen Criticals listed here are fully fixed. CRT-005 is fixed in the compiler and the synthesized contract and still open in the deployed CeremonyHelper.sol. The current release is v0.9.5.
  • Three of the fixes are refusals rather than implementations: CRT-004 (E518), CRT-007 (E505, which leaves the registry construct unusable) and the v0.9.4 bounty's E426 / E427 / E512 / E522 class. Refusing to compile is the intended answer while a correct lowering does not exist.
  • Every fix is compile-time. Contracts already deployed keep their pre-fix bytecode permanently; recompiling does not retro-fix an artifact that is already on chain.
  • Every cycle above is a Kairos Lab internal review. The unqualified word “audited” is deliberately not used: a third-party firm audit is the gate for V1.0.
  • The compiler is testnet-only and its cryptographic primitives are mocked. Do not place real value at risk.
  • Per-finding files, including the High, Medium, Low and Informational findings, live in the covenant-security-reviews archive (publication pending).