Skip to content

Repository files navigation

Sky Safe Transaction Decoder

"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.

What It Does

  1. Fetches transaction data from the Safe Transaction Service API
  2. Calculates EIP-712 hashes (Domain, Message, Safe TX) independently
  3. Verifies the calculated hash matches the API-provided hash
  4. Re-encodes decoded parameters and compares with raw calldata
  5. Runs security analysis (delegate calls, gas token attacks, owner modifications)
  6. 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

Quick Start

CLI

Always pin the version. Unpinned npx runs 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 520

Or install globally with a pinned version:

npm install -g @shield3/sky-safe-cli@0.3.0
sky-safe verify --address 0x... --nonce 520

Web UI

Or run locally:

pnpm install
pnpm --filter @shield3/sky-safe-ui dev

Offline single-file build

For 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.

Verifying a release build

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 release

This 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.

Address Config CSVs

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,active

See 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.

Managing My Safes
  • 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.

From Source

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

Supported Networks

Network Chain ID
Ethereum Mainnet 1
Base 8453
Sepolia Testnet 11155111

Project Structure

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)

Security Analysis

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

Example Warnings

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.json

Custom Decoders

The 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.

Sourcify Fallback

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).

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass (pnpm test)
  5. Submit a pull request

Publishing to npm

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 *.tgz

Trust Assumptions

Users 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 int256 where the contract has uint256 renders the word 0xffff…ff9c as -100 rather 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.

License

AGPL-3.0-only (matching upstream)

References

About

No description, website, or topics provided.

Resources

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages