October 20, 2026
|
Exploit Postmortem
Cover image for "Rain Card Program's $1.1M Loss and How Olympix Would Have Prevented It"

Rain Card Program's $1.1M Loss and How Olympix Would Have Prevented It

Date: August 28, 2026

Chain: Solana (proceeds bridged to Ethereum)

Loss: $1,118,597 in USDC and USDT, per VeriChains

Class: a two-signature approval that one signature could satisfy

At 13:40 UTC on August 28, 2026, a brand-new Solana wallet received about 1.79 SOL. It held no admin key and had no history with the program it was about to drain. It first touched the card programs around 16:49 UTC, and by 18:37 it had made itself an administrator over thousands of user collateral accounts behind crypto debit cards issued by Avici, Tria and other neobanks, and withdrawn $1,118,597 in USDC and USDT.

Every one of those cards ran on the same piece of infrastructure, an outdated version of a card-collateral program supplied by the payments company Rain. The program required two independent signed approvals before anyone could add a collateral admin, and it counted two. Both approvals were built from the same signature, because the program never checked that its second signature check was actually pointed at a second signature.

One Program Behind Many Cards

Rain connects stablecoin settlement to traditional card networks for neobank partners. On Solana, it supplies a shared program those partners deploy to hold card collateral, and VeriChains found Rain running the same vulnerable program 25 times, one deployment per partner, out of 66 Rain programs in total. When an Avici user tops up a Visa-integrated card, the funds move into an on-chain collateral account that backs their spending, and Avici said its self-custodial wallets on Solana and EVM networks weren't affected by what followed.

The program handles roughly forty actions, from opening an account to posting a card charge and generating monthly statements, and it has two ways of checking permission. Routine admin work uses a normal Solana transaction signature. Anything that touches user collateral uses a heavier path, where admin keys sign an approval off-chain, the approval is submitted on-chain with SubmitSignatures, and enough of the collateral account's admins have to agree before the program acts. The attacker went after that second path.

Blockaid found that the drained program at 26DkA9...eL4a ran an outdated contract version, and that three other deployments carried bytecode with an identical hash. Newer versions were already live across most of Rain's fleet, so the flaw survived only in the deployments that hadn't been moved to them.

How Solana Checks a Signature

Solana doesn't verify Ed25519 signatures inside each program. A transaction includes a separate instruction addressed to the Ed25519 precompile, which the runtime checks natively, and the transaction fails if any signature in it doesn't verify. According to Solana's precompile documentation, that instruction carries one small table per signature with seven fields: where to find the 64-byte signature, the 32-byte public key and the message, each given as a byte offset plus the index of the instruction holding those bytes. An index of u16::MAX means the precompile instruction itself, and any other value points at another instruction in the same transaction.

A program that relies on the result reads the precompile instruction back from the transaction and parses those tables to learn which keys signed which messages. That flexibility puts a duty on the program. Reading the public key at an entry's offsets tells you which key the entry names, while the offsets and indexes decide which bytes the runtime actually verified. Unless the program confirms that each entry's indexes point at its own data, a name and a check can belong to two different keys. It's the difference between a notary stamping a document and someone stapling that stamp to a different page.

Inside Rain's Signature Counter

Rain's program source isn't public, so this post doesn't quote it. VeriChains pulled the deployed program binaries from the chain and compared the vulnerable version with the patched one byte for byte. It identifies the routine as get_ed25519_signatures, which loops over the entries in the precompile instruction, reads the public key each entry names and adds it to the list of approvers without checking that the entry's public key index and message index point back at the entry itself.

The patched version adds exactly those two checks, and VeriChains reports that the broken binary contains neither while the fixed one contains both. Replaying the attacker's forging transaction against the old binary succeeds, and against the new one it's rejected. The failed transactions from the attack also leaked the program's error messages along with source file and line numbers, which gave analysts a view into the code path even without the source.

Forging the Second Approval

In forging transaction 2ofQrrK3S6BTva4EJdBfoUBQaaFenhScCnxXcTsgQoaappfDbQpvFGuKhrBVi6Aa5rVrjw3kHwYBwiDQAFuEoo1J, sent at 16:52:17 UTC, the first Ed25519 entry carried a real signature from the attacker's own throwaway key, which controlled nothing. The second entry carried a genuine admin's public key alongside a signature that's just the byte 0x09 repeated 64 times. The runtime would have rejected that garbage outright, but the second entry's indexes sent the check back to the first entry's real signature, so the runtime verified that signature, which passed.

Rain's program then read the admin key from the second entry and counted it as an approval. One signature from a key the attacker had just generated satisfied a two-of-two quorum, and the program accepted the attacker as authorized to act on user collateral. Two and a half minutes later, at 16:54:49 UTC, the first withdrawal pulled 1,779.97 USDC out of a user's collateral account.

Thousands of Accounts in About Two and a Half Hours

With a forged approval that worked, the attacker ran the same three instructions over and over. SubmitSignatures submitted the forged approval, AddCollateralAdmin made the attacker's key an admin on a collateral account, and WithdrawCollateralAsset pulled the stablecoins out. Blockaid reports that the first two successful drains landed three seconds apart and that the automated run lasted about 2 hours and 29 minutes, with 2,945 admin additions and 5,288 withdrawals among 8,233 core exploit transactions. VeriChains counted 21,405 transactions signed by the attacker that day, about 3,845 of them failed, and 5,288 withdrawals across 2,682 accounts, for $825,770 in USDC and $292,827 in USDT.

Four of the 25 vulnerable deployments were drained. The largest gave up about $628,000, all in USDC, across 3,279 withdrawals, and the next about $434,000 in a mix of USDC and USDT across 1,312, while a third lost about $56,000 and the fourth just $64. The attacker stopped on the largest target at 18:37 UTC, 41 minutes before Rain's patch reached it at 19:18, and on the other three the gaps between the attacker's last activity and the patch were 22, 24 and 78 minutes. Of the 21 untouched deployments, 13 had no user accounts, and one with more than 34,000 user accounts carried the same flaw but held only about $4,800.

Avici reported $500,859 drained from 1,685 users and Tria reported $431,945 from 636 customers, and Blockaid also named Solayer Pay as affected. Users' self-custodied wallets and private keys weren't touched, since the attack only reached the collateral accounts backing card balances, and Blockaid notes that no phishing or compromised keys were involved because victims never signed the draining transactions.

From Stablecoins to Tornado Cash

The attacker swapped the stablecoins for SOL and consolidated 10,000 SOL into one wallet, which then split into roughly 7,000 and 3,000 SOL. Through deBridge, the funds crossed to Ethereum and arrived as about 418 ETH. The Ethereum wallet gathered around 456 ETH in total and sent 455.9 ETH into the Tornado Cash router in 23 transactions, which Blockaid timed between 19:20 and 19:49 UTC, starting two minutes after Rain's patch landed on the largest program. Blockaid linked two Ethereum addresses to the initial financing of the attacker's Solana activity, but no one has been publicly identified and nothing has been recovered.

Rain said its monitoring found a vulnerability affecting a small number of programs on an outdated version of its Solana card contract, upgraded every program still running it, and reported no further unauthorized activity. VeriChains adds that Rain removed the rogue admin keys and forged approvals the attack left behind and moved accounts to a model where a single owner controls each vault. It also notes that all 66 of Rain's programs were controlled by one ordinary wallet holding about 0.1 SOL, with no multisig, which it flags as a separate governance risk.

The market reaction was immediate. KuCoin's summary of CoinDesk's report says the AVICI token fell as much as 49%, from a 24-hour high of $0.43 to a record low of $0.217, before recovering to about $0.378, and that Tria's token fell more than 10% at one point. Avici announced on August 29 that every affected user had been refunded in full with an extra 10% cashback on the withdrawn amount, and Spendnode reports that Rain funded those reimbursements. Avici also filed a report with the FBI's Internet Crime Complaint Center. Tria committed to refunding its 636 customers in full, and as of October 20, 2026, Spendnode's coverage, last updated October 17, still described Tria's refunds as pledged without a published confirmation that they had landed.

Why an Old Version Was Still Running

The flaw lived in an outdated version of a program that Rain had already moved past in most of its fleet. Blockaid found newer versions live elsewhere and the old bytecode in exactly four deployments, and VeriChains found it in 25. Each partner deployment is its own program, so a fix to the current version only protects the deployments that get upgraded to it, and the ones that stay behind keep whatever checks their version shipped with. None of the published analyses says whether Rain's program was audited, or which version an audit would have covered.

The bug itself sat where two separately correct pieces met. Solana's precompile verified exactly the bytes it was told to verify, and Rain's counter counted exactly the keys it read. Neither was wrong in isolation, and the gap was the missing rule tying the key the program read to the bytes the runtime checked.

How Olympix Tests What a Signature Check Actually Checked

Olympix reads a program the way an attacker does, as states, transitions and entry points, and it takes in the surrounding context to work out the rules the system depends on but never spells out. In Rain's program, the state worth guarding is who holds admin rights over each collateral account, and AddCollateralAdmin is the one instruction that changes it. The signal here is a quorum count that takes a signer's identity from verification data without confirming that the data describes itself, so the identity being counted and the identity being verified come from two places that nothing ties together.

The rule that count is supposed to enforce fits in one sentence: adding a collateral admin needs approvals from two distinct keys, and each of those keys must really have signed. A program's state space looks infinite, but it isn't. The engine maps every path into AddCollateralAdmin, keeps the feasible ones where the transaction carries a verification instruction the runtime accepts, and narrows those to the paths where the set of keys the program counts can differ from the set of keys the runtime verified, since those are the only ones that can break the rule. In the contracts Olympix analyzes, the engine attacks a flagged path like this from two sides. One method starts from the rule and executes paths into the admin-granting instruction until one adds an admin with fewer than two real signers. The other starts from the signal and runs known attack strategies against it, and aliased verification offsets are a known strategy against signature quorums.

Against a quorum check built this way, the counterexample would contain the same pieces as the August 28 transactions: a verification instruction with two entries, a real signature from a key that controls nothing in the first, a second entry that names an admin while its signature index points back at the first, a submit step that records the approval, and an admin-granting call that succeeds, followed by a withdrawal that the new admin can now make. The minimal condition is a single one: any entry whose indexes don't point at itself. For EVM contracts, Olympix returns that kind of sequence with a generated Forge test that replays it in the team's existing suite.

Olympix has flagged this signature-quorum class before in other codebases. Those findings came from Solidity, and Rain's program is Rust on Solana, so the precedent here is the class of bug, a counter that credits a verification to a key it never checked, rather than a claim that Olympix would have run on Rain's code. For EVM teams, the same check applies to any contract that counts approvals from signature data, and the engine runs automatically in development, at every merge and before release, so a team sees each approval path's standing across every version it still has deployed before the next one ships.

Count the Keys, Not the Entries

Rain's program did count two approvals, and the runtime did verify a real signature. The gap sat between the two, in the step that decided which key a verification belonged to, and it lived in an outdated version that several deployments were still running. Any multi-signature check that reads identity from one place and trust from another needs a rule tying them together, and every deployed version needs to be held to it. If your contracts rely on signature quorums or precompile results, book a technical walkthrough.

On-Chain References

Protocol: Rain card-collateral program (used by Avici, Tria and other neobanks)

Chain: Solana, with proceeds bridged to Ethereum

Date: August 28, 2026 (attacker funded 13:40 UTC; first program interaction about 16:49 UTC; drain stopped 18:37 UTC)

Loss: $1,118,597 ($825,770 USDC and $292,827 USDT, per VeriChains)

Attacker wallet (Solana): FVNFzq...nCEj

Attacker wallet (Ethereum): 0x2cE2...38D5

Ethereum funding addresses linked by Blockaid: 0xa1a1...f25c and 0x7750...f559

Tornado Cash router used for deposits: 0xd90e...F31b

Exploit tx, forged approval (Submit_signatures, 16:52:17 UTC): 2ofQrr...oo1J

Exploit tx, first USDC withdrawal (Withdraw_collateral_asset, 1,779.97 USDC, 16:54:49 UTC): 2oE6hQ...3M5L

10,000 SOL consolidation tx: 27hZes...Cije

Drained program 1: CWgkFB...1gkz

Drained program 2: 26DkA9...eL4a

Drained program 3: 3zVB27...zDuc

Drained program 4: 8r2jms...CBNN

Sources

VeriChains: Rain Hack Analysis, Ed25519 Signature Forgery Drains $1.1M from Visa Card Vaults

Blockaid: $1.1M Rain Ecosystem Exploit, How Onchain Monitoring Gives Stablecoin Card Issuers Fleet-Level Coverage

crypto.news: Rain contract exploit drains $1.1M from card users

Bleap: Avici Attack, How a $1M+ Exploit Hit the Solana Crypto Neobank

Spendnode: Avici Completes Refunds With 10% Cashback After Solana Card Exploit

Spendnode: Tria Details Rain Card Vulnerability, 636 Users Refund

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.