Bybit's signers signed what the screen showed. The screen was lying.
On 21 February 2025 three people approved what their screens described as a routine transfer, and roughly 1.5 billion dollars left the building. They were not careless and they were not blind signing. They were reading a display that had been rewritten by someone else. This is a lesson about the difference between what you see and what you sign, and why that gap exists in almost every approval screen in finance.
Picture the workflow, because it is deliberately boring. A large exchange keeps most customer assets in cold storage. Periodically, someone moves a tranche from cold to warm so the hot wallets can be topped up. It is scheduled, it is rehearsed, and it is approved by a multisignature wallet, which means several named humans must each independently authorise it before anything executes.
That control is well designed. It is the same instinct as requiring two signatures on a corporate cheque. One compromised person cannot move the money. One compromised laptop should not either, because the other signers are on their own laptops, looking at their own screens, exercising their own judgement.
On 21 February 2025, Bybit's signers opened the Safe{Wallet} interface, the widely used web front end for that kind of multisignature wallet. Each of them read a transaction summary that described the routine transfer they were expecting. Each of them checked it. Each of them approved.
The bytes they actually authorised did not describe a transfer. They described an upgrade: a change to the wallet's implementation contract, replacing the logic that governed the wallet with logic written by the attacker. Once enough signatures were collected and the transaction executed, the wallet stopped obeying its audited code and started obeying the attacker's, and the assets were drained. Public post mortems place the loss at approximately 1.5 billion dollars, the largest theft in the history of cryptocurrency, later attributed by the FBI to North Korean state actors.
Three competent people, following a rehearsed procedure, using the recommended tooling, with a multisignature control functioning exactly as designed. They had no realistic way to catch it. That last sentence is the one worth staying with, because it means the lesson is not "be more careful".
Short answer: Blind signing means approving data you cannot read. The Bybit signers could read their screens perfectly well; the screen simply described something other than what they were signing, because a compromised web front end controlled both. The fix is not better decoding. It is separating the thing that displays the transaction from the thing that holds the key, and signing canonical bytes that the verifier independently recomputes.
What actually happened on 21 February 2025?
The chain of events is worth walking slowly, because every link in it generalises far beyond cryptocurrency.
The compromise was upstream, not at the exchange
Bybit's own systems were not the entry point. According to the public analyses from Safe, alongside independent post mortems from firms including BlockSec and Sygnia, the attackers compromised a developer machine belonging to Safe{Wallet}, the provider of the multisignature interface. From there they were able to modify the front end assets that the Safe web application served to users.
Pause on the shape of that. The attacker did not need to breach the exchange. They breached a supplier of the exchange's user interface. The exchange's own security controls, its key management, its procedures and its people were all irrelevant to the initial access, because the attack arrived through a screen.
The injected code was targeted and patient
The malicious JavaScript was reportedly conditional. It did not misbehave for every visitor, which would have been noticed within hours by thousands of users. It activated for a specific target. For everyone else the application behaved normally, which is why nothing looked wrong until the moment it mattered.
This is standard tradecraft for front end supply chain attacks and it defeats the most common defence, which is that someone would surely notice. Nobody notices code that only misbehaves once, for one wallet, on one afternoon.
The signers were shown one thing and asked to sign another
Here is the mechanical heart of it. In a Safe multisignature wallet, what the signers approve is a structured transaction object. Among its fields is one that decides how the target code is executed. A normal value means "call this address", which is what a routine transfer looks like. Another value means "delegatecall this address", which means "run that code as if it were this wallet's own code, with access to this wallet's own storage".
Delegatecall is a legitimate and necessary feature. It is also the difference between asking a contractor to fix your roof and handing the contractor the deed to your house. The transaction the signers approved used it to overwrite the storage slot that tells the wallet which implementation contract to obey.
The interface, however, rendered a friendly summary that described the routine transfer. The summary was generated by the compromised page. The bytes were generated by the compromised page. The signature covered the bytes. The human read the summary.
What the screen said:
Transfer ETH from Cold Storage 1 to Warm Wallet 3
Amount (a routine tranche)
Operation Call
What the signature actually covered:
to 0x{attacker-controlled contract}
value 0
data 0x{payload writing a new implementation address}
operation 1 // delegatecall, not call
...
=> the wallet's masterCopy pointer now points at attacker code
After the signatures, everything worked correctly
This is the part people find hardest to accept. Nothing failed. The signatures were valid. The multisignature threshold was genuinely met. The blockchain faithfully executed exactly what had been authorised. Every cryptographic component performed flawlessly.
The system did precisely what it was told. The problem was entirely in the gap between what the humans believed they were telling it and what they actually told it. No amount of stronger cryptography closes that gap, because the cryptography was never wrong.
Why is this not a blind signing failure?
The instant lesson circulated after the incident was "never blind sign". That advice is good and it is aimed at the wrong failure.
Blind signing means approving data you cannot read: your device shows you an opaque hexadecimal blob, you cannot decode it, and you approve anyway because the alternative is not getting your work done. It is a genuine and widespread problem, and the industry response, decoding calldata into human readable summaries, is the correct response to it.
But the Bybit signers were not blind signing. They were shown a clear, readable, human friendly summary. They read it. They understood it. It was false.
The distinction matters enormously for what you do next. If the failure is blind signing, the fix is better decoding, and you invest in richer transaction summaries. If the failure is a lying display, better decoding actively makes things worse, because a more convincing summary produced by a compromised page is a more convincing lie. You have improved the attacker's user interface.
What is Display Signer Fusion?
Here is the name for the underlying pattern, and once you can name it you will find it everywhere in enterprise finance.
Display Signer Fusion is the condition where the component that describes a transaction to a human and the component that authorises it share a single compromise domain. Compromise one and you control both. The human's understanding and the human's authority have been welded together inside a machine that an attacker can own.
The analogy is signing a contract that somebody else is holding. They hold the pages, they let you read the top sheet, and you sign the bottom of a stack you were never able to inspect. It does not matter how carefully you read the page they showed you. Your reading and your signature were mediated by the same party, and that party chose what you saw.
In a web based approval workflow, the browser tab is that party. It renders the description, it builds the payload, and it triggers the signature. Everything the human relies on to form an intent, and everything the system relies on to record it, passes through one process. The security of the entire control reduces to the integrity of a page that was assembled from a content delivery network, a build pipeline, a package registry, and a handful of third party scripts.
Does a hardware wallet fix this?
Partly, and understanding the boundary is the most useful technical distinction in this article.
A hardware wallet does separate the key from the computer. The private key never leaves the device, so malware on the laptop cannot extract it. That is a real and valuable property, and it defeats an entire class of attack.
What a hardware wallet does not automatically do is separate the display from the computer. Its screen shows what its firmware can decode from the transaction it was handed. If the firmware understands the transaction type, it can show a meaningful summary, which is what the industry calls clear signing. If it does not, it shows a hash or a truncated blob, and the human is back to blind signing on a two centimetre screen.
Three constraints follow, and none of them are criticisms of the devices:
- Coverage is bounded by the firmware. Clear signing works for transaction types the device has been taught. Novel or complex contract interactions frequently fall outside that set, and those are exactly the interactions worth scrutinising.
- A tiny screen is a poor place to compare a forty character address. In practice people check the first four and last four characters, which attackers have known for years, which is why address poisoning works.
- Enterprise approvers do not carry them. A treasurer approving a wire, an administrator granting a role, a controller releasing a payment run: none of these people have a hardware wallet, and no plausible programme is going to give one to every approver in a global finance function.
So hardware wallets are a good answer to key extraction and a partial answer to display integrity within the cryptocurrency niche. They are not an answer available to the ninety nine percent of high value approvals that happen in ordinary corporate software.
What is canonicalization and why is it the crux?
This is the part that most write ups skip, and it is where the actual engineering lives. Take it slowly.
Suppose we decide the fix is to show the transaction on a second device, a phone, so the compromised laptop cannot control what the human reads. Good instinct. Now ask the awkward question: what exactly does the phone display, and where did it get it?
If the phone receives a friendly summary from the laptop and shows that, we have achieved nothing at all. The laptop is compromised, so the summary is whatever the attacker wants. We have merely moved the lie onto a nicer screen.
The phone must therefore derive what it displays from the same bytes it is about to sign, and those bytes must have exactly one possible meaning. That is what canonicalization provides.
The same data, many possible byte strings
Consider a payment instruction as ordinary structured data. Written as JSON, all of the following are the same logical object and different byte strings: keys in a different order, extra whitespace, a number written as 42000 or 42000.0 or 4.2e4, a string escaped differently, or an extra field the verifier ignores but the signature covers.
If the signature covers "whatever bytes happened to be produced", then two parties can hold different byte strings that both look correct and mean different things. Ambiguity in serialisation becomes ambiguity in meaning, and ambiguity in meaning is exactly the hole the attacker climbs through.
Canonical form: one meaning, one byte string
A canonicalization scheme is a set of deterministic rules that turn a logical object into exactly one byte string. Sort the keys. Fix the number format. Fix the escaping. Forbid unknown fields. Anyone applying the rules to the same object gets the same bytes, always.
The web has RFC 8785, the JSON Canonicalization Scheme, for precisely this. The Ethereum ecosystem has EIP-712, which hashes typed structured data along with a domain separator so that a signature for one contract on one chain cannot be replayed against another. European payment regulation encodes the same idea in law: the regulatory technical standards on strong customer authentication under PSD2 require dynamic linking, meaning the authentication code must be specifically linked to the amount and the payee, and any change to either must invalidate it.
Three different worlds, one identical insight. The authorisation must be welded to the specific facts of the specific transaction, in a form with no wiggle room.
The rule that actually closes the hole
Canonical bytes alone are not enough. The verifier must recompute them from its own copy of the transaction data, and check the signature against that, rather than against bytes the client supplied.
This is the whole ball game and it is easy to get wrong. If the server verifies the signature against the bytes the browser sent, then a compromised browser sends bytes that match its lie, and the check passes while meaning nothing. If the server independently canonicalises the transaction it is about to execute and verifies the signature against that, then a mismatch between what was shown and what would execute becomes arithmetically impossible to hide.
# Server side. The only ordering that is safe.
tx = load_pending_transaction(tx_id) # the server's own record
canonical = jcs_canonicalize({ # RFC 8785, deterministic
"action": "treasury.transfer",
"from": tx.source_account,
"to": tx.destination_account,
"amount": tx.amount_minor_units,
"currency": tx.currency,
"operation": tx.operation, # 'call' vs 'delegatecall' lives here
"not_after": tx.expires_at
})
digest = sha256(canonical)
# The signature must verify against OUR digest, never the client's.
assert verify_ed25519(signature, digest, enrolled_key_for(approver))
assert digest == receipt.payload_sha256
execute(tx)
Read the comment on the third assertion again. The client is not asked what was signed. The client is told what was signed, by a server that recomputed it. A compromised front end can still lie to the human, but it can no longer produce a signature that matches the lie, because the human's device signed the canonical bytes of the real transaction and the human saw those same bytes rendered from that same source.
What does display separated signing look like in practice?
The pattern has three parts and is not conceptually complicated.
First, the canonical payload goes to the second device without passing through the first device's control. In our implementation the desktop displays a rotating pattern that the enrolled phone claims, establishing a channel bound to that session. The phone then fetches the canonical payload directly. It never asks the desktop what the transaction says, because the desktop's opinion is exactly what we are removing from the trust path.
Second, the phone renders the payload from those canonical bytes. Not a summary handed to it. A projection computed locally from the bytes it is about to sign. Destination account in full, amount, currency, the operation type spelled out in words, and the expiry. If the projection and the bytes ever disagree, that is a bug in one small piece of code that can be audited once, rather than a property of whatever the browser felt like rendering.
Third, the human confirms with a liveness check and the device signs. The result is an Ed25519 receipt that verifies offline against a published key, so an auditor or an insurer can confirm what was approved without calling anyone's API, including ours.
Applied to the Bybit shape, the signer's phone would have shown something close to this, derived from the bytes rather than from the page:
Treasury wallet 0x1Db9...4bC7
UPGRADE IMPLEMENTATION CONTRACT
Operation delegatecall (executes as this wallet)
Target 0x9fA2...c113 (first seen 6 minutes ago)
Effect replaces the wallet's implementation
This is NOT a transfer.
Requested from a session on desktop-7. Expires in 4 minutes.
Sign to authorise. Decline to block and alert security.
A signer looking at that declines. Not because they are an expert in proxy storage layouts, but because they came to approve a routine transfer and the independent screen is describing a contract upgrade. The mismatch is the alarm, and mismatches are only visible when there are two independent descriptions to compare.
Does this only matter for cryptocurrency?
No, and this is the part that should worry a treasurer more than it worries an exchange.
Cryptocurrency simply provides the clearest examples, because the losses are instant, public, and irreversible, so the post mortems get written. The same structure exists in far more ordinary software, where it produces slower and quieter losses that rarely get published.
| Approval surface | Who controls the description? | Independent display path? |
|---|---|---|
| Bank corporate payment portal | The browser page | No |
| Treasury or AP platform release screen | The browser page | No |
| ERP payment run approval | The client or browser | No |
| Cloud console role grant | The browser page | No |
| Multisignature wallet with hardware device | Page proposes, device decodes | Partial, bounded by firmware |
| Approval pushed to an independent companion device | Canonical bytes, rendered locally | Yes |
Every row except the last two has the same shape that cost Bybit 1.5 billion dollars. The reason we do not have a long public list of corporate portal equivalents is not that they are safe. It is that a manipulated wire in a private company produces a quiet insurance claim rather than a public chain analysis thread.
Front end supply chain compromise is now routine rather than exotic. Content delivery networks, package registries, build pipelines and third party scripts are all standing paths into the page a human is about to trust. And a new one has arrived: agentic browsers and extensions that can read and rewrite what a page shows, which adds another party with edit rights over the description you are relying on. If you want that thread, we pull on it in proving an agent is authorised.
What this does not fix
The honest boundaries, and they are real.
A compromised phone is still a compromised signer. This separates two domains; it does not create an uncompromisable one. If the same attacker owns both the laptop and the phone, they own the approval. What has changed is the cost: two independent compromises of two different device classes belonging to the same person, which is a substantially harder operation than owning one supply chain.
Threshold signing is not shipped. Our delegation and receipt model produces N independent receipts for an N of M quorum, not a single threshold signature. That is adequate for most treasury workflows and it is not the same thing as native multiparty computation, and we would rather say so than imply otherwise.
Rich contract decoding on the companion device is roadmap. Today the strong case is structured payloads that canonicalise cleanly: wires, payee changes, role grants, payment releases. Rendering arbitrary smart contract calldata into reliable plain language on a phone is genuinely hard and we have not solved it.
It cannot rescue a signer who understands and proceeds. If someone reads an accurate description of a bad transaction and signs anyway, whether from social pressure, coercion or haste, the signature is valid and correct, because it records what actually happened. What you gain is an unambiguous record of exactly what they were shown at the moment they agreed.
It does not prevent the upstream compromise. Nothing here stops a developer machine from being breached or a package from being poisoned. It removes the ability of that compromise to convert into an authorised transaction, which is a different and more achievable goal than preventing all supply chain attacks.
What to do this week
- Inventory your approval surfaces. List every screen in your organisation where a human authorises something above a material threshold, and mark which of them have an independent display path. For most organisations the honest answer is zero, and writing that down is the whole argument.
- Ask your providers a direct question. "When my approver clicks approve, what exactly is signed, and does your server verify the signature against bytes it recomputed or bytes my browser sent?" The quality of the answer tells you a great deal, and the delay before you get one tells you more.
- Canonicalise before you cryptograph. If your approvals sign a rendered string or an opaque request identifier, fix that first. RFC 8785 for structured data, EIP-712 for on chain transactions. Ambiguity in serialisation is ambiguity in meaning.
- Verify server side against a recomputed digest. Audit one endpoint today to confirm it does not verify against client supplied bytes. This is the single most common implementation error in the whole pattern.
- Add an out of band confirmation to your highest value flow. Even a simple independent channel that restates destination and amount, derived from the server's record rather than the client's, catches this class of attack. Start with one flow.
- Rehearse the decline. Your signers have practised approving. Run one exercise where the second screen disagrees with the first, and make sure everyone knows that declining is the correct, encouraged, career safe response.
If it helps to see it working, the signing demo shows a payload rendered and signed on a companion device, and the integration docs cover canonicalization and offline receipt verification.
Frequently asked questions
What is blind signing and how do you prevent it? Blind signing is authorising data you cannot read, typically an undecoded hexadecimal blob. Decoding calldata into readable summaries prevents it. That is not what happened at Bybit, where signers read a clear summary that was false, so decoding alone would not have helped. Preventing that requires rendering and signing on a device separate from the one that could be lying.
How was Bybit hacked in February 2025? Public post mortems describe a compromise of a Safe{Wallet} developer machine leading to malicious JavaScript in the multisignature web interface. The page showed signers a routine transfer while the transaction they authorised replaced the wallet's implementation contract with attacker controlled code. Roughly 1.5 billion dollars was drained, and the FBI attributed the operation to North Korean actors.
Does a hardware wallet prevent user interface injection? It prevents key extraction, which is valuable, and it partially addresses display integrity through clear signing. The coverage is bounded by what the firmware can decode, small screens make address comparison unreliable, and enterprise approvers outside cryptocurrency generally do not carry one at all.
What does WYSIWYS mean? What you see is what you sign: the guarantee that the description a human reads is derived from the exact bytes their key authorises, rather than from a separate channel that could disagree with them. It requires canonical serialisation and a verifier that recomputes the bytes rather than trusting what the client sent.
Why is canonicalization necessary for transaction signing? Because the same logical transaction can be serialised into many different byte strings, and if the signature covers whichever bytes happened to be produced, two parties can hold different bytes that both verify and mean different things. Canonical form, such as RFC 8785 or EIP-712, gives one object exactly one byte string, so a signature can only mean one thing.
Could this happen to a corporate banking portal? The structure is identical. A browser page describes the payment and triggers the approval, so anything that can rewrite that page controls both. The reason there is no public catalogue of such incidents is that a manipulated corporate wire produces a private insurance claim rather than a public post mortem.
Sources
- Safe, incident updates and post mortem statements on the 21 February 2025 Bybit transaction, published at safe.global.
- BlockSec, Bybit incident analysis: a Web2 breach enables the largest crypto hack in history. Mechanism of the masterCopy substitution.
- Sygnia and other independent incident responders, technical analyses of the Safe{Wallet} developer machine compromise and the conditional front end injection, February and March 2025.
- Federal Bureau of Investigation public statements attributing the Bybit theft to North Korean state actors, 2025.
- Ethereum Improvement Proposals, EIP-712: Typed structured data hashing and signing.
- Internet Engineering Task Force, RFC 8785, JSON Canonicalization Scheme.
- European Banking Authority, regulatory technical standards on strong customer authentication and secure communication under PSD2, dynamic linking requirements.
- Federal Bureau of Investigation, Internet Crime Complaint Center 2025 Internet Crime Report, cryptocurrency linked fraud totals.
The cryptography was never wrong. The gap was between what three people believed they were authorising and what they actually authorised, and no stronger signature closes a gap like that.