October 20, 2026
|
Exploit Postmortem
Cover image for "Reddio's $23K Loss and How Olympix Would Have Prevented It"

Reddio's $23K Loss and How Olympix Would Have Prevented It

Date: September 5, 2026

Chain: Ethereum

Loss: 9.25 ETH, about $23K at the time of the exploit

Class: permissionless asset registration that let one balance back two share classes

At 16:03:11 UTC on September 5, 2026, a wallet with no capital of its own borrowed 1,139.6 WETH from Balancer, pushed more than a thousand ETH through Reddio's RedSonic Vault, paid every wei of the loan back, and walked away with 9.2525 ETH. The whole thing happened inside the constructor of a contract that was gone by the time the block was sealed.

The haul was small, about $23,000, but the route to it is worth studying. RedSonic let anyone register a new token as a vault asset. The attacker registered stETH, a token the vault already held for its ETH depositors, and from that moment the same stETH balance counted toward two different classes of vault shares. Depositing into one class raised the price of the other, so the attacker could cash out both.

Two Share Classes Reading One Balance

RedSonic Vault lives inside an EIP-2535 Diamond proxy at 0x4315...126f, a single contract address that routes each function call to a separate implementation contract called a facet. Etherscan labels the address Reddio: Vault Facet. The Diamond's own source is verified on Etherscan, and it lists facets with names like InvestmentManagerFacet and VTokenGeneratorFacet. SlowMist identified the vulnerable vault logic as the facet at 0x92ec...4704, whose source isn't verified, so the exact Solidity can't be quoted here.

Depositors put ETH into the vault and receive rsvETH, a share token whose price rises as the vault's assets grow. Part of that ETH sits with Lido as stETH, Lido's liquid staking token. According to ExVul's alert, the rsvETH price comes from a function called getTotalAssetBalance(ETH), and for the Lido position that function returns the vault's raw stETH balance, the result of asking Lido how much stETH the vault address holds.

The vault also supports other ERC-20 assets, each with its own share token. New assets are added through registerErc20(), and ExVul reports that this function had no access restriction at all. Anyone could call it.

A share price read from the vault's real holdings is standard, and multi-asset vaults need a way to list new assets, but these two pieces stopped being safe the moment they met. Registering stETH as a separate asset created rsvstETH, a second share class backed by stETH, and stETH deposited for rsvstETH landed in the very balance getTotalAssetBalance(ETH) reads to price rsvETH. One deposit of stETH now showed up twice: once as the backing for the new rsvstETH shares, and once as extra value behind every rsvETH share.

The rule a builder should take from this is simple. When two claims draw on the same pool of assets, the accounting has to assign each unit to exactly one of them. A coat check that hands out two tickets for the same coat works only until both ticket holders show up.

One Transaction, Three Contracts, Zero Capital

The attacker's wallet, 0x70f2...982C, sent a contract-creation transaction in block 25,912,201. The Etherscan transaction page shows three contracts created along the way, and the token transfers in that transaction lay out the attack step by step.

The helper contract at 0x39a2...09d5 received 1,139.62 WETH from the Balancer Vault as a flash loan and unwrapped it. It deposited 1,130.26 ETH into RedSonic and received 1,118.00 rsvETH. ExVul estimates that position at about 99% of all rsvETH, which meant almost any increase in the rsvETH price would flow to the attacker.

Next, the helper sent 9.36 ETH to Lido and received 9.36 stETH. ExVul's breakdown places the registerErc20(stETH) call in this flow, and the transaction confirms its effect: the helper deposited 9.34 stETH into the vault and was minted 9.34 rsvstETH, a share token for stETH that existed only because the attacker had just created it.

Those 9.34 stETH were also counted in the balance that priced rsvETH, even though no new rsvETH had been minted. The helper then burned its 1,118.00 rsvETH and received 1,139.51 ETH from the vault, about 9.25 ETH more than it had put in. It then burned its 9.34 rsvstETH and got its 9.34 stETH straight back. The same stETH had paid out twice.

To close out, the helper swapped its 9.36 stETH for 9.35 ETH in Lido's Curve pool, rewrapped and returned all 1,139.62 WETH to Balancer, and forwarded 9.2525 ETH to the contract the attacker had deployed, which passed it on to the attacker's wallet. The transaction cost about 0.0007 ETH in gas.

According to SlowMist's alert, the vault's 2% withdrawal cap didn't stop the manipulation. The bytecode of the unverified vault implementation contains revert messages such as "ethAmount exceeds 2% of vTokenAmount", which suggests a per-withdrawal limit, but the attacker owned nearly all of rsvETH, so a cap measured against the attacker's own share balance had little to hold back.

Where the 9.25 ETH Went

The extra 9.25 ETH came out of the vault's own assets, the ETH and stETH that back every other rsvETH holder's shares. Nothing about the attack required an exotic trick. It took a public function, a token the vault already used, and enough borrowed capital to own the share class whose price it was about to move.

ExVul flagged the exploit within the hour, and SlowMist followed a few hours later with a root-cause alert that tagged Reddio. Nexus Mutual noted on September 10 that Reddio hadn't made a statement and that the funds were untouched in the attacker's wallet. As of October 20, 2026, Blockscout shows that wallet still holding about 9.26 ETH, and the most recent post on Reddio's X account is from November 2025, so the team hadn't addressed the incident there.

How Olympix Catches the Second Ticket

Olympix reads a vault as an attacker does: as states, transitions and entry points. It also takes in the context around the code, such as how assets get listed and how share prices are computed, to infer the invariants the system depends on. In RedSonic, the state that matters is the asset balance each share class can redeem against, and every entry point that changes what that balance means is a path worth mapping, including the ones that add new share classes.

The signal Olympix's static analysis surfaces here is a pairing of two facts: a public registration function that any caller can use to create a new share class, and a share-price function that reads a raw token balance that the new class's deposits can also land in. Either one alone is common. The combination marks the path from registerErc20() to a redemption as worth attacking, and it narrows thousands of possible call sequences down to the few that register an asset, deposit it, and redeem the shares whose price it touched.

The invariant for this vault fits in one sentence: every unit of assets the vault holds should back the redeemable value of exactly one share class, so the total that all classes can redeem never exceeds what the vault holds. BugPoCer infers that rule from how deposits mint shares and how redemptions pay out, then runs Counterexample-Guided Invariant Refutation, executing paths from the vault outward until one breaks it.

The counterexample it returns is the September 5 attack in miniature: deposit ETH for rsvETH, register stETH through registerErc20(), deposit stETH for rsvstETH, redeem rsvETH, redeem rsvstETH, and finish with more ETH and stETH out than went in. The minimal conditions are that the caller holds most of the rsvETH supply and that the newly registered asset is the same token the ETH position is priced from. BugPoCer packages that sequence with a generated Forge test that replays it, so the team sees the double redemption at the merge that made registerErc20() public, long before the vault holds a single depositor's ETH. Because Olympix runs in development, at every merge and before release, the same invariant would be re-checked each time a new asset or facet is added to the Diamond.

One Balance, One Owner

RedSonic's loss was small because the attacker could only inflate the share price by the amount of stETH it was willing to cycle, but the flaw itself had no ceiling built into the design. Any multi-asset vault that prices shares from live balances has to be sure that no two share classes can ever read the same tokens, and that the function adding new assets can't be used to make that happen. To see how Olympix maps the paths between your vault's listing functions and its share prices, book a technical walkthrough.

On-Chain References

Protocol: Reddio RedSonic Vault

Chain: Ethereum

Date: September 5, 2026, 16:03:11 UTC

Loss: 9.2525 ETH (about $23K at the time)

Attacker EOA: 0x70f2...982C

Deployed exploit contract: 0xaeFA...35b3

Helper contract: 0x39a2...09d5

RedSonic Vault (Diamond proxy): 0x4315...126f

Vault implementation facet (source unverified): 0x92ec...4704

Exploit tx: 0xe3cb...791a

Block: 25912201

Sources

ExVul: Alert on the RedSonic Vault exploit

SlowMist: TI Alert on the Reddio loss

Nexus Mutual: Incident note on Reddio RedSonic

Blockonomi: RedSonic Vault Exploit Drains 9.25 ETH in Ethereum Flash Loan Attack

More from Olympix:

No items found.

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