Ethereum
Signature and replay

Giddy's $1.3M Loss and How Olympix Would Have Prevented It

Loss
Loss
Exploit date
April 23, 2026

Date: April 23, 2026

Chain: Ethereum

Loss: Yield Basis gauge tokens from three Giddy BTC vaults, about $1.3M

Class: EIP-712 signature that covers the swap calldata but not the token, amount or contract it runs against

Giddy built its Bitcoin vaults so that nothing moved without a signature from its own backend. Every deposit, withdrawal and compound on a GiddyVaultV3 vault carried a signed authorization, and the contract refused to act on any request that Giddy's authorized signer hadn't approved. On April 23, 2026, at 11:57:47 UTC, a single transaction got three of those vaults to approve an attacker's contract to spend their Yield Basis positions, and about $1.3 million in BTC liquidity left with it.

The signature check passed every time, because the attacker never touched the part that was signed. Giddy's EIP-712 hash covered the raw calldata of each swap but left out the fields that decided which token got approved, how much and to which contract, so a valid authorization could be pointed at the vault's own holdings and a contract the attacker wrote. As of October 23, 2026, Giddy hadn't posted about the incident on its X account.

Bitcoin Yield Through a Signed Backend

Giddy is an app that offers earning vaults, and in November 2025 it promoted Bitcoin vaults that put deposits to work in Yield Basis, a protocol that pays yield on BTC liquidity. Each GiddyVaultV3 vault sits behind a beacon proxy and pairs with a strategy contract, here StrategyYieldBasis, which holds the vault's Yield Basis position as gauge tokens: g(yb-tBTC), g(yb-cbBTC) and g(yb-WBTC), one for each wrapped bitcoin the vaults supported. The gauge tokens are what the vault's users actually owned.

Moving between a user's deposit token and those positions takes swaps, and Giddy doesn't hard-code a DEX. Instead, each request carries a list of SwapInfo entries, one per swap, naming the token to sell, the token to buy, the amount, the aggregator contract to call and the calldata to send it. Giddy's backend builds those entries, signs the request, and the vault verifies that signature before it executes anything. A compound call works the same way: the vault hands the signed swap list to its strategy, which claims rewards, runs the swaps and deposits the proceeds back into Yield Basis.

That design puts all of the vault's trust in one place. As long as the signature binds every detail of every swap, the backend decides exactly what the vault does, and a user, or anyone else, can only submit what the backend approved.

A Hash Over the Calldata and Nothing Else

From the verified GiddyVaultV3 source on Blockscout, this is the part of _validateAuthorization that builds the signed message:

      bytes memory dataArray;
      for (uint256 i = 0; i < auth.vaultSwaps.length; ++i) {
          dataArray = abi.encodePacked(dataArray, keccak256(auth.vaultSwaps[i].data));
      }
      for (uint256 i = 0; i < auth.compoundSwaps.length; ++i) {
          dataArray = abi.encodePacked(dataArray, keccak256(auth.compoundSwaps[i].data));
      }

      bytes memory data = abi.encodePacked(
          VAULTAUTH_TYPEHASH,
          abi.encode(
              auth.nonce,
              auth.deadline,
              auth.amount,
              keccak256(dataArray)
          )
      );

The two loops walk through every swap in the request and hash one field from each, the data field holding the calldata for the aggregator. The second block wraps those hashes together with the nonce, the deadline and the top-level amount, and that's the whole message the backend signs. The type hash says the same thing in its own words, VaultAuth(bytes32 nonce,uint256 deadline,uint256 amount,bytes[] data). Each SwapInfo also carries fromToken, toToken, amount and aggregator, and none of those four fields appears anywhere in the hash, so changing them leaves the signature exactly as valid as it was.

Everything around that block is careful. The same function rejects an authorization past its deadline, rejects a nonce that's already been used, recovers the signer from the digest, asks Giddy's strategy factory whether that signer is authorized, and only then marks the nonce as spent. The checks are all correct, and they all protect a message that describes less than the request does.

Those unsigned fields are the ones that decide what happens to the vault's money. From the verified GiddyLibraryV3 source, executeSwap starts by approving the swap's aggregator for the swap's token:

    if (!isFromTokenNative) {
      SafeERC20.forceApprove(IERC20(swap.fromToken), swap.aggregator, swap.amount);
    }

Every argument on that line comes from the part of the request nobody signed. The function then calls the aggregator with the signed calldata and checks two things afterward: that the source balance went down by something greater than zero and no more than the amount, and that the destination balance of the buy token went up. BlockSec's analysis points out that an attacker who picks both tokens and the aggregator can satisfy those checks with a contract of their own, and Verichains reaches the same conclusion, calling the root cause incomplete signature coverage.

The withdraw path in the same vault shows that the code did care about token identity in one place. It reverts if a swap's fromToken doesn't match the vault's base token. The strategy's swapRewardTokens function, which runs the compound swaps, has no equivalent check, and it executes each swap from the strategy's own balance, where the gauge tokens live.

The lesson for builders is about what a signature actually commits to. A backend signature is only as strong as the fields it covers, and every input that can change who gets paid, which token moves or which contract runs needs to be inside the hash. Signing the instruction while leaving its target unsigned is like a bank verifying the signature on a check without looking at the payee line.

Three Vaults in One Transaction

The attacker's wallet, 0x81Fe3D7d35dFeFa15b9E6800B6aeFC3358E7b156, sent a contract-creation transaction in block 24942492. The new contract, 0x50a5312bF627b6be07e60015ED3d418e992D76eB, deployed a second contract at 0x7326a1Ab0d696Ae317958D136D6e4c693Ea34528, which BlockSec and Verichains identify as the malicious aggregator and which also minted its own throwaway token. According to BlockSec, the attacker reused a backend-authorized signature whose signed calldata suited the plan, kept the signature, nonce and data fields unchanged, and rewrote the unsigned ones, setting fromToken to the strategy's gauge token and aggregator to the attacker's contract. Verichains notes that replay wasn't the problem, since a successful authorization burns its nonce, and that the signature simply never covered the fields that mattered.

The transaction trace follows the compound path through each vault. The vault's beacon proxy delegates to the GiddyVaultV3 implementation at 0x5F0Ad32C00641D1d2Bb628fF341e0D4bb4494318, which calls its StrategyYieldBasis strategy to run the swaps, and the strategy approves the attacker's aggregator to spend its gauge tokens. The logs show the aggregator taking a sliver of the gauge token and minting some of its own throwaway token back to the strategy, which satisfied both balance checks in executeSwap, and BlockSec notes that this is how the attacker got past the check that tokens had been received without performing a real swap.

The approval granted for the swap's full amount remained in place once the swap returned. BlockSec describes the attacker then using it to move the strategy's gauge tokens outside the protected execution path, and the token transfers show the result for each vault. The tBTC strategy at 0xC99FC715E73294FD03B7C09d9a438A98F6C76ec3 sent 3.53 g(yb-tBTC) to the attacker's wallet, the cbBTC strategy at 0x0d5e628A44E7Ec94a2054A6c454127cfe5FcB690 sent 6.94 g(yb-cbBTC), and the WBTC strategy at 0x870fcD63DB2c68D8079166E311b1118B8aA26eD7 sent 6.27 g(yb-WBTC). Each vault then finished its compound call and emitted its usual Yield event, as if nothing had happened.

BlockSec stresses what the attacker didn't need: no cracked signature, no flash loan and no price manipulation. BlockSec and Verichains both put the loss at about $1.3 million.

Unwinding the Positions

The gauge tokens landed in the attacker's own wallet, and the cleanup started within minutes. Blockscout shows the wallet claiming rewards from a Yield Basis gauge at 12:02:11 UTC, then calling withdraw on two Yield Basis LT contracts and approving and swapping tBTC, cbBTC and WBTC through a DEX aggregator router. At 12:46:11 UTC the wallet sent ETH through Mayan's forwarder contract, a cross-chain swap service, and it used the same forwarder again early on April 24, alongside a series of transfers to another address.

BlockSec published its analysis on April 26 in its weekly security roundup, and Verichains followed on May 5 with a longer breakdown titled When Signing Is Not Secure. BlockSec's recommendations line up with the gap: sign every field that affects execution, including the aggregator, both tokens and the amount, keep an allowlist of aggregators, and restrict the buy token to the vault's expected base tokens. As of October 23, 2026, Giddy's X account showed no post about the exploit, and no Giddy post-mortem or compensation plan had been published.

Where Two Reviewed Pieces Met

The vulnerability didn't live in one function. The signing scheme in GiddyVaultV3 decided what the backend committed to, and the swap routine in GiddyLibraryV3 decided which of those values it trusted, and each looks reasonable when read alone. The vault hashes the calldata it sends to aggregators, which is the part that looks most dangerous, and the library uses the token, amount and aggregator fields it's handed, which is what any swap helper does. The flaw only appears when someone traces a single SwapInfo from the signature check to the approval and asks which of its fields the signer ever saw.

Audits stay essential for code like this, and they cover the scope they're given in the time they have. Exploits like this one tend to surface exactly where two separately written pieces meet, a signature format defined in one contract and consumed by a library in another, and a review that reads them in different sittings has to hold that whole path in mind to see the gap.

How Olympix Tests What the Signature Actually Binds

Olympix reads a vault the way an attacker does, as states, transitions and entry points, and takes in the context around the code, here the signed authorization model the comments and type hash describe, to infer the rules the system depends on. In GiddyVaultV3, the state that matters is who can spend the strategy's gauge tokens, and deposit, withdraw and compound are the entry points that can change it. The engine maps every path from those entry points to a token approval or transfer and uses signals to decide which ones to attack first. Olympix's static analysis surfaces the signal here: a struct field that flows into forceApprove as the spender and the token, on a path whose only authorization is a signature that never hashes that field.

The invariant for this vault fits in one sentence: a strategy should only approve or transfer its holdings in the way Giddy's signer authorized. BugPoCer takes that rule and tries to break it with Counterexample-Guided Invariant Refutation, working outward from the vault as a whole. It narrows from every path in the vault and strategy, to the feasible ones an outside caller can reach with a valid authorization, to the ones where an unsigned SwapInfo field reaches an approval of the strategy's tokens, and the compound path through swapRewardTokens breaks the rule on the first try because nothing between the signature and the approval checks the token or the spender.

The counterexample BugPoCer hands back is concrete: a validly signed compound request whose unsigned fields name the strategy's gauge token and a contract the caller controls, the approval the strategy grants that contract, the token movement that follows, and the gauge balance that ends up outside the vault, along with the minimal conditions that trigger it and a generated Forge test that replays it. That's the April 23 transaction, produced at the merge that introduced the signing scheme rather than on mainnet.

Olympix has flagged this class before in other codebases, where a signed message left out a parameter that controlled where value went. Because the engine runs automatically in development, at every merge and before release, a team gets a standing view of every path where a signed request carries fields the signature never saw.

Sign the Fields That Move the Money

Giddy's vaults asked for a backend signature on every action, and the signature was checked correctly every time. It just covered the calldata while leaving the token, the amount and the spender open to whoever submitted the request. Any time a contract acts on a signed instruction, every field that changes the outcome has to be inside the hash, and that's a property an engine can test on every merge. To see how Olympix maps what your signatures really bind and returns a runnable counterexample for every field they miss, book a technical walkthrough.

On-Chain References

Protocol: Giddy (GiddyVaultV3 Bitcoin vaults on Yield Basis)

Chain: Ethereum

Date: April 23, 2026, 11:57:47 UTC

Loss: 3.53 g(yb-tBTC), 6.94 g(yb-cbBTC) and 6.27 g(yb-WBTC), about $1.3M per BlockSec and Verichains

Attacker EOA: 0x81Fe...b156

Attack contract: 0x50a5...76eB

Malicious aggregator: 0x7326...4528

GiddyVaultV3 implementation (verified): 0x5f0a...4318

tBTC strategy: 0xC99F...6ec3

cbBTC strategy: 0x0d5e...B690

WBTC strategy: 0x870f...6eD7

Vault proxies: 0x9C24...05b7, 0x51b9...d080, 0x1D85...8e67

Exploit tx: 0x5edb...82e5

Block: 24942492

Sources

BlockSec: Weekly Web3 Security Roundup, April 26, 2026

Verichains: When Signing Is Not Secure

Giddy: Bitcoin Vaults and YieldBasis

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.