Skip to content

docs: add Safe testnet custody handoff#111

Merged
dirtybits merged 2 commits into
mainfrom
docs/safe-testnet-handoff
Jul 20, 2026
Merged

docs: add Safe testnet custody handoff#111
dirtybits merged 2 commits into
mainfrom
docs/safe-testnet-handoff

Conversation

@dirtybits

@dirtybits dirtybits commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

Adds comprehensive documentation and helper scripts for managing testnet-only Safe custody workflows. This includes:

  • SAFE_TESTNET_KEYSTORE_HANDOFF.md: Guidance on creating and verifying exportable Foundry keystores for testnet Safe owners, proposers, and executors. Establishes the production boundary and emphasizes that these are testnet-only resources.
  • SAFE_TESTNET_OWNERSHIP_MANIFEST.md: Template for recording Safe ownership metadata, including proposed owners, threshold, proposer/executor roles, and authority handoff tracking. Explicitly marks all fields as unapproved draft pending human review.
  • SAFE_TESTNET_TRANSACTION_RUNBOOK.md: Checklist for Safe transaction review and execution, covering snapshot, build, simulation, hash verification, signature collection, execution, and post-state verification.
  • create_safe_test_accounts.sh: Helper script that creates or verifies encrypted Foundry keystores, generates 1Password passwords, backs up keystores to 1Password items, and verifies byte-for-byte consistency. Refuses to overwrite existing keystores or pair mismatched passwords.
  • run_foundry_with_op_password.sh: Wrapper for cast and forge commands that reads passwords from 1Password into temporary 0600 files without exposing them to the clipboard or child environment variables.

All scripts and documentation emphasize testnet-only scope, explicit human approval gates, and the production boundary. No real keys, 1Password items, Safe contracts, RPC calls, or transactions are created or modified.

Testing

Verification passed:

  • Bash 3.2 syntax
  • Repository formatting
  • Chain capability-map verification
  • Mocked creation and idempotent rerun
  • Password/keystore matching
  • Backup mismatch and missing-local-key fail-closed checks
  • Temporary 0600 password wrapper
  • Signed-commit verification
  • No real keys, 1Password items, Safe contracts, RPC calls, or transactions were touched.

Note

Medium Risk
New custody/signing helpers touch keystore and 1Password flows; misuse or bypassing documented approval gates could expose keys or enable unintended broadcasts via the Foundry wrapper.

Overview
Introduces a testnet-only Safe custody prep package for Base Sepolia: three linked docs plus scripts/safe/ helpers, all scoped as unapproved checklists (not authorization to deploy, change owners, or broadcast).

Documentation: SAFE_TESTNET_KEYSTORE_HANDOFF.md ties account creation to explicit human inputs, .env.safe.local public config, and production boundaries. SAFE_TESTNET_OWNERSHIP_MANIFEST.md is a draft 2-of-3 manifest with pending owners and approval gates. SAFE_TESTNET_TRANSACTION_RUNBOOK.md defines a seven-step review flow (Safe snapshot through execTransaction simulation, hash review, execution, and post-finality verification).

Scripts: create_safe_test_accounts.sh creates or re-verifies Foundry keystores against 1Password (required vault, no overwrite, byte-for-byte backup checks, 0700/0600 perms); create_foundry_keystore.expect feeds passwords to cast wallet new without argv/env leakage; run_foundry_with_op_password.sh supplies a temp --password-file from op:// for cast/forge. test_safe_account_helpers.sh mocks op/cast for vault-required, idempotent rerun, and password-leak checks.

Reviewed by Cursor Bugbot for commit 41636ca. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ac42d59a-afe3-41c5-aa34-f1c032e2b5d0)

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add testnet Safe custody handoff docs and Foundry/1Password helper scripts

📝 Documentation ✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Add testnet-only Safe custody docs: keystore handoff, ownership manifest, transaction runbook.
• Add Bash tooling to create/verify Foundry keystores and back them up to 1Password.
• Add a password-file wrapper to run cast/forge without clipboard or env var exposure.
Diagram

