§00 / Security

Every finding.
Every fix.

External code review across all six Anchor programs in May 2026. Fourteen findings, twelve of them critical or high. Below: the live status of every patch, what changed in code, what’s left for v2, and how to report anything the audit missed.

§01

Summary

Coil’s six Anchor programs were reviewed by an external security researcher in May 2026 over a two-day window. The audit produced 14 findings: 8 critical, 4 high, 2 medium. Each finding includes a working exploit demonstrating the vulnerability against a fork of Solana mainnet.

Of those, 10 were patched and redeployed within 24 hours of the report. 2 require no code change because they’re operational (multisig setup, RPC key rotation). 2 have documented v2 plans (one requires migrating an existing on-chain token to a new mint; one is a pricing-semantics design pass) and are scheduled for the next major upgrade.

10

fixed in code

2

operational

2

documented for v2

§02

Findings scoreboard

ID
C1
Severity
Critical
Finding
Wrapper insolvency via harvest_dividend_unsafe (DRAIN-A)
Status
Fixed
ID
C1b
Severity
Critical
Finding
DRAIN-B via unbounded mock_set_rate
Status
Fixed (rate capped)
ID
C1/C3 deep
Severity
Critical
Finding
Solvency invariant on harvest_dividend
Status
Fixed (round 2)
ID
C2
Severity
Critical
Finding
Cross-user YT yield drain via plain SPL transfer
Status
Documented; v2 fix needs Token-2022 YT migration
ID
C3
Severity
Critical
Finding
Maturity-day PT+YT redemption race
Status
Fixed via solvency invariant
ID
C4
Severity
Critical
Finding
Backed Finance permanentDelegate clawback
Status
Adapter circuit breaker + dapp disclosure
ID
C5
Severity
Critical
Finding
Single key controls every program upgrade and runtime authority
Status
Operational, multisig migration in progress
ID
C6
Severity
High
Finding
withdraw_reserves ignores outstanding obligations
Status
Fixed (defense-in-depth check)
ID
C7
Severity
Critical
Finding
coil_vault permanently a hello-world stub
Status
Dead refs removed, real vault is v2
ID
C8
Severity
High
Finding
Oracle pre-emption / silent hijack of future markets
Status
Fixed (governance constraint)
ID
C9
Severity
High
Finding
Permissionless init_* across all 5 programs
Status
Fixed (governance constraint)
ID
C10
Severity
Medium
Finding
Helius RPC API key exposed in dapp bundle
Status
Operational, rotation pending
ID
H1
Severity
High
Finding
Lending PT pricing semantics mismatch with splitter
Status
Design pass for v2
ID
H2
Severity
Medium
Finding
AMM init_amm_pool permissionless
Status
Fixed

§03

Fixed in code

C1, DRAIN-A, root cause removed

The wrapper had two harvest paths: a Pyth-validated harvest_dividend and an unsafe harvest_dividend_unsafe kept for testing. Anyone could call the unsafe path and inflate the yield index over wall-clock time without depositing any underlying. Over months this would silently render the wrapper insolvent against outstanding sSTRC shares.

The unsafe instruction was deleted entirely. The discriminator is now unrecognized; calling it returns Anchor’s InstructionFallbackNotFound error (verified by simulating against mainnet after deploy).

C1b, DRAIN-B, rate capped

mock_set_rate previously accepted any u32 dividend rate. An admin compromise could set u32::MAX, then call harvest, then drain the entire vault in one block. The rate is now capped at MAX_RATE_BPS = 5000 (50 percent APR), which is still well above any legitimate STRC dividend and turns the attack from a one-block catastrophe into bounded solvency drift.

C1/C3 deep, solvency invariant on harvest

Even with the unsafe path removed and the rate capped, the maturity-day race (C3) could occur if sy_index drifted past actual underlying reserves. harvest_dividend now enforces an explicit invariant before mutating state: sy_index × sstrc_supply / INDEX_SCALE ≤ underlying_vault.amount. Harvests that would render the vault undercollateralized against outstanding shares revert; the keeper must deposit dividend backing first. This closes the maturity-day race entirely as long as the invariant holds.

C4, Backed Finance permanentDelegate

STRCx is a Token-2022 mint with a permanentDelegate extension owned by Backed Finance. They retain the on-chain power to clawback STRCx from any account, including Coil’s adapter vault, for compliance compulsion. The protocol can’t prevent the action itself, but can prevent it from cascading. strcon_adapter.wrap now refuses new deposits whenever strcon_vault.amount < wstrcon_mint.supply. If Backed exercises the delegate, no new user can compound the loss by depositing fresh STRCx into an undercollateralized adapter.

C6, withdraw_reserves outstanding-debt check

The lending program already capped withdrawals at total_reserves − total_borrowed, but the audit recommended a stronger token-level invariant for future multi-LP designs. Now also enforces reserve_balance_post ≥ total_borrowed at the SPL Token account level, regardless of bookkeeping field drift.

C7, dead coil_vault stub

The mainnet coil_vault program is a 2KB hello-world stub from the initial deploy. Its upgrade authority is the System Program, which can never sign, making the program ID permanently a no-op. The dapp’s references to that program ID were removed; any consumer that touches the vault PDA now hits the devnet program instead, which is at least valid Anchor code. Real auto-rebalanced vault deploys under a fresh program ID in the next release.

C8 + C9, governance constraints on init_*

Every init_* instruction across the five programs (oracle, splitter, wrapper, lending, adapter) had only a signer requirement on the authority account. An attacker could pre-empt the PDA for any future market tuple, becoming the authority and hijacking initial parameters. All five now enforce address = GOVERNANCE on the authority constraint. Pre-emption attempts fail at the constraint check before any state writes.

H2, AMM init_amm_pool

The splitter’s AMM pool initializer was permissionless and would let an attacker create a pool with a skewed starting ratio that blocked legitimate seeding. Same fix pattern as C8/C9: address = GOVERNANCEconstraint added.

+ Limits hardening

Per-obligation borrow cap added to the lending program at 50 percent of total reserves. Even if the LTV check fails open, no single user can drain more than half the pool. Bounds the blast radius of any future bug or extreme oracle-pricing edge case.

§04

Known limitations

C2, YT cross-user yield drain

The splitter’s UserYieldState.cumulative_at_last_claim snapshot is decoupled from YT balance. A plain SPL Token transfer of YT does not refresh either party’s snapshot. Pre-register at low cumulative yield, wait for cum to grow, acquire YT via transfer, then claim, and you over-claim by yt_balance × (cum_now − snapshot_at_register) against historical YT holders.

The right fix requires one of (a) migrating YT to Token-2022 with a transfer hook that CPIs back to splitter to refresh both sides, (b) per-yt accounting, which needs migrating every existing UserYieldState account to a larger struct, or (c) Token-2022 default-frozen state so only splitter instructions can move YT. All three are v2 work.

Current real exposure is bounded: only one YT atom is outstanding (the developer’s test position), so there is no historical yield for an attacker to drain. The limitation matters once YT trades on liquid AMM pools and other holders accumulate snapshots over time.

H1, lending PT pricing semantics

The lending oracle prices PT in USDC at par face value. The splitter’s redeem_pt at maturity pays amount raw wstrc, which at current STRC price is roughly 2.8x the par-USDC value. Lending under-prices PT collateral; borrowers can only borrow about 36 percent of true redemption value. Not exploitable, but suboptimal. v2 design: pick one redemption semantics (USDC face or wstrc face) and align both programs.

§05

Operational follow-ups

C5, multisig migration

Every program upgrade authority and every runtime authority field is currently held by a single EOA. One key compromise ends the protocol. Migration plan:

  • Create a 3-of-N Squads multisig with at least three independent signers.
  • Transfer each program upgrade authority to the multisig vault PDA via solana program set-upgrade-authority.
  • Update the GOVERNANCE constant in all five programs to point at the multisig vault.
  • Redeploy all five programs (one final upgrade as the EOA), at which point only multisig signatures can call privileged instructions or upgrade the binaries.

Step 1 is the user-facing blocker. Steps 2-4 take roughly ten minutes once the multisig vault PDA is known.

C10, Helius API key rotation

The dapp embeds a Helius mainnet RPC key in the client bundle. Helius keys are read-only so there’s no protocol-level drain, but it’s a paid credential exposed to every visitor (rate-limit and cost abuse). Fix path: rotate the existing key in the Helius dashboard, set a referrer restriction to the production domain on the new key, redeploy. Long-term: move RPC calls behind a server-side proxy so the key is never bundled.

