The Safe web app can’t execute a transaction that a PicoQuorum key approved once there are two or
more signatures. It fails with GS021 because the chip’s approval is an EIP-1271 contract signature,
and the web app rebuilds every stored confirmation as a plain signature. Until this is fixed
upstream, tools/quorum live exec executes instead.
Jason submits everything to Safe by hand. This folder holds the drafts and the evidence:
| File | What |
|---|---|
| ISSUE.md | The issue, ready to paste: title plus body in their bug-report template. The reproduction script is inlined in a <details> block |
| PR.md | The pull request, ready to paste: their PR template, with the Mermaid summary they require for AI-authored PRs and the CLA line. Replace #ISSUE with the issue number |
| repro/ | repro.mjs, the self-contained reproduction (how to run it): Anvil fork of Base Sepolia, throwaway keys, protocol-kit’s own buildSignatureBytes for both layouts. It also prints the test fixtures (--fixture) |
| 0001-fix-keep-EIP-1271-contract-signatures-when-rebuildin.patch | The fix and its test, one commit made with git format-patch against their dev at 5717e3a. This is the pre-review version; see Status for what was submitted |
ISSUE.md, PR.md and the patch name no personal address, Safe or transaction. The patch’s test fixture comes from throwaway keys on a local fork. Keep it that way:
temp-safe-screenshots/redacted/ (local
and gitignored). There are four from the desktop browser, and 20–23 from a mobile browser.
The mobile ones are the web app on a phone, not Safe’s mobile app. They were made from macOS text recognition and checked by running it again: no
address, ENS name or timestamp is left, and no identicon either. Look them over once more before
uploading.
SCR-20260925-jtap-redacted.png showing only
the two GS021 messages and “Cannot estimate”. It shows no Safe name, address or clock.Jason McPheron <jason.mcpheron@gmail.com> as author,
and that shows on the PR. To use GitHub’s private address instead, run
git commit --amend --reset-author after setting git config user.email <id>+jmcpheron@users.noreply.github.com.
The id is in GitHub → Settings → Emails. Kept the gmail address (decided 2026-09-25).jmcpheron/safe-wallet-monorepo, branch fix/contract-signatures-existing-tx from dev
at 004b06107a9717What was submitted differs from these drafts. The versions on GitHub are the reference:
toSafeSignature’s doc comment and the test file’s header are cut to one line each, since
their AGENTS.md asks for one-line commentsts-sender.test.ts, mobile create.test.ts), each failing
without the fixFixes #8808, and a Co-Authored-By: Claude trailer#8808 filled in, and the call-site tests describedvalidateTxSignatures now skips contract signatures, and gas
estimation and Tenderly get the corrected bytesWaiting on:
action_required when the PR opened, as
expected for a first-time contributor.# after forking on GitHub
git clone https://github.com/jmcpheron/safe-wallet-monorepo.git && cd safe-wallet-monorepo
git remote add upstream https://github.com/safe-global/safe-wallet-monorepo.git
git fetch upstream
git checkout -b fix/contract-signatures-existing-tx upstream/dev
# the fix, as a commit with its original message and authorship
git am ../pico-quorum/docs/upstream/safe-wallet-gs021/0001-fix-keep-EIP-1271-contract-signatures-when-rebuildin.patch
# once the issue exists, add its number:
git commit --amend # add a line "Fixes #NNNN" above "Refs #3673"
corepack enable && yarn install
yarn workspace @safe-global/utils test src/utils/__tests__/safeTransaction.test.ts
yarn workspace @safe-global/web test src/services/tx/tx-sender
yarn workspace @safe-global/utils type-check && yarn workspace @safe-global/web type-check && yarn workspace @safe-global/mobile type-check
yarn workspace @safe-global/utils lint && yarn workspace @safe-global/web lint
yarn prettier
git push -u origin fix/contract-signatures-existing-tx
# open the PR from the fork's branch into safe-global:dev, with PR.md as the description
If git am doesn’t apply cleanly because dev has moved on, git am --3way usually does. The
change is small, so it can also be redone by hand: the helper and its test, plus two call sites.
Review, 2026-09-25. A review of the drafts led to:
All adopted. The Safe.sol link now points at the GS021 check itself (L306-L316); the suggested range stopped short of it.
What’s been verified so far:
repro/repro.mjs:
ExecutionSuccess, the recipient paid, and the
logged bytes equal the fixed layoutverify --changed passes: type-check, lint, Prettier and the 364 related tests.verify:changed runs 7039 related tests: 3 timed out in two
spaces/Policies form suites that don’t touch this code, and both suites pass on their own
(34/34).repro/repro.mjs was re-run at block 47307399 with the same results. A second run on the same
fork is identical, and --fixture matches the committed test data byte for byte.checkNSignatures it is “the contract signature’s offset points
inside the static part”: s < requiredSignatures * 65.createExistingTx
adds each confirmation with staticPart: () => data, dynamicPart: () => '', isContractSignature: false.
{owner}{offset = 65}{00}{length}{data}, so it is concatenated whole, with its offset still at 65.addSignaturesToTx.'' which is wrong for nested Safe signatures.”
It was closed as “not planned” in Feb 2026, with no fix.EthSafeSignature(signer, data, true) plus
buildSignatureBytes lay contract signatures out correctly (offsets past all the static parts).
The web app just never marks them as contract signatures.Add to this as it comes in: date, chain, what was seen, links.
0xEDEDC0063B23A309f4cAdA5e1225Ef21D8A30a43:
Safe 1.5.0 L2, 2-of-3.
0x61D2…637F, the Pico’s SafeWebAuthnSignerProxy 0x2843…5E53, and later
0x1289…f2DA.api.safe.global/tx-service/basesep got 201: the emulator’s on #0, the Pico’s on #1–#3.execTransaction at block 47276343, the block before it
executed:
tools/quorum live exec, which lays the signatures out the same way as
protocol-kit, executed #0–#3.
#1
and #2
are the Pico’s approvals.0xDebDE58b3ed0867d19285f6C8B0d81AdBC326Da9:
Safe 1.5.0 L2, 2-of-3.
0x61D2…637F, the Pico’s SafeWebAuthnSignerProxy 0x2843…5E53
(deployed on Base in 0xe5e8ad92…),
and 0xb78e…AdBD8.safeTxHash 0x4c1e4f18…cef63ad6) sends 0.000001 ETH:
0xb78e… confirmed with an EOA signature (65 bytes).api.safe.global/tx-service/base stored it as
CONTRACT_SIGNATURE, 385 bytes.execTransaction at block 51784275, with #0 pending:
temp-safe-screenshots/ (local only), with
redacted copies in redacted/.0x27d0D792…A43E and passkey signer 0x4D5Df238…5E91, which
own a fresh 2-of-2 Safe 1.5.0 at 0x28EB931d…7A71 on an Anvil fork (block 47295478). None of it
is on a public chain.ExecutionSuccess), and the SafeMultiSigTransaction event logs exactly those bytes.
That is the patch’s test fixture.