Task: P3-6.2 Adversarial Suite — Threat-Model Doc + Multi-Actor Scenario Tests (#154) + Testnet-Parameter Readiness (#155)
ID: task_240926_17
Assigned to: Robbert Abimbola (@robertocarlous)
Created: 2026-09-24
Status: Open (assigned)
Dates: Sep 24 – Oct 1, 2026. This is the same 7-day window as task_240926_16: both tasks are due by Oct 1.
Repo: https://github.com/InfiniteZeroFoundation/DevNet
Base branch: develop — spec written against commit a62a508 (2026-09-24); re-pull before starting regardless of when you begin.
Roadmap ref: P3-6.2 (Adversarial & Stress Testing), P3-5.1/5.2 (tokenomics parameters, MECHANISM_DESIGN §9 items 2, 13)
Tracking:
- #154 (closes, pending review). Scope agreed in the #154 review comment, which supersedes the issue's original scope list and acceptance criteria.
- #155 (Part 3; closes once both the code below and the Fri Sep 25 meeting's values have landed). Scope agreed in the #155 review comment.
- Discussion #164 (progress updates, questions, PR links). task_240926_16 tracks separately in Discussion #163.
#154's blockers (#146 S1/S2/S5/S6 slashing, #149 S4 dispute second phase) both merged on 2026-09-11/12, so the adversarial suite is unblocked. The #154 review found three problems with the issue as written.
-
Several scenarios have no defined expected outcome today. A test without one ends up either marking a known gap as passing or failing by design.
- Mis-framed auditor/aggregator scenario: the issue lists "Auditor colluding with aggregator to pass a bad T2 model: S1 slash path". That doesn't happen.
- Auditors only judge client local models, via an eligibility vote and a score per model index. They never see T1/T2 output. Aggregators only consume the approved set (
approvedModelIndexes,DINTaskCoordinator.sol:579). - S1 only catches missed votes. S3 (median deviation) is shadow-mode, with
s3SlashingEnabled = false(DINTaskAuditor.sol:151). - The real cross-role surface is the approved-set handoff, split into three rows in Part 1.
- Auditors only judge client local models, via an eligibility vote and a score per model index. They never see T1/T2 output. Aggregators only consume the approved set (
- Sybil subgroup capture: no defence exists beyond per-identity stake and the S6 capacity cap.
- Owner + aggregator dispute suppression:
resolveDispute/settleRecomputationareonlyOwner, so the owner is the adjudicator.
- Mis-framed auditor/aggregator scenario: the issue lists "Auditor colluding with aggregator to pass a bad T2 model: S1 slash path". That doesn't happen.
-
The dispute-lifecycle expectations in the issue are inverted. See §2 below.
-
Every liveness and dispute-lifecycle scenario the issue lists already has a unit test:
SlashingInvariants.t.sol(18 tests)PR146SlashingRegression.t.sol(3 tests)DisputeResolution.t.sol(27 tests)
What's missing is multi-actor, multi-GI scenarios run through the real GI state machine.
So Parts 1–2 go in order: a short threat-model doc (reviewed first), then the scenario suite it defines. Part 3 (#155) is independent: small code changes that get the testnet tokenomics parameters ready to set, ahead of the values being decided at the Fri Sep 25 meeting.
A few pages, not a mechanism design. It covers:
-
Scenario matrix: one row per scenario, with these columns:
Field Meaning Attacker(s) Role(s) and collusion set Capability What they control: keys, stake, adjudication, timing Protocol response today Contract + function + reason code, or "none" Expected test outcome DEFENDED: the attack fails or is slashed, with the amount asserted. COST-BOUNDED: the attack succeeds; the test asserts the cost (stake at risk vs. gain). KNOWN GAP: the attack succeeds; the test asserts that and links a tracking issue. Trust assumption e.g. "owner is honest adjudicator (S4, pre-decentralization)", "sequencer doesn't grind blockhash" -
Minimum rows. Add more if you find them:
- Sybil operator, N identities, registering across consecutive GIs → S6 capacity cap / escalation
- Sybil capture of k-of-n seats in an audit batch or T1 subgroup → COST-BOUNDED (stake required vs. seats captured)
- Recidivist missing votes across GIs → S5 escalation to full slash + jail → excluded from the next GI's registration (
isValidatorActiveis false while jailed) - Auditor bloc approves a poisoned client model (auditor + client collusion; no aggregator needed, since honest aggregators fold in whatever
approvedModelIndexesreturns) → S3 shadow-modeAuditorScoreDeviationemitted, no slash → KNOWN GAP until S3 graduates - T1 aggregator majority submits a wrong CID (aggregators only; no auditor needed) → the wrong CID wins consensus, and the honest dissenter is slashed under
AGG_T1_BAD_CONSENSUS. S4 dispute is the only remedy. The doc decides whether this is DEFENDED (via S4, under the owner-adjudicator assumption) or a GAP. - Cross-role: one operator holding both roles, plus owner batch-routing → nothing prevents one validator registering as both auditor and aggregator in the same GI (only the shared concurrent-registration cap applies), so one operator controls both the filter and the combiner.
- Colluding aggregators can skip any robust-aggregation step in the model's
aggregator.py(e.g. outlier clipping) that would otherwise drop an approved poisoned model. That part is model-dependent and happens off-chain. On-chain it looks identical to the wrong-CID row, since the contract only sees CIDs, so document it in the doc rather than trying to test it in Forge. - The model owner can route the poisoned model into the colluding T1 batch, because
_shuffleUintArrayis seeded fromblock.timestamp+msg.sender, which is the same randomness class as #156. - The doc should state whether dual-role registration in one GI is intended. If it isn't, it's a KNOWN GAP with a new issue.
- Colluding aggregators can skip any robust-aggregation step in the model's
- Challenger right, owner rejects the dispute → KNOWN GAP under the owner-adjudicator assumption
- Owner steering the dispute recomputation subgroup → DEFENDED once task_240926_16 Part A's future-block seed lands (KNOWN GAP before it)
- Reward manipulation: auditor submits inflated scores → whether
_medianOfbounds their effect on reward share (DINTaskAuditorreward settlement; seeRewardEngine.t.sol) - Duplicate-update gaming → deferred: depends on BlockFLow fold-in scoring (#39), which isn't implemented. List it, don't test it.
-
Trust-assumptions section: every assumption the DEFENDED rows rely on, stated once.
-
Gaps-before-audit list: the KNOWN GAP rows, each with its tracking issue. Open a new issue where none exists and link it.
Review gate: open the doc as its own PR (or post a draft on #154) and get Umer's sign-off on the matrix, especially the expected-outcome column, before writing Part 2 tests against it. Aim to have it up within the first 2 days of the window.
- One test per matrix row, named by outcome class:
test_defended_<scenario>test_costBounded_<scenario>test_knownGap_<scenario>
- Each test gets an attack-narrative NatSpec block: who the attacker is, what they do, what the protocol does, and which matrix row it implements.
- Run scenarios through the real GI lifecycle (register → LMS → evaluation → aggregation → slash → end). Cross-GI attacks (S5/S6/Sybil) span ≥2 GIs.
- Don't re-test single-function paths already covered by
SlashingInvariants.t.sol,PR146SlashingRegression.t.sol, orDisputeResolution.t.sol. Reuse their setup helpers where you can, rather than duplicating fixtures. - KNOWN GAP tests assert the attack succeeds. When the gap closes they fail, and get flipped to
test_defended_in the fixing PR. Say so in each one's NatSpec.
settleRecomputation(confirmed=true): the recomputation confirmed the original CID was wrong. The challenger's bond is returned, and matching original aggregators are slashed (S4_INVALID_AGGREGATION).settleRecomputation(confirmed=false): the original CID stood. The challenger's bond is forfeited, and nobody is slashed.expireDispute: the bond is fully returned, and the fresh subgroup is slashed (S4_FRESH_SUBGROUP_TIMEOUT).
- Part 1 (doc) has no dependency. Start it immediately.
- Subgroup-steering scenario: write it against task_16 Part A's future-block seed (
lockDisputeSeed), not commit-reveal. Before Part A merges, it's atest_knownGap_that assertsblockhash(block.number - 1)steering works. Flip it in or after Part A's PR. - Bond/forfeiture destination assertions: write these after task_16 Part B merges, or assert through its helper.
settleRecomputation(false)moves from full-bond-to-treasury to 50/50 burn/treasury. - Everything else in Part 2 can proceed in parallel with task_16.
Roadmap P3-6.2's "10 simultaneous GIs × 20 clients, gas within L2 budget". Extend GasSimulation.t.sol, which already has at-scale pieces, rather than starting a new harness. This is a stretch goal. If time runs short, list it in the doc as deferred rather than rushing it.
The actual values (mintCap, dinPerEth and its authority, the four DinEmission params, the S1/S2 default and who may set it, MIN_STAKE) are decisions for the Fri Sep 25 meeting, not for this task. See the decision table in the #155 review. This part makes the code ready, so the decision lands as a config change:
- 3a. Parameterise
foundry/script/DeployPlatform.s.sol.- Read these with
vm.envOr(...), following the existing pattern inDeployFairLaunchDistributor.s.sol:52-58:dinPerEth,mintCap, the fourDinEmission.initializeargs (today literals at:131-134),MIN_STAKE, and the S5/S6 params. - Default every one to today's value, so local deploys and existing tests are unchanged.
- After deploy, call
DinCoordinator.updateDinPerEth/setMintCapandDinValidatorStake.setMinStake/setS5RecidivismParams/setS6NoParticipationThresholdonly when an override is set, andconsole.logthe effective values. - Document the env keys in the script header and in
.env.example. - Extend
DeployPlatform.t.solto assert that overrides take effect, and that defaults are unchanged when unset.
- Read these with
- 3b. Bug fix: a 0 bps S1/S2 slash fraction bricks the GI.
setS1SlashFractionBps(DINTaskAuditor.sol:210) andsetS2SlashFractionBps(DINTaskCoordinator.sol:1071) accept0.- The computed
s1Amount/s2Amount(DINTaskAuditor.sol:1290,DINTaskCoordinator.sol:888) is then 0, andDinValidatorStake.slashPartialrevertsInvalidSlashAmounton a zero amount (:301). - So
slashAuditors/slashAggregatorsrevert whenever anyone missed a vote or submission, and the GI can't get past slashing. The same happens through rounding ifMIN_STAKE * bps / 10000 == 0. - Fix both: reject
bps == 0in both setters, and skip theslashPartialcall when the computed amount is 0. - Add regression tests for the setter guard and the rounding path.
- Don't change who can set the fraction; that's a meeting decision.
- 3c.
DinEmissionNatSpec. The header (DinEmission.sol:20) says emission "decays by (decayBps/10000) per epoch". The code multiplies bydecayBps / 10000(:258), i.e. it's a retention fraction (8000= 20% decay/epoch). Fix the header to matchinitialize's@param(:126). - 3d. After the meeting: put the agreed values into
.env.sepolia_op_devnet(or the testnet env file, if one is created for DevNet 3.0) through 3a's keys, in a small follow-up commit to the same PR or a separate one. If the meeting changes authority (e.g. S1/S2 fractions become DAO-bounded, orupdateDinPerEthgets a different gate), that's new contract work: open an issue for it rather than growing this part.
Part 3 touches DINTaskAuditor.sol/DINTaskCoordinator.sol (the two setters and slash loops). Those files are also touched by task_240926_16, so rebase on develop after whichever task_16 PR merges first. The change is small and shouldn't conflict meaningfully.
-
Developer/design/adversarial-threat-model.md: scenario matrix (all minimum rows), trust-assumptions section, and gaps-before-audit list; signed off by Umer before Part 2 tests are written against it -
foundry/test/P3Adversarial.t.sol:- one test per matrix row, named by outcome class, each with an attack-narrative NatSpec block linking its row
- cross-GI scenarios span ≥2 GIs through the real lifecycle
- no duplication of single-function paths already covered in the three suites named above
- dispute expectations match §2
- Every KNOWN GAP test links an open issue (new issues opened where needed)
- Full
forge build && forge testgreen - Stretch: 10 GIs × 20 clients stress scenario in
GasSimulation.t.sol, or listed as deferred in the doc - Part 3a:
DeployPlatform.s.solreads the tokenomics params viavm.envOrwith today's values as defaults; overrides applied post-deploy and logged; keys documented in.env.example;DeployPlatform.t.solcovers override and default cases - Part 3b:
setS1SlashFractionBps/setS2SlashFractionBpsreject0; slash loops skipslashPartialwhen the computed amount is 0; regression tests for both - Part 3c:
DinEmissionheader NatSpec says retention, not decay - Part 3d: Fri Sep 25 meeting values put into the network env file (follow-up commit once decided)
- Update #154's issue text (or note the supersession on the issue) so it reflects the doc-first scope and the corrected dispute semantics
Three PRs from separate branches into develop:
-
PR 1 (Part 1): the threat-model doc, references #154. Merge gate for Part 2's expected outcomes.
-
PR 2 (Part 2):
P3Adversarial.t.sol(+ stretch), references #154 (closes). In the description, list:- each matrix row → test name → outcome class
- which rows flipped from
knownGaptodefendedbecause task_16 had merged by then
-
PR 3 (Part 3): references #155. In the description, list every env key with its default, and note that the 3b fix covers both the setter and the rounding path. The meeting's values follow as 3d. #155 closes when they land.
Post all PR links and progress updates in Discussion #164.
- The expected-outcome column is the judgment call in this task. Where it's unclear whether something should be DEFENDED or is a GAP (e.g. the colluding-T1-majority row), flag it in the doc PR. Don't pick silently.
- Timeline: this shares the Sep 24 – Oct 1 window with task_240926_16's three PRs. If both can't fit, say so early in #164. The likely trade is the stretch stress scenario first, then any rows that depend on task_16 Parts A/B merging.
- Parts 1–2 are test and doc work only. No contract changes there. If a scenario reveals a real bug, open an issue and add a
test_knownGap_for it; don't fix it in those parts. Part 3's two setter guards + zero-amount skip are the only contract changes in this task. - Part 3 doesn't depend on the meeting. 3a–3c can land before Sep 25, and only 3d waits for the values.