Task: Security Fixes for Your Own Findings — DinEmission.fundGI Mint/Drain (#226) + Manifest Path Traversal in dincli (#227)
ID: task_061026_22
Assigned to: Similoluwa Abidoye (@Abidoyesimze)
Created: 2026-10-06
Status: Open (assigned)
Dates: No fixed deadline. Parts A and B are independent and can both start immediately. Post progress in the tracking discussion so the review can be scheduled.
Repo: https://github.com/InfiniteZeroFoundation/DevNet
Base branch: develop. The spec was written against commit a1fcce2 (2026-10-05). Re-pull before starting, whenever you begin.
Roadmap ref: P3-6 (security hardening before audit). DevNet 2.0 launch readiness: both findings affect every network the platform is deployed to.
Tracking:
- Part A: #226 (closes, pending review). Scope agreed in the #226 status update.
- Part B: #227 (closes, pending review). Scope agreed in the #227 status update.
- Discussion #233 (progress updates, questions, PR links)
- Your other open issues, not in this task:
Neither finding is fixed on develop:
| Issue | State at a1fcce2 |
|---|---|
| #226 | DinEmission.fundGI(gi, taskAuditor) (foundry/src/DinEmission.sol:174) is external with no caller or taskAuditor check beyond != address(0). It mints to itself, calls safeIncreaseAllowance(taskAuditor, amount), then calls IDINTaskAuditor(taskAuditor).depositRewards. emissionState/giEmissionFunded are keyed by taskAuditor, so a fresh address restarts the decay schedule. DinCoordinator.mintCap defaults to 0 (uncapped). DinEmission holds no registry reference (storage: coordinator, dinToken, schedule params, the two mappings, uint256[50] private __gap at :115). |
| #227 | DinContext.ensure_file_exists (dincli/cli/context.py:490) does file_path.parent.mkdir(parents=True, exist_ok=True) and writes the fetched file, with no containment check. dincli/services/ipfs.py:81 _normalize_path only logs a warning for /etc, /boot, /dev, /proc. Manifest-driven call sites build <base> / Path(manifest[...]["path"]), so an absolute path replaces the base and .. escapes it. |
Correction to #226's recommendation: DINModelRegistry.getModelIdByTaskAuditor(taskAuditor) (foundry/src/DINModelRegistry.sol:422) returns (bool exists, uint256 modelId). Model ID 0 is a real model (modelId = models.length at approval), so the check must use exists, not modelId != 0.
| Part | Overlaps with | Resolution |
|---|---|---|
| A | task-plan-051026-2 (PR #229) TP-3 adds dincli model-owner rewards fund-emission, which calls fundGI |
Start now. TP-3 follows whatever check lands here. Mention the new revert in your PR so the command can check it first |
| A | task-plan-051026-1 (PR #219) changes DINTaskAuditor, not DinEmission |
None |
| B | task-plan-051026-2 TP-3 adds commands to dincli/cli/auditor.py and dincli/cli/context.py |
Start now. Whichever PR lands second rebases. Keep the helper small and in one place so the rebase is mechanical |
Both Parts are independent of each other. One PR each, from separate branches into develop.
- Add a
DINModelRegistryreference toDinEmission:- Storage: declare it after the existing state and before
__gap, and shrink__gapfrom 50 to 49.DinEmissionis behind a transparent proxy, so the existing slot order must not change. (Amended at the PR No. 234 merge:__gapstays at 50. Nothing ondevelopis deployed yet, so per the "Core rules" instorage_layout.mdnew variables go above__gapwithout shrinking it.) - Set it in
initialize(a new argument,InvalidAddresson zero, like the other two) and through anonlyOwnersetter with an event, for an already-initialised proxy. The setter also rejects zero. - Interface: add a minimal
getModelIdByTaskAuditor(address) returns (bool, uint256)(plusmodelDisabled(uint256) returns (bool)if you take A2's option) to the interface block at the top of the file. Don't import the whole registry.
- Storage: declare it after the existing state and before
- In
fundGI, before any state change:(bool exists, ) = registry.getModelIdByTaskAuditor(taskAuditor); if (!exists) revert UnregisteredTaskAuditor(taskAuditor);with a new custom error. Keep the existing zero-address check first. - Keep
fundGIpermissionless. Its NatSpec argument (the amount is fixed by the schedule) holds once the recipient is constrained. Update the NatSpec to say the recipient must be an approved model's task auditor, and why (#226).
Whether fundGI also refuses a model with modelDisabled[modelId] == true. Recommended: refuse. Emission for a model the DIN-Representative disabled is hard to justify, and the check is one extra view call. Say which you chose. This doesn't settle #224 M-2 (task contracts ignoring disableModel), which stays open.
foundry/script/DeployPlatform.s.sol:DINModelRegistryis deployed at step 10, beforeDinEmissionat step 13, so pass the registry proxy intoDinEmission.initialize. UpdateDeployPlatform.t.sol/DeployPlatformScript.t.solassertions that read the emission wiring.Documentation/technical/storage_layout.md## DinEmission: add the new slot and the gap change.Documentation/technical/contracts/doc forDinEmission(if present) andfoundry-src-security-review.md: record #226 as a post-review finding, fixed, in the same format as the other fix notes.dincli/abis/: regenerate any bundled ABI that changed (dincli system dump-abi --official --artifact foundry/out/<C>.sol/<C>.json --output dincli/abis).
- Your two PoCs become regression tests, now expecting the revert:
test_fundGI_arbitraryTaskAuditor_drainsMintedDIN→ expectsUnregisteredTaskAuditor, with no mint (totalMintedunchanged).test_fundGI_repeatedWithFreshFakeAuditors_eachGetsUndecayedAmount→ each fresh address reverts.
- The existing
EmissionTests.t.solhappy paths keep passing. TheirMockTaskAuditorRealhas to be registered in a real (or minimal mock) registry, so the fixture changes and the assertions don't. - Setter: owner-only, zero-address revert, event emitted.
- Upgrade safety:
UpgradeValidation.t.solgreen. If there's an upgrade-path test forDinEmission, the new slot doesn't collide. - Break-then-fix: remove the registry check, confirm both PoC regression tests fail, then restore.
-
fundGIreverts for anytaskAuditorthe registry doesn't map to an approved model (plus the disabled-model decision from A2, stated in the PR) - Registry reference set in
initializeand through an owner setter, appended before__gap(kept at 50, see the A1 amendment);storage_layout.mdupdated -
DeployPlatform.s.solwires it; deploy tests updated - Both PoC tests committed as regression tests; existing emission tests pass; break-then-fix shown in the PR
-
cd foundry && npm ci && forge build && forge testgreen across the full suite, includingUpgradeValidation.t.sol - Bundled ABIs refreshed where they changed; security-review doc notes #226 as fixed
- Add
resolve_manifest_path(base_dir: Path, manifest_path: str | Path) -> Pathin one shared place (dincli/cli/utils.pyor next toensure_file_exists). It:- rejects an absolute
manifest_path(Path(manifest_path).is_absolute()); - resolves
(base_dir / manifest_path).resolve()and rejects it unlessis_relative_to(base_dir.resolve()); - raises a clear error naming the manifest key and the offending path. It never re-anchors silently.
- rejects an absolute
- Symlinks:
resolve()follows them, so a symlink inside the cache dir that points outside is rejected. Keep it that way and say so in the PR.
Replace each <base> / Path(manifest[...]["path"]) with the helper:
dincli/cli/client.py:129-130dincli/cli/aggregator.py:336-337,:557-558dincli/cli/auditor.py:450-453dincli/cli/modelownerd/aggregation.py:399-400dincli/cli/modelownerd/auditor_batches.py:144dincli/cli/modelownerd/model.py:103-104,:219-220- the task-contract artifact path in
dincli/cli/context.py:316(model_base_path / artifact_rel_path)
Grep for any others (git grep -n '\["path"\]' dincli) and list what you changed in the PR.
ensure_file_exists gets a containment check too. Give it the trusted base directory, or check against CACHE_DIR and the task directory it's called for, so a future call site that forgets the helper still can't write outside. Leave _normalize_path's warning in place. It covers non-manifest paths and isn't the fix.
pytest, in the existing monkeypatch style (see tests/test_ipfs_config.py, tests/test_dintoken.py). No network, IPFS or Docker.
- Helper:
- a normal relative path resolves inside the base;
- an absolute path is rejected;
../../xis rejected;- a symlink inside the base pointing outside is rejected.
- Call sites: at least one per role (client, auditor, aggregator, model owner) plus the artifact path. A manifest with
"path": "/tmp/…/outside"raises beforeretrieve_from_ipfsis called (monkeypatch it and assert it's never reached). ensure_file_existsbackstop: a direct call with an outside path raises.- Break-then-fix: bypass the helper at one call site, confirm its test fails, then restore.
- One shared
resolve_manifest_pathhelper rejecting absolute paths,..escapes and symlink escapes - Every manifest-driven call site uses it (list them in the PR);
ensure_file_existshas the backstop check - Tests as in B4, with break-then-fix shown in the PR
-
pytest -m "not integration"green -
Documentation/public/manifest.md: one sentence thatpathfields must be relative and stay inside the model's directory
Two PRs from separate branches into develop:
- PR 1 (Part A): closes #226. Include the disabled-model decision, the storage-layout diff, and the break-then-fix output.
- PR 2 (Part B): closes #227. Include the call-site list and the break-then-fix output.
Post each PR link and progress updates in Discussion #233.
- Flag the
DinEmission.solstorage change for Umer's review. It's a proxy contract, and storage layout is the thing to get right. - Nothing on
developis deployed to a live network (Sepolia runs the older DevNet 1.0 contracts), so Part A needs no migration script. The owner setter is for completeness on already-initialised proxies. - If a cleaner check than the registry mapping turns up for Part A (for example, emission keyed by
modelIdrather than auditor address), raise it on #226 before building it. Re-keyingemissionStatechanges storage meaning and touches PR #229's planned command. - Keep each PR to its issue. Other findings you spot along the way go in new issues, as you did with #221–#227.