2026-08-01 · Web3 Security
Every signature a wallet produces commits to a 32-byte digest, and whether the wallet can turn that digest back into a readable sentence is decided by the site making the request. On 21 February 2025 three professional signers at Bybit approved a digest built from ten named fields, one of which let the attacker’s contract rewrite the wallet.
Case file
| Field | Value |
|---|---|
| Technique | Blind signing: approving a signature request whose payload the signer cannot read |
| Mechanism | An ECDSA signature covers a 32-byte digest. Decoding it for display needs structure the request may not carry |
| Methods | eth_sign, personal_sign (EIP-191), eth_signTypedData_v4 (EIP-712), eth_sendTransaction, ERC-2612 permit |
| Standards | EIP-191 (Final, 2016), EIP-712 (Final, 2017), ERC-2612 (Final, 2020), ERC-7730 (Draft, 2024) |
| Vendor removals | MetaMask PR 24756, merged 1 August 2024, -2,294 lines. Safe PR 5090, merged 24 February 2025, -166 lines |
| Worked example | Bybit, 21 February 2025. Three confirmations recorded by Safe as ETH_SIGN, each with v = 31 |
| Clear-signing coverage | 247 calldata and 125 EIP-712 descriptors across 51 projects in the ERC-7730 registry, counted 1 August 2026 |
| Court, docket, prosecution | Not established. No offence attaches to a design property |
| Posture as of 1 August 2026 | eth_sign gone from MetaMask and from Safe’s interface. Signing a bare digest through personal_sign still works everywhere |
| Defensive action | Refuse any request that resolves to a hash. Keep on-device blind signing switched off |
What the record shows
What is a wallet actually asking you to sign?
The cryptography operates on 32 bytes. Whatever appears on the confirmation screen, the private key signs a digest, and the wallet’s job is to work backwards from the request to a sentence a person can check. Two standards decide whether that is possible.
EIP-191, “Signed Data Standard”, by Martin Holst Swende and Nick Johnson, created 20 January 2016 and Final, sets the envelope: 0x19 <1 byte version> <version specific data> <data to sign>. The leading 0x19 exists for one reason, stated in the specification: “The initial 0x19 byte is intended to ensure that the signed_data is not valid RLP.” Properly framed signed data can never also be a valid Ethereum transaction. The standard registers three version bytes: 0x00 for data with an intended validator, 0x01 for EIP-712 structured data, and 0x45 for personal_sign.
EIP-712, “Typed structured data hashing and signing”, by Remco Bloemen, Leonid Logvinov and Jacob Evans, created 12 September 2017 and Final, supplies the structure. Its motivation section names the problem in one sentence: “Currently signed messages are an opaque hex string displayed to the user with little context about the items that make up the message.” An EIP-712 payload carries field names, field types, and a domain that includes the verifying contract, so a wallet can render a named recipient and a token amount in place of a hash.
A wallet shows words when the request contains named fields, and hex when it contains a digest. The site asking makes that choice, and the signer sees only the result.
| Request | What is signed | What a wallet can show | What it authorises |
|---|---|---|---|
eth_sendTransaction |
An RLP transaction: to, value, data, gas, nonce |
Recipient, amount, and the decoded call where the ABI is known | One on-chain action, after which the nonce is spent |
personal_sign |
0x19 0x45, Ethereum Signed Message:\n, length, message |
The message, where it is text. A 32-byte digest has no text form | Whatever a contract accepts it for, including a Safe transaction |
eth_signTypedData_v4 |
0x19 0x01, the domain separator, the struct hash |
Every field by name and type, plus the verifying contract | Whatever the struct describes, up to a standing allowance |
permit (ERC-2612) |
An EIP-712 Permit: owner, spender, value, nonce, deadline |
Spender, amount and expiry, by name | An allowance any address may submit until the deadline |
eth_sign |
A bare 32-byte digest. MetaMask’s build applied no prefix | The hex | Anything the digest turns out to stand for |
Why eth_sign was deleted twice
Ethereum’s own JSON-RPC specification still lists the method. src/eth/sign.yaml in ethereum/execution-apis describes eth_sign as returning “an EIP-191 signature over the provided data”, and go-ethereum’s TransactionAPI.Sign signs keccak256("\x19Ethereum Signed Message:\n" + len(message) + message). On a node, the method prefixes.
MetaMask’s build did not. MetaMask Improvement Proposal 3, “Discontinuing the eth_sign signature method”, created 15 May 2024 with status Accepted, states the objection: the method “allows blind signing of arbitrary data without the \x19Ethereum Signed Message prefix, posing a significant phishing risk”, and the danger is that “the hex data being signed can’t be decoded and made readable to the signer”. Without the prefix the signed bytes are indistinguishable from a transaction hash, which is the failure the 0x19 byte was introduced to close in 2016.
MetaMask pull request 24756, “chore: Fully remove eth_sign“, was opened on 23 May 2024 and merged on 1 August 2024, touching 115 files for 159 additions and 2,294 deletions. Its manual test step is a check any reader can run: call the method from the test dapp and expect Error: The method "eth_sign" does not exist / is not available. MetaMask’s current signing documentation offers eth_signTypedData_v4 and personal_sign, and marks eth_sign deprecated.
Safe removed its own fallback three days after the Bybit theft. Pull request 5090, “Fix(Blind signing): remove eth_sign”, was opened by katspaugh at 09:49:17 UTC on 24 February 2025 and merged at 13:51:42 UTC the same day against the release branch, changing 9 files for 33 additions and 166 deletions. The body reads in full: “Remove the outdated eth_sign signing fallback.”
The deleted code is the part worth reading. apps/web/src/services/tx/tx-sender/sdk.ts held a function returning the methods a Safe of a given version would accept:
export const getSupportedSigningMethods = (safeVersion) => {
if (!hasSafeFeature(SAFE_FEATURES.ETH_SIGN, safeVersion)) {
return [SigningMethod.ETH_SIGN_TYPED_DATA]
}
return [SigningMethod.ETH_SIGN_TYPED_DATA, SigningMethod.ETH_SIGN]
}
tryOffChainTxSigning then looped over that list, calling sdk.signTransaction with each method in turn and swallowing any error that was neither a user rejection nor the last attempt. A wallet that failed to produce an EIP-712 signature fell through to ETH_SIGN without asking. The replacement is a single line: return sdk.signTransaction(safeTx, SigningMethod.ETH_SIGN_TYPED_DATA). The same loop was removed from tryOffChainMsgSigning in apps/web/src/utils/safe-messages.ts.
One naming detail matters for what follows. Safe’s ETH_SIGN path did not call eth_sign. It called signMessage over the raw 32 bytes of the Safe transaction hash, which reaches a wallet as personal_sign, then adjusted the recovery byte. The comment in Safe’s own protocol-kit gives the rule: “The Safe’s expected V value for ECDSA signature is: 27 or 28; 31 or 32 if the message was signed with a EIP-191 prefix. Should be calculated as ECDSA V value + 4.”
The Bybit transaction, field by field
Safe’s transaction service, queried again on 1 August 2026, returns the exploit transaction on Safe 0x1Db92e2EeBC8E0c075a02BeA49a2935BcD2dFCF4 as nonce 71, to0x96221423681A6d52E184D440a8eFCEbB105C7242, value 0, operation 1, safeTxGas 45,746, baseGas 0, gasPrice 0, and both gasToken and refundReceiver at the zero address. In Safe’s Enum.sol the Operation enum has two members, Call at 0 and DelegateCall at 1. The data field is 68 bytes: the selector 0xa9059cbb for transfer(address,uint256), the address 0xbDd077f651EBe7f7b3cE16fe5F2b025BE2969516, and an amount of zero. All three confirmations carry signatureTypeETH_SIGN, and each signature ends in 0x1f, decimal 31.
A Safe transaction is EIP-712 typed data by construction. encodeTransactionData in the contract version Bybit’s wallet ran returns abi.encodePacked(byte(0x19), byte(0x01), domainSeparator, safeTxHash), where safeTxHash is the keccak hash of SAFE_TX_TYPEHASH and ten fields. Hashing the type string SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce) reproduces the constant 0xbb8310d4…86d8 compiled into the contract, and hashing EIP712Domain(address verifyingContract) reproduces 0x035aff83…4749. operation is a named field of that struct. Taking the ten values as the transaction service returns them and running that computation returns 0xb3476d061aeb8fc1d605a873c483a2402d88a68a9cdd1a8b47655dd55ba004f8, the safeTxHash the service records.
What the three devices were handed was that hash. In the Safe contract, checkNSignatures branches on the recovery byte:
} else if (v > 30) {
// If v > 30 then default va (27,28) has been adjusted for eth_sign flow
currentOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s);
}
Recovering each of the three signatures confirms which path was taken. Against the prefixed digest 0x28eddb7e7d1dca66673b339ca554c63603dede0512d6da0300cf782f68a8a260, all three recover to the owner addresses the transaction service names: 0x1F4EB0a9…2211, 0x3Cc3A225…C21A and 0xe3dF2cCE…5a9E. Against the unprefixed hash, the same three signatures recover to three addresses that own nothing. The signers used the prefixed personal-message path over a 32-byte digest. The full account of the theft, the front-end compromise behind it and the FBI attribution is in Bybit: the largest theft in the asset class.
What hardware clear signing does and does not decode
Ledger’s own Academy page, “What Is Clear Signing?”, updated 16 April 2026, defines the term as two requirements: an intent showing what kind of approval is being sought and by which application, and transaction fields written in language the reader understands. Ledger’s framing of the alternative is that asking someone to blind sign is “like asking them to sign a blank check”.
Ledger also documents the limit, in its own words: “Occasionally, you may interact with a new or complex smart contract that the app cannot fully interpret. In these cases, the device cannot display the transaction details in a clear, human-readable format. Instead, it will show a blind signing warning and display only the raw hash.” Ledger states that blind signing is disabled by default and that enabling it means opening the relevant coin app, going to Settings, and accepting a warning.
The decoding depends on a description file the device can find. ERC-7730, “Structured Data Clear Signing Format”, created 7 February 2024 and still Draft, defines a JSON format carrying the formatting information needed to render calldata, an EIP-712 message or an ERC-4337 user operation. Ledger names the Generic Parser, shipped in 2025, as the component that reads those files in place of the per-application plugins that had capped adoption, and says governance of the standard has moved to the Ethereum Foundation. That transfer is checkable: github.com/LedgerHQ/clear-signing-erc7730-registry returns a 301 to github.com/ethereum/clear-signing-erc7730-registry. Counting the registry tree on 1 August 2026 gives 247 calldata descriptors and 125 EIP-712 descriptors across 51 projects.
Device support for typed data is older than the registry. Trezor’s firmware changelog records “Ethereum – support for EIP712 – signing typed data” in 2.4.3, released 8 December 2021, and message-hash display for EIP-712 in 2.9.1 on 17 September 2025.
Support on the device does not settle what the software in front of it sends. MetaMask’s signing documentation carries a standing note: “MetaMask supports signing transactions using Trezor and Ledger hardware wallets. These wallets only support signing data using personal_sign.” Rabby issue 1816, “Support clear-sign for EIP712 on Ledger”, opened on 23 October 2023, records that “Ledger show only hashes when user prompted to sign EIP712 with Rabby and there is no easy way to verify what these hashes correspond to”. It closed on 6 March 2025. Rabby issue 2969, opened 21 May 2025 and still open, reports the wallet showing “Unknown Signature Type” on a standard personal_sign login while still allowing the user to sign.
What I think is going on
The phrase does a lot of damage. “Blind signing” sounds like a toggle somebody left on, so people go looking for the setting, find it, switch it off, and believe they have dealt with the problem.
Look at what the Bybit hash actually is. Ten fields. to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce. Every one named and typed in a struct definition compiled into the contract since 2019, and every one recoverable by anyone willing to spend twenty lines of Python on it, as I did above. The information was never destroyed. It got flattened into 32 bytes on its way to the screen.
Sit with the fourth field on that list. operation was set to 1, which meant delegatecall, which meant Bybit’s wallet was about to run somebody else’s code against its own storage. Signed as EIP-712 typed data, operation would have reached the device with its name attached, and the devices could do that: Trezor shipped EIP-712 in December 2021 and Ledger had been building clear signing for years. What arrived at the secure screen was a hash, and a secure screen displaying a hash is a very expensive way to look at noise.
Here is the part I had not expected to find.
Nobody chose eth_sign. A loop chose it. The function Safe deleted in PR 5090 tried EIP-712 typed data first, caught whatever came back, checked whether the user had rejected, and if not, moved to the next method on the list. That next method was ETH_SIGN. It fires on error, which is the exact condition under which nobody is watching. A hardware wallet that could not produce a typed-data signature through whatever middleware sat in the path would degrade to a bare digest, and the person holding the device would see a different screen with no explanation of why. Nobody wrote that as a bug. It is a compatibility fallback, and fallbacks are the first thing I would go looking for in any wallet stack I was reviewing, because they get written once and then never read again.
The naming precision is worth having. Those three signatures carry v = 31, and 31 only recovers correctly with the \x19Ethereum Signed Message:\n32 prefix applied. I checked both paths. So what Safe’s interface labelled ETH_SIGN reached the wallet as personal_sign over a 32-byte digest, and MetaMask deleting eth_sign in August 2024 would not have stopped it. personal_sign is a supported, recommended, present-tense method in every wallet shipping today, and you can hand it any 32 bytes you like.
Which is why EIP-712 is not the answer people want it to be. ERC-2612 permit is EIP-712. Fully typed, fully renderable, and on Scam Sniffer’s measurement the largest single source of the phishing losses it tracks, which I go through in Approval phishing and the drainer economy. A wallet can show you spender, value and deadline in plain language and you can still approve them, because reading a request and judging a counterparty are different skills and only the first one has been standardised. Clear signing tells you what. It has nothing to say about whether.
Then the coverage number, which I think is the most honest thing on this page. 372 descriptors, 51 projects. That is the shared registry the whole clear-signing effort runs on, as of this morning. Set it against the number of deployed contracts a person might plausibly touch: a device decodes what it has a description for and shows a hash for everything else. New protocol, anything launched last week, anything built to be unfamiliar. The attacker picks the contract, so the attacker also picks whether your device can read it.
The timelines tell you the industry knew. Rabby’s Ledger EIP-712 issue sat open from October 2023 to March 2025. MetaMask took from May 2024 to August 2024 to finish removing a method it had already disabled by default a year earlier. Safe took three days, and it took a billion-dollar loss to buy those three days.
What reduces your exposure
No product fixes this. What follows narrows specific risks and leaves others untouched, which is worth saying at the top and not only at the end.
Treat a hash on the screen as the end of the conversation. If the confirmation shows hex, a field labelled data with no explanation, or a message that is plainly a digest, nothing on that screen can be checked by a person. Reject it and go and find out what the site was asking for. That removes the class of loss where you would have refused had you been able to read the request, and does nothing about a request you can read and approve anyway.
Leave on-device blind signing switched off. Ledger ships it disabled. Every reason you will be given to enable it is a reason the application in front of you could not describe what it wanted. Keeping it off costs you access to legitimate contracts that lack a descriptor, which is a real inconvenience and the correct trade.
Prefer a wallet that decodes the call, and know what that buys. A wallet that turns calldata into a sentence with a token name, an amount and a counterparty is doing the work that matters at the moment of signing. It is still software on a machine, and at Bybit the surface computing the display and the surface computing the payload were the same compromised code. A decoded screen narrows the risk that you sign something you could not read. It does not establish that the screen is telling the truth.
Give configuration changes their own approval path. Owners, threshold, modules, the guard, the singleton or master-copy pointer, and on a Safe transaction the operation field. A payment moves value once. These change the rules governing every future payment, so they deserve a separate path and a second reader. Most signing interfaces put them on the same one as a transfer.
If you operate a treasury, rebuild the hash somewhere else. The EIP-712 computation is deterministic and public. Take the fields from the transaction service, compute the digest on a machine that did not load the front end, and compare it against what the device shows. Every step appears in the verification log below. It catches a tampered payload and cannot catch a payload that is exactly what the proposer intended and wrong anyway.
Two limits to hold on to. A signature you have already produced cannot be recalled, so none of this helps after the fact. And clear signing improves what you are shown without improving the decision you make once you have seen it.
Sources
- EIP-191, “Signed Data Standard”, Martin Holst Swende and Nick Johnson, created 20 January 2016, status Final.
https://eips.ethereum.org/EIPS/eip-191 - EIP-712, “Typed structured data hashing and signing”, Remco Bloemen, Leonid Logvinov and Jacob Evans, created 12 September 2017, status Final.
https://eips.ethereum.org/EIPS/eip-712 - ERC-2612, “Permit Extension for EIP-20 Signed Approvals”, created 13 April 2020, status Final.
https://eips.ethereum.org/EIPS/eip-2612 - ERC-7730, “Structured Data Clear Signing Format”, created 7 February 2024, status Draft.
https://eips.ethereum.org/EIPS/eip-7730 - Ethereum execution APIs,
src/eth/sign.yaml.https://github.com/ethereum/execution-apis/blob/main/src/eth/sign.yaml - go-ethereum,
internal/ethapi/api.go,TransactionAPI.Sign.https://github.com/ethereum/go-ethereum/blob/master/internal/ethapi/api.go - MetaMask Improvement Proposal 3, “Discontinuing the eth_sign signature method”, 15 May 2024, status Accepted.
https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-3.md - MetaMask extension, pull request 24756, “chore: Fully remove
eth_sign“, merged 1 August 2024.https://github.com/MetaMask/metamask-extension/pull/24756 - MetaMask developer documentation, “Sign data”.
https://docs.metamask.io/wallet/how-to/sign-data/ - Safe wallet monorepo, pull request 5090, “Fix(Blind signing): remove eth_sign”, opened and merged 24 February 2025.
https://github.com/safe-global/safe-wallet-monorepo/pull/5090 - Safe smart account contracts v1.1.1 and v1.3.0,
GnosisSafe.solandcommon/Enum.sol.https://github.com/safe-global/safe-smart-account - Safe protocol-kit,
src/utils/signatures/utils.ts,adjustVInSignature.https://github.com/safe-global/safe-core-sdk - Safe transaction service API, multisig transaction
0xb3476d061aeb8fc1d605a873c483a2402d88a68a9cdd1a8b47655dd55ba004f8, queried 1 August 2026.https://api.safe.global/tx-service/eth/api/v1/ - Ledger Academy, “What Is Clear Signing?”, published 17 July 2024, updated 16 April 2026.
https://www.ledger.com/academy/topics/ledgersolutions/what-is-clear-signing - ERC-7730 clear-signing registry.
https://github.com/ethereum/clear-signing-erc7730-registry - Trezor firmware,
core/CHANGELOG.md, releases 2.4.3, 2.6.0 and 2.9.1.https://github.com/trezor/trezor-firmware/blob/main/core/CHANGELOG.md - Rabby, issue 1816, “Support clear-sign for EIP712 on Ledger”, 23 October 2023 to 6 March 2025.
https://github.com/RabbyHub/Rabby/issues/1816 - Rabby, issue 2969, “Unknown Signature Type”, opened 21 May 2025.
https://github.com/RabbyHub/Rabby/issues/2969
Related on this desk
- The 3 best crypto cold wallets — the device side of the same question.
Disclosure. Max Avery is affiliated with Digital Ascension Group (DAG). Investment advisory services are offered through DAG Wealth, an SEC-registered investment adviser (CRD No. 328627). Registration does not imply a certain level of skill or training. DAG is not a law firm and does not provide legal or tax advice. Custody arrangements with third-party independent qualified custodians reduce certain risks but do not eliminate them. Nothing here is investment, legal, or tax advice, or a recommendation to buy or sell any asset. This article describes matters of public record; charges are allegations and defendants are presumed innocent unless and until proven guilty.