Skip to content

security: DinEmission.fundGI lets anyone mint and drain DIN via an unvalidated taskAuditor address #226

Description

@Abidoyesimze

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

  1. Attacker deploys a trivial contract whose depositRewards(uint256, uint256 amount) just does dinToken.transferFrom(msg.sender, attacker, amount).
  2. Attacker calls DinEmission.fundGI(gi, address(thatContract)).
  3. In one transaction: DinEmission mints amount to itself, approves the attacker's contract for amount, then calls into it — which immediately pulls the full amount via the allowance.
  4. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions