tl;dr
Signing Safe (EIP-712) transactions carries risks due to UI spoofing—a vulnerability where malicious interfaces can deceive users into signing harmful transactions (as seen in the $1.46B Bybit hack). The fully offline web app was introduced to calculate tx hashes without using Safe API.
1. Problem Statement: UI Spoofing Risk in Safe Transaction Signing
The EIP-712 standard used by Safe multisig wallets requires users to sign a structured data hash to approve transactions. A significant vulnerability occurs when a dApp or web wallet’s user interface is compromised or maliciously crafted to display transaction details that don’t match the actual parameters used to generate messageHash
, domainHash
and safeTxHash
presented to the user’s wallet (e.g., MetaMask, Ledger) for signing.
This UI spoofing and transaction data mismatch attack vector was used in the Bybit security incident on February 21, 2025, when signers were reportedly tricked by a manipulated UI into signing hashes for malicious transactions disguised as routine operations, leading to an estimated $1.46 billion loss. This highlighted the critical need for signers to independently verify the hash derived from the true, intended transaction parameters before signing.
2. Solution Overview: Safe Transaction Hash Verifier Tool
There’s a CLI solution made by pcaversaccio, which was later forked and upgraded by Cyfrin to work in offline mode.
The Lido DAO Tech Infra team has developed a standalone, fully offline web app containing JavaScript code that functions as a client-side calculator for Safe EIP-712 transaction hashes. This solution is based on the offline script, but tailored for signers within Lido DAO to simplify the mundane process of signing multisig transactions without needing to interact with the CLI.
- Purpose: To allow Safe users to independently generate the expected hashes from manually entered transaction parameters.
- Core Function: It replicates the EIP-712 hash generation process specific to Safe contracts, allowing users to verify the hash presented by their signing wallet against a hash calculated from known, intended inputs.
- Environment: The tool runs entirely within the user’s web browser using JavaScript. It uses the
ethers.js
library, which is fully embedded within the HTML file’s script section, for cryptographic operations and ABI encoding. Beyond initial file loading, it requires no backend or external network calls. The tool is distributed via IPFS and the initial repo cloning.
3. Technical Implementation Details
The tool implements the EIP-712 typed data signing standard as required by various versions of the Safe smart contracts.
- Core Standard: EIP-712 (
eth_signTypedData_v4
structure). - Hashing Algorithm:
keccak256
, accessed via the embeddedethers.js
library (ethers.keccak256()
andethers.solidityPackedKeccak256()
). The embedded library originates from a specific officialethers.js
v6 permalink. - Key Calculations & Logic:
- Version Handling: The tool includes logic (
compareVersions
function) to adapt calculations based on the provided Safe contractversion
string (e.g., “1.3.0”). - Domain Separator (
domainHash
):- For Safe versions
< 1.3.0
: Calculated usingkeccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH_OLD, verifyingContract))
whereverifyingContract
is the Safe address. - For Safe versions
>= 1.3.0
: Calculated usingkeccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, chainId, verifyingContract))
, incorporating thechainId
. The correctDOMAIN_SEPARATOR_TYPEHASH
constant is used based on this version check. Encoding is performed viaethers.AbiCoder.defaultAbiCoder().encode()
.
- For Safe versions
- Message Payload (
messageHash
):- The
SafeTx
struct parameters are ABI-encoded. The struct definition (and thus theSAFE_TX_TYPEHASH
) differs slightly for versions< 1.0.0
(usingdataGas
instead ofbaseGas
). The tool selects the appropriateSAFE_TX_TYPEHASH
constant (SAFE_TX_TYPEHASH
orSAFE_TX_TYPEHASH_OLD
). - Parameters encoded:
(bytes32 typeHash, address to, uint256 value, bytes32 dataHash, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 nonce)
. Note:data
itself is first hashed (ethers.keccak256(data)
) before being included in the struct encoding. - The ABI-encoded struct string is then hashed:
messageHash = ethers.keccak256(encoded_struct_data)
.
- The
- Final Signed Hash (
safeTxHash
): The final EIP-712 hash digest is constructed according to the standard:safeTxHash = ethers.solidityPackedKeccak256(["bytes1", "bytes1", "bytes32", "bytes32"], ["\x19", "\x01", domainHash, messageHash])
.
- Version Handling: The tool includes logic (
- Input Parameters: Required user inputs:
chainId
,safeAddress
,toAddress
,value
(wei),data
(hex),operation
(0 or 1),nonce
.- It also takes Safe-specific parameters, pre-filled with defaults but editable:
version
(“1.3.0”),safeTxGas
(0),baseGas
(0),gasPrice
(0),gasToken
(address(0)),refundReceiver
(address(0)). - Optional fields allow inputting hashes from external sources for comparison:
domainHashProvided
,messageHashProvided
,safeTxHashProvided
.
- Output Hashes: The tool displays the calculated:
safeTxHash
,domainHash
,messageHash
. - Runtime Requirements: Requires a modern web browser with JavaScript enabled. The necessary
ethers.js
library is self-contained within the HTML file. This ensures the tool functions fully offline after loading and does not depend on external script hosting services (CDNs). - Code Structure: Key JavaScript functions include
calculateHashes
,calculateDomainHash
,compareVersions
,validateAndGetVersion
, along with DOM manipulation for input/output and event handling, all within theindex.html
file.
4. Suggested Verification Process
The process includes verifying the integrity of the tool itself before using it to verify transaction hashes:
- Tool Acquisition: Obtain the
index.html
tool file exclusively from trusted sources, such as one’s internal repository clone or the official, verified IPFS link (see Section 6). - Tool Integrity Verification (
index.html
- SHA-384): Before execution, verify the integrity of the obtainedindex.html
file.- Calculate the SHA-384 hash of the downloaded
index.html
file using standard methods (e.g.,openssl dgst -sha384 -binary index.html | openssl base64 -A
, or verified online tools). - This calculated hash must match the known-good hash provided in one’s official documentation (see Section 6:
d2VLr...
). A mismatch indicates the file may have been tampered with; in this case, do not proceed.
- Calculate the SHA-384 hash of the downloaded
- Embedded Library Verification (
ethers.js
snippet - SHA-384): Specifically verify the integrity of the embeddedethers.js
code within the HTML file.- The JavaScript code block within the
<script>
tag (originating from the specifiedethers.js
v6 permalink) should be extracted into a temporary file. - Calculate the SHA-384 hash of this extracted code snippet (e.g., using
openssl dgst -sha384 -binary ethers_extracted.js | openssl base64 -A
). - This hash must match the known-good hash for the embedded library (see Section 6:
NRAZj...
). A mismatch prevents usage and indicates potential tampering.
- The JavaScript code block within the
- Tool Execution Environment: After verifying both the
index.html
file and the embedded library code through hash comparison, open theindex.html
file in a clean browser instance with all extensions disabled to minimize potential external interference. - Parameter Gathering & Entry: Gather the precise transaction parameters from the verified source and meticulously input them into the now-verified tool’s form, double-checking for accuracy.
- Hash Generation: The “Generate Hashes” button is used to calculate the
safeTxHash
,domainHash
, andmessageHash
based on the entered parameters. - Wallet Interaction & Comparison: The signing process is initiated via standard procedures. When the hardware or software wallet presents the final EIP-712 hash for signing, perform an exact, character-by-character comparison between this wallet-presented hash and the
safeTxHash
generated by the verified tool. - Signing Decision Protocol:
- On Match: If the hashes match exactly, it provides strong cryptographic assurance that the signature request corresponds to the intended parameters. Proceed with approving the signature.
- On Mismatch: If there is any discrepancy, immediately abort the signing process. A mismatch requires an internal review to determine the cause before taking any further action on this transaction.
5. Security Benefit
This two-stage verification process—verifying the tool and then the transaction hash—provides a defense against UI spoofing attacks. It confirms that the tool in use is untampered and then uses this tool to confirm the transaction hash matches the intended parameters. This directly addresses the interaction-layer risks exposed by the Bybit incident.
6. Availability and Verification Hashes
- Distribution: The
safe-tx-hashes-ui
tool (index.html
) is available via:- GitHub Repository: GitHub - lidofinance/safe-tx-hashes-ui
- Source
ethers.js
Permalink: The embedded library code originates from this specific file version:https://github.com/ethers-io/ethers.js/blob/ce7212d03d6867081603794f0480f31d053823c4/dist/ethers.umd.min.js
(Raw Link) - Usage: Download/save the verified
index.html
file. Open it locally in your web browser. No installation or internet connection (after loading) required.
7. Disclaimer
While this tool and process aid in verification, its effectiveness relies on users carefully performing integrity checks and accurately inputting all transaction parameters. Any incorrect checks or inputs will compromise the verification process. Users must fully understand the transaction they intend to sign.