Hypr Bridge Reinit Drain
Exploit Transactions
Victim Addresses
0x40c31236b228935b0329eff066b1ad96e319595eEthereum0x31adda225642a8f4d7e90d4152be6661ab22a5a2EthereumLoss Breakdown
Similar Incidents
LiteV3 Bridge Aggregator Proxy Initialization Race Enabled Unauthorized UUPS Takeover
35%Tara Light Client Quorum-Check Ordering Bug Enables Forged Bridge Finalization and WETH Drain
35%USDTStaking Approval Drain
32%0x7CAE Approved-Spender Drain
31%VINU Reserve Drain
31%Audius Governance Reinitialization and Treasury AUDIO Drain
31%Root Cause Analysis
Hypr Bridge Reinit Drain
1. Incident Overview TL;DR
On Ethereum mainnet, an attacker drained 2,570,000 HYPR from the legacy Optimism L1 bridge escrow used by Hypr via transaction 0x51ce3d9cfc85c1f6a532b908bb2debb16c7569eb8b76effe614016aac6635f65. The opportunity existed in pre-state σ_B at block 18774584, immediately before that exploit transaction, when bridge proxy 0x40c31236b228935b0329eff066b1ad96e319595e already held 2570012000000000000000000 raw HYPR units and still trusted the legitimate messenger 0xf0e9A593D3b252511adfB5eA16EBF0a0450b2A11. The attacker first deployed helper contract 0xbA6fA6e8500cD8eEDa8EbB9DFbCC554fF4A3EB77 in transaction 0x3e6171fd239d192b390faf89e04e3d993388658bb5b546bf31e072beb52fcf0d, then used that helper to overwrite the bridge messenger and finalize a forged withdrawal. The root cause was a re-initialization bug in the legacy L1StandardBridge implementation at 0xe468b43b4ae4d750cd6a5d7edacc1a751302c99c: initialize(CrossDomainMessenger) remained publicly callable because modifier clearLegacySlot zeroed storage slot 0 immediately before reinitializer(2) checked it. That reset the OpenZeppelin Initializable version guard on every call and let any unprivileged caller replace messenger with an attacker-controlled contract. Once messenger was replaced, the helper spoofed xDomainMessageSender() as 0x4200000000000000000000000000000000000010, satisfying the bridge's onlyOtherBridge authentication and releasing escrowed HYPR to the attacker.
2. Key Background
The primary victim component was the legacy Optimism L1StandardBridge proxy at 0x40c31236b228935b0329eff066b1ad96e319595e. This proxy is a verified L1ChugSplashProxy, which forwards non-owner calls through fallback() and receive() to the current implementation. That matters because any external caller who is not the proxy owner can still reach public implementation functions exposed behind the proxy.
The bridge implementation at 0xe468b43b4ae4d750cd6a5d7edacc1a751302c99c is a verified L1StandardBridge. Its job is to escrow L1-native assets and release them when the canonical cross-domain messenger proves a withdrawal from the paired bridge on the remote chain. Before the exploit, an RPC snapshot at block 18774584 showed:
slot0 = 0x...02, meaning the contract had already completedreinitializer(2).messenger() = 0xf0e9A593D3b252511adfB5eA16EBF0a0450b2A11, the legitimate messenger.deposits(HYPR, l2HYPR) = 2570012000000000000000000.HYPR.balanceOf(bridge) = 2570012000000000000000000.
The bridged asset was the HYPR L1 token proxy at 0x31adda225642a8f4d7e90d4152be6661ab22a5a2, backed by implementation 0x5877bb958999ec60addafa3e053f451a81ddbfe3. The token itself was not the bug source. The token only reflected the bridge releasing assets that it already held in escrow.
The attacker sequence was entirely permissionless. EOA 0x3ea6ba6d3415e4dfd380516c799aafa94e420519 deployed a helper contract, then invoked it. The helper contract became the bridge messenger, reported the canonical remote bridge address when queried, and caused the bridge to finalize a fake L2-to-L1 withdrawal to recipient 0x5b8d598b354f5760b2a65f492154e7a3df46d1be.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ATTACK, not an MEV opportunity or a token-accounting bug. The vulnerable components were the verified legacy L1StandardBridge implementation at 0xe468b43b4ae4d750cd6a5d7edacc1a751302c99c and the L1ChugSplashProxy at 0x40c31236b228935b0329eff066b1ad96e319595e that exposed the public initializer to arbitrary callers. The intended invariant is that once the bridge is initialized, unprivileged users must never be able to change the trusted messenger, because all withdrawal finalization authority flows from that messenger and the paired remote bridge identity. The verified source breaks that invariant by running modifier clearLegacySlot before reinitializer(2). clearLegacySlot executes sstore(0, 0), and slot 0 is exactly where Initializable stores its initialization version. As a result, every external call to initialize resets _initialized to zero just before the guard checks it, so reinitializer(2) succeeds again even after the bridge was already initialized. After the attacker rewrote messenger, the standard bridge authentication path in onlyOtherBridge became attacker-satisfiable because the attacker-controlled messenger could return the expected OTHER_BRIDGE value on demand. The exploit conditions were simple and public: the proxy had to expose initialize, the implementation had to preserve the clearLegacySlot reinitializer(2) ordering, the bridge had to hold escrowed HYPR, and the attacker had to deploy a helper that could call the bridge and return the expected cross-domain sender.
4. Detailed Root Cause Analysis
The verified L1StandardBridge source shows the defect directly:
// Verified L1StandardBridge source
modifier clearLegacySlot() {
assembly {
sstore(0, 0)
}
_;
}
function initialize(CrossDomainMessenger _messenger) public clearLegacySlot reinitializer(2) {
__StandardBridge_init({ _messenger: _messenger });
}
That code is dangerous because the bridge relies on OpenZeppelin Initializable state stored in slot 0. The validator independently confirmed by RPC that slot 0 already contained version 2 at block 18774584, immediately before the exploit. Under correct behavior, a second call to initialize should have reverted. Instead, the bridge accepted the call because clearLegacySlot zeroed slot 0 first.
The authentication path that the attacker targeted is equally explicit in the verified StandardBridge base contract:
// Verified StandardBridge source
modifier onlyOtherBridge() {
require(
msg.sender == address(messenger) && messenger.xDomainMessageSender() == address(OTHER_BRIDGE),
"StandardBridge: function can only be called from the other bridge"
);
_;
}
Once messenger is attacker-controlled, the bridge no longer has a trustworthy root of authority. The exploit trace shows that exact sequence. First, the helper deployed by 0x3ea6...0519 called the bridge proxy and reached the implementation initializer:
Exploit execution trace
0xbA6f...EB77::swap(HYPR, l2HYPR)
0x40C3...595e::initialize(0xbA6f...EB77)
0xE468...c99C::initialize(0xbA6f...EB77) [delegatecall]
emit Initialized(version: 2)
storage slot 3: 0xf0e9...2A11 -> 0xbA6f...EB77
This trace is incompatible with a correctly locked initializer because the pre-state already had initialization version 2. The only coherent explanation is that the public initializer was reopened by the slot-zeroing modifier.
The second stage was the forged withdrawal. In the same trace, the bridge finalized finalizeERC20Withdrawal(address,address,address,address,uint256,bytes) and accepted the helper's spoofed cross-domain identity:
Exploit execution trace
0xE468...c99C::finalizeERC20Withdrawal(HYPR, l2HYPR, bridge, 0x5b8d...D1BE, 2570000000000000000000000, 0x)
0xbA6f...EB77::xDomainMessageSender() [staticcall]
return 0x4200000000000000000000000000000000000010
HYPR::transfer(0x5b8d...D1BE, 2570000000000000000000000)
The balance-diff artifact confirms the financial effect of that trace. The bridge proxy lost 2570000000000000000000000 HYPR, and recipient 0x5b8d598b354f5760b2a65f492154e7a3df46d1be gained exactly that amount. The tracked bridge escrow for (HYPR, l2HYPR) fell from 2570012000000000000000000 to 12000000000000000000, leaving only 12 HYPR of dust after the exploit. The exploit therefore violated three concrete security principles: initialization was not one-time, privileged authentication roots were mutable by any caller, and a legacy-storage compatibility shim overwrote guard state that should have remained untouchable.
5. Adversary Flow Analysis
The exploit used two Ethereum mainnet transactions:
-
0x3e6171fd239d192b390faf89e04e3d993388658bb5b546bf31e072beb52fcf0d- Sender:
0x3ea6ba6d3415e4dfd380516c799aafa94e420519 - Effect: deployed attacker helper
0xbA6fA6e8500cD8eEDa8EbB9DFbCC554fF4A3EB77 - Feasibility: any unprivileged EOA can deploy an arbitrary helper contract
- Sender:
-
0x51ce3d9cfc85c1f6a532b908bb2debb16c7569eb8b76effe614016aac6635f65- Sender:
0x3ea6ba6d3415e4dfd380516c799aafa94e420519 - Target: helper
0xbA6fA6e8500cD8eEDa8EbB9DFbCC554fF4A3EB77 - Calldata selected the HYPR bridge pair and triggered the takeover plus forged withdrawal
- Sender:
The helper contract's role was straightforward. It stored the bridge address, the attacker-chosen recipient, and a function that returned the canonical remote bridge address. It then executed two calls in order: initialize(attackerHelper) and finalizeERC20Withdrawal(HYPR, l2HYPR, bridge, recipient, amount, ""). The bridge trusted both calls because the proxy forwarded the public initializer and because onlyOtherBridge only checked the mutable messenger field plus xDomainMessageSender().
The adversary-related accounts are defensible and concrete:
0x3ea6ba6d3415e4dfd380516c799aafa94e420519: deployer and exploit sender EOA.0xbA6fA6e8500cD8eEDa8EbB9DFbCC554fF4A3EB77: attacker helper that became the forged messenger.0x5b8d598b354f5760b2a65f492154e7a3df46d1be: profit recipient that received the stolen HYPR.
No privileged key access, governance control, or non-public dependency was required. The attack only needed the public bridge proxy, public implementation logic, public token addresses, and standard mainnet transaction inclusion.
6. Impact & Losses
The measurable loss was 2570000000000000000000000 raw HYPR units, which is 2,570,000 HYPR at 18 decimals. The bridge had held 2,570,012 HYPR in tracked escrow immediately before the exploit, and the attacker drained all but 12 HYPR of residual dust. The loss was borne by the bridge escrow at 0x40c31236b228935b0329eff066b1ad96e319595e, not by a defect in the HYPR token contract itself.
The validated success predicate is direct HYPR profit. Recipient 0x5b8d598b354f5760b2a65f492154e7a3df46d1be started with 0 HYPR in the balance-diff artifact and ended with 2570000000000000000000000 raw units, for a positive delta of the same amount. Gas was paid in ETH by 0x3ea6ba6d3415e4dfd380516c799aafa94e420519, but that does not change the positive HYPR-denominated exploit predicate. No separate non-monetary oracle was needed because the profit predicate alone conclusively establishes exploit realization.
The exploit also changed a critical security control: messenger() on the bridge was rewritten from 0xf0e9A593D3b252511adfB5eA16EBF0a0450b2A11 to attacker helper 0xbA6fA6e8500cD8eEDa8EbB9DFbCC554fF4A3EB77. That state change was not incidental. It was the enabling condition that converted a protected cross-domain withdrawal path into a permissionless drain path.
7. References
- Exploit transaction:
0x51ce3d9cfc85c1f6a532b908bb2debb16c7569eb8b76effe614016aac6635f65 - Helper deployment transaction:
0x3e6171fd239d192b390faf89e04e3d993388658bb5b546bf31e072beb52fcf0d - Victim bridge proxy:
0x40c31236b228935b0329eff066b1ad96e319595e - Bridge implementation:
0xe468b43b4ae4d750cd6a5d7edacc1a751302c99c - HYPR token proxy:
0x31adda225642a8f4d7e90d4152be6661ab22a5a2 - Verified bridge source used for validation: Etherscan verified contracts for
L1ChugSplashProxy,L1StandardBridge, andStandardBridge - Validator-confirmed RPC observations: pre-state slot
0,messenger(),deposits(HYPR,l2HYPR), andHYPR.balanceOf(bridge)at block18774584 - On-chain execution evidence: full exploit trace and balance-diff artifacts collected for transaction
0x51ce3d9cfc85c1f6a532b908bb2debb16c7569eb8b76effe614016aac6635f65