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-monitorNon-interactive (CI/agents):
rflow new --template safe-monitor --yes --output ./safe-watch \
--answer safe_address=0xYourSafe --answer safe_version=1.4What it generates
Four event-triggered workflows over one contracts: entry:
| workflow | event | notifies |
|---|---|---|
safe-owner-added | AddedOwner | the new owner address |
safe-owner-removed | RemovedOwner | the removed owner address |
safe-threshold-changed | ChangedThreshold | the new signature threshold |
safe-execution | ExecutionSuccess | safeTxHash + 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
| key | type | default |
|---|---|---|
project_name | string | safe-monitor |
network / chain_id / rpc_env / rpc_url | network / chain_id / env_var / string | ethereum / 1 / ETH_RPC / a public RPC |
safe_name | contract | TreasurySafe |
safe_address | address | zero placeholder โ replace it |
safe_version | choice 1.3 | 1.4 | 1.3 โ v1.4 Safes index owner/txHash, pick the ABI that matches your deployment |
min_payment | token_amount | 0 (whole native tokens; every execution notifies) |
channel | string | ops |
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 startRehearse 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.jsonProduction checklist
-
safe_addresspoints at your real Safe (not the zero placeholder) -
safe_versionmatches the deployed Safe (1.4indexesowner/txHash; the wrong ABI means events will not decode) - telegram credentials in
.env, test withrflow test - the channel pages a human, not a muted group
-
rflow validate --preflightpasses against your RPC
Common modifications
- watch several Safes: duplicate the
contracts:entry and point extra workflows at it, or runrflow newper Safe - alert on failed executions too: add a workflow on
ExecutionFailure(already in the packaged ABI) - raise
min_paymentto 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