# Valid signatures, wrong transaction: the Bybit and Safe signing supply chain

> Bybit's 2025 loss involved a tampered Safe front end. Independent SafeTx hash checks can detect substituted fields when signing and display disagree.

Source: https://sheng.page/en/posts/bybit-safe-signing-supply-chain/ · Published: 2026-11-01 · Author: Sheng

On 21 February 2025, Bybit's ETH cold wallet signed a malicious transaction during a routine transfer. That transaction moved no funds itself; it replaced the wallet's logic, and roughly 400,000 ETH, plus stETH, cmETH and mETH, were drained afterwards in separate transactions that no longer checked the original three signatures. The logic swap carried the three valid signatures the Safe contract required, and the contract executed it as written. Public analyses trace what the signers saw to one tampered front end. A valid signature establishes that the key holder signed those fields. Whether they were the fields the signer meant to approve has to be checked separately.

The transaction used `delegatecall`, which runs another contract's code against the caller's own storage, so the attacker's code could change the wallet's state. A Safe wallet is a proxy that forwards calls to an implementation contract whose address sits in slot 0, the first storage location; overwriting it replaced the wallet's logic.

## What the public investigations establish

- **Developer workstation and cloud access.** Cointelegraph, reporting Safe and Mandiant's March 2025 findings, says Safe's AWS setup required re-authentication every 12 hours and the attackers tried and failed several times to register their own MFA device. They then compromised a Safe developer's macOS machine, possibly through malware, and reused the developer's AWS session token while it was still valid, so no fresh MFA check was needed while that session remained active.
- **Deployed artefact.** Sygnia's investigation, as relayed by Bybit's CEO and reported by BleepingComputer, found that Safe{Wallet}'s AWS S3 bucket served malicious JavaScript modified two days before the attack. Bybit's statement, as quoted by SlowMist, gives 19 February. SlowMist identifies the changed file as `_app-52c9031bfa03da47.js` from urlscan records, and Wayback Machine has a capture from 19 February at 17:29 UTC.
- **Trigger.** NCC Group's reading of the captured script is that it swapped fields only when the Safe address matched a list and the original transaction was a plain call (`operation == 0`).
- **Execution.** Verichains records the transaction at 14:13:35 UTC on 21 February: `to` was `0x9622…7242`, `operation` was `1` and `data` held calldata for `transfer(address,uint256)`.
- **Front end reverted.** Sygnia says S3 served clean JavaScript again about two minutes after the transaction executed.
- **Review scope.** Sygnia found no intrusion evidence in Bybit's own infrastructure, and Safe said an external review found no vulnerabilities in the source of its smart contracts, front end and services.

The FBI attributed the attack to North Korea in its notice of 26 February 2025, calling the activity TraderTraitor.

![The publicly reported Bybit path: after failing to register their own MFA device, attackers compromise a Safe developer's macOS workstation and reuse a still-valid AWS session token, the front-end JavaScript in S3 is modified on 19 February, signers open app.safe.global and see a routine transfer, their hardware wallets receive a transaction with operation = 1, three valid signatures execute it on 21 February, a delegatecall rewrites slot 0 and assets are drained, and about two minutes later S3 is restored to clean JavaScript](/images/bybit-safe-signing-supply-chain/attack-path.en.png)

## Where the failures need different fixes

The public analyses do not identify one exploited here: the logic swap still required three valid signatures, and the threshold check ran. That does not rule out other bugs.

A tampered deployment artefact calls for checks on the release process. Sound source code can still reach the browser as a modified S3 file, so check who can write to the bucket, whether a write raises an alert, and whether each served file traces to a known commit's build.

The signing side also needs to check for a mismatch between the displayed and the signed transaction. BlockSec, citing Bybit CEO Ben Zhou, says the signers saw an ordinary transfer in Safe while a different payload went to their Ledger devices. In the script NCC Group published, the whole set of transaction fields (`to`, `value`, `data`, `operation`, `safeTxGas`) is put back to the original values after signing and only then submitted as a proposal, so the signature still covers the altered fields. A proposal is the pending transaction Safe's backend holds while other signers add theirs. From this NCC Group infers that the web app kept showing the intended transaction, and concludes that the signers blind-signed: they trusted the Safe web app and did not carefully check the contents on the device. Safe temporarily removed its native Ledger integration, which it described as the method used, and added validation of the transaction hash, data and signatures.

## Three signers, one copy of the JavaScript

These three signers held independent keys, but each relied on the same JavaScript loaded from the same site when deciding whether to sign. Tampering with that source could mislead all of them. A higher threshold would add no independent check if the additional signers used the same front end.

A multisig review therefore covers the software each signer uses to inspect transactions, whether those tools share a CDN or cloud account, and whether the hardware wallet can decode and display the fields itself.

## Recompute the hash from what the screen shows

The SafeTx hash is a digest of a Safe transaction, computed from fields such as `to`, `value`, `data`, `operation` and `safeTxGas` together with the Safe's address; it is the value the EIP-712 signature covers. A signer who recomputes it in a separate tool from the on-screen fields and compares the result with the hash the hardware wallet is about to sign sees a substituted field as a mismatch.

The example follows Safe 1.1.1, which the public analyses identify as the implementation behind Bybit's proxy; its domain contains only `verifyingContract`, with no chain ID. The displayed transaction is an invented routine transfer. The signed one takes `to`, `data`, `operation` and `safeTxGas` from the public analyses with a made-up `nonce`, so its hash is not that of the on-chain transaction.

```python
from Crypto.Hash import keccak

def k(b: bytes) -> bytes:
    return keccak.new(digest_bits=256, data=b).digest()

def word(x) -> bytes:
    if isinstance(x, str):
        x = int(x, 16)
    return x.to_bytes(32, "big")

SAFE_TX_TYPEHASH = k(b"SafeTx(address to,uint256 value,bytes data,uint8 operation,"
                     b"uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,"
                     b"address gasToken,address refundReceiver,uint256 nonce)")
DOMAIN_TYPEHASH_V111 = k(b"EIP712Domain(address verifyingContract)")  # Safe 1.1.1 has no chainId

def safe_tx_hash(safe: str, tx: dict) -> str:
    domain = k(DOMAIN_TYPEHASH_V111 + word(safe))
    body = k(SAFE_TX_TYPEHASH + word(tx["to"]) + word(tx["value"]) + k(bytes.fromhex(tx["data"]))
             + word(tx["operation"]) + word(tx["safeTxGas"]) + word(0) + word(0)
             + word(0) + word(0) + word(tx["nonce"]))
    return "0x" + k(b"\x19\x01" + domain + body).hex()

SAFE = "0x1db92e2eebc8e0c075a02bea49a2935bcd2dfcf4"
# What the UI shows: a hypothetical routine transfer to a made-up address
shown = {"to": "0x00000000000000000000000000000000000000b0", "value": 10**18,
         "data": "", "operation": 0, "safeTxGas": 0, "nonce": 0}
# What the hardware wallet is asked to sign: to, data, operation and safeTxGas
# come from the public analyses; the nonce is made up
signed = {"to": "0x96221423681a6d52e184d440a8efcebb105c7242", "value": 0,
          "data": "a9059cbb" + "bdd077f651ebe7f7b3ce16fe5f2b025be2969516".rjust(64, "0") + "0" * 64,
          "operation": 1, "safeTxGas": 45746, "nonce": 0}

DELEGATECALL_ALLOWLIST = {"0x00000000000000000000000000000000000000c1"}  # made up: a MultiSend deployment you have reviewed

def review(tx: dict) -> list[str]:
    findings = []
    if tx["operation"] == 1 and tx["to"].lower() not in DELEGATECALL_ALLOWLIST:
        findings.append("delegatecall target not on allowlist: %s" % tx["to"])
    if tx["data"][:8] == "a9059cbb" and tx["value"] == 0 and tx["operation"] == 1:
        findings.append("transfer(address,uint256) via delegatecall; the name says nothing about behaviour")
    return findings

h_shown, h_signed = safe_tx_hash(SAFE, shown), safe_tx_hash(SAFE, signed)
print("domain type", "0x" + DOMAIN_TYPEHASH_V111.hex())
print("safetx type", "0x" + SAFE_TX_TYPEHASH.hex())
print("shown hash ", h_shown)
print("signed hash", h_signed)
print("match      ", h_shown == h_signed)
for f in review(signed):
    print("finding    ", f)
```

```text
domain type 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749
safetx type 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8
shown hash  0xb1e3a4fa5d96c2c924777da4cf354a710ddc318f0f6688cb5df071e43f85e718
signed hash 0x20eb91008f8bcae8517c47d4311fc4016fef61ed72406eb2eee404dd9ca2369c
match       False
finding     delegatecall target not on allowlist: 0x96221423681a6d52e184d440a8efcebb105c7242
finding     transfer(address,uint256) via delegatecall; the name says nothing about behaviour
```

The two typehashes match the constants in the Safe 1.1.1 source; the full computation still needs checking against a known on-chain transaction (needs verification). The comparison must use fields independent of the tampered front end and lead the signer to refuse on a mismatch. That refusal blocks the transaction only if the remaining signers cannot reach the threshold.

Both checks in `review` concern `operation = 1`, a `delegatecall`. The Verichains and NCC Group disassembly shows the attacker's `transfer` did no ERC-20 transfer; it wrote its first argument into slot 0. So `to` points at the code run by delegatecall, while the new implementation written into slot 0 is a different address carried in the calldata. A decoder that only labels the selector `0xa9059cbb` shows a transfer of 0 tokens, so a signer could miss the wallet's logic being replaced.

A hash comparison only catches a mismatch between display and signature. When the screen accurately shows a malicious transaction the signer does not understand, a rule such as a `delegatecall` allowlist has to catch it, enforced outside the signers.

## Decoders and simulators need a trustworthy source too

A decoder or simulator deployed from the same cloud account as the affected front end, or reading the same API, cannot provide independent evidence. Check where a tool comes from before using it to inspect calldata, destinations, `delegatecall` or implementation changes.

- Decode and compute hashes in an offline or local tool, pinned to a source version you build yourself, without loading code live from a web page.
- Prefer clear signing that displays the EIP-712 fields, or at least shows the SafeTx hash on the device for comparison with an independent tool. BlockSec says a hardware wallet can show the payload being signed, so the Ledger screen would have differed from the web app. Raw multisig data is hard to read on a small screen, making the screen alone a weak check.
- Signers can use the official web app, a CLI or a self-hosted front end, but that only isolates them if deployment rights and data sources are separate too.
- Treat simulation as supporting information, and verify the simulator as well.

## Mastra: publish rights and install scripts

The Mastra incident of June 2026 shows how a package install can compromise a developer machine. The mechanism differs from Bybit's and cannot explain the Safe workstation's initial compromise.

Socket reports that between roughly 01:15 and 02:36 UTC on 2026-06-17, malicious patch versions of 141 `@mastra/*` packages were published from the account `ehindero`, which Socket describes as an npm user account with legitimate publish rights. The versions added a dependency, `easy-day-js`, a typosquat of `dayjs`. Its version 1.11.22 runs `setup.cjs` from a `postinstall` hook, a script npm runs automatically after installing a package. Where install scripts are allowed, it runs during `npm install` before the application imports the package.

Socket says the malware reads Chrome, Edge and Brave history, checks the host against 166 wallet extension IDs, profiles the machine and sets itself to start at login. The analysed sample only lists wallet extensions without copying their data; later modules could do more.

Socket does not say whether the account was taken over or used by its holder; the holder and permission status need verification. Until Mastra or npm says more, the publishing record does not show the account was stolen, nor does it support blaming an individual or a permission-management failure.

## Defences you can test in isolation

Run these checks on a local fork or a testnet, with a test Safe and fake assets. Each assumes one layer is already compromised and tests whether the defences behind it still hold.

| Defence | Acceptance check |
|---|---|
| Recompute the SafeTx hash in an independent tool before signing | With the front end replaced by one that alters `to` and `operation`, at least one signer's comparison fails and blocks signing, and the remaining keys cannot reach the threshold |
| Enforce a `delegatecall` allowlist independently of the signers | A transaction with `operation = 1` and an unlisted target is refused by the signing policy or a guard, without relying on a signer's eyes. A guard is a contract check hooked into `execTransaction`; Safe 1.1.1 has none, so using one means moving to a version that supports it (version needs verification) |
| Server checks that hash and signatures agree when a proposal is submitted | A proposal whose signatures cover a different hash from the one its fields produce is rejected |
| Traceable front-end artefacts | Every JavaScript file served in production hashes to the output of some CI build; a manual upload to the bucket raises an alert |
| Separate cloud permissions for developer sessions | A developer's everyday session cannot write to the production bucket; that goes through a deployment path that needs a second person's approval |
| No install scripts in CI or on developer machines | Adding a package with a `postinstall` hook to a test repo and installing it does not run the script |

## A provisional comparison with the 2026 Binance incident

The Binance hot wallet incident of 29 October 2026 also involved valid signatures moving assets, though any comparison with Bybit remains provisional. EIP-7702 lets an ordinary address sign an authorisation. Once it takes effect on-chain, calls to that address run the nominated contract's code. TRM Labs observed one Ethereum transaction carrying 50 EIP-7702 authorisations signed by a Binance hot wallet, plus signed transfers without delegation on nine other chains at varying times, including later Avalanche and Polygon outflows. TRM assesses that the evidence points to compromised hot wallet signing infrastructure; that does not establish the entry point.

On 30 October, Binance co-CEO Richard Teng said the attackers used EIP-7702 to bypass normal approval and control a hot wallet, and that investigators had found breach points beyond the employee laptop first suspected, with the full sequence still under investigation. Whether the cases share an entry point, wallet setup or signing flow awaits the forensic report (needs verification).

## Still unclear

- The Safe workstation's initial compromise: Cointelegraph suggests malware, while BlockSec mentions social engineering. These accounts can coexist, but neither establishes the initial sequence; Safe's original post and the Mandiant report need reading directly.
- Which other addresses were on the script's trigger list, and why: NCC Group does not fully explain.
- The device models and firmware Bybit's signers used, and the screens they saw: these need Bybit's or Safe's account; NCC Group's Trezor screenshots are research demonstrations.

## Notes

- The signatures were valid; what the signers saw did not match what they signed.
- The keys were independent, but signers sharing one front end had one source of information.
- Recomputing the SafeTx hash in a tool independent of the front end catches substituted fields; `delegatecall` still needs an allowlist enforced outside the signers.
- In the Mastra case, an account with publish rights introduced a dependency with a `postinstall` hook, which ran malicious code during `npm install` where install scripts were allowed. Socket does not say whether the account was taken over. This mechanism does not explain how the Safe workstation was first compromised.

## Further reading

- BleepingComputer, [Lazarus hacked Bybit via breached Safe{Wallet} developer machine](https://www.bleepingcomputer.com/news/security/lazarus-hacked-bybit-via-breached-safe-wallet-developer-machine/): 26 February 2025, relaying Sygnia, Verichains and Safe on S3 tampering and restoration, Bybit forensics and Ledger integration. Underlying reports were not read directly.
- Cointelegraph, [Safe Wallet releases Bybit hack post-mortem](https://cointelegraph.com/news/safe-wallet-releases-bybit-hack-post-mortem): 6 March 2025, reporting Safe and Mandiant on the workstation, AWS session token and MFA.
- NCC Group, [In-Depth Technical Analysis of the Bybit Hack](https://www.nccgroup.com/research/in-depth-technical-analysis-of-the-bybit-hack/): 10 March 2025, researcher analysis of triggers, field substitution and restoration after signing, and the blind-signing conclusion.
- Verichains, [Technical Analysis of the Bybit Hot Wallet Exploit](https://blog.verichains.io/p/technical-analysis-of-the-bybit-hot): 25 February 2025, transaction time, addresses and disassembly of `transfer` writing slot 0; it calls the wallet a hot wallet, where Bybit and Sygnia say cold wallet.
- SlowMist, [Bybit's $1.5 Billion Theft Unveiled](https://slowmist.medium.com/bybits-1-5-billion-theft-unveiled-safe-wallet-front-end-code-tampered-84b78f0fa9c2): Modified file name and Wayback capture time.
- BlockSec, [Bybit Incident: A Web2 Breach Enables the Largest Crypto Hack in History](https://blocksec.com/blog/bybit-incident-a-web2-breach-enables-the-largest-crypto-hack-in-history): Safe proxy, threshold, transactions, the sweep functions that skip signature checks after the swap, and the difficulty of reading multisig data on a small hardware wallet screen.
- Safe source, [GnosisSafe.sol v1.1.1](https://github.com/safe-global/safe-smart-account/blob/v1.1.1/contracts/GnosisSafe.sol): The example's `DOMAIN_SEPARATOR_TYPEHASH` and `SAFE_TX_TYPEHASH` constants.
- FBI/IC3, [North Korea Responsible for $1.5 Billion Bybit Hack](https://www.ic3.gov/PSA/2025/PSA250226): 26 February 2025, North Korea attribution and TraderTraitor name.
- Socket, [Mastra npm packages compromised](https://socket.dev/blog/mastra-npm-packages-compromised): Primary investigation: package count, timing, `easy-day-js` and `postinstall`.
- TRM Labs, [North Korea steals USD 6.36 billion from Binance](https://www.trmlabs.com/resources/blog/north-korea-steals-usd-6-36-billion-from-binance-in-the-largest-hack-in-history): 29 October 2026, on-chain observations and TRM's assessment; Binance had not published a root cause at the time of that report.
- CoinDesk, [Binance discovers multiple breach points in $6.3 billion attack](https://www.coindesk.com/tech/2026/10/30/binance-uncovers-wider-breach-in-usd6-3-billion-exploit-targets-sunday-withdrawal-restart): 30 October 2026, Richard Teng's account and unresolved findings.
- Wikipedia, [Bybit](https://en.wikipedia.org/wiki/Bybit): Citation index for dates, amounts and FBI attribution, not evidence of cause.

> Ideas and technical judgement by Sheng; drafted with Claude · topic chosen by Sheng; text, case notes and examples written by Claude, run on Linux with Python 3.13.18 and pycryptodome 4.2.0.