ByAUJay
“DAO Treasury Multisig” GetFindings Version-Utils: Tuning AlertID Rules for ExecutionFailure
Decision‑makers’ summary: Execution failures on Safe (formerly Gnosis Safe) treasuries are easy to miss because the outer transaction can succeed while the internal call fails. This guide shows how to wire Forta’s oz‑gnosis events feed and version‑aware rules to detect and triage ExecutionFailure with near‑zero noise across Safe 1.3.0 and 1.4.1, including code, alert tuning, and ops runbooks. (polygonscan.com)
Why this matters now
- Safe remains the de‑facto DAO treasury smart account. Since mid‑2024 the core line has been 1.3.0 on many chains, with the 1.4.x family actively maintained and rolled out through 2025. Your treasury may upgrade in‑place via SafeMigration, so your monitoring must understand which version you’re on today and tomorrow. (github.com)
- Safe emits ExecutionSuccess and ExecutionFailure events from execTransaction. Crucially, a chain explorer may show “Success” while the internal call reverted; Safe still emits ExecutionFailure and even pays the executor refund. If you only watch receipt.status, you will miss real failures. (polygonscan.com)
This article gives you a production‑grade pattern to subscribe to Forta alerts, isolate the ExecutionFailure signal for your multisigs, and reduce false positives with version‑aware rules.
ExecutionFailure 101 (for Safe treasuries)
What the Safe contract does on execution:
- Emits one of:
- event ExecutionSuccess(bytes32 txHash, uint256 payment)
- event ExecutionFailure(bytes32 txHash, uint256 payment)
- Reserves minimum gas for event emission (and some pre/post work), and enforces guards GS010/GS013 to protect execution and estimation semantics. Practically, you can end up with a successful outer tx but an internal failure; only the event tells the truth. (polygonscan.com)
Common DAO scenarios that trigger ExecutionFailure:
- Multisend batch step reverts due to downstream call failure, but outer Safe transaction posts successfully (classic “success with internal failure” symptom). (ethereum.stackexchange.com)
- Insufficient safeTxGas or underestimated inner call gas; Safe enforces a stipend and may pay the executor even if inner call fails. (polygonscan.com)
- Module‑triggered execution fails (e.g., Zodiac/roles/delay or custom ops module). Safe also exposes ExecutionFromModuleFailure — monitor both if you use modules. (pkg.go.dev)
Key takeaway: You need event‑level monitoring for ExecutionFailure on every treasury Safe across every chain you operate on.
The monitoring stack you’ll use
- Forta oz‑gnosis events bot: emits alerts for events from OpenZeppelin and Safe repositories, including ExecutionFailure. We’ll subscribe and then filter to your treasury addresses and chains. (docs.forta.network)
- Forta SDK v2: use initialize + handleAlert, and getAlerts for backfills or calibration. You’ll key off AlertEvent fields such as alertId/name, chainId, and addresses. (docs.forta.network)
- Optional correlation feeds:
- “Successful transactions with internal failures” template to cross‑check the condition where the receipt is “success” but inner calls failed. (docs.forta.network)
- Attack/Scam combiners or governance kits if you want higher‑level narratives (e.g., failure after a parameter change). (docs.forta.network)
Version‑Utils: a pragmatic pattern to keep rules in sync with Safe versions
In practice, you’ll have multiple Safes on different versions across chains. Our “Version‑Utils” pattern is a small config and helper that:
- Detects the Safe version per address at boot (reads VERSION constant or consults safe‑deployments).
- Assigns the right set of alertId/name filters per version (because bots and event signatures may evolve).
- Captures per‑version edge cases (e.g., 1.3.0 gas semantics vs 1.4.1 guard behavior) for your downstream severity logic. (github.com)
Why not hard‑code? Because many DAOs will migrate 1.3.0 → 1.4.1 using SafeMigration; rules that assume a single version age poorly and create pager fatigue. (docs.safe.global)
Concrete example: subscribing to ExecutionFailure for your treasury Safes
We’ll consume alerts from the oz‑gnosis events feed via initialize/handleAlert and filter for ExecutionFailure.
Step 1: define your treasury set and chains
// config/treasuries.ts export const TREASURIES = { 1: [ // Ethereum mainnet "0xYourMainnetSafe", "0xAnotherMainnetSafe" ], 137: [ // Polygon "0xYourPolygonSafe" ], 42161: [ // Arbitrum One "0xYourArbitrumSafe" ] };
Step 2: minimal version-utils
// utils/version-utils.ts import { ethers } from "ethers"; import deployments from "@safe-global/safe-deployments"; // JSON addresses/abis export type SafeVersion = "1.3.0" | "1.4.1" | "unknown"; export async function getSafeVersion(provider: ethers.providers.Provider, safeAddr: string): Promise<SafeVersion> { // Try read VERSION() from the Safe proxy via ABI, else fall back to deployments lookup const abi = ["function VERSION() view returns (string)"]; try { const c = new ethers.Contract(safeAddr, abi, provider); const v: string = await c.VERSION(); if (v.startsWith("1.3.0")) return "1.3.0"; if (v.startsWith("1.4.1")) return "1.4.1"; } catch {} // Fallback: heuristic via bytecode hash against known deployments (left as exercise) return "unknown"; }
The Safe contracts repo documents the supported versions and migration process; use this to gate your logic (e.g., attach different severities for failures on 1.4.1 after an upgrade). (github.com)
Step 3: subscribe to oz‑gnosis events and filter for ExecutionFailure
// src/agent.ts import { AlertEvent, Finding, FindingSeverity, FindingType, HandleAlert, Initialize, InitializeResponse } from "forta-agent"; import { TREASURIES } from "../config/treasuries"; import { getSafeVersion } from "../utils/version-utils"; const OZ_GNOSIS_EVENTS_BOT = "oz-gnosis-events"; // use actual botId from Forta app once verified in staging // store run-time cache of safe versions const versionCache = new Map<string, string>(); export const initialize: Initialize = async (): Promise<void | InitializeResponse> => { // Build Forta subscriptions: one for the OZ/Gnosis bot; keep alertIds empty until we discover them from staging return { alertConfig: [ { botIds: [OZ_GNOSIS_EVENTS_BOT] } // we'll filter in code by alert.name and addresses ] }; }; function isTreasury(chainId: number, addr: string): boolean { return (TREASURIES[chainId] || []).some(a => a.toLowerCase() === addr.toLowerCase()); } export const handleAlert: HandleAlert = async (alertEvent: AlertEvent) => { const f: Finding[] = []; // quick chain/address filter const chainId = alertEvent.chainId; const addrHit = (alertEvent.alert.addresses || []).find(a => isTreasury(chainId, a)); if (!addrHit) return f; // normalize by alert.name when alertId is unknown/changes across versions const name = (alertEvent.name || alertEvent.alert.name || "").toLowerCase(); if (!name.includes("executionfailure")) return f; // fetch Safe version for added context (cached) if (!versionCache.has(addrHit)) { // supply a provider here or ask a sibling bot versionCache.set(addrHit, "unknown"); } const version = versionCache.get(addrHit) || "unknown"; // severity tuning: stricter on 1.4.1 post-migration, lower on known-estimation-flows const severity = version.startsWith("1.4") ? FindingSeverity.High : FindingSeverity.Medium; f.push(Finding.from({ name: "Safe ExecutionFailure observed", description: `ExecutionFailure on treasury ${addrHit} (v${version})`, alertId: "SAFE-EXECUTION-FAILURE", severity, type: FindingType.Suspicious, addresses: [addrHit], metadata: { chainId: String(chainId), botId: alertEvent.botId, txHash: alertEvent.transactionHash || "", } })); return f; }; export default { initialize, handleAlert };
Notes
- Forta’s AlertEvent fields (alertId, name, chainId, addresses, etc.) are documented; if the oz‑gnosis bot changes alertId formatting, the code above continues to work by matching on alert name. In staging, log incoming alertId values and pin them in config to harden production. (docs.forta.network)
- You can also request only certain alertIds directly in initialize once confirmed (smaller volume, lower costs). (docs.forta.network)
Calibrating the exact AlertID: a safe way to “learn” IDs
Because alertId strings are bot‑defined, we recommend a short “learning” pass in non‑production:
- Backfill last N days of oz‑gnosis alerts for your treasury addresses with getAlerts, grouping by alertId/name.
- Inspect which alertIds consistently label ExecutionFailure across chains/versions.
- Pin those IDs in a ruleset file, versioned by Safe version and chain.
Example GraphQL to explore recent oz‑gnosis alerts and extract names/IDs:
# POST https://api.forta.network/graphql query recentAlerts($input: AlertsInput) { alerts(input: $input) { pageInfo { hasNextPage endCursor } alerts { name alertId chainId addresses source { bot { id } transactionHash } } } }
Use variables with your botIds and addresses, e.g., createdSince=600000 (10 minutes), and then widen the window. Forta’s docs provide working examples and the required fields to pass. (forta-network.github.io)
Reducing noise: five precision levers that actually work
-
Require multi‑evidence for severity upgrades
- Combine oz‑gnosis ExecutionFailure with “Successful txn with internal failures” to eliminate explorer misreads and surface genuinely broken ops. (docs.forta.network)
-
Version‑aware severities
- On 1.3.0, some teams intentionally trigger inner revert during gas estimation or dry runs; mark as Informational if safeTxGas=0 is known in your workflow. On 1.4.1+, treat any ExecutionFailure post‑upgrade as Medium/High unless whitelisted. (polygonscan.com)
-
Module‑aware filtering
- If you use modules (Delay, Roles, SafeSnap/Reality), also subscribe for ExecutionFromModuleFailure; this catches automation failures that wouldn’t appear in owner‑initiated execs. (pkg.go.dev)
-
Chain and bot confirmation filters
- Limit to your active chainIds and require scanNodeConfirmations ≥ 2 in getAlerts for backfills to reduce transient anomalies. (docs.forta.network)
-
Address scoping and allowlists
- Restrict to verified treasury Safe addresses and known module addresses. Keep a separate staging set for testing so ops noise never hits prod channels. (github.com)
Ops runbook: what to do when ExecutionFailure fires
- Triage within 10 minutes:
- Check Safe tx hash and event logs for ExecutionFailure, and inspect the inner revert reason if available. The Safe code documents GS013 (“require success or non‑zero safeTxGas/gasPrice”) and the gas accounting that often explains failures. (polygonscan.com)
- Verify whether the transaction used Multisend; if yes, inspect which step failed and re‑execute only the failed action with correct gas. (ethereum.stackexchange.com)
- Governance linkage:
- If you run Snapshot/SafeSnap or Reality modules, correlate with the proposal ID to ensure the on‑chain execution aligns with the approved payload. If execution fails, you may need a re‑proposal or an emergency fix. (forum.gnosis.io)
- Post‑mortem checklist:
- Wrong target, outdated ABI, or a module permission denial?
- Insufficient safeTxGas? Increase and retry.
- Module‑level revert? Inspect ExecutionFromModuleFailure and module logs. (pkg.go.dev)
Safe version nuances that change how you alert
- 1.3.0 behavior: clear ExecutionFailure/Success events, reserved gas for events, and outer success may mask inner revert; refund semantics tied to tx.origin/refundReceiver. Your rules should avoid paging on known “estimation reverts.” (polygonscan.com)
- 1.4.1 line: introduced changes to support ERC‑4337, guard checks, and broader L2 coverage. If your org recently ran SafeMigration, treat any ExecutionFailure in the first week as High priority to catch mis‑configured fallback handlers or modules. (github.com)
Keep your “version‑utils” mapping current using the safe‑deployments package; the repo tracks deployments and ABIs per chain so your address/version detection remains accurate. (github.com)
Forta deployment hygiene (so you don’t miss real failures)
- SLA and publishing: monitor event.batch‑publish.error on scanners; a stuck publisher lowers SLA and may drop alerts. Your pipeline should alert you if SLA KPIs drift. (docs.forta.network)
- Use paid API keys and the v2 SDK’s handleAlert to consume bot alerts reliably; throttle getAlerts backfills and prefer cursor‑based pagination. (docs.forta.network)
- Consider layering Nethermind’s governance/security bots (they track Safe admin changes, upgrades) to correlate “config drift → failure.” (forta.org)
Production checklist for “DAO Treasury Multisig: ExecutionFailure” monitoring
- Scope:
- Enumerate all treasury Safes per chain; record owner threshold, modules, fallback handler.
- Detect and cache Safe version at boot; schedule a weekly verify job. (github.com)
- Subscriptions:
- Forta oz‑gnosis events bot; optionally, “Successful txn with internal failures.” (docs.forta.network)
- Rules:
- Address‑allowlist = only your safes/modules.
- AlertId/name = ExecutionFailure; confirm exact bot alertId in staging, then pin.
- Severity policy = version‑aware; boost post‑migration or after admin changes.
- Correlations:
- Module failure events (ExecutionFromModuleFailure).
- Governance proposal IDs (SafeSnap/Reality) when applicable. (pkg.go.dev)
- Ops:
- PagerDuty/Slack sink only for High; keep Medium to triage board.
- Post‑mortem template includes safeTxGas and failing step in Multisend. (ethereum.stackexchange.com)
- Resilience:
- Watch scanner SLA and publishing health; add a canary alert that fires from a known lab Safe once per day. (docs.forta.network)
Bonus: Backfill and governance reporting
Run a weekly backfill to quantify “failed‑to‑executed” ratio:
- Query oz‑gnosis alerts for ExecutionFailure across your safes last 7 days.
- Join with your proposal metadata (amount, beneficiary, module) to detect systematic issues (e.g., a mis‑configured module or a per‑chain gas policy). Forta provides examples of GraphQL queries and filtering knobs like severities and scanNodeConfirmations. (forta-network.github.io)
Deliver an exec‑ready dashboard KPI:
- Per chain: ExecutionFailure count, % retried successfully within 24h, top revert reasons.
- Post‑upgrade window (1.4.1 migration): highlight any persistent failures. (docs.safe.global)
Security footnotes for leaders
- The Safe event semantics are not optional: relying on outer tx status is unsafe. Your teams must key off ExecutionFailure/Success to know what actually happened inside execTransaction. The Etherscan‑verified source shows exactly where these are emitted and why explorers may show “success” despite inner failure. (polygonscan.com)
- If you automate via modules, include module failure events in your SLOs; a silent module can stall payroll or market ops. The getamis Gnosis kit exposes those event types for indexing or bots. (pkg.go.dev)
- Forta’s curated kits include an “internal failures” detector and oz‑gnosis events feed; leverage them instead of building raw log parsers. This shortens time‑to‑coverage and reduces maintenance. (docs.forta.network)
What good looks like in 2026
- Every treasury Safe is monitored across all active chains for ExecutionFailure via Forta oz‑gnosis events.
- A version‑aware ruleset keeps AlertID/name filters aligned as you migrate to 1.4.1+ and add modules.
- A weekly report tracks failure rates, retries, and governance linkage.
- Pager fatigue is under control: only net‑new, high‑risk failures page the on‑call; “expected” estimation reverts are auto‑suppressed.
If you want this wired into your existing SIEM, we can ship the Forta consumer as an external bot with your logging format and RBAC. Forta supports external bots and alert submission if you need to enrich findings downstream. (docs.forta.network)
References and further reading
- Safe contract source (1.3.0) showing ExecutionFailure/Success and guards GS010/GS013; explains “outer success, inner failure” semantics. (polygonscan.com)
- Forta SDK and alert consumption (initialize, handleAlert, getAlerts). (docs.forta.network)
- Forta starter kits: oz‑gnosis events and “Successful transactions with internal failures.” (docs.forta.network)
- Safe releases and migration guidance (1.4.1). (github.com)
- Module failure events (ExecutionFromModuleFailure) in Safe tooling. (pkg.go.dev)
TL;DR implementation kit
- Subscribe to Forta oz‑gnosis events. Filter by:
- addresses ∈ your treasury/module allowlist,
- name contains “ExecutionFailure” (pin exact alertIds after staging),
- chainId ∈ your live networks. (docs.forta.network)
- Maintain a version‑utils map (VERSION() or deployments) to tune severity and suppression rules per Safe version. (github.com)
- Correlate with the “internal failures” bot for higher precision, and monitor module failure events if you automate. (docs.forta.network)
Adopt this pattern and ExecutionFailure will stop being a surprise line item in your treasury retros — it becomes a fast, explainable signal you can act on.
Like what you're reading? Let's build together.
Get a free 30‑minute consultation with our engineering team.

