Task: Security-Review Follow-Through — Low Findings L-2/L-3/L-6 (#159, #160, #161) + Aggregation-Side H-2/M-1 (#156)
ID: task_240926_18
Assigned to: Similoluwa Abidoye (@Abidoyesimze)
Created: 2026-09-24
Status: Open (assigned)
Dates: Sep 24 – Oct 1, 2026 (7 days). Part A can start immediately. Parts B and C are gated on task_240926_16 PRs merging (see §Sequencing), so if those slip, say so early in the tracking discussion.
Repo: https://github.com/InfiniteZeroFoundation/DevNet
Base branch: develop — spec written against commit 3e52351 (2026-09-24); re-pull before starting regardless of when you begin.
Roadmap ref: P3-6 (security hardening before audit); findings H-2, M-1, L-2, L-3, L-6 of foundry-src-security-review.md (your own review, task_060726_4)
Tracking:
- Part A: #159, #160, #161 (all close, pending review). Scope agreed in the review comments: #159, #160, #161.
- Parts B + C: #156 (closes once both land). Scope and sequencing agreed in the #156 review comment, which supersedes the issue's
keccak256(cid, salt)hash and its auditor-side claim. - Discussion #165 (progress updates, questions, PR links)
- Related, owned by Robbert, not in scope: task_240926_16 (dispute seed #151, treasury forwarding #152, lifecycle events #153), task_240926_17 (adversarial suite #154, tokenomics params #155)
None of the four issues is implemented on develop:
| Issue | Finding | State at 3e52351 |
|---|---|---|
| #159 | L-2 | DinCoordinator.depositAndMint (:86-94) still has no mintAmount == 0 check |
| #160 | L-3 | DINModelRegistry.requestModelRegistration (:161-200) still only checks msg.value < requiredFee and records feePaid: msg.value |
| #161 | L-6 | Both task-contract constructors (DINTaskCoordinator.sol:223, DINTaskAuditor.sol:400) still take dependency addresses unchecked. Both now also take a modelId_ argument, which the issue text predates. |
| #156 | H-2/M-1, aggregation side | _shuffleAddressArray still seeds from blockhash(block.number - 1) (DINTaskCoordinator.sol:553), _shuffleUintArray from block.timestamp + msg.sender (:565). submitT1Aggregation/submitT2Aggregation still take a plaintext CID (:694, :790). |
Two corrections to #156 as written:
- "Already fixed on the auditor side by PR #63" is only half true. PR #63 added commit-reveal for auditor scores (M-1, auditor side). The auditor batch shuffle (
DINTaskAuditor._shuffleAddressArray/_shuffleUintArray,:743/:779, used bycreateAuditorsBatches) is still blockhash/timestamp-seeded. H-2 is open on both sides. Part B covers both. - The issue only names the address shuffle.
_shuffleUintArray(the model-index shuffle) decides which approved models land in which T1 batch. task_240926_17's threat model relies on it for the "owner routes a poisoned model into a colluding T1 batch" row, so it's in scope too.
Robbert's two tasks run Sep 24 – Oct 1 and touch the same files. Each part below is ordered to avoid conflicts:
| Part | Touches | Conflicts with | Rule |
|---|---|---|---|
| A (#159/#160/#161) | DinCoordinator.depositAndMint, DINModelRegistry.requestModelRegistration, both task-contract constructors + setDINTaskAuditorContract |
Nothing in task_16/17 touches these functions. task_17 Part 3a calls updateDinPerEth from the deploy script only. |
Start immediately. |
| B (#156 H-2) | _shuffleAddressArray/_shuffleUintArray in both task contracts, autoCreateTier1AndTier2, createAuditorsBatches |
task_16 Part A1 changes _shuffleAddressArray to take an explicit bytes32 seed, and explicitly leaves the autoCreateTier1AndTier2 seed to #156. |
Start after task_16 PR 1 (Part A) merges. Build on its helper and don't fork a second shuffle. |
| C (#156 M-1) | submitT1Aggregation/submitT2Aggregation/finalize*, the S2 slash loop, GIstates enum, dincli aggregator commands |
task_16 Part C adds _setGIstate and emits T1AggregationSubmitted/T2AggregationSubmitted in the submit functions. Its subgraph half (C5) regenerates the uint8 → GIstates mapping. task_17 Part 3b edits the S2 slash loop (:888). |
Start after task_16 PR 3 (Part C) merges. Rebase on task_17 PR 3 if it has merged. |
task_17 Part 2 will contain test_knownGap_ tests for the batch-routing / shuffle-steering rows. When Part B (or C) closes one of those gaps, flip that test to test_defended_ in your PR, as task_17's Notes require.
Three small, independent fixes. One PR, one commit per issue.
- In
DinCoordinator.depositAndMint, after computingmintAmount, addif (mintAmount == 0) revert ZeroMintAmount();with a new custom error. DinCoordinatoris behind a transparent proxy. A new error adds no storage, but runUpgradeValidation.t.solanyway.- Test in
DinCoordinator.t.sol: set a non-rounddinPerEthviaupdateDinPerEth, send amsg.valuethat rounds to 0, and assert the revert and that no ETH is retained.
- Shape: refund the excess. This is the issue's own preference, and it tolerates the stale-fee race. If you prefer exact-match, raise it on #160 first.
- Record
feePaid: requiredFee, notmsg.value. - Refund
msg.value - requiredFeetomsg.senderafter themodelRequests.pushand the event (checks-effects-interactions), and revert on a failed send with a custom error. - Reentrancy:
DINModelRegistryhas no guard today. AddReentrancyGuardTransient, asDinCoordinatoralready does (DinCoordinator.sol:7). It uses transient storage, so the proxy storage layout is unchanged. Confirm that againststorage_layout.mdandUpgradeValidation.t.sol. dincli/cli/task.py:263already sends exactlyrequired_fee, so no dincli change is needed. Confirm that.- Tests in
DINModelRegistry.t.sol:- an overpayment is refunded and
feePaid == requiredFee - an exact payment is unchanged
- a refund to a reverting receiver reverts the whole call
- an overpayment is refunded and
- Out of scope, just note it in the PR:
rejectModel(:250) doesn't refundfeePaidat all. Whether a rejected request should get its fee back is a policy question, not an L-3 fix.
DINTaskCoordinatorconstructor: revertTC_InvalidAddressifdinvalidatorStakeContract_address == address(0).DINTaskAuditorconstructor: revertTA_InvalidAddressif either address argument is zero.- Also
DINTaskCoordinator.setDINTaskAuditorContract(:234). It's one-shot and advancesGIstateright away, so passingaddress(0)there bricks the contract the same way. Same check, same error. - Both errors already exist. Don't add new ones.
- Tests: one revert test per argument, plus the setter. Put them wherever the existing constructor/setup tests live, or in
SecurityFindings.t.solnext to your other PoCs, and say which in the PR.
-
depositAndMintreverts on a zeromintAmount; test at a non-rounddinPerEth -
requestModelRegistrationrefunds the excess, recordsfeePaid = requiredFee, and isnonReentrant; tests for overpay/exact/reverting-receiver; storage layout confirmed unchanged - Zero-address checks on both constructors and
setDINTaskAuditorContract; revert tests for each -
forge build && forge testgreen, full suite, includingUpgradeValidation.t.sol - Bundled ABIs in
dincli/abis/refreshed if they changed (new errors change the ABI) - Audit doc: mark L-2/L-3/L-6 as fixed, with a link to the PR
Starts after task_16 PR 1 (Part A) merges.
task_16 Part A fixes the dispute-redo shuffle (BL-11) with a future-block seed fixed in advance plus a permissionless lock. Part B uses the same pattern for the two regular batch-creation calls. We end up with one randomness pattern across the protocol, and one sequencer-trust caveat.
- Anchor at the preceding state transition, which the owner can't aim because the seed block is still in the future:
- T1/T2:
closeLMsubmissionsEvaluation(DINTaskCoordinator.sol:472) setsaggSeedBlock[GI] = block.number + seedDelay - auditor batches:
closeLMsubmissions(:411, the transition toLMSclosed) setsauditSeedBlock[GI]the same way
- T1/T2:
- A permissionless
lock…Seed(GI)requiresblock.number > seedBlock. It storeskeccak256(abi.encodePacked(blockhash(seedBlock), GI, <domain tag>)), and if more than 256 blocks have passed (blockhash == 0) it re-anchors. Mirror task_16 A3 exactly, and never store or use a zero-derived seed. autoCreateTier1AndTier2andcreateAuditorsBatchesrevert if the seed isn't locked. Otherwise they don't change.- Both shuffles take the locked seed, domain-separated so the address and index shuffles don't correlate, e.g.
keccak256(seed, "AGG")/keccak256(seed, "IDX"). Dropblock.timestampandmsg.senderfrom_shuffleUintArrayentirely. - Delay parameter: reuse task_16's
disputeSeedDelayif it fits, otherwise add one with the same setter + non-zero-guard pattern. Say which in the PR. DINTaskAuditor's shuffles: give them the same seed-taking signature as task_16 A1's coordinator helper. Don't add a third shuffle variant.- NatSpec: state the residual trust assumption (the OP Stack sequencer produces
blockhash), exactly as task_16 A4 does. VRF stays the mainnet follow-up.
- The model owner now needs one extra call per batch-creation step (lock the seed), and must wait
seedDelayblocks first. - Add it to
dincli/cli/modelownerd/aggregation.py(beforeautoCreateTier1AndTier2,:36) andauditor_batches.py. Poll untilblock.number > seedBlock, then lock, then create. If the seed is already locked (someone else locked it), skip the lock step. - Update
Documentation/public/workflows/model-workflow.mdfor the extra step. - Tests in
tests/using the existingmonkeypatch+CliRunnerpattern.
- Seed anchored at the preceding transition; permissionless lock with the >256-block re-anchor; zero seed never used
-
autoCreateTier1AndTier2andcreateAuditorsBatchesrequire a locked seed; all four shuffles (address + index, both contracts) use it with domain separation; noblockhash(block.number - 1),block.timestamp, ormsg.senderentropy left in any shuffle - Tests:
- timing independence: the same GI batched at two different blocks (
vm.snapshot/vm.revertTo+vm.roll) yields identical T1 batches and identical auditor batches - gating, re-anchor, lock guards, permissionless lock (as in task_16 A's test list)
- timing independence: the same GI batched at two different blocks (
- Any task_17
test_knownGap_for shuffle steering / batch routing flipped totest_defended_ - dincli lock-then-create flow + tests;
model-workflow.mdupdated -
forge build --sizesbefore/after in the PR.DINTaskAuditoris 22,382 B at3e52351, ~2.2 KB under EIP-170, and task_16 B/C will add to it. If either contract crosses 24,576 B, stop and raise it in the tracking discussion. Don't split contracts on your own initiative. - Bundled ABIs refreshed; audit doc H-2 marked fixed (both sides);
storage_layout.mdupdated if it records the changed state
Starts after task_16 PR 3 (Part C) merges.
Mirror PR #63's auditor commit-reveal, with one hardening:
- Bind the committer into the hash:
commitHash = keccak256(abi.encode(cid, salt, msg.sender, GI, tier, batchId)), notkeccak256(cid, salt)as #156 proposes.- Without
msg.sender, a lazy aggregator copies an honest peer's commit hash, waits for that peer's reveal, and reveals the same(cid, salt). That is exactly the free-riding M-1 is meant to stop. - PR #63's auditor hash (
keccak256(abi.encodePacked(score, vote, salt)),DINTaskAuditor.sol:1127) has this same weakness. Don't fix it here. Open a new issue for it (auditor-side M-1 residual) and link it in the PR.
- Without
- Reveal-window states.
T1AggregationStartedbecomes the commit window, then a newT1AggregationRevealStartedstate, thenfinalizeT1Aggregation. Same for T2 with a newT2AggregationRevealStarted. The owner opens each reveal window, the same way the auditor-side reveal is opened.- Enum placement: insert in lifecycle position, not append. This is the 2026-08-27 precedent recorded in
DINShared.sol:25-34. - This shifts every later ordinal, so you must:
- update
dincli/cli/utils.py'sstates/stateDescriptionmirrors - update
DINShared.md§2.1 - post the new ordinal table on PR #29 so the subgraph mapping (task_16 C5) gets regenerated. Robbert owns the subgraph; don't push to his branch.
- update
- Route every new transition through task_16's
_setGIstate.
- Enum placement: insert in lifecycle position, not append. This is the 2026-08-27 precedent recorded in
- Votes count only on reveal.
t1Votes/t2Votesandt1SubmissionCID/t2SubmissionCIDare written in reveal, not commit.finalize*counts only revealed submissions. TheT1AggregationSubmitted/T2AggregationSubmittedevents (task_16 C1) move to the reveal function, with signatures unchanged. AddT1AggregationCommitted/T2AggregationCommittedevents only if the subgraph needs them; ask on #29. - Commit but no reveal = missed submission for S2. Check that the S2 slash loop and the dispute/recomputation paths (
:912,:947,:1275) read revealed state only, and that reward-weight accounting infinalize*(task_100926_11'saggregatorWeight) is unchanged for revealed aggregators. - Don't change dispute semantics. Disputes open against the finalized CID exactly as today.
This is the same shape of work as PR #63's auditor side (auditor.py, lms_evaluation.py, utils.py):
dincli/cli/aggregator.py: split the T1 (:342) and T2 (:494) submit paths into commit + reveal. Persist the salt locally the same way the auditor commit-reveal does, and never regenerate it on retry.dincli/cli/modelownerd/aggregation.py: commands to open the T1/T2 reveal windows.- Tests following PR #63's pattern.
-
commitT1Aggregation/revealT1Aggregationand T2 equivalents; hash bindsmsg.sender, GI, tier, batchId; new reveal states inserted in lifecycle position - Votes, CIDs, and task_16's submission events are written on reveal only; unrevealed = missed for S2; rewards/disputes unaffected
- Tests (a new
AggregatorCommitReveal.t.sol, mirroringAuditorCommitReveal.t.sol):- copy attack defeated: an aggregator replaying a peer's commit hash can't reveal it
- a wrong salt/CID reveal reverts; reveal outside the window reverts; double commit/reveal reverts
- a commit without reveal is S2-slashed
- full GI walk through the new states
-
dincli/cli/utils.pystate mirrors,DINShared.md§2.1, and the ordinal table posted on PR #29 - dincli commit/reveal for aggregators + model-owner reveal-window commands, with tests;
model-workflow.mdupdated - New issue opened for the auditor-side commit-hash binding gap and linked
-
forge build --sizesbefore/after in the PR (same EIP-170 rule as Part B;DINTaskCoordinatoris 20,159 B at3e52351) -
forge build && forge test+pytestgreen; ABIs refreshed; audit doc M-1 (aggregation side) marked fixed - Update #156's issue text (or comment) to reflect the three-PR split, the sender-bound hash, and the auditor-shuffle correction above
Three PRs from separate branches into develop, each rebased on develop after the previous one (and the gating task_16 PR) merges:
- PR 1 (Part A): references #159, #160, #161 (closes). Say whether A3's tests went into
SecurityFindings.t.soland confirm A2's storage layout is unchanged. - PR 2 (Part B): references #156 (H-2 half). Include the delay-parameter decision,
--sizesnumbers, and any task_17 tests flipped. - PR 3 (Part C): references #156 (closes). Include the new ordinal table,
--sizesnumbers, and the new auditor-side issue link.
Post each PR link and progress updates in Discussion #165.
- Flag every
DINTaskCoordinator.sol/DINTaskAuditor.sol/DINShared.solchange for Umer's review, same as prior tasks in this area. - Parts B and C will rebase onto Robbert's in-flight work. If a task_16 PR stalls, don't build Part B/C against its branch. Say so in the tracking discussion, and pick up Part A follow-ups or the auditor-side issue write-up in the meantime.
- If you'd rather use VRF or multi-party commit-reveal than the future-block seed for Part B, raise it on #156 before building it. The point of Part B is one consistent pattern with task_16 Part A.
- C-1 (registration gas-DoS) and M-2 (
disableModelkill-switch) remain out of scope, as #156 already states.