graph TD
  opUser(["Operator"]) --> handoffDoc[["Keystore handoff doc"]] --> createScript["create_safe_test_accounts.sh"] --> opVault{{"1Password (op)"]}
  createScript --> keystoreDir[("Foundry keystores")] --> foundry{{"cast/forge"}}
  handoffDoc --> manifestDoc[["Ownership manifest"]] --> runbookDoc[["Tx runbook"]]
  handoffDoc --> wrapperScript["run_foundry_with_op_password.sh"] --> foundry
  wrapperScript --> opVault
  subgraph Legend
    direction LR
    _doc[["Document"]] ~~~ _script["Script"] ~~~ _fs[("Local store")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Rely solely on `op run` + `CAST_PASSWORD=op://...` (no password-file wrapper)
  • ➕ Fewer moving parts; no temp-file management
  • ➕ Consistent with existing Foundry patterns for keystore creation
  • ➖ Harder to support commands that require --password-file placement or forbid env usage
  • ➖ Environment variables can leak via process inspection or shell debugging if misused
2. Use non-exportable signers even on testnet (hardware wallet / Safe UI)
  • ➕ Closer to production custody posture; reduces risk of exportable-key sprawl
  • ➕ Avoids storing password+keystore together even for testnet
  • ➖ More friction for automated simulations and scripted workflows
  • ➖ Not always practical for repeated testnet deploy/rollback cycles
3. Back up encrypted keystores via a different secret store (e.g., SOPS/Git-crypt, Vault)
  • ➕ Clearer separation of password vs artifact, better access controls in some orgs
  • ➕ Auditable policies and rotation workflows
  • ➖ Higher setup/maintenance cost than 1Password for small testnet workflows
  • ➖ May not match existing operator tooling expectations

Recommendation: Keep the PR’s approach: it optimizes for testnet operational safety (fail-closed checks, idempotent reruns, explicit production boundary) while staying compatible with Foundry’s keystore model. The temp password-file wrapper is a pragmatic improvement over clipboard-based workflows and avoids exporting secrets to child environments; just ensure reviewers align that storing password+encrypted keystore in the same 1Password item is strictly testnet-only as stated.

Files changed (5) +799 / -0

Documentation (3) +377 / -0
SAFE_TESTNET_KEYSTORE_HANDOFF.mdAdd testnet Safe keystore creation and custody boundary guidance +186/-0

Add testnet Safe keystore creation and custody boundary guidance

• Introduces a testnet-only handoff guide explaining Safe vs EOA key responsibilities, required human inputs, and explicit approval gates. Documents how to use the included scripts to create/verify Foundry keystores, back them up to 1Password, and run Foundry commands using a password-file without clipboard exposure.

docs/SAFE_TESTNET_KEYSTORE_HANDOFF.md

SAFE_TESTNET_OWNERSHIP_MANIFEST.mdAdd Base Sepolia Safe ownership manifest template +70/-0

Add Base Sepolia Safe ownership manifest template

• Adds a public metadata template for proposed owners, threshold, proposer/executor roles, and authority handoff tracking. Marks all fields as unapproved draft and reiterates that recording addresses is not approval.

docs/SAFE_TESTNET_OWNERSHIP_MANIFEST.md

SAFE_TESTNET_TRANSACTION_RUNBOOK.mdAdd Safe transaction review and execution checklist +121/-0

Add Safe transaction review and execution checklist

• Provides a step-by-step runbook for snapshotting Safe state, building/decoding transactions, simulating from Safe context, hashing, collecting signatures, executing, and verifying post-state. Emphasizes stopping conditions, independent verification, and separate broadcast approval.

docs/SAFE_TESTNET_TRANSACTION_RUNBOOK.md

Other (2) +422 / -0
create_safe_test_accounts.shCreate/verify Foundry keystores and enforce 1Password backup consistency +305/-0

Create/verify Foundry keystores and enforce 1Password backup consistency

• Adds a Bash helper to create or verify encrypted Foundry keystores for explicitly provided testnet roles, generating per-account 1Password passwords and attaching encrypted keystore backups. Enforces fail-closed behavior (no overwrite, mismatch detection, and byte-for-byte backup verification) and tight file permissions (0700 dir, 0600 files).

scripts/safe/create_safe_test_accounts.sh

run_foundry_with_op_password.shRun cast/forge with a temporary password file sourced from 1Password +117/-0

Run cast/forge with a temporary password file sourced from 1Password

• Adds a wrapper that reads an 'op://vault/item/field' password into a temporary 0600 file, substitutes a single '{password_file}' placeholder in the command, and cleans up on exit. Avoids clipboard usage and avoids exporting secrets as environment variables while supporting Foundry commands that require '--password-file'.

scripts/safe/run_foundry_with_op_password.sh

@qodo-code-review

qodo-code-review Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 17 rules

Grey Divider


Remediation recommended

1. Secret passed via env ✓ Resolved 🐞 Bug ⛨ Security
Description
scripts/safe/create_safe_test_accounts.sh supplies the keystore password to cast wallet new via
CAST_PASSWORD=... op run, which results in the resolved plaintext secret being placed in the
environment of the cast process. This increases the chance of password exposure via process
inspection/debug tooling or inheritance by child processes, and it contradicts the repo’s own
password-file approach used elsewhere in this PR.
Code

scripts/safe/create_safe_test_accounts.sh[R166-169]

+  info "Creating encrypted Foundry keystore '$account'"
+  CAST_PASSWORD="$secret_ref" op run -- \
+    cast wallet new "$KEYSTORE_DIR" "$account" >&2
+  chmod 600 "$keystore"
Relevance

⭐⭐⭐ High

Team has accepted multiple “avoid secret-driven behavior/env misuse” hardening changes (e.g., PR
#103, PR #69).

PR-#103
PR-#69

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The account-creation helper uses CAST_PASSWORD=... op run -- cast wallet new ..., which places the
resolved secret into the spawned process environment. The wrapper script added in the same PR
demonstrates the intended safer pattern: read the secret into a 0600 temp file and pass the file
path instead of exporting the secret via env.

scripts/safe/create_safe_test_accounts.sh[157-170]
scripts/safe/run_foundry_with_op_password.sh[97-117]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When creating a new Foundry keystore, the script uses `CAST_PASSWORD="$secret_ref" op run -- cast ...`, which causes `op run` to resolve the `op://...` reference and inject the *plaintext* password into the environment of the launched `cast` process. Environment-based secret passing is more leak-prone than the temp-0600 password-file approach used by `run_foundry_with_op_password.sh`.

## Issue Context
This PR already includes `scripts/safe/run_foundry_with_op_password.sh`, which reads the password into a temporary `0600` file and substitutes it into the `cast`/`forge` command without exporting it as an environment variable.

## Fix Focus Areas
- scripts/safe/create_safe_test_accounts.sh[157-170]
- scripts/safe/run_foundry_with_op_password.sh[97-117]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Absolute paths in docs ✓ Resolved 🐞 Bug ⛨ Security
Description
docs/SAFE_TESTNET_KEYSTORE_HANDOFF.md embeds operator-local absolute paths including a specific
username, unnecessarily disclosing identity and internal filesystem/repo layout in the repository
contents (and therefore git history if merged). This is avoidable by replacing them with generic
placeholders or repository-relative references.
Code

docs/SAFE_TESTNET_KEYSTORE_HANDOFF.md[R21-26]

+This package adapts two operator-machine references:
+
+- `/Users/andysustic/Repos/calypso-infra/scripts/chainlink/create_ccv_test_accounts.sh`
+  demonstrates the testnet keystore lifecycle.
+- `/Users/andysustic/Repos/calypso-infra/docs/chainlink/chainlink_ccv_key_custody.md`
+  defines the exportable-test-key versus non-exportable-production boundary.
Relevance

⭐⭐ Medium

No prior review evidence about banning /Users/username paths in docs; only general docs cleanup
accepted (PR #58).

PR-#58

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The doc explicitly contains /Users/... paths with a real username, which is unnecessary for the
workflow and constitutes an information disclosure in the repo content.

docs/SAFE_TESTNET_KEYSTORE_HANDOFF.md[19-30]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`docs/SAFE_TESTNET_KEYSTORE_HANDOFF.md` includes absolute workstation paths (with a real username) under “Reference Provenance”. This leaks operator identity and internal local-repo layout into the repo.

## Issue Context
The provenance can be preserved without embedding a specific `/Users/<name>/...` path by using generic placeholders (e.g., `<operator-machine>/...`) or by describing the references without absolute paths.

## Fix Focus Areas
- docs/SAFE_TESTNET_KEYSTORE_HANDOFF.md[19-30]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread docs/SAFE_TESTNET_KEYSTORE_HANDOFF.md Outdated
Comment thread scripts/safe/create_safe_test_accounts.sh
Comment thread docs/SAFE_TESTNET_KEYSTORE_HANDOFF.md Outdated
Comment thread scripts/safe/create_safe_test_accounts.sh Outdated
Comment thread docs/SAFE_TESTNET_TRANSACTION_RUNBOOK.md Outdated
Comment thread docs/SAFE_TESTNET_TRANSACTION_RUNBOOK.md Outdated
Comment thread docs/SAFE_TESTNET_TRANSACTION_RUNBOOK.md Outdated
@cursor

cursor Bot commented Jul 20, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_2dccd194-5304-4bb8-8c5c-2d783088c27c)

@dirtybits
dirtybits merged commit e4e88b6 into main Jul 20, 2026
4 checks passed
@dirtybits
dirtybits deleted the docs/safe-testnet-handoff branch July 20, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant