Skip to content

security(anchor): close on-chain audit findings (init_config takeover, escrow decoy, rent misroute) - #313

Merged
kh0ra merged 2 commits into
mainfrom
security/anchor-fixes
Jun 21, 2026
Merged

kh0ra merged 2 commits into
mainfrom
security/anchor-fixes

Conversation

@kh0ra

@kh0ra kh0ra commented Jun 21, 2026

Copy link
Copy Markdown
Member

On-chain program security re-audit — 3 confirmed findings fixed

A focused re-audit of the Anchor settlement program (real-money layer) surfaced 2 HIGH fund-safety bugs and 1 INFO consistency defect. All three are fixed here. The program compiles via anchor build --no-idl (SBF toolchain); the host rustc in this env can't run anchor 0.30.1's IDL step (pre-existing toolchain mismatch), so CI regenerates the canonical IDL on build.

H-1 (HIGH) — init_config front-run takeover

init_config was first-call-wins with an unconstrained admin: Signer. On a fresh deployment an attacker could land the first call, become admin, install their own arbitrator multisig, then drive every disputed job to themselves and drain escrow + bonds.

Fix: gate on the program's upgrade authority. The instruction now carries the program + its ProgramData account and requires program_data.upgrade_authority_address == admin. No hardcoded key — the deployer is the only valid initializer across devnet/mainnet/CI.

  • init_config.rs: added program: Program<Covenant> + program_data: Account<ProgramData> with constraints.
  • Clients updated to pass the two new accounts (ProgramData = PDA [programId] under the BPF upgradeable loader): sdk/src/client.ts, app/scripts/init-config.mjs, tests/covenant.ts.
  • Bundled IDLs patched (app/lib, sdk/idl, sdk/src/idl) and validated against the Anchor coder.

H-2 (HIGH) — escrow token account not PDA-pinned (decoy substitution)

cancel_job / finalize_payment / resolve_dispute constrained escrow_token_account only by owner+mint, not by its canonical PDA seeds. An attacker could pass a self-created empty token account (same mint, authority = job_escrow): the handler transfers nothing, closes the decoy, closes the job PDA, and permanently strands the real escrow — unauthenticated, repeatable, permanent fund loss on any expired Accepted job (cancel_job Path B is callable by anyone after the deadline).

Fix: all three paths now pin seeds = [b"escrow_token", job_escrow], bump, matching create_job and the already-pinned bond account.

I-1 (INFO) — escrow rent misrouted on finalize

finalize_payment refunded the escrow token-account rent (paid by the poster in create_job) to the taker. Now refunds to the poster, consistent with cancel_job and resolve_dispute.

Verification

  • anchor build --no-idl → exit 0 (SBF program compiles with all changes).
  • SDK tsc --noEmit → exit 0.
  • node --check app/scripts/init-config.mjs → OK.
  • Patched IDLs load into @coral-xyz/anchor BorshInstructionCoder.
  • IDL diffs are minimal (+9 lines each) — account order: admin, config, program, program_data, system_program (matches Rust field order).

Test plan

  • CI regenerates the IDL on its pinned toolchain and diff matches the hand-patched copies.
  • anchor test on a localnet validator: init_config succeeds for the deployer, fails for a non-upgrade-authority signer.
  • Decoy-escrow test: cancel_job with a substituted empty token account is rejected by the seed constraint.
  • finalize_payment rent lands on the poster.
  • Rebuild + redeploy the program (mainnet milestone) for these to take effect on-chain.

…r, escrow decoy, rent misroute

On-chain re-audit of the settlement program surfaced three confirmed
fund-safety issues. All fixes compile via `anchor build --no-idl` (SBF).

H-1 (HIGH) — init_config front-run takeover
  `init_config` was first-call-wins with an unconstrained `admin` signer, so
  on a fresh deployment anyone could land the first call, make themselves
  admin, and install their own arbitrator multisig — then drive every
  disputed job to themselves and drain escrow + bonds. Now gated on the
  program's upgrade authority: the call carries the program + its ProgramData
  account, and the handler requires `program_data.upgrade_authority == admin`.
  No hardcoded key; the deployer is the only valid initializer across
  devnet/mainnet/CI. IDL + SDK client + init-config.mjs + test updated to
  pass the two new accounts (ProgramData = PDA [programId] under the BPF
  upgradeable loader).

H-2 (HIGH) — escrow token account not PDA-pinned (decoy substitution)
  cancel_job / finalize_payment / resolve_dispute constrained
  `escrow_token_account` only by owner+mint, not by its canonical PDA seeds.
  An attacker could pass a self-created empty token account (same mint,
  authority = job_escrow): the handler would transfer nothing, close the
  decoy, close the job PDA, and permanently strand the real escrow — an
  unauthenticated, repeatable, permanent fund-loss on any expired Accepted
  job. All three paths now pin `seeds = [b"escrow_token", job_escrow], bump`,
  matching create_job and the already-pinned bond account.

I-1 (INFO) — escrow rent misrouted on finalize
  finalize_payment refunded the escrow token-account rent (paid by the
  poster in create_job) to the taker. Now refunds to the poster, consistent
  with cancel_job and resolve_dispute.

Note: the on-chain IDL is regenerated by `anchor build` in CI's pinned
toolchain (host rustc here can't run anchor 0.30.1's IDL step); the bundled
IDL copies were patched by hand to mirror the expected output and validated
against the Anchor coder. Program must be rebuilt + redeployed for these to
take effect on-chain (mainnet milestone).
@changeset-bot

changeset-bot Bot commented Jun 21, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 63965c9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Jun 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
covenant Ready Ready Preview, Comment Jun 21, 2026 1:09am

buy_claim already blocked buyer == seller but not buyer == poster. Since the
poster funds the escrow, buying the claim lets them route the escrow payout
back to themselves and then grief the taker via a dispute. Add a require!
mirroring the existing BuyerIsSeller guard. Compiles via `anchor build --no-idl`.
@kh0ra
kh0ra merged commit 005ce75 into main Jun 21, 2026
10 of 12 checks passed
@kh0ra
kh0ra deleted the security/anchor-fixes branch June 21, 2026 10:23

This branch was successfully deployed

1 active deployment
Preview — 63965c9a Deployed Jun 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant