Multi-chain
Arbitrary call

Gyroscope's $807K Loss and How Olympix Would Have Prevented It

Loss
Loss
Exploit date
January 30, 2026

Date: January 30, 2026

Chain: Arbitrum to Ethereum (Chainlink CCIP)

Loss: 6.1M GYD drained from the bridge escrow, about $807K extracted

Class: bridge receiver that runs caller-supplied calldata against a caller-chosen address

Gyroscope's bridge contract on Ethereum held more than six million GYD, the protocol's stablecoin, as backing for every GYD that users had moved to Arbitrum. It released those tokens only when Chainlink's cross-chain messaging confirmed that a matching message came from Gyroscope's own contract on Arbitrum, and that check worked exactly as written on January 30, 2026. At 20:27:59 UTC, an authentic message arrived from Arbitrum, and the escrow followed its instructions by approving a stranger to spend all of its GYD.

Thirteen minutes later, that stranger pulled 6,099,337 GYD out of the escrow, and over the next two hours sold what the market would absorb, about $807,000 according to CertiK. The bridge let whoever sent a transfer choose the recipient and attach calldata to run against it, and nothing stopped the recipient from being the GYD token contract itself.

A Bridge That Could Also Make Calls

GYD moves between chains through Chainlink CCIP, a cross-chain messaging network. On Ethereum, the GydL1CCIPEscrow contract, deployed behind a proxy at 0xa1886c8d748DeB3774225593a70c79454B1DA8a6, locks GYD when users bridge out and releases it when they bridge back. On Arbitrum, the L2Gyd contract at 0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8 is GYD itself and sends the bridge messages back to Ethereum. Both sides expose the same bridgeToken function, which takes a destination chain, a recipient, an amount and an optional bytes field the source describes as calldata for the recipient on the destination chain.

That optional field turned the bridge into something more than a token mover. It let a user bridge GYD and, in the same message, call a contract on the other side. It also meant the escrow on Ethereum would make an external call on behalf of whoever started the transfer on Arbitrum, and every call it made would come from the address holding Gyroscope's locked GYD.

Authentic Messages, Arbitrary Contents

