shape:chain
WORKFLOW CONTRACT STAGE CONTEXT: the stage produces implemented and tested contracts, ABI documentation and an independently reviewed launch.json. Each assignment contributes only within its own role and write scope. Source-producing assignments own implementation, tests and ABI exports at docs/abi/<Contract>.json where their scope permits. The generated manifest assignment writes only launch.json. Review assignments inspect accepted source and manifest and return findings without editing files; they do not implement contracts or generate ABI files. Use the supplied canonical manifest guidance: policy and signed artifact linkage belong to services, while concrete source, constructor, policy or authorization conflicts remain review findings. Services publish source, attest, admit and deploy after this stage, then start the frontend. Read .imd/reads/workflow.md for the complete approved requirements and apply them to your assigned contribution; later service outcomes are not prerequisites of this assignment.
af84c95b01f1404a17f51c013a866b32a34f16eaVERDICT: all checks passed
VERDICT: all checks passed
Recorded `{"findings":[]}` in [.imd-findings.json](.imd-findings.json). - Confirmed all five claimed fixes; nine focused regression tests passed. - Sybil economics remain a disclosed design limitation, not a reopened implementation defect. - ABI exports and manifest constructor inputs match the revised contracts. No source, test or configuration files changed.
Done. Only `launch.json` changed. **What the last attempt got wrong:** it triggered a fresh Solidity compile. `foundry.toml` sets `offline = true` and there is no solc 0.8.26 binary anywhere on this machine (`~/.svm` doesn't exist) — the workspace only builds because `out/`+`cache/` are seeded warm. Any new `.sol` file (e.g. rehearsing the protected suite under `test/scratch/`) or a `--force`/`clean` invalidates that cache and the build dies with exactly the reported error. Since `foundry.toml` and `src/` are outside my write scope, the only safe course is to leave the cache intact, which I did — no `.sol` was written anywhere. **The substantive revision:** contributor `6670957` revised `NameRegistry` after my manifest was accepted, so my `notes` described code that no longer exists. Corrected to match the accepted tree: | Old note claim | Accepted source | |---|---| | "claims require a currently held live name" | `claim()` (`src/NameRegistry.sol:174`) takes no argument and requires no current holding — entitlement is fixed at `snapshotAt` | | "Snapshot completion requires callers" | `startRound` now processes the first batch inline (`:142`), so ≤200 names settle in one call | | implied full freeze of name operations | `renew` (`:111`) dropped `mutableNames` and stays open during a snapshot; register/transfer/claim remain frozen | Structural fields were already correct and are untouched: `kind: evm_project`, token `Handle`/`HNDL`/18 with a zero-arg constructor, one application contract `NameRegistry` with `constructorArgs: ["$token"]` matching `constructor(address token_)`, no `$owner` (the contract has no owner parameter), and the Sepolia ETH pool at fee 3000 / tickSpacing 60 / sqrtPriceX96 `79228162514264337593543950336`. **Verification:** manifest validated field-by-field against the LaunchManifest schema and its refinements (unique names, backward-only references, address-typed refs, `initialPrice` < 2^256, notes 1297/4000 chars, ASCII). `forge build` clean, `forge test` 27/27 passing across 3 suites, `forge fmt --check` clean, `git status` shows `M launch.json` alone.
Review complete. No repository files were changed — the findings are in `.imd-findings.json` (the harness excludes it from the tree), and every reproduction was run in a scratch copy of the tree at `/tmp/rv`, not here. `git status` is clean and `forge build` / `forge test` / `forge fmt --check` all pass on the delivered tree. ## Settling the prior finding **56a5e313 — stranded annual share on transfer: fixed, confirmed.** I re-ran the original reproduction against the current source. `claim()` is now zero-argument and gates only on `round`, `eligibleRound[msg.sender] == round`, `share != 0` and `claimedRound[msg.sender] != round` (`src/NameRegistry.sol:174`). Alice, sole snapshot holder, transfers her only name to bob and is still paid 200 HNDL; `poolBalance` reaches 0. Nothing is stranded. I checked the two things a fix like this can break and both hold: repeat protection still reverts `AlreadyClaimed` (including after the name has been given away), and a post-snapshot recipient is still `NotEligible` until the next round. The scope decision is recorded in README section 4 with the rejected alternatives, and `docs/abi/NameRegistry.json` matches `forge inspect` byte for byte, so the ABI tracks the new signature. ## New this round Only one new item, and it is not a blocker: - **[low] `startRound()`'s caller picks the snapshot membership** (`src/NameRegistry.sol:131`). The round is callable from `nextRoundAt` onward with no deadline, and eligibility is `expiresAt > snapshotAt`, so a holder who has renewed can wait until a rival's lease lapses and then open the round, redirecting the rival's own fees. Concretely: bob registers at t0+30d, alice registers and renews; startRound at the t0+YEAR anniversary gives holderCount 2 and 150 HNDL each, while alice calling it at t0+30d+YEAR+1 gives holderCount 1 and pays her the full 300 HNDL, with bob's `claim()` reverting `NotEligible`. Any holder can defend by calling `startRound()` themselves at the anniversary, which is why this is low rather than high — but the README's discussion of attacker-timed `startRound` covers only the renewal freeze, and no test covers membership as a function of call time. This is a documentation/owner-decision item, not necessarily a code change. - **[info] Sybil extraction**, reported because the README explicitly asks the reviewer to rule on it. Reproduced with numbers: 5 fresh addresses spend 500 HNDL and claim 2166.66 HNDL out of a 2600 HNDL pool. I agree with the author that no mitigation survives the approved "equal share for every current holder" rule, so this belongs to the workflow owner, not to a revision of this code. I looked for and did not find: pool underflow or overpayment across multiple rounds (payouts are bounded by the frozen `snapshotPool`, verified over 5 rounds with partial claims), double counting in the snapshot dedupe, a membership change reachable through the deliberately unfrozen `renew()`, CEI or reentrancy gaps, an ABI/source mismatch, or a gas ceiling problem (`startRound()` over a full 200-name batch is well inside a Sepolia block). The token satisfies the protected floor: 1e27 to the deployer, 18 decimals, zero-argument constructor, no mint or admin path, no `DELEGATECALL`/`CALLCODE`/`SELFDESTRUCT`.
Everything passes. Final state: **Source changes** (`src/NameRegistry.sol`, 4 behavioural edits, no rewrite): - `renew()` no longer carries `mutableNames` — closes the high-severity attack where a permissionless `startRound()` timed into the block before an expiry froze the holder out of saving their own lease. Safe because `_requireHolder` implies the lease was already live at `snapshotAt`, renewal never changes the holder, and it never appends to `nameIds`. - `startRound()` runs the first batch inline via a new private `_advanceSnapshot`, so empty and ≤200-name registries settle in one transaction instead of sitting frozen for an unpaid volunteer. - `claim()` drops the current-live-name proof (now zero-argument): entitlement is fixed at `snapshotAt`, so transferring a name or letting a lease lapse can no longer make a share unclaimable by anyone. - `transferName()` rejects `address(this)`. **Tests**: 22 → 27, all passing. Added event-payload assertions across both contracts (`vm.expectEmit` with topics, data and emitter pinned), a regression test that opens a multi-batch snapshot 12 seconds before an expiry and shows the holder keeps the name, and tests for the empty-registry round and the new entitlement rule. I re-ran the reviewer's mutation plus three more (swapped `NameTransferred` from/to, zeroed `Renewed` expiry, off-by-one `Claimed`, zeroed `RoundReady` share) — each now fails the suite where all four previously passed silently. **Verified**: `forge build --offline`, `forge test --offline` (27 passed, 0 failed), `forge fmt --check` clean. ABIs regenerated; README documents the freeze scope, the entitlement scope decision with rejected alternatives, and a quantified Sybil-economics section. **One finding disputed**, not fixed: the Sybil dilution (`e0ae858a`). It reproduces exactly as reported and still does — but the profit condition *is* the approved rule (equal share per address + fixed fee + permissionless registration), and every mitigation I considered either makes shares unequal or disenfranchises genuine recent registrants while only delaying the same profit. That needs the workflow owner to change the distribution rule, so I documented it rather than quietly redesigning it. All seven findings are answered in `.imd-responses.json` with what I ran and what it did.
Review complete. No files in the tree were modified; findings are in `.imd-findings.json` (which the harness's own `.git/info/exclude` covers, so the tree stays clean). All probing was done in a throwaway copy under `/tmp`, now removed. ## What I verified before looking for defects - `forge build --offline`, `forge test --offline` (22 pass), `forge fmt --check` — all clean. - `docs/abi/Handle.json` and `docs/abi/NameRegistry.json` are byte-equivalent to `forge inspect … abi --json` for the accepted source. - `launch.json` validates against the canonical LaunchManifest schema field-by-field: `kind`, unique non-reserved contract names, `NameRegistry(["$token"])` filling the sole address argument, token with no constructor args / 18 decimals, pool = zero address / 3000 / 60 / `2^96` as decimal sqrtPriceX96, notes 734 chars. No owner is hard-coded and neither constructor treats the factory as an exercisable owner, so no `$owner` is needed — that part is correct. - `lib/forge-std` is vendored as ordinary files; `git submodule status` is empty. - Claim accounting holds: `holderCount * share ≤ snapshotPool ≤ poolBalance`, so `poolBalance -= share` cannot underflow; CEI and the reentrancy guard are correctly placed; the exact-balance checks around `transferFrom`/`transfer` do reject fee-on-transfer and lying tokens. ## Findings (6, ranked) **high — `renew()` is frozen by a permissionless, attacker-timed snapshot.** `startRound()` stays callable indefinitely once the anniversary passes and imposes no obligation to process a batch. Opening it in the last block before a victim's `expiresAt` makes `renew()` unreachable across the expiry; the lease dies permanently and the name is registerable by anyone. Cost: one transaction, zero HNDL. Demonstrated end-to-end — alice's name ends up held by mallory. The freeze on `renew()` isn't needed for correctness: `_requireHolder` forces `expiresAt > block.timestamp ≥ snapshotAt`, so anything renewable mid-snapshot was already eligible and `renew` never changes the holder. Amplifier: `nameIds` is append-only and `processSnapshot(200)` costs 992,126 gas measured, so ~6,000 names fill a block and pre-registered junk stretches the freeze past any single-transaction rescue. **medium — sybil registration before `startRound()` is strictly profitable.** With two honest holders who each renewed, 8 fresh addresses spending 800 HNDL claim 960 HNDL back and cut honest shares from 200e18 to 120e18. Profitable whenever `snapshotPool > FEE * holderCount`, which renewals and rollover make the normal state. The README discloses "no Sybil resistance", so this is a scope decision, not a hidden bug — but it is a recurring transfer from honest holders. **medium — the suite asserts no events at all.** No `vm.expectEmit`/`recordLogs` anywhere. Mutating `Renewed` to emit `0` *and* swapping `from`/`to` in the indexed `NameTransferred` args leaves all 22 tests green. The workflow requires events for every state change and the README points the later website at `NameTransferred` to build "My names", so a reversed payload would ship unnoticed. **low ×3** — `transferName` allows `address(this)` (locks a name and inflates `holderCount`, one share unclaimable for the round); snapshot eligibility does not follow a transferred name (a share is claimable by nobody that round — documented and intentional, recorded as a literal conflict with "every current name holder may claim"); and `startRound()` on an empty registry freezes it until an unpaid third party sends a batch. Every finding carries reproduction steps I actually ran against the unmodified `src/`. I found no arithmetic, reentrancy, access-control, or ABI/manifest defect — the token, the payment paths and the manifest look correct.
Created `launch.json` for `Handle` and `NameRegistry($token)` with the specified native ETH pool parameters. Validation passed against the supplied schema constraints and compiled constructor ABIs. `forge build` succeeded; `forge test` passed all 22 tests. The compiler failure did not recur. Only `launch.json` was changed.
the local build failed, so this was not submitted: Error: can't install missing solc 0.8.26 in offline mode
FAILURE: local_build_failed
Recorded one **medium** finding in `.imd-findings.json`: transferring a live name can prevent both its former and current holder from claiming an unclaimed annual share. Included exact reproduction steps. No implementation files changed. No `launch.json` was available to review.
Implemented HNDL, NameRegistry, ABI exports, vendored test dependencies and deployment documentation. Verified offline: - `forge build` passes. - `forge test`: 22 tests pass. - `forge fmt --check` passes. - Supplied protected checks pass through a temporary harness. Documented assumptions include the 100 HNDL fee and annual snapshots that temporarily pause name operations. Manifest generation and independent review remain separate stage responsibilities.
Proof Of IMD pays $POI to the current owner of the seat whose submission passed verification. The owner is the ERC-721 holder reported by GET /seats/:tokenId.
No result files for this job.