All audits
FailedSolanaIndependent

Cashio Infinite Mint (2022)

Smart contract security audit by TierZero · 2023-06-15

Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.

Findings summary

1
Critical
2
High
2
Medium
1
Low
1
Informational

Independent research — not a commissioned audit. This report is TierZero's own post-mortem analysis of a publicly documented incident, compiled from public disclosures and community writeups. TierZero was not involved with Cashio and did not audit the protocol before the exploit.

Cashio was a Solana stablecoin protocol that collapsed in a single transaction on March 23, 2022. Public reporting puts the loss at approximately $52 million, with the attacker minting a reported figure north of 2 billion fake CASH tokens against no real backing. It remains one of the clearest examples in Solana's history of what happens when a program trusts caller-supplied accounts instead of verifying them against a known-good root.

What the protocol did

Cashio (CASH) was an algorithmic, over-collateralized stablecoin built on top of Saber's "Crate" framework. Users deposited Saber LP tokens — specifically LP shares representing UST/USDC liquidity — into a Cashio "Bank," and in exchange the protocol minted CASH pegged to $1. Redemption ran the same path in reverse: burn CASH, withdraw the underlying Saber LP collateral. The design assumed that any account presented to the program as "the collateral vault of a registered Bank" really was that vault. That assumption is where the protocol died.

The vulnerability

The bug class is missing account/ownership validation — sometimes called a "fake account" or "unverified PDA" bug — and it's endemic to Solana programs because every account referenced in an instruction is supplied by the caller, not derived internally by the runtime. A program has to explicitly check that each account is the one it claims to be: correct owner, correct discriminator, correct derivation from program-controlled seeds, and correct linkage to the specific state object (Bank, Vault, Pool) the instruction is operating on.

Cashio's mint instruction validated surface properties of the incoming accounts — token type, mint field, decimals — but did not walk the full chain back to a trusted root to confirm the "Bank" and its "collateral vault" were the ones actually registered on-chain with real deposited value. An attacker could permissionlessly create a brand-new, entirely fake Saber LP mint, a fake token account claiming to hold that mint, and a fake Bank/Crate account structured to pass the shallow checks, then hand all of it to Cashio's mint instruction as if it were genuine collateral.

// Illustrative — pattern of the flaw, not the original source
pub fn mint_cash(ctx: Context<MintCash>, amount: u64) -> Result<()> {
    let collateral_account = &ctx.accounts.collateral_token_account;
    let bank = &ctx.accounts.bank;

    // Checks the mint field matches what `bank` claims —
    // but `bank` itself was never verified as the canonical,
    // previously-initialized Bank for this collateral type.
    require!(collateral_account.mint == bank.collateral_mint, ErrorCode::BadMint);

    // Proceeds trusting `bank` and `collateral_account` are genuine.
    token::mint_to(cpi_ctx, amount)?;
    Ok(())
}

Every individual field check passes. What's missing is a check that bank.key() equals the program-derived address the protocol actually registered, and that collateral_account.key() equals the vault stored inside that legitimate bank. Without that, the attacker supplies a self-consistent but entirely fabricated universe of accounts.

How the exploit worked

  1. The attacker created a fake token mint that mimicked a legitimate Saber LP token, plus a token account under that fake mint.
  2. The attacker created a fake "Bank" record pointing at that fake mint and fake vault, satisfying every field-level check Cashio's program ran.
  3. The attacker called the mint instruction, passing the fake Bank and fake collateral account in place of real ones. The program never verified these were the canonical, previously-registered instances — it only checked internal consistency between the fake objects, which the attacker fully controlled.
  4. The instruction minted CASH against zero genuine collateral. The attacker repeated this to mint a reported multi-billion-token supply.
  5. Newly minted CASH was swapped through Saber and other Solana AMM pools for real assets (predominantly UST and USDC at the time) before the peg broke and CASH went to zero, leaving liquidity providers and holders holding the loss.

How an audit catches this

This is exactly the class of bug that account-validation review exists to find, and it's why we treat every instruction handler as a graph-traversal problem, not a field-by-field checklist. Concretely, in a Solana program review we:

  • Enumerate every account passed into a state-mutating instruction and trace whether it is derived from program-owned seeds (PDA) or merely asserted by the caller.
  • For each non-PDA account, confirm the program checks its pubkey against a value stored in already-verified state — not just its type or embedded fields, which an attacker fully controls when the account is freshly created.
  • Write adversarial tests that construct a parallel, fake object graph (fake mint, fake vault, fake Bank) and attempt to pass it through mint/withdraw paths, exactly as the Cashio attacker did.
  • Check for "root of trust" gaps anywhere a program accepts a struct that itself references other accounts — the vulnerability class detailed in our piece on how Anchor account discriminators can still leave the door open to type-confusion attacks applies here directly, since discriminator checks alone don't establish that an account is the registered instance.

This is standard scope in a smart contract audit, and it's one of the fixed items on our own Solana program security audit checklist for trading systems.

Remediation

The fix is structural, not cosmetic. Every collateral-bearing instruction needs to derive or verify the full account chain against program state before trusting any balance or mint field on it:

require_keys_eq!(bank.key(), Bank::pda(&crate_key).0, ErrorCode::InvalidBank);
require_keys_eq!(collateral_account.key(), bank.collateral_vault, ErrorCode::InvalidVault);

Beyond the immediate patch, protocols in this category should add: a mint-rate circuit breaker that halts issuance on anomalous volume, an independent reserve-vs-supply invariant checked either on-chain or by a monitoring bot, and a pause authority usable within minutes of an anomaly — Cashio's team could not stop the bleeding once the first fraudulent mint landed. Programs with this much value at stake also benefit from the kind of dependency and upgrade-path scrutiny we cover in our note on upgradeable Solana programs and BPF loader risk, since a pause mechanism is only as good as the upgrade authority controlling it.

If your protocol accepts caller-supplied collateral, vault, or pool accounts anywhere in its instruction set, get the account-validation graph reviewed before mainnet — that's the core of what a smart contract audit is for.

Need your contracts audited?

Manual review + tooling across TON, Solana and EVM — certificate and full report included.

Get an audit