From the verified GydL1CCIPEscrow source on Blockscout, this is the function that handles an incoming message:

  function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage)
    internal
    override
  {
    address expectedSender =
      chainsMetadata[any2EvmMessage.sourceChainSelector].targetAddress;
    if (expectedSender == address(0)) {
      revert ChainNotSupported(any2EvmMessage.sourceChainSelector);
    }
    address actualSender = abi.decode(any2EvmMessage.sender, (address));
    if (expectedSender != actualSender) {
      revert MessageInvalid();
    }

    (address recipient, uint256 amount, bytes memory data) =
      abi.decode(any2EvmMessage.data, (address, uint256, bytes));
    uint256 bridged = totalBridgedGYD;
    bridged -= amount;
    totalBridgedGYD = bridged;

    gyd.safeTransfer(recipient, amount);
    if (data.length > 0) {
      recipient.functionCall(data);
    }

The first half is solid. It looks up which contract should be sending messages from the source chain, rejects unsupported chains and rejects any message that didn't come from that contract, so only L2Gyd on Arbitrum can trigger a release. The second half trusts the contents of that message completely. It decodes a recipient, an amount and a data field, all of which a user chose on Arbitrum, sends the GYD to the recipient and then, if any data came along, calls the recipient with it.

The recipient can be any address, and the data can be any function call. CertiK's analysis shows what that allowed: the attacker named the GYD token contract as the recipient and attached a call to its approve function. When the escrow made that call, GYD saw the escrow approving the attacker, because the escrow was the caller. The sender check had verified that the message came from Gyroscope's contract, and it had no way to know that the user behind the message was asking the bridge to sign away its own balance.

The lesson for builders is about whose authority a call carries. A contract that executes caller-supplied calldata lends its own identity to that caller for the length of the call, so the set of targets it'll call has to exclude every contract where its identity is worth something, starting with the token it's holding in escrow. CertiK makes the same point, saying that restricting the recipient and disallowing the GYD token address would have prevented the attack.

Forty Minutes From Arbitrum to Approval

The attacker's wallet, 0x7DD4075A6eAe9f18309F112364f0394C2DfA8102, started on Arbitrum. At 20:07:51 UTC it called bridgeToken on L2Gyd with a tiny amount of GYD, the GYD token on Ethereum as the recipient, and the approve call as data. Twenty minutes later, at 20:27:59 UTC in Ethereum block 24350070, Chainlink's CCIP off-ramp delivered the message to the escrow. The escrow verified the sender, sent its tiny amount of GYD to the token contract and made the call, which according to CertiK granted the attacker an unlimited approval over the escrow's GYD.

At 20:40:59 UTC, in block 24350135, the attacker called transferFrom on GYD and moved 6,099,337.38 GYD from the escrow to its own wallet. That GYD now sat in a single address, with far less market liquidity than it would take to sell it at face value. Blockscout shows the wallet working through Balancer's Vault and Relay's cross-chain contracts over the next hour and a half, redeeming and swapping into other assets, and CertiK puts the value the attacker extracted at about $807,000, capped by the liquidity available.

Then the wallet turned to Tornado Cash. Between 22:17 and 22:34 UTC, it sent two deposits of 100 ETH, ten of 10 ETH and two of 0.1 ETH to the Tornado Cash router, 300.2 ETH in total, matching CertiK's count. Smaller deposits followed on February 2 and February 6.

A Bounty Offer, Then a Deadline

Gyroscope paused its liquidity pools on January 30 and said there was an issue with its cross-chain contract, according to CertiK, and it offered the attacker a 33% white hat bounty. On February 3, Gyroscope governance addressed the attacker's wallet directly on X with a narrower deal: return 200 ETH by 18:30 UTC on February 5 to the GovernanceManager contract at 0x78EcF97572c3890eD02221A611014F30219f6219, keep the remaining 100-plus ETH as a fixed white hat settlement, and Gyroscope would stop investigating. The team said that settlement would give the protocol a chance to make users whole by cancelling GYD's system surplus.

The deadline passed without a return. Gyroscope posted on February 5 that it had traced the attacker's off-chain activity, including an IP address and device fingerprint, and on February 24 it asked for 90% of the funds within 48 hours, saying FTL Labs would then close its investigation and focus on returning rescued funds to users. As of October 23, 2026, that February 24 post was still pinned to Gyroscope's X profile, and the attacker's wallet showed no transfer to the governance contract.

How Olympix Finds the Call That Carries the Bridge's Name

Olympix reads a bridge the way an attacker does, as states, transitions and entry points, and takes in the context around the code, here the bridge's own description of the data field as calldata for the recipient, to infer the rules the system relies on. In GydL1CCIPEscrow, the state that matters is the escrow's GYD balance and the allowances it has granted, and _ccipReceive is the entry point a user on another chain can reach. The engine maps every path through it and uses signals to decide which to attack. Olympix's static analysis surfaces the signal here: a low-level call whose target and calldata both come from decoded message data a user controls, made by a contract that holds a token balance, with nothing excluding that token as a target.

The invariant this escrow depends on fits in one line: locked GYD should only leave the escrow as the amount named in a bridge message, sent to that message's recipient. BugPoCer takes that rule and tries to break it with Counterexample-Guided Invariant Refutation, working outward from the escrow as a whole. It narrows from every path into the contract, to the ones a message from L2Gyd can reach, to the ones that change the escrow's GYD balance or allowances by more than the bridged amount, and the path where the recipient is the GYD token and the data is an approval breaks the rule with a single message.

The counterexample is the January 30 sequence produced in development: a bridge transfer naming GYD as the recipient with an approval attached, the escrow's call that grants the allowance, and the transferFrom that empties the escrow, along with the minimal conditions that trigger it and a generated Forge test that replays it. Olympix has flagged this class before in other codebases, where a contract forwarded arbitrary calls and let callers borrow its privileges. Because the engine runs automatically at every merge and before release, the finding would have surfaced when the data field was wired into _ccipReceive, long before the escrow held six million GYD.

Composability Has a Caller

Gyroscope's escrow checked who sent each message and never checked what the message asked it to do in its own name. Any contract that runs calldata it didn't write needs a short list of addresses it'll never call, and its own token belongs at the top of that list. To see how Olympix traces every external call your contracts make on someone else's behalf and returns a runnable counterexample for each one that breaks an invariant, book a technical walkthrough.

On-Chain References

Protocol: Gyroscope (GYD CCIP bridge)

Chain: Arbitrum to Ethereum

Date: January 30, 2026, 20:07:51 UTC (Arbitrum), 20:27:59 and 20:40:59 UTC (Ethereum)

Loss: 6,099,337.38 GYD drained, about $807K extracted per CertiK

Attacker EOA: 0x7DD4...8102

GYD token (Ethereum): 0xe07F...1e8A

GydL1CCIPEscrow proxy (victim): 0xa188...A8a6

GydL1CCIPEscrow implementation (verified): 0x79eC...bbc5

L2Gyd (Arbitrum): 0xCA5d...32b8

Bridge tx (Arbitrum): 0x51c2...60c7

CCIP delivery and approval tx: 0x4573...c600

Drain tx (transferFrom): 0xe03a...405c

Drain block: 24350135

First 100 ETH Tornado Cash deposit: 0x7fef...d9da

Gyroscope GovernanceManager: 0x78Ec...6219

Sources

CertiK: Gyroscope Incident Analysis

Gyroscope: Settlement proposal to the GYD bridge exploiter

Gyroscope: Request to return 90% of funds

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.