§00 / Security

Every finding.
Every fix.

External code review across all six Anchor programs in May 2026. Twelve findings, ten 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 something we 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 12 findings: 7 critical, 3 high, 2 medium. Each finding includes a working exploit demonstrating the vulnerability against a fork of Solana mainnet.

Of those, 9 were patched and redeployed within 24 hours of the report. 2 required no code change because they’re operational (multisig setup, RPC key rotation). 1 has a documented v2 plan that requires migrating an existing on-chain token to a new mint, which is scheduled for the next major upgrade.

9

fixed in code

2

operational

1

documented v2 fix

§02

Findings scoreboard

IDSeverityFindingStatus
C1CriticalWrapper insolvency via harvest_dividend_unsafe (DRAIN-A)Fixed
C1bCriticalDRAIN-B via unbounded mock_set_rateFixed (rate capped)
C1/C3 deepCriticalSolvency invariant on harvest_dividendFixed (round 2)
C2CriticalCross-user YT yield drain via plain SPL transferDocumented; v2 fix needs Token-2022 YT migration
C3CriticalMaturity-day PT+YT redemption raceFixed via solvency invariant
C4CriticalBacked Finance permanentDelegate clawbackAdapter circuit breaker + dapp disclosure
C5CriticalSingle key controls every program upgrade and runtime authorityOperational, multisig migration in progress
C6Highwithdraw_reserves ignores outstanding obligationsFixed (defense-in-depth check)
C7Criticalcoil_vault permanently a hello-world stubDead refs removed, real vault is v2
C8HighOracle pre-emption / silent hijack of future marketsFixed (governance constraint)
C9HighPermissionless init_* across all 5 programsFixed (governance constraint)
C10MediumHelius RPC API key exposed in dapp bundleOperational, rotation pending
H1HighLending PT pricing semantics mismatch with splitterDesign pass for v2
H2MediumAMM init_amm_pool permissionlessFixed

§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. We can’t prevent the action itself, but we 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 team’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 (OtterSec, Neodyme) is a v2 milestone.

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).

ProgramMainnet IDResponsibility
coil_strcon_adapterFZuHa…SmxcWToken-2022 STRCx ↔ legacy SPL wSTRCx, 1:1, with audit C4 circuit breaker
coil_wrapperGCWiW…aFWPsSTRCx mint with sy_index yield accumulator, Pyth-validated harvest, solvency invariant
coil_splitterEMCoX…oM3FsSTRCx ⇄ PT + YT, market state, AMM pool primitive
coil_oracleEJ5Fp…K6ffLinear-discount PT oracle, deterministic walk from entry to par
coil_lendingDJwFE…RrZ7PT-collateralized USDC pool, 75% LTV, per-obligation borrow cap
coil_vaultECgTv…5Y55Hello-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. We don’t have a bug bounty program yet but we will acknowledge contributions publicly and prioritize fixes immediately.

  • Contact: open a private security advisory on the GitHub repository (Settings, Security, Advisories, New advisory). This is the preferred channel.
  • If you can&rsquo;t use GitHub advisories, find us on Twitter and DM rather than disclosing publicly.
  • Please include a written description of the issue, a reproducible exploit if possible (surfpool fork preferred), and the expected impact.
  • We commit to acknowledging within 48 hours and patching critical issues within 7 days of confirmation.

Stay current

The full source, every commit, every patch.