DEV Community

PandaSecurity
PandaSecurity

Posted on Originally published at panda-security.xyz

Troves Exploit: A Retired Rebalancer Drained 8.57 WBTC

Originally published at Troves Exploit: A Retired Rebalancer Drained 8.57 WBTC

On 31 October 2026, an attacker removed about 8.57 WBTC-equivalent from the Troves xWBTC/WBTC vault on Starknet. Community estimates put the loss at roughly $660K. The incident landed in the same week as the Binance hot wallet theft and got far less attention, but for anyone who builds or audits upgradeable DeFi systems it is arguably the more instructive of the two. Troves says the attack went through a legacy rebalancing contract that kept its permissions after it had been replaced.

No novel cryptography, no compromised key, no oracle failure. Just an old contract that was supposed to be retired and was still trusted.

This post walks through what the vault does, what the on-chain transaction shows, why stale permissions are such a common failure, and what teams should change in their deployment and decommissioning process.

TL;DR

  • Protocol: Troves (formerly STRKFarm), a vault-based yield platform on Starknet.
  • Affected vault: the Ekubo xWBTC/WBTC vault (on-chain name tEkubo xWBTC/WBTC). Troves says no other vault was affected.
  • Chain: Starknet mainnet.
  • Loss: about 8.57 WBTC-equivalent per Troves' on-chain message; Troves' first public update said about 8.6 BTC.
  • Root cause (per Troves): a legacy rebalancing contract retained permissions over the vault after being replaced.
  • Response: Troves revoked the contract permissions, paused affected and potentially exposed vaults, and offered a 1 WBTC whitehat bounty open until 3 November 2026, 23:59 UTC.

What Troves Is

Troves is a Starknet yield platform that packages multi-step DeFi strategies into vaults. Users deposit once, and the vault handles lending, liquidity provision and rebalancing for them. According to Troves' own documentation, the vault owners are access control contracts with separate roles for operations such as rebalancing, deposits and withdrawals, and the super admin of that access control is a timelock.

The affected product was a concentrated liquidity vault that provides liquidity to the Ekubo xWBTC/WBTC pool. xWBTC is Endur's liquid staked BTC token, and WBTC is wrapped bitcoin. Because the two assets should trade close to a fixed ratio, the vault concentrates liquidity in a narrow range and needs periodic rebalancing to stay in range. That rebalancing is the job of a privileged contract.

Rebalancing logic tends to change over time. Teams ship a new strategy, a new router or a new rebalancer, point the vault at it, and move on. The question that matters for this incident is what happens to the old one.

What Happened On-Chain

Troves' on-chain message to the attacker names the exploit transaction:

0x0634307777205294f636d481808a7b9a9e208415a26dd19c8ada269b9332e330

We pulled the receipt from a public Starknet RPC. The transaction was included in block 17,220,375 with a block timestamp of 31 October 2026, 08:25:21 UTC, and it executed successfully. Its sender is the account 0x42dc2ebccae3e55ce3747d2974c801fb3e0facf8d7f4810012a0986a89da5e3, which made a single call into a helper contract at 0x13705ea5f9e591ff6ffe3926bf22fceb19317c049838de09750a60259d38c72.

Reading the token transfer events in order gives a clear outline of the attack, even before Troves publishes a full post-mortem:

  1. Flash loan. The helper contract receives exactly 10 WBTC from 0x3976cac2...d24124 at the start of the transaction, and sends exactly 10 WBTC back to the same address near the end. That is the shape of a flash loan.
  2. Swaps through Ekubo and a routing contract. The borrowed WBTC is pushed through the Ekubo core contract via a router that skims a 0.1% fee on each leg, and an Ekubo position NFT is minted to the helper.
  3. Vault funds move. The vault contract (tEkubo xWBTC/WBTC, 0x2ea99b49...4380a) receives about 9.43 WBTC out of Ekubo, which looks like its liquidity being pulled from the position, and then sends about 9.38 WBTC to a separate contract at 0x2e0af295...184067, which forwards almost all of it straight back into Ekubo. Shortly after, a similar amount of WBTC leaves Ekubo towards the attacker's route. Depositors' liquidity is being moved and traded inside a transaction the attacker controls.
  4. Exit. The helper ends up with more WBTC than it borrowed, repays the 10 WBTC loan, and transfers about 6.32 WBTC to the attacker's account.

We are deliberately careful here. Troves has not yet published which function the legacy contract exposed, or confirmed which address in the trace is the legacy rebalancer, so we do not assert it. The difference between the 6.32 WBTC that reached the sender in this one transaction and the 8.57 WBTC-equivalent Troves reports may come from other assets, other transactions or price conversion, and should be read against Troves' post-mortem when it is released.

Troves' founder said on X that the exploit was contained within about 40 minutes. Troves' first public statement followed at 14:45 UTC the same day.

Root Cause: Permissions That Outlived Their Contract

The one-line root cause Troves gave is the most important part of the story: the legacy rebalancing contract retained permissions after being replaced.

In a role-based setup, a vault trusts certain addresses to call sensitive functions such as "move liquidity", "swap", "withdraw from position" or "deposit into position". When a team upgrades its rebalancing logic by deploying a new contract and granting it the role, the old contract keeps that role unless someone explicitly revokes it. Nothing breaks. Monitoring sees a healthy vault. The new rebalancer works. And the old one sits there, still trusted, running code that nobody is reviewing any more.

The danger depends on what the old contract exposes. If it has any function that:

  • can be called by anyone, or by any address that passes a weak check,
  • triggers privileged vault actions, and
  • lets the caller influence prices, amounts or destinations,

then whoever finds it can make the vault act against its own depositors.

Here is a simplified, illustrative Solidity sketch of the pattern. It is not Troves' code, which is written in Cairo, and it is not the exploited function:

// Simplified illustration only. Not the Troves contract.
contract LegacyRebalancer {
    IVault public immutable vault;

    // Originally assumed to be called only by a keeper bot,
    // but never restricted on-chain.
    function rebalance(uint256 amount, bytes calldata swapData) external {
        // The vault trusts this contract via REBALANCER_ROLE.
        vault.withdrawLiquidity(amount);
        _swap(swapData);              // caller-controlled route and limits
        vault.depositLiquidity();
    }
}

// Upgrade day:
// accessControl.grantRole(REBALANCER_ROLE, address(newRebalancer));
// Missing:
// accessControl.revokeRole(REBALANCER_ROLE, address(legacyRebalancer));
Enter fullscreen mode Exit fullscreen mode

If rebalance lets the caller pick the swap route or the slippage limit, a flash loan is enough to move the pool price, have the vault rebalance at that bad price, and capture the difference. We saw a close relative of this pattern in the KolanHook Uniswap v4 exploit, where a permissionless trigger forced a buyback with no meaningful price limit.

Why This Keeps Happening

Stale permissions are one of the most common findings we see in real audits, and they are one of the hardest to catch with a code review alone, because the bug is not in the code that is in scope. It is in the deployment state.

A few recurring reasons:

  • Audits are scoped to new code. The new rebalancer is reviewed. The old one was reviewed months ago under different assumptions, and nobody re-checks what it can still do.
  • Roles are additive. Granting a role is part of the upgrade script. Revoking the old one is a separate step that is easy to forget, especially if revocation must go through a timelock.
  • "Legacy" is not a state on-chain. A contract that a team considers retired is, from the chain's point of view, identical to an active one until its rights are removed.
  • Off-chain assumptions decay. A function that was "only ever called by our keeper" was safe while the keeper was the only caller that knew about it. That is not access control.

We covered a near-identical failure on Avalanche, where a Safe kept a stale approval to an address that later had code deployed to it. The asset and chain were different, the lesson was the same: trust that is never revoked eventually gets used by someone else.

Lessons for Builders and Auditors

Treat decommissioning as a deployment

Every upgrade script that grants a role to a new contract should revoke it from the old contract in the same governance action, and tests should assert that the old address no longer holds the role. If the revocation needs a timelock, queue it together with the grant.

Keep an on-chain inventory of privileged addresses

Maintain a list of every address holding each role, generated from chain state rather than from documentation. Review it on every release. Role enumeration is cheap; an unexplained entry is a finding.

Audit the permission graph, not only the contracts

When you scope an audit, include every contract that currently holds a privileged role on the in-scope system, even if it is "old". Our smart contract security checklist includes access control and role management items that apply directly here.

Never let a caller choose price limits on vault funds

Any path that swaps or rebalances vault liquidity should enforce slippage bounds derived from a trusted price source, not from caller input or the current spot price alone. That single control turns a flash loan attack from profitable into pointless.

Make retired contracts inert

Where possible, give contracts an explicit kill switch that a guardian can trip on retirement, so that even if a role is forgotten the code path reverts.

Monitor for unusual privileged calls

Alert on any call into a privileged vault function from a contract other than the current, expected one. In this incident the trigger was a contract the team had already moved away from, which is exactly the kind of caller an allowlist-based alert would flag.

Key Addresses and Transactions

Item Value
Exploit tx (Starknet block 17,220,375) 0x0634307777205294f636d481808a7b9a9e208415a26dd19c8ada269b9332e330
Attacker account (tx sender) 0x42dc2ebccae3e55ce3747d2974c801fb3e0facf8d7f4810012a0986a89da5e3
Attacker helper contract 0x13705ea5f9e591ff6ffe3926bf22fceb19317c049838de09750a60259d38c72
Troves xWBTC/WBTC vault (tEkubo xWBTC/WBTC) 0x2ea99b4971d3c277fa4a9b4beb7d4d7d169e683393a29eef263d5d57b4380a
WBTC on Starknet 0x3fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac
Troves recovery address (Ethereum) 0x91926046e148c66869d71B9Fa37306e82DDc8934

Sources

Closing Thoughts

The Troves incident is small next to this week's headline losses, but its root cause is one almost every protocol carries in some form: a contract the team stopped thinking about that the chain still trusts. Upgrades are not finished when the new code works; they are finished when the old code can no longer do anything. If you are planning an upgrade, a migration or a vault launch, talk to our team or check our audit pricing to have the full permission graph reviewed, not just the new files.

Top comments (0)