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.
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
By severity
| Severity | Findings | Closed in code | Acknowledged |
|---|
| Critical | 1 | 1 | 0 |
| High | 8 | 8 | 0 |
| Medium | 3 | 3 | 0 |
| Low | 14 | 13 | 1 |
| Total | 26 | 25 | 1 |
By validation
13Findings with a validated proof of concept
1 Critical, 8 High, 3 Medium, 1 Low · 13 closed
13Observations 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 id | Severity | Finding | Status | Closed by |
|---|
| 15 | #270299 | Critical | Legacy settlement permits unproven oracle-authorized payouts | Closed | verify_and_payout removed from lib.rs; instructions/verify_and_payout.rs deleted; a claim with no proof plans unprovable |
| 1 | #270230 | High | Partial Pyth updates fund claims | Closed | verify_and_payout_v2.rs requires VerificationLevel::Full |
| 2 | #270231 | High | Settlement evidence is not policy-bound | Closed | PolicyPriceTerms written at purchase by create_policy; v2 requires evidence.feed_id == terms.feed_id and evidence.subject_decimals == terms.subject_decimals |
| 3 | #270239 | High | Governance loss window is unenforced | Closed | GOVERNANCE_DRAIN_WINDOW enforced in verify_and_payout_governance.rs |
| 13 | #270296 | High | Declared upgrade and controller coverage cannot be settled | Closed | declare_governance_baseline.rs refuses program_upgrade_authority, controller and a non-zero threshold |
| 19 | #270303 | High | Repeated checkpoints erase freeze-transition evidence | Closed | checkpoint_authority.rs advances prev_* only when the control tuple changes |
| 20 | #270304 | High | Governance payouts do not require a proven authority transition | Closed | Purchase writes a PolicyAuthorityCheckpoint with a verified owner and no predecessor; settlement classifies live state against the latest permitted reading |
| 23 | #270308 | High | Baselines need not predate the claimed takeover | Closed | effective_at maturity checked against the pre-incident reading in verify_and_payout_governance.rs |
| 26 | #270311 | High | Refresh erases the baseline usable at claim time | Closed | Whole predecessor retained; view_at in state/governance_baseline.rs selects the declaration in force at claim time |
| 6 | #270256 | Medium | Malformed trigger signatures strand claims | Closed | Base58 decode + 64-byte length check in state/insurance_policy.rs, applied by both claim entrypoints |
| 8 | #270258 | Medium | Policy schema version is not enforced | Closed | InsurancePolicy::assert_readable called by 12 instructions — every policy consumer |
| 18 | #270302 | Medium | Unresolved pending claims permanently reserve coverage | Closed | expire_policy.rs closes a ClaimPending policy past expiry, lock and CLAIM_RESOLUTION_GRACE |
| 4 | #270249 | Low | Paid claims lose proof qualification | Closed | claims.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 id | Severity | Finding | Status | Closed by |
|---|
| 10 | #270293 | Low | Unbound oracle quantity can inflate loss-based payouts | Closed | v2 requires evidence.subject_quantity <= terms.max_subject_quantity, error SubjectQuantityExceedsPolicy |
| 9 | #270292 | Low | Unverified evidence digest recorded as settlement commitment | Closed | All four proof paths refuse an all-zero bundle_hash |
| 7 | #270257 | Low | Invalid policy codes permanently block lifecycle | Closed | Covered by assert_readable (version, state, trigger ranges) |
| 14 | #270297 | Low | Unsupported roles not rejected before wildcard | Closed | permits_role in state/governance_baseline.rs returns false for an unknown role before the wildcard |
| 24 | #270309 | Low | Optional named roles permit the default pubkey | Closed | Some(Pubkey::default()) refused in declare_governance_baseline.rs |
| 5 | #270250 | Low | Agent-error proof kind is unstable | Closed | BREACH_* moved to constants.rs; AgentErrorBreachKind exported from @covantic/shared |
| 22 | #270307 | Low | SDK policy tiers exceed the on-chain range | Closed | InsurableTier + isInsurableTierValue in packages/shared/src |
| 25 | #270310 | Low | Manifest commitment excludes declared authority fields | Closed | governanceManifestCommitment covers every field plus an extension digest; test at packages/api/tests/governance-manifest-commitment.test.ts |
| 21 | #270306 | Low | Proof-status audit signal relies on unauthenticated logs | Closed | Same change as finding 4 — status written at payout, reconciled from the PDA |
| 11 | #270294 | Low | Demo timing feature can be deployed with production claims | Closed | devnet-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 | #270295 | Low | Exploit-lock documentation conflicts with executable policy | Closed | LOCK_EXPLOIT = 3600 (production), shared TS EXPLOIT: 3600, README coverage table states 1 hour — all three agree |
| 17 | #270301 | Low | Public PDA documentation conflicts with canonical seeds | Closed | State-account comments name the *_SEED constants |
| 16 | #270300 | Low | Unauthenticated trade evidence can support fabricated payouts | Acknowledged — not fixed | Not 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.
- 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.
- 02
- 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.
- 04
Compare with our run
On 2026-09-15, at commit 31a5d49:
| Suite | Passed | Failed |
|---|
| Anchor suite, all 4 test files | 103 | 0 |
| — of which v12-regressions.test.ts | 22 | 0 |
| Program unit tests (cargo test) | 38 | 0 |
- 05
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.