Security

V12 audit of commit 11b28f6

V12 audited the Covantic repository at commit 11b28f6 and reported 26 findings. This page records how each one was handled, as verified on 2026-09-15 against commit 31a5d49, tagged audit-2026-09-15. It describes that one commit — not the code before it, and not any commit after it.

Auditor
Audited commit
Verified commit
Verified on
2026-09-15

This page is written by the Covantic team from two documents in our repository. It is not a V12 publication and implies no endorsement by V12.

Summary

25 of 26 findings closed in code, 1 acknowledged

One finding is acknowledged and not fixed: finding 16 (#270300), “Unauthenticated trade evidence can support fabricated payouts”. What bounds it, and what fixing it would take

By severity

SeverityFindingsClosed in codeAcknowledged
Critical110
High880
Medium330
Low14131
Total26251

By validation

13

Findings with a validated proof of concept

1 Critical, 8 High, 3 Medium, 1 Low · 13 closed

13

Observations marked Invalid by the auditor’s validator

13 Low · 12 closed, 1 acknowledged

Scope

What the audit covered

V12 audited the repository mihailShumilov/covantic at 11b28f6. Its core is the Solana program in packages/anchor/programs/covantic, which holds the three contracts below. Browse the program at the tag.

Policy purchase

A holder buys cover for an agent. The price comes from an oracle-signed risk attestation, and the purchase writes the records a later claim is judged against.

create_policyupsert_attestationcancel_policy

Staker-funded vault

Stakers deposit USDC into the vault that backs every policy and earn from the premiums it collects. Withdrawal is two-step, behind a cooldown.

stakerequest_unstakeexecute_unstakeclaim_rewards

Parametric payout engine

A claim settles only through the proof instruction for its trigger, which re-derives the payout bound from state the program reads itself.

submit_claimoracle_submit_claimverify_and_payout_v2verify_and_payout_exploitverify_and_payout_governanceverify_and_payout_agent_errorexpire_policy

Some findings concern the off-chain code around the program instead: the claim keeper, the policy indexer and the shared TypeScript types (findings 4, 21, 22 and 25).

Findings

Every finding, and the change that closed it

The number is the order the report lists a finding in; the V12 id is the auditor's own. The report splits its findings into two classes, and so does this page.

Findings with a validated proof of concept (13)

Each carries a machine-validated proof of concept in the report, with validation reasoning, a patch and the validation output.

#V12 idSeverityFindingStatusClosed by
15#270299CriticalLegacy settlement permits unproven oracle-authorized payoutsClosedverify_and_payout removed from lib.rs; instructions/verify_and_payout.rs deleted; a claim with no proof plans unprovable
1#270230HighPartial Pyth updates fund claimsClosedverify_and_payout_v2.rs requires VerificationLevel::Full
2#270231HighSettlement evidence is not policy-boundClosedPolicyPriceTerms written at purchase by create_policy; v2 requires evidence.feed_id == terms.feed_id and evidence.subject_decimals == terms.subject_decimals
3#270239HighGovernance loss window is unenforcedClosedGOVERNANCE_DRAIN_WINDOW enforced in verify_and_payout_governance.rs
13#270296HighDeclared upgrade and controller coverage cannot be settledCloseddeclare_governance_baseline.rs refuses program_upgrade_authority, controller and a non-zero threshold
19#270303HighRepeated checkpoints erase freeze-transition evidenceClosedcheckpoint_authority.rs advances prev_* only when the control tuple changes
20#270304HighGovernance payouts do not require a proven authority transitionClosedPurchase writes a PolicyAuthorityCheckpoint with a verified owner and no predecessor; settlement classifies live state against the latest permitted reading
23#270308HighBaselines need not predate the claimed takeoverClosedeffective_at maturity checked against the pre-incident reading in verify_and_payout_governance.rs
26#270311HighRefresh erases the baseline usable at claim timeClosedWhole predecessor retained; view_at in state/governance_baseline.rs selects the declaration in force at claim time
6#270256MediumMalformed trigger signatures strand claimsClosedBase58 decode + 64-byte length check in state/insurance_policy.rs, applied by both claim entrypoints
8#270258MediumPolicy schema version is not enforcedClosedInsurancePolicy::assert_readable called by 12 instructions — every policy consumer
18#270302MediumUnresolved pending claims permanently reserve coverageClosedexpire_policy.rs closes a ClaimPending policy past expiry, lock and CLAIM_RESOLUTION_GRACE
4#270249LowPaid claims lose proof qualificationClosedclaims.proof_kind written by claim-keeper.ts, reconciled from the evidence PDA by policy-indexer.ts

Observations marked Invalid by the auditor’s validator (13)

Invalid is the verdict of V12’s own validator: these findings carry a description, a root cause and an impact, but no reproduction and no patch. 12 of the 13 are closed in code; the other one is acknowledged and not fixed — see the open item.

Show all 13 observations
#V12 idSeverityFindingStatusClosed by
10#270293LowUnbound oracle quantity can inflate loss-based payoutsClosedv2 requires evidence.subject_quantity <= terms.max_subject_quantity, error SubjectQuantityExceedsPolicy
9#270292LowUnverified evidence digest recorded as settlement commitmentClosedAll four proof paths refuse an all-zero bundle_hash
7#270257LowInvalid policy codes permanently block lifecycleClosedCovered by assert_readable (version, state, trigger ranges)
14#270297LowUnsupported roles not rejected before wildcardClosedpermits_role in state/governance_baseline.rs returns false for an unknown role before the wildcard
24#270309LowOptional named roles permit the default pubkeyClosedSome(Pubkey::default()) refused in declare_governance_baseline.rs
5#270250LowAgent-error proof kind is unstableClosedBREACH_* moved to constants.rs; AgentErrorBreachKind exported from @covantic/shared
22#270307LowSDK policy tiers exceed the on-chain rangeClosedInsurableTier + isInsurableTierValue in packages/shared/src
25#270310LowManifest commitment excludes declared authority fieldsClosedgovernanceManifestCommitment covers every field plus an extension digest; test at packages/api/tests/governance-manifest-commitment.test.ts
21#270306LowProof-status audit signal relies on unauthenticated logsClosedSame change as finding 4 — status written at payout, reconciled from the PDA
11#270294LowDemo timing feature can be deployed with production claimsCloseddevnet-fast-lock is a #[cfg(feature)] gate; scripts/deploy-devnet.sh passes it only with --fast-lock and refuses on mainnet; checklist in docs/DEPLOYMENT.md
12#270295LowExploit-lock documentation conflicts with executable policyClosedLOCK_EXPLOIT = 3600 (production), shared TS EXPLOIT: 3600, README coverage table states 1 hour — all three agree
17#270301LowPublic PDA documentation conflicts with canonical seedsClosedState-account comments name the *_SEED constants
16#270300LowUnauthenticated trade evidence can support fabricated payoutsAcknowledged — not fixedNot fixed. See the open item.
Open item

The one finding that is not fixed

Finding 16 · #270300LowMarked InvalidAcknowledged — not fixed

Unauthenticated trade evidence can support fabricated payouts

What is not proven

verify_and_payout_v2 authenticates the Pyth reference price and checks timing, arithmetic and a minimum deviation, but executed_price, subject_quantity and trigger_block_time are accepted from the evidence account as assertions about a historical trade. The policy’s persisted trigger_tx_signature is not used to prove that the asserted execution occurred.

This is accepted, not closed.

What bounds it today
  • the feed, decimals and maximum quantity are fixed per policy at purchase (PolicyPriceTerms), so the oracle cannot choose the instrument or inflate the size after the fact;
  • the reference price must be a fully verified Pyth update sitting next to the asserted trade within MAX_PRICE_EVIDENCE_SKEW;
  • the payout is capped by what the signed price can support;
  • the evidence commitment and the lock window make a fabricated claim falsifiable before it pays.

The limitation is documented in the docblock on verify_and_payout_v2.

What closing it would take

Closing it properly means binding settlement to the trigger transaction itself, which is a design change rather than a patch.

Operations

Operational notes

Not findings, but residual risk on already-deployed state.

  • pnpm gov:migrate must be run once after the program upgrade. The authority crank grows an undersized checkpoint on its own, and the attestation publisher grows an undersized attestation.
  • Policies bought before the upgrade have no PolicyPriceTerms; their oracle-manipulation claims plan unprovable and go to manual review.
  • Policies bought before the upgrade have no purchase-time authority reading, so a takeover landing before the crank’s second post-upgrade tick is not provable and goes to review.
Verify

How to verify this yourself

Every statement above is pinned to one tag. Check it out, run the suite, and compare the result and the source with the two documents.

  1. 01

    Check out the tagged commit

    git clone https://github.com/mihailShumilov/covantic.git
    cd covantic
    git checkout audit-2026-09-15

    audit-2026-09-15 points at 31a5d492ac3548621f69c8f7323c4c5affb42e92.

  2. 02

    Read the regression suite

    packages/anchor/tests/v12-regressions.test.ts names its cases after the finding numbers. It is the executable counterpart to the verification document.

  3. 03

    Run it

    With Agave 3.1.14 and anchor-cli 1.0.2, from the repository root:

    pnpm install
    pnpm --filter @covantic/shared build
    cd packages/anchor
    anchor build --ignore-keys
    cargo test -p covantic --lib
    anchor test --skip-build --skip-local-validator --skip-deploy --provider.cluster localnet

    Keep --provider.cluster localnet: Anchor.toml points the provider at devnet. The suite runs in process on solana-bankrun, so it needs no validator and deploys nothing.

  4. 04

    Compare with our run

    On 2026-09-15, at commit 31a5d49:

    SuitePassedFailed
    Anchor suite, all 4 test files1030
    — of which v12-regressions.test.ts220
    Program unit tests (cargo test)380
  5. 05

    Read the documents

docs/audit/ also holds two earlier reports, AUDIT-2026-08-25-bf01e5e8.md and AUDIT-2026-08-31-2a0c9a70.md. Both are internal reviews written in-house, not independent audits, and nothing on this page relies on them.