Repository navigation
Description
Summary
DinEmission.fundGI(uint256 gi, address taskAuditor) is external, callable by anyone, and never validates that taskAuditor is a real, registry-registered task-auditor contract. Since taskAuditor is both the recipient of a fresh ERC-20 allowance over newly-minted DIN and the target of an external call, an attacker can supply their own contract address and drain the mint in a single transaction — repeatably, bypassing the emission decay schedule entirely.
Where
foundry/src/DinEmission.sol (verified against develop @ 740a613), fundGI (~L174-201):
function fundGI(uint256 gi, address taskAuditor) external nonReentrant returns (uint256 amount) {
if (taskAuditor == address(0)) revert InvalidAddress();
if (giEmissionFunded[taskAuditor][gi]) revert GIAlreadyFunded(gi);
EmissionState storage state = emissionState[taskAuditor];
if (!state.started) { state.currentEmissionPerGI = initialEmissionPerGI; state.started = true; }
if (state.currentEpoch >= maxEpochs) revert EmissionExhausted();
amount = state.currentEmissionPerGI;
...
coordinator.mintEmission(address(this), amount); // mints to DinEmission itself
dinToken.safeIncreaseAllowance(taskAuditor, amount); // approves arbitrary `taskAuditor`
IDINTaskAuditor(taskAuditor).depositRewards(gi, amount); // external call to arbitrary address
...
}Neither DinCoordinator.mintEmission (only checks the caller is DinEmission itself) nor the real DINTaskAuditor.depositRewards (just does dinToken.safeTransferFrom(msg.sender, address(this), amount), trusting whoever calls it) constrains taskAuditor. emissionState/giEmissionFunded are keyed by this same attacker-chosen address, so a fresh fake contract each time also resets the decay schedule — there's no protection beyond DinCoordinator.mintCap, which defaults to 0 (uncapped, confirmed in DinCoordinator.initialize()).
The function's own doc comment rationalizes the open caller ("the amount is deterministic from the epoch schedule so there is no benefit to restricting the caller") — but that reasoning only covers the amount, not the recipient. It misses that taskAuditor is the allowance grantee and call target, which is the actual exploitable surface. Reads as a genuine oversight, not an accepted risk — no mention of this in Developer/BACK_LOG.md or Documentation/technical/audits/foundry-src-security-review.md.
Exploit
- Attacker deploys a trivial contract whose
depositRewards(uint256, uint256 amount)just doesdinToken.transferFrom(msg.sender, attacker, amount). - Attacker calls
DinEmission.fundGI(gi, address(thatContract)). - In one transaction:
DinEmissionmintsamountto itself, approves the attacker's contract foramount, then calls into it — which immediately pulls the full amount via the allowance. - Repeatable indefinitely with fresh fake contracts, each getting the full undecayed
initialEmissionPerGI(the decay schedule is keyed per-taskAuditor, so a new address always starts fresh).
Verified with a from-scratch Foundry PoC against develop @ 740a613 (not reusing any other draft): test_fundGI_arbitraryTaskAuditor_drainsMintedDIN (one-call full drain — attacker balance +amount, DinEmission balance returns to 0, totalMinted genuinely inflated) and test_fundGI_repeatedWithFreshFakeAuditors_eachGetsUndecayedAmount (3 fresh fake contracts each independently get the full undecayed amount) both pass. The existing EmissionTests.t.sol suite (21 tests) also still passes unmodified — nothing in it exercises an unregistered taskAuditor, confirming this isn't an already-tested/accepted boundary.
Impact
Unbounded DIN token theft and inflation, gated only by attacker gas cost — mintCap is 0 (uncapped) by default on every network including the current devnet.
Recommendation
Validate taskAuditor before minting/approving — e.g. DINModelRegistry.getModelIdByTaskAuditor(taskAuditor) != 0 or DinValidatorStake.isSlasherContract(taskAuditor), mirroring the check DINModelRegistry.requestModelRegistration already does for coordinator/auditor pairs elsewhere in the codebase.
Activity
umeradl commentedon Oct 6, 2026
Status update: confirmed; scheduled as task_061026_22 Part A
Checked against
develop@a1fcce2. The finding holds.DinEmission.fundGI(foundry/src/DinEmission.sol:174) isexternaland permissionless, and only rejectsaddress(0). It then mints to itself (coordinator.mintEmission), grantstaskAuditoran allowance, and callsIDINTaskAuditor(taskAuditor).depositRewards, all on an address the caller chooses.emissionStateandgiEmissionFundedare keyed by that same address, so each fresh fake contract starts the schedule over atinitialEmissionPerGI.DinCoordinator.mintCapdefaults to 0 (uncapped). This is a mint-and-drain with no cost beyond gas.Can be done now: yes. It goes in task_061026_22 as Part A, assigned to you. Notes for the fix:
DINModelRegistry.getModelIdByTaskAuditor(taskAuditor)returns(bool exists, uint256 modelId), not an id where 0 means "none". Model 0 is a real model ID (modelId = models.length), so the check has to useexists, notmodelId != 0. Only auditors of approved models should be fundable. Whether a disabled model (modelDisabled[modelId]) is also refused is a decision to flag in the PR.DinEmissionhas no registry reference today. It holdscoordinatoranddinToken. Adding one is a new storage variable taken from__gap(:115; shrink the gap by one), set ininitializeand/or by an owner setter, and wired infoundry/script/DeployPlatform.s.sol.UpgradeValidation.t.soland the storage-layout doc must stay consistent.DinValidatorStake.isSlasherContract: slasher status is granted before registry approval, and covers coordinators as well as auditors. The registry's auditor mapping is the tighter allow-list.test_fundGI_arbitraryTaskAuditor_drainsMintedDIN,test_fundGI_repeatedWithFreshFakeAuditors_eachGetsUndecayedAmount) become regression tests that now expect a revert. The existingEmissionTests.t.solfixtures need a registered auditor.Coordination: task-plan-051026-2 (PR No. 229) TP-3 adds a
dincli model-owner rewards fund-emissioncommand on top offundGI, and has to follow whatever check lands here. Full spec: task_061026_22 (linked from the tracking discussion once it's open).docs(tasks): add task_061026_22 — DinEmission.fundGI registry check (#…
fix(foundry): DinEmission.fundGI only funds registered task auditors (#…
umeradl commentedon Oct 7, 2026
Done by PR No. 234, merged into
developas2fa8ce8, with a review follow-up in07fd597. Thedeveloppush CI for07fd597is green. Closing by hand, because a merge intodevelopdoesn't trigger closing keywords (mainis the default branch).Checked on
origin/develop@07fd597:fundGIkeeps the zero-address check first. Then it reads(bool exists, uint256 modelId) = modelRegistry.getModelIdByTaskAuditor(taskAuditor)and revertsUnregisteredTaskAuditorif!exists. That happens before any state write, mint or allowance (foundry/src/DinEmission.sol:213-217). It checksexists, notmodelId != 0, so model 0 is handled. The registry fills_modelIdByTaskAuditoronly inapproveModel(foundry/src/DINModelRegistry.sol:251), so only auditors of approved models can be funded.fundGIrevertsModelDisabledForEmission(modelId)whenmodelRegistry.modelDisabled(modelId)is true (DinEmission.sol:216). The rationale is recorded inDocumentation/technical/audits/foundry-src-security-review.md.IDINModelRegistry public modelRegistry(DinEmission.sol:129). It is set ininitialize, which revertsInvalidAddresson zero (:158-164), and by anonlyOwnersetModelRegistrythat rejects zero and emitsModelRegistryUpdated(:175-178).DeployPlatform.s.sol:285-291passesdinModelRegistryProxytoinitialize.storage_layout.mdandDeployPlatform.mdstep 13 match.test_fundGI_unregisteredTaskAuditor_revertsBeforeAnyStateChangeandtest_fundGI_repeatedFreshUnregisteredAuditors_allRevert. There is alsotest_fundGI_disabledModel_reverts, plus threesetModelRegistrytests (foundry/test/EmissionTests.t.sol:200-270).UpgradeValidation.t.solstill validatesDinEmission. The full suite is green in the CI run above.__gapfrom 50 to 49.07fd597restoreduint256[50], because nothing ondevelopis deployed yet. Per the "Core rules" instorage_layout.md, new variables go above__gapwithout shrinking it.modelRegistrystays at slot 9, and the A1 amendment in task_061026_22 records the change.Follow-ups outside this issue:
fundGIis still permissionless for registered auditors, so anyone can use up a model's schedule by funding far-future GIs. This was found in the PR No. 234 review and is tracked in issue No. 238.dincli model-owner rewards fund-emissioncommand, which has to follow this check, is task-plan-051026-2 TP-3, forwarded as task_061026_21.