"Don't trust, verify!" - Independently verify Safe transaction hashes before signing.
TypeScript tool for calculating and verifying Safe multisig transaction hashes with security analysis and protocol-specific decoding. Available as a CLI, web UI, and core library.
Based on pcaversaccio/safe-tx-hashes-util.
- Fetches transaction data from the Safe Transaction Service API
- Calculates EIP-712 hashes (Domain, Message, Safe TX) independently
- Verifies the calculated hash matches the API-provided hash
- Re-encodes decoded parameters and compares with raw calldata
- Runs security analysis (delegate calls, gas token attacks, owner modifications)
- Displays results with protocol-specific decoding when available, falling back to a verified ABI from Sourcify when nothing else decodes the call
Safe versions: v0.1.0 through v1.5.0
Always pin the version. Unpinned
npxruns whatever the registry serves at request time, which means a malicious publish could be executed before anyone notices. The examples below pin to a known-good version — bump it intentionally when you upgrade. See SECURITY.md for the recommended verification flow (build from source) for high-assurance use.
npx @shield3/sky-safe-cli@0.3.0 verify \
--address 0xf65475e74C1Ed6d004d5240b06E3088724dFDA5d \
--nonce 520Or install globally with a pinned version:
npm install -g @shield3/sky-safe-cli@0.3.0
sky-safe verify --address 0x... --nonce 520- Cloudflare Pages: sky-safe-tx-decoder.pages.dev
- IPFS: sky-safe-tx-decoder
Or run locally:
pnpm install
pnpm --filter @shield3/sky-safe-ui devFor the highest-assurance setup — no hosting provider, no server, no npm to
run — build a single self-contained HTML file you open directly in a browser:
pnpm install
pnpm --filter @shield3/sky-safe-ui build:offline
# → packages/ui/dist-offline/index.html (double-click to open)Everything (JS + CSS) is inlined into that one file, so it runs from file://
with no external requests for app code. Audit it once, optionally pin its hash,
and run it from disk — the verification code is yours, not re-served by a CDN on
every visit. It still fetches transaction data from the Safe Transaction Service
API, so an internet connection is required; only the app code is local.
This tool is what you use to decide whether to sign a transaction, so the
integrity of the tool itself is security-critical: a tampered build could show
"✓ hashes match" when they don't and trick you into signing. So a release isn't
just a file you have to trust — it ships with a cryptographic proof of where it
came from. Each tagged release attaches the offline index.html, its .sha256,
and a keyless build-provenance attestation.
Three independent layers, weakest to strongest:
- SHA-256 checksum — integrity. Confirms the file wasn't altered versus the published value. (On its own, weak: whoever can swap the file can change the number next to it.)
- Build-provenance attestation — authenticity + origin. Cryptographically proves this exact file was produced by GitHub Actions, building this repo at a specific commit, via the official release workflow — not from someone's laptop or modified source. It is keyless (signed by the workflow run's verified identity via Sigstore + GitHub OIDC — no secret key a maintainer could leak) and recorded in a public transparency log.
- Reproducible build — don't trust, verify. The build is deterministic, so you can rebuild from source and get a byte-identical file. You don't have to trust the published binary at all.
After downloading, verify before trusting:
# Integrity — matches the published checksum
shasum -a 256 -c sky-safe-tx-decoder-<tag>.html.sha256
# Provenance — confirms GitHub Actions built this exact file from this repo/commit
gh attestation verify sky-safe-tx-decoder-<tag>.html --repo 0xShield3/sky-safe-tx-decoder
# Strongest — reproduce it yourself and compare the hash
git checkout <tag>
pnpm --filter @shield3/sky-safe-ui build:offline
shasum -a 256 packages/ui/dist-offline/index.html # should match the releaseThis protects against a swapped or backdoored release file, a hand-uploaded build from a compromised maintainer account, and tampering between source and binary. It does not prove the source code itself is bug-free (audit or trust the code separately), and verification is a one-time check before you open the file — the browser doesn't re-check it at runtime.
The web UI loads two independent, session-only CSV files by drag-and-drop. They share one schema but play different roles, so updating one never disturbs the other:
| File | Role | In the app |
|---|---|---|
| Address book | Managed externally (team-owned). Labels known addresses during review. | Read-only. Replace by loading a fresh file. |
| My Safes | Your personal Safe shortcuts (the home-page dropdown). | Editable (add/remove) and exportable. |
Each file self-identifies with a marker comment on the first line, so the UI can reject a file dropped on the wrong slot:
# sky-safe-config: address-book
address,label,verification_date,status
0xCe01C90dE7FD1bcFa39e237FE6D8D9F569e8A6a3,Sky LockstakeEngine,2026-05-01,active# sky-safe-config: my-safes
type,network,address,label,verification_date,status
safe,ethereum,0xf65475e74C1Ed6d004d5240b06E3088724dFDA5d,Treasury Safe,2026-05-10,activeSee examples/address-book-template.csv
and examples/my-safes-template.csv. Older
address books with only address,label,verification_date,status still load.
Config is kept in memory for the current browser session and is not persisted to local storage — the CSV files you keep externally are the source of truth. This is intentional for a signing tool: a cached copy could be silently tampered with or drift stale, so each session you re-load fresh files.
- Settings page (top-right link): add a Safe (network + address + label), edit a Safe's label, remove Safes, and export My Safes back to CSV.
- Capture an unsaved Safe: when you open a Safe that isn't in My Safes, a banner offers to label it and add it to the session list. Adding does not download anything — you export once, when you're ready, from Settings (or the config bar).
- Export (config bar or Settings): writes My Safes as a CSV that re-imports exactly (round-trip), with the kind marker. Remember to export to persist your in-session changes.
- Because the address book and My Safes are separate files, you can update the managed address book anytime without losing your Safes.
git clone https://github.com/0xShield3/sky-safe-tx-decoder
cd sky-safe-tx-decoder
pnpm install
pnpm build
# Run CLI
pnpm dev:cli verify --address 0x... --nonce 520
# Run UI dev server
pnpm --filter @shield3/sky-safe-ui dev| Network | Chain ID |
|---|---|
| Ethereum Mainnet | 1 |
| Base | 8453 |
| Sepolia Testnet | 11155111 |
packages/
core/ # @shield3/sky-safe-core - Hash calculation, decoding, security analysis
cli/ # @shield3/sky-safe-cli - Command-line interface
ui/ # @shield3/sky-safe-ui - Web interface (React + Vite)
The tool detects:
- Untrusted delegate calls - Flags delegate calls to contracts not on the trusted list
- Gas token attacks - Custom gas token + custom refund receiver combinations
- Owner/threshold modifications - Direct and nested (via MultiSend) changes to Safe owners
The examples/ directory contains crafted transactions that trigger each warning type:
sky-safe verify --file examples/gas-token-attack.json
sky-safe verify --file examples/untrusted-delegatecall.json
sky-safe verify --file examples/owner-modification.json
sky-safe verify --file examples/guard-set.json
sky-safe verify --file examples/module-enable.json
sky-safe verify --file examples/multiple-issues.jsonThe decoder registry provides protocol-specific human-readable transaction explanations. A decoder is pinned to one contract address on one network — it never guesses an ABI from a selector lookup. Every decoding is re-encoded and byte-compared against the raw calldata before it is shown.
Currently included:
| Protocol | Contract | Signatures |
|---|---|---|
| Sky Protocol LockstakeEngine | 0xCe01C90dE7FD1bcFa39e237FE6D8D9F569e8A6a3 |
13 — urn management, staking, borrowing, delegation, rewards, multicall |
| Sky Protocol SPBEAM | 0x36B072ed8AFE665E3Aa6DaBa79Decbec63752b22 |
7 — set, file (two overloads), rely, deny, kiss, diss |
| Sky Protocol StUsdsRateSetter | 0x30784615252B13E1DbE2bDf598627eaC297Bf4C5 |
7 — set, file (two overloads), rely, deny, kiss, diss |
| MultiSend | Standard Safe MultiSend | Batch transaction decoding |
SPBEAM sets stability fees and the savings rate within governance-configured bounds.
The Safe Transaction Service does not decode it. Rate values show basis points and
percentage; ilk identifiers show their ASCII label alongside the full bytes32.
StUsdsRateSetter sets the stUSDS savings rate, ilk duty, debt ceiling and supply cap within governance-configured bounds. The Safe Transaction Service holds no ABI for this contract at all.
When no decoder covers a contract and the Safe Transaction Service returns nothing, the Sourcify fallback fetches the contract's verified ABI and decodes with that instead. See Sourcify fallback below.
See the core package README for how to add your own.
Web UI only. When the Safe Transaction Service returns no decoded data and no built-in decoder covers the contract, the app fetches the contract's verified ABI from Sourcify and decodes with that.
- Sourcify serves the ABI of source that has been verified against the contract's on-chain bytecode. This is stronger than a selector database: it is not "someone submitted a signature for these four bytes", it is "this is the ABI of the exact contract deployed at this address".
- Proxies are followed. EIP-1967 proxies are resolved to their implementation, so proxied calls decode instead of showing as raw bytes. The implementation address is read from chain state via a public RPC and is shown alongside the decoding.
- Contracts missing from Sourcify are imported. On a miss, the app asks Sourcify to import and verify the source from a block explorer, then retries. No Etherscan API key is needed.
- The result is re-encode-verified, exactly like a Safe API decoding. A decoding that does not re-encode to the signed bytes is shown as a hard warning, never as a decoding.
Controlled by a Settings toggle, on by default. The request to Sourcify reveals which contract — and so which transaction — is being inspected. A signer who does not want that egress can turn it off, and then no request is made.
The CLI does not use this fallback (#19).
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass (
pnpm test) - Submit a pull request
Use pnpm pack then npm publish on the tarball. This ensures workspace:* dependencies are resolved to real versions. Do not use npm publish directly from a package directory.
# Core (publish first)
cd packages/core && pnpm pack && npm publish shield3-sky-safe-core-*.tgz --access public && rm *.tgz
# CLI
cd ../cli && pnpm pack && npm publish shield3-sky-safe-cli-*.tgz --access public && rm *.tgzUsers must trust:
- This TypeScript implementation
- viem cryptographic library
- Safe Transaction Service API data — however, the tool independently re-encodes decoded parameters and verifies they match the raw calldata and EIP-712 hashes, so API-provided decoded data is not blindly trusted
- Hardware wallet secure screen
When the Sourcify fallback is enabled, two further parties are trusted for how a decoding is labelled and rendered:
- Sourcify, for the ABI.
- The public RPC, for a proxy's implementation address.
The re-encode check pins the bytes, not their interpretation. It proves that the decoded call re-encodes to exactly the calldata being signed — selector included. It does not prove that the ABI describes what the contract does.
What a hostile ABI can change:
- Parameter names, freely. The names are not part of the selector.
- The function name and the argument types, at the cost of a 4-byte selector collision.
Roughly 2^32 hashes, which is cheap. Where the head encoding layout is unchanged, this
alters the displayed value itself — declaring
int256where the contract hasuint256renders the word0xffff…ff9cas-100rather than a number close to 2^256. The decoding still re-encodes to the signed bytes and is still marked verified.
A decoding is therefore never a substitute for knowing what the contract does. Turning the fallback off in Settings removes both parties.
AGPL-3.0-only (matching upstream)