§06

Counterparty risk

Strategy Inc

STRC is junior debt-equivalent. If Strategy defaults (extended BTC bear, inability to roll convertible offerings, dividend suspension), STRC redemption value collapses. STRC holders rank ahead of common equity but behind senior debt. Backed Finance’s STRCx tracks STRC’s actual market value, so any underlying default flows through to every Coil position.

Backed Finance

STRCx is a Backed-issued claim on STRC held at a regulated custodian. Backed retains permanentDelegate, freezeAuthority, pausableConfig, and scaledUiAmountConfig.authority on the STRCx mint. Their stated policy is to use these only for legal compulsion, but the on-chain capability exists. Coil surfaces this on every relevant page and includes the circuit breaker in the adapter to prevent loss compounding.

Smart-contract

Six Anchor programs, roughly 3000 lines of Rust, audited externally in May 2026 with all in-scope findings remediated or documented. The full per-finding status is in the scoreboard above. A formal audit by a recognized firm is a v2 milestone before scaled TVL is enabled.

Liquidation

Buy Leveraged and Auto-loop deposit PT collateral and borrow USDC. The lending pool enforces 75 percent LTV; positions that breach can be liquidated for a 5 percent bonus. Risk is bounded by the deterministic PT oracle (linear walk to par), which doesn’t cascade like a market-tracking oracle would during volatile sell-offs.

Oracle

Pyth’s STRC/USD feed is used for the spot price snapshot during dividend harvest. Wrapper validates feed ID, age (5 minutes max), verification level, and confidence interval (1 percent of price max) before accepting any update. PT pricing for the lending pool is deterministic and does not depend on Pyth at runtime.

Liquidity

The lending pool is bootstrap-sized today (100 USDC of reserves; scales as governance seeds more). Per-obligation borrow capped at 50 percent of total reserves. AMM pools for YT/USDC and PT/USDC are not yet seeded; Buy Fixed and Buy Leveraged work today, Sell Yield via the YT/USDC pool awaits liquidity bootstrapping.

§07

Architecture

Six Anchor programs deployed to Solana mainnet. Upgrade authority is currently a single deploy keypair; multisig migration is the next operational step (audit C5).

Program
coil_strcon_adapter
Mainnet ID
FZuHa…SmxcW
Responsibility
Token-2022 STRCx ↔ legacy SPL wSTRCx, 1:1, with audit C4 circuit breaker
Program
coil_wrapper
Mainnet ID
GCWiW…aFWP
Responsibility
sSTRCx mint with sy_index yield accumulator, Pyth-validated harvest, solvency invariant
Program
coil_splitter
Mainnet ID
EMCoX…oM3F
Responsibility
sSTRCx ⇄ PT + YT, market state, AMM pool primitive
Program
coil_oracle
Mainnet ID
EJ5Fp…K6ff
Responsibility
Linear-discount PT oracle, deterministic walk from entry to par
Program
coil_lending
Mainnet ID
DJwFE…RrZ7
Responsibility
PT-collateralized USDC pool, 75% LTV, per-obligation borrow cap
Program
coil_vault
Mainnet ID
ECgTv…5Y55
Responsibility
Hello-world stub, permanently frozen, pending fresh-ID redeploy in v2

The atomic leveraged loop composes 8 instructions across 5 programs in one signature: MarginFi flash-borrow, Jupiter swap, adapter wrap, wrapper deposit, splitter split, lending deposit-collateral, lending borrow, MarginFi flash-repay. Tx fits the 1232-byte v0 message cap by combining a coil-owned LUT with Jupiter’s per-route LUTs.

§08

Reporting a vulnerability

If you find something the audit missed, please report it privately. There is no bug-bounty program yet, but contributions will be acknowledged publicly and fixes prioritized immediately.

  • Contact: open a private security advisory on the GitHub repository (Settings, Security, Advisories, New advisory). This is the preferred channel.
  • If GitHub advisories are not an option, DM @coilfi_app on Twitter rather than disclosing publicly.
  • Please include a written description of the issue, a reproducible exploit if possible (surfpool fork preferred), and the expected impact.
  • Acknowledgement target: within 48 hours. Critical-issue patch target: within 7 days of confirmation.

Stay current

The full source, every commit, every patch.