Starknet
Access control

Troves' $662K Loss and How Olympix Would Have Prevented It

Loss
Loss
Exploit date
October 31, 2026

Troves had already moved on from the contract that drained its xWBTC/WBTC vault. A newer rebalancer was running the vault's keeper duties, and the old one had been set aside. Nobody took its keys away, though. At 08:25:21 UTC on October 31, 2026, an attacker borrowed zero STRK from Vesu, named that retired rebalancer as the receiver of the loan, and let its flash-loan callback hand the attacker's own parameters to the vault's rebalance function. One transaction later the vault had lost 6.34 WBTC and 2.17 xWBTC, about 8.57 WBTC-equivalent, worth $662,031 at the time.

Two things had to be true for that to work. The legacy rebalancer's on_flash_loan callback ran its rebalancing logic without checking who had started the flash loan, and the vault still trusted that contract with its RELAYER role. Either fix on its own would have stopped this transaction. Troves' Cairo source for the legacy rebalancer isn't public and the team hasn't published a post-mortem yet, so everything below comes from the transaction's call trace, the deployed contracts' ABIs and Troves' own statements.

A Vault That Lives in a Narrow Band

Troves, formerly STRKFarm, is a yield platform on Starknet that wraps DeFi strategies into vaults. The affected vault, named "tEkubo xWBTC/WBTC" on-chain, provides liquidity to an Ekubo pool pairing WBTC with xWBTC, Endur's liquid staked Bitcoin. Because xWBTC accrues staking yield, it trades a little above WBTC, and at the block before the attack Endur's own contract valued one xWBTC at 1.0318 WBTC. The two move together, so the vault concentrates its liquidity in a tight price range where it earns the most fees.

A tight range needs upkeep. When the price drifts, someone has to pull the liquidity, swap to the right mix and redeposit it around the new price. The deployed vault exposes that as rebalance(new_bounds, swap_params), and only an address holding the vault's RELAYER role can call it. The caller decides where the new bounds go and which swap the vault performs on the way. That's a lot of trust to place in one role, and it's meant for a keeper the team controls.

The Keeper That Never Left

At some point Troves replaced its rebalancing contract with a newer version. According to Troves' statement on X, the exploit "involved a legacy rebalancing contract that retained permissions after being replaced." The trace shows exactly what those permissions meant in practice. The old contract still held RELAYER, so any call it made to vault.rebalance went through.

On its own, a stale role is a dormant risk. What turned it into an open door was the legacy rebalancer's flash-loan callback. Lending protocols like Vesu send the borrowed funds to a receiver and then call that receiver's on_flash_loan function, and the receiver is expected to check that it asked for the loan itself. The legacy contract didn't. Anyone could request a flash loan from Vesu, name the legacy rebalancer as the receiver and supply the callback data, and the rebalancer would act on that data with the vault's RELAYER authority behind it. The vault only asked one question, whether its caller held the role, and the answer was still yes.

Inside the 08:25 Transaction

The attacker's account, 0x42dc2e...a5e3, sent a single call to a helper contract it controlled, and the helper did the rest in Starknet block 17,220,375. It opened two flash loans from lending pools, 10 WBTC from one and 5 xWBTC from another. It put the 5 xWBTC into a fresh Ekubo liquidity position of its own, then swapped 5 WBTC for about 2.99 xWBTC through AVNU, which routed the trade into the same Ekubo pool and pushed its price away from where the vault's liquidity sat.

With the pool skewed, the helper asked Vesu's Singleton V2 contract for a flash loan of 0 STRK and named the legacy rebalancer as the receiver. Vesu called the rebalancer's on_flash_loan, and the rebalancer called vault.rebalance. The trace shows the vault checking its caller's role with its access control contract and then carrying out the instructions it had been given. It pulled about 9.42 WBTC of liquidity out of its Ekubo position, swapped about 0.05 WBTC into xWBTC through AVNU and redeposited roughly 9.37 WBTC and 0.06 xWBTC inside the new bounds the attacker had chosen.

That redeposit put most of the vault's Bitcoin exactly where the attacker could take it. The helper sold 1 xWBTC back through AVNU and received about 11.26 WBTC, draining the liquidity the vault had just placed. It then pulled its own Ekubo position back out, repaid the 10 WBTC and 5 xWBTC flash loans and sent the profit to the attacker's account: 6.32387631 WBTC and 1.92309017 xWBTC. The whole sequence ran inside one transaction, so no one had a chance to react between steps.

Counting What the Vault Lost

The vault's own view functions make the loss easy to reconstruct. At the end of block 17,220,374, converting its full share supply to assets gave 6.33994533 WBTC and 2.99167978 xWBTC in its Ekubo position. After the attack, the same call returned zero WBTC and 0.82631648 xWBTC. Adding a small change in the vault's idle balances, the vault lost 6.34028478 WBTC and 2.16569203 xWBTC. At Endur's on-chain rate that's 8.5749 WBTC-equivalent, which matches the "approximately 8.57 WBTC-equivalent" Troves cited in its on-chain message to the attacker. At WBTC's price of about $77,205 at 08:25 UTC, the loss comes to $662,031.

Other figures have circulated. Troves' first public statement gave a preliminary loss of approximately 8.6 BTC. DefiLlama lists $666,935, and an early alert put the loss at about $857K, which is too high for what left the vault. An analysis by PandaSecurity traced about 6.32 WBTC reaching the attacker's account and couldn't account for the rest. The missing piece is the 1.92 xWBTC that left the helper contract in the same transaction. Together with the WBTC, the attacker kept about 8.31 WBTC-equivalent, worth roughly $641K. The rest of the vault's loss stayed with the swap routes and fee collectors the attack passed through.

Forty Minutes and a Bounty Offer

Troves moved quickly once it saw the drain. In an update on X, the team said it had revoked the vulnerable legacy contract and every rebalancer permission on its LST vaults within about 40 minutes of the exploit, and paused three vaults as a precaution: xWBTC/WBTC, xSTRK/STRK and xstrkBTC/strkBTC. Its investigation found the exploit limited to the xWBTC/WBTC vault, with no impact identified on other Troves vaults.

At 14:49 UTC the same day, Troves sent an on-chain message on Ethereum from its designated recovery wallet, offering the attacker a 1 WBTC whitehat bounty for returning the remaining funds by November 3, 2026 at 23:59 UTC. As of November 13, 2026, Troves hadn't announced a return of funds or published the detailed post-mortem it promised, along with information on options for affected depositors.

How Olympix Would Have Caught the Orphaned Callback

Olympix reads a protocol the way an attacker would, as states, transitions and entry points across every contract that can touch the money. For a vault like this one, the invariant that matters is simple to state: the vault's liquidity should only move to bounds and swaps the protocol's own keeper chose. Every path that reaches vault.rebalance is a path that can break that invariant, so the engine maps those paths and narrows them to the ones an outside account can actually drive.

The legacy rebalancer's on_flash_loan sits right on one of those paths. It's an external entry point, a lending protocol will call it on behalf of anyone who requests a loan, and the data it acts on comes from the borrower. Had the team run BugPoCer on the vault and both rebalancers together, it would have started from the vault's invariant and searched outward for a sequence of calls that breaks it. A sequence exists: deploy a contract, take a flash loan to skew the Ekubo pool, request a zero-amount Vesu flash loan with the legacy rebalancer as receiver, pass rebalance parameters that park the vault's liquidity at the skewed price, and swap back. BugPoCer would have returned that sequence as a counterexample, with the minimal conditions and a generated snforge test reproducing it. That test would have been the October 31 drain, produced in development, with no depositor funds at risk.

BugPoCer supports Cairo and Starknet, so a codebase like Troves' is in scope. Because the engine runs at every merge and before every release, the replacement of a rebalancer is exactly the kind of change it would have been looking at. The counterexample would have shown the old contract's role as a live path into the vault for as long as it existed, making the fix obvious: add an initiator check to on_flash_loan, revoke RELAYER from the old contract, or both.

Retiring a Keeper Is Part of the Upgrade

Role-based vaults concentrate a lot of power in a few addresses, and it's easy to think of those addresses as people or bots the team controls. They're contracts, and every external function on them is part of the vault's attack surface for as long as the role stays granted. Replacing a keeper means revoking the old one in the same change, and any callback a keeper exposes needs to confirm that the call it's answering is one it started.

It's the same shape of mistake that cost INK Finance $140K in May: a check confirmed the caller was on an approved list and never asked whether the request itself made sense. Audits remain essential for catching this kind of flaw in the code they review, and continuous analysis covers the system state between reviews, including which contracts still hold which roles after an upgrade. If your protocol hands privileged roles to keeper contracts, book a technical walkthrough to see the counterexamples Olympix would produce for your vaults.

On-Chain References

Exploit tx (Starknet): 0x063430...e330

Attacker account: 0x042dc2...a5e3

Attacker helper contract: 0x013705...8c72

Legacy rebalancer (flash-loan receiver): 0x02f218...8f80

Victim vault (tEkubo xWBTC/WBTC): 0x002ea9...380a

Vesu Singleton V2 (flash-loan lender): 0x000d8d...0160

Troves on-chain message to attacker (Ethereum): 0x6145fc...59e9

Troves recovery wallet (Ethereum): 0x919260...8934

See what Olympix finds on your own code

Send us a repo that's already been audited, and we'll run Olympix at the same commit and show you what comes back.