# Security Audit Report — ResurrectBeaconV1 ## Scope & Target Pin | | | |---|---| | **Contract** | `ResurrectBeaconV1` | | **On-chain address** | `0x136c191B5e6541532E42Ecd7C719C29D7ecdf468` | | **Chain** | Ethereum mainnet (chain id 1) | | **Source verification** | Sourcify full match (`creationMatch: match`, `runtimeMatch: match`) — verified live at audit time via `GET https://sourcify.dev/server/v2/contract/1/0x136c191B5e6541532E42Ecd7C719C29D7ecdf468` | | **Compiler** | solc 0.8.24+commit.e11b9ed9, optimizer runs=20000, evmVersion=cancun, viaIR=false | | **Files reviewed** | `src/ResurrectBeaconV1.sol` (single file, 37 lines) | | **Mode** | Full scope (entire contract — no file/scope pin in the client's job description beyond the address itself) | | **Methodology** | Three-phase: Phase 0 context (opus, 3 agents) → Phase 1 breadth/ethskills (sonnet, 5 domain agents) → Phase 2 depth/pashov (sonnet, 12 attacker-mindset agents, blind to Phase 1, staggered in 4 waves) → Phase 3 hybrid reconciliation with coverage gate | | **Confidence floor** | Findings reported at confidence ≥ 50; below that, listed only as Leads | **Client brief:** audit focus requested on TTL and record-size validation, expiry arithmetic, event encoding, DoS/griefing risks, ABI compatibility, and confirmation of no owner/storage/upgrade/reentrancy/ETH-handling surface. All six areas are explicitly addressed below (see Threat Model and Findings). --- ## Severity counts **Severity counts:** 0 Critical · 0 High · 0 Medium · 2 Low · 1 Informational ## Reconciliation summary Overlap (both phases): 1 theme (namespace/content/identity binding gap) · Phase-1-only: 2 (min-TTL imprecision; the umbrella off-chain-verification framing) · Phase-2-only: 1 (misleading `RecordTooLarge(0)` error) · Re-examined leads kept: 1 (namespace-trust theme, promoted to a corroborated Low finding via 6-agent convergence across both phases) · demoted: 0 · Coverage holes closed: 0 (K=0 — both phases already covered the full, minimal attack surface; the contract is a single 37-line, single-function target with no code the hunting phases missed). **Confidence floor used:** 50. No finding or lead fell below this floor; nothing was excluded on confidence grounds. ## Coverage gate `Entrypoints: 4 external/public in source (1 state-changing, 3 constant getters), 4 in access-control inventory — match.` `Coverage: 4 entrypoints in inventory, 4 addressed. 8 threat-catalog rows, 8 answered. Holes closed this pass: 0.` Every privileged/value-moving entrypoint (there is exactly one, `announce`) was examined by all 5 Phase 1 agents and all 12 Phase 2 agents independently. Every threat-catalog row below is answered by a finding or by an explicit "invariant holds" note from at least one agent, cross-checked in reconciliation. --- ## Access-Control Inventory | Contract.function | Guard | Who can call | State written | Moves value? | |---|---|---|---|---| | `announce(bytes32,uint32,uint32,bytes)` (announce.sol:25-36) | None — no modifier, no `msg.sender` check. Only argument-range checks: `ttl` (L28), `peerRecord.length` (L31-33) | Any address, unlimited repetition | None — zero storage variables exist in the contract; sole effect is `emit` at L35 | No — non-payable; reverts on any nonzero `msg.value` | | `VERSION()` (L9, constant getter) | None | Anyone | None | No | | `MAX_TTL()` (L10, constant getter) | None | Anyone | None | No | | `MAX_RECORD_BYTES()` (L11, constant getter) | None | Anyone | None | No | **Roles:** none. Independently re-derived by 3 separate agents across all three phases (Phase 0, Phase 1 access-control, Phase 2 access-control): no `owner`, no `Ownable`/`AccessControl`, no `modifier` declarations anywhere in the file, no `msg.sender` read anywhere, no constructor/initializer, no upgrade hooks (despite "Beacon" in the name — NatSpec at L6-7 explicitly disclaims the proxy-beacon meaning), no pause, no withdrawal/sweep, no `selfdestruct`. **Confirmed true, matching every claim in the contract's own NatSpec (L5-7).** **Unguarded list:** `announce` is the only state-changing entrypoint and it is reachable by an arbitrary caller — but it writes zero persistent contract state (no storage variables exist at all); its only effect is a log entry (L35), which is chain history, not contract storage, and has no reentrancy surface (zero external calls exist in the contract). --- ## Threat Model | # | Actor | What they can reach | What they could gain | Resolution | |---|---|---|---|---| | T1 | Any caller | `announce()`, arbitrary `namespace`, no registry | Publish under any namespace, including one another party uses | **Addressed by Finding 1** — disclosed design tradeoff, real off-chain-consumer risk | | T2 | Any caller | `announce()` with arbitrary `peerRecord` bytes | Inject unauthenticated bytes relying entirely on off-chain verification | **Addressed by Finding 1** — invariant holds on-chain (contract makes no false authenticity claim); risk lives entirely in unverified off-chain consumer behavior | | T3 | Any caller | Unlimited fee-only-gated repeated calls | Flood the log stream / grief a namespace | **Invariant holds** — examined by DoS, economic-security, and general agents across both phases; no on-chain cross-caller cost amplification exists (confirmed structurally: no shared mutable state, no loops, no queue). Off-chain framing folded into Finding 1 | | T4 | Any caller | Boundary values of `ttl`/`peerRecord.length` | Bypass bounds or trigger unexpected behavior at edges | **Invariant holds** — I1/I2 enforcement verified by 4 independent agents (precision-math, boundary, invariant, numerical-gap) with concrete boundary-value traces (0, 1, MAX, MAX+1); no bypass found | | T5 | Any caller | `validUntil` arithmetic (uint64 cast + addition) | Force truncation/overflow to corrupt `validUntil` | **Invariant holds** — verified unreachable by ~13 orders of magnitude of margin by 3 independent agents (precision-math in both phases, numerical-gap) | | T6 | Any caller | Sending value via `announce{value:x}` or plain transfer | Lock ETH with no withdrawal path | **Invariant holds** — confirmed non-payable, no receive/fallback, by access-control agents in both phases and the boundary agent | | T7 | Block proposer | `block.timestamp` skew | Skew `validUntil` by seconds | **Addressed by Finding 3** (Informational) — minimum TTL of 1 second is proportionally sensitive to skew, though nothing on-chain depends on it | | T8 | Any caller | ABI/event-encoding edge cases (dual-purpose errors, non-indexed `validUntil`) | Confuse an off-chain decoder | **Addressed by Finding 2** — the `RecordTooLarge(0)` misnaming is the concrete instance of this risk; general event/topic encoding itself verified correct by 3 independent agents (general, execution-trace, periphery) | --- ## Findings ### [1] Off-chain trust boundary: `namespace`/`ttl`/`recordType` carry no on-chain binding to `peerRecord` content or announcer identity **Severity:** Low **Confidence:** 85 **Origin:** `[both]` — corroborated independently by 6 agents across both phases: Phase 1 (`evm-audit-signatures` — 2 findings; `evm-audit-general` + `evm-audit-dos` — 1 merged finding) and Phase 2 (economic-security — 2 leads; execution-trace — 1 lead; first-principles — 1 finding + 1 lead; trust-gap — 1 lead), all reaching the same root cause from independent angles while blind to each other. **Location:** `announce()`, ResurrectBeaconV1.sol:25-36 (all four parameters); NatSpec L5, L20-24 **Description:** `announce` accepts `namespace` (bytes32), `recordType` (uint32), `ttl` (uint32), and `peerRecord` (bytes) as four fully independent, unauthenticated, caller-controlled values with zero on-chain relationship enforced between them, and no binding to `msg.sender` (which is never read or emitted). This is a disclosed, deliberate design choice — the contract is explicitly "permissionless, immutable, log-only" (NatSpec L5) and never claims to verify anything about the record's authenticity on-chain. However, six independent audit passes converged on the same practical consequence from five distinct angles, which is worth surfacing to the client as a single consolidated risk: - **Namespace squatting/impersonation:** any address can `announce` under any `namespace` value, including one a legitimate party expects to be theirs by convention — first-to-post or highest-visibility wins whatever off-chain indexing rule a consumer applies. - **Third-party replay with an attacker-chosen validity window:** a third party can resubmit a `peerRecord` byte string observed in a prior log verbatim, choosing any `ttl` in `[1, MAX_TTL]` independent of the original signer's intent — a stale, compromised, or abandoned record can be kept perpetually "fresh-looking" via a new 90-day window, with no revocation path (there is no storage, so nothing can be un-announced). - **Eviction-by-TTL race:** an attacker can outcompete a legitimate operator's record by always re-announcing with `ttl = MAX_TTL`, at negligible, unboundedly-repeatable gas cost (no rate limit, no fee beyond gas, no dedup). - **Namespace/content-type mismatch:** `namespace`/`recordType` are never cross-checked against `peerRecord`'s actual content, so a consumer that selects its parsing/verification routine by the event's topics rather than a self-describing claim inside the verified payload can be misdirected. - **Unauthenticated spam:** unlimited free-form calls into a targeted namespace can bury legitimate records in any indexer that shows only the most-recent N entries per namespace. None of these are exploitable as an on-chain bug — `announce` performs exactly the validation it documents, and every guard/invariant governing its own state (none exists) holds. The risk is entirely conditional on off-chain consumer behavior this audit cannot see: specifically, whether any indexer/subscriber trusts `namespace`/`recordType`/`validUntil` as authoritative, or treats "most recent"/"longest TTL" as a freshness signal, without independently verifying a signature embedded inside `peerRecord` that is itself bound to the namespace/type it claims. Given the contract's own NatSpec calls these "signed peer records" and "self-authenticating," a reader could reasonably (and wrongly) infer stronger on-chain guarantees than exist. **Proof of Concept:** P calls `announce(nsA, 1, 3600, recordP)`, intending a short-lived announcement. Attacker M observes the log and calls `announce(nsA, 1, 7776000, recordP)` verbatim — a new log emits with identical content but `validUntil` ~90 days out, attributable on-chain only to M's transaction (the event never carries `msg.sender`), indistinguishable in payload from P's original to any consumer inspecting only event fields. **Recommendation:** No on-chain fix is required or appropriate given the deliberately minimal, storage-free design — adding registries or signature verification would contradict the contract's stated purpose. Instead: 1. Soften or annotate the NatSpec at L5/L20-24 to state explicitly that the contract performs zero cryptographic verification and that "self-authenticating" is entirely an off-chain property. 2. Document, for integrators, that every consumer of `PeerAnnounced` MUST independently verify a signature (or equivalent proof of namespace control) embedded inside `peerRecord` before trusting or displaying a record, and MUST NOT treat `namespace`/`recordType`/`validUntil`/recency/TTL-length as trust signals on their own. 3. Recommend the off-chain signing scheme itself commit to (include in the signed payload) the `namespace` and `recordType` it targets, so consumers can reject a record whose embedded claim doesn't match the event's topics. --- ### [2] Misleading error semantics: `RecordTooLarge(0)` fires for an empty record, not an oversized one **Severity:** Low **Confidence:** 90 **Origin:** `[phase2: agents 7 (first-principles), 8 (asymmetry)]` — independently identified, identical proof and fix **Location:** ResurrectBeaconV1.sol:31-33 **Description:** `if (recordLength == 0 || recordLength > MAX_RECORD_BYTES) revert RecordTooLarge(recordLength);` reuses one error for two opposite failure conditions — "empty" and "oversized" — but the error's name only semantically matches the second. Calling `announce` with an empty `peerRecord` reverts with `RecordTooLarge(0)`, which is internally contradictory: a length of zero is the opposite of "too large." This contrasts with the sibling `InvalidTTL` guard at L28, whose name correctly and neutrally covers both its zero and over-max sub-conditions. Any off-chain caller, relayer, or tooling that pattern-matches on the error selector to decide remediation (e.g., "truncate the payload and retry" on `RecordTooLarge`) would receive a false diagnostic signal for the empty-payload case, since there is nothing to truncate. **Proof of Concept:** Call `announce(bytes32(0), 0, 100, "")` (valid `ttl`, empty `peerRecord`). `recordLength = peerRecord.length = 0` triggers the first sub-condition of the `||` at L31, reverting with selector `RecordTooLarge(uint256)` and argument `0` — the "too large" error firing on a too-small (empty) input. **Fix** ```diff - error RecordTooLarge(uint256 supplied); + error RecordTooLarge(uint256 supplied); + error EmptyRecord(); uint256 recordLength = peerRecord.length; - if (recordLength == 0 || recordLength > MAX_RECORD_BYTES) { - revert RecordTooLarge(recordLength); - } + if (recordLength == 0) revert EmptyRecord(); + if (recordLength > MAX_RECORD_BYTES) revert RecordTooLarge(recordLength); ``` --- ### [3] Minimum TTL of 1 second permits sub-precision expiry windows under proposer timestamp skew **Severity:** Informational **Confidence:** 70 **Origin:** `[phase1: general]` **Location:** `announce()`, ResurrectBeaconV1.sol:28 **Description:** The TTL lower bound only rejects `ttl == 0`, so `ttl = 1` (one second) is accepted. Block-proposer timestamp skew (on the order of seconds) is immaterial for day-scale TTLs but is proportionally significant at the 1-second floor. This has no on-chain consequence, since nothing in this contract (or, per the protocol map, downstream by design) reads `validUntil` after emission — it is purely an advisory hint for off-chain consumers. **Proof of Concept:** Not exploitable on-chain — no on-chain logic is gated on `validUntil`. An off-chain subscriber that treats a 1-second-TTL record as precisely bounded could be misled by a few seconds of proposer skew; this is an off-chain interpretation risk, not a contract defect. **Recommendation:** Optional. If downstream consumers are expected to rely on short validity windows with second-level precision, consider raising the effective minimum TTL (e.g., `ttl >= 60`), or document in NatSpec that sub-minute TTLs should not be treated as precise. --- ## Areas independently verified with no finding (both phases, high confidence) - **No owner, storage, upgrade, reentrancy, or ETH-handling surface** — the client's explicit ask. Confirmed independently by Phase 0's context-building, Phase 1's `evm-audit-access-control` agent, and Phase 2's `access-control` agent, each re-deriving the claim from source rather than trusting the protocol map. All match the NatSpec's own claims (L6-7) exactly. - **Expiry arithmetic** (`uint64(block.timestamp) + uint64(ttl)`, L35) — the only arithmetic in the entire contract. Verified safe across 4 independent passes (Phase 1 precision-math; Phase 2 math-precision, invariant, numerical-gap agents) using concrete boundary values (ttl=1, ttl=MAX_TTL, ttl=MAX_TTL+1) — the narrowing cast and checked addition are both unreachable-overflow by roughly 13 orders of magnitude of margin under any timestamp this chain will ever produce. - **DoS/griefing risk** — the client's explicit ask. Confirmed no on-chain-exploitable DoS vector exists: no loops, no SSTORE anywhere, no shared mutable state, cost is affine (not superlinear) in `peerRecord` size and hard-capped at ~120-130k gas by the 4096-byte limit. One caller's activity cannot raise another's cost. (The off-chain-consumer-facing spam angle is folded into Finding 1.) - **ABI compatibility / event encoding** — the client's explicit ask. Event topic layout (`namespace`, `recordType` indexed; `validUntil`, `peerRecord` in data), error selectors, and encode/decode field ordering were checked by 3 independent agents (Phase 1 general; Phase 2 execution-trace, periphery) with no mismatch found; calldata-length spoofing was specifically attempted and confirmed impossible (the Solidity ABI decoder validates length/offset against calldata size before the function body ever executes). - **TTL and record-size validation** — the client's explicit ask. Both guards (L28, L31-33) were boundary-tested by 5 independent agents across both phases with concrete inclusive/exclusive boundary values; both are correctly inclusive at the upper bound and correctly reject zero, with no off-by-one. ## Leads None remain unresolved. All leads raised during Phase 2 (5, across 4 agents) were reconciled into Finding 1 above as corroborating angles of a single underlying theme; no lead was dropped, demoted without explanation, or left open. --- > This review was performed using a three-phase AI-orchestrated methodology (context-building → ethskills breadth checklists → pashov-style attacker-mindset agents) across 20 total sub-agent passes. AI analysis can never verify the complete absence of vulnerabilities and no guarantee of security is given. The target audited here is unusually small (37 lines, one state-changing function, no storage, no external calls) — this is reflected honestly in the finding count and severity distribution rather than inflated to appear more thorough. Independent human review and, if this contract is a template for future non-trivial deployments, ongoing monitoring are recommended as standard practice.