Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Safe multisig monitor

Template: safe-monitor ยท category: security ยท risk: monitor_only

Watches a Safe multisig for the events that matter: AddedOwner, RemovedOwner, ChangedThreshold, and ExecutionSuccess (optionally only above a gas-refund threshold). Owner-set and threshold changes only happen when the multisig itself is being rewired โ€” exactly the events you want a human paged on within seconds.

When to use it

  • your treasury, protocol admin, or ops funds sit behind a Safe
  • you want an independent alert channel that does not depend on the Safe UI
  • as a compromise tripwire: an attacker adding an owner or dropping the threshold shows up immediately

Generate it

rflow new --template safe-monitor

Non-interactive (CI/agents):

rflow new --template safe-monitor --yes --output ./safe-watch \
  --answer safe_address=0xYourSafe --answer safe_version=1.4

What it generates

Four event-triggered workflows over one contracts: entry:

workfloweventnotifies
safe-owner-addedAddedOwnerthe new owner address
safe-owner-removedRemovedOwnerthe removed owner address
safe-threshold-changedChangedThresholdthe new signature threshold
safe-executionExecutionSuccesssafeTxHash + gas-refund payment
# recipe: partial
workflows:
  safe-owner-added:
    trigger:
      event:
        contract: TreasurySafe
        name: AddedOwner
        network: ethereum
        confirmations: 0
        start_block: latest
        end_block: live
    steps:
      - id: alert
        notify:
          channel: ops
          message: "SAFE OWNER ADDED on TreasurySafe: ${{ trigger.args.owner }} - tx ${{ trigger.tx_hash }}"
    on_failure: dead_letter
 
  safe-execution:
    trigger:
      event:
        contract: TreasurySafe
        name: ExecutionSuccess
        network: ethereum
        # payment is the gas refund the Safe paid; 0 = notify on EVERY execution
        where: "${{ trigger.args.payment >= wei('0', 18) }}"
        ...

Inputs

keytypedefault
project_namestringsafe-monitor
network / chain_id / rpc_env / rpc_urlnetwork / chain_id / env_var / stringethereum / 1 / ETH_RPC / a public RPC
safe_namecontractTreasurySafe
safe_addressaddresszero placeholder โ€” replace it
safe_versionchoice 1.3 | 1.41.3 โ€” v1.4 Safes index owner/txHash, pick the ABI that matches your deployment
min_paymenttoken_amount0 (whole native tokens; every execution notifies)
channelstringops

Required env vars

DATABASE_URL, the RPC env var (default ETH_RPC), TG_BOT_TOKEN, TG_CHAT_ID โ€” all listed in the generated .env.example.

Safety notes

Monitor-only: no signer:, no relayers:, no transactions โ€” the relayer engine never boots. Every workflow ends in on_failure: dead_letter so a failed notification is journaled, never lost. Triggers fire at head (confirmations: 0): for a security tripwire you want the earliest possible signal, and a false alert from a reorged-out event is a feature, not a bug.

Run it locally

docker compose up -d     # postgres on localhost:5448
# fill .env (RPC + telegram credentials)
rflow validate
rflow start

Rehearse the alerts without touching a real Safe (dry-run, nothing sent):

rflow test safe-owner-added --fixture fixtures/owner-added.json
rflow test safe-execution --fixture fixtures/execution-success.json

Production checklist

  • safe_address points at your real Safe (not the zero placeholder)
  • safe_version matches the deployed Safe (1.4 indexes owner/txHash; the wrong ABI means events will not decode)
  • telegram credentials in .env, test with rflow test
  • the channel pages a human, not a muted group
  • rflow validate --preflight passes against your RPC

Common modifications

  • watch several Safes: duplicate the contracts: entry and point extra workflows at it, or run rflow new per Safe
  • alert on failed executions too: add a workflow on ExecutionFailure (already in the packaged ABI)
  • raise min_payment to only see executions with a meaningful gas refund
  • route owner changes to pagerduty and executions to telegram: add a second channel with rflow add notification