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