Reproduced Exploit

ContributionNft.mint lets the proposer pick the impact that gets paid

ContributionNft.mint checks that the caller is the proposal proposer and then stores coreId, isModel, and datasetId with no further check. ServiceNft.updateImpact reads that core's maturity and, when datasetId is nonzero, writes datasetImpactWeight of the raw impact onto that id.

Apr 2025•Ethereum•access-control•3 min read

Loss

Proposer is paid 10000 reward tokens. The honest core pays 100. Excess is 9900

Chain

Ethereum

Category

access-control

Date

Apr 2025

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 61825-h-04-public-contributionnftmint-leads-to-cascading-issues-lo. Standalone Foundry PoC and full write-up: 61825-h-04-public-contributionnftmint-leads-to-cascading-issues-lo_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/access-control/missing-auth · vuln/logic/reward-calculation

Key info#

FieldValue
LossProposer is paid 10000 reward tokens. The honest core pays 100. Excess is 9900
Vulnerable contractContributionNft.mint
Attacker EOA0x1111111111111111111111111111111111111111
Attack contractExploit
Attack txExploit.run()
Chain / block / dateEthereum model · one block · 2025-04
Compilersolc 0.8.27, cancun, via IR
Bug classProposer-only mint stores an arbitrary core and dataset id

TL;DR#

ContributionNft.mint checks that the caller is the proposal proposer and then stores coreId, isModel, and datasetId with no further check. ServiceNft.updateImpact reads that core's maturity and, when datasetId is nonzero, writes datasetImpactWeight of the raw impact onto that id.

Core 1 maturity is 100 tokens. Core 9 maturity is 10000 tokens. The proposer mints on core 9 and points datasetId at a side NFT they also own. Weight is 8000, so the dataset impact is 8000 and the proposal impact is 2000. Minter.mint pays both. The proposer receives 10000. The reserve keeps 10000 of its 20000. An admin-gated mint of the honest core pays 100 and the attacker's call reverts.

The vulnerable code#

SOLIDITY
require(msg.sender == dao.proposalProposer(proposalId), "Only proposal proposer can mint Contribution NFT");
require(parentId != proposalId, "Cannot be parent of itself");
coreOf[proposalId] = coreId;
// @> VULN: proposer stores an arbitrary core and datasetId
isModel[proposalId] = isModel_;
datasetId[proposalId] = datasetId_;

Root cause#

The proposer gate does not constrain the core or the dataset the impact accounting will trust.

Preconditions#

  • The caller can create a proposal, so they are its proposer.
  • A matured core exists. Here core 9 is 10000 and core 1 is 100.
  • Dataset weight is 8000 of 10000. The minter pays both the proposal impact and the dataset impact from a 20000 reserve.

Attack walkthrough#

  1. Propose and mint a side NFT the attacker owns.
  2. Propose again and mint with core 9, isModel true, and datasetId equal to the side NFT.
  3. mintService sets dataset impact to 8000 and proposal impact to 2000.
  4. Minter.mint transfers 10000 to the attacker. Excess over the honest 100 is 9900.
  5. Control: mint requires the admin. The attacker's mint reverts. The admin mints core 1 and is paid 100.

Diagrams#

flowchart TD Propose[Proposer mints with core 9 and their own dataset id] --> Impact[Service impact becomes 8000 plus 2000] Impact --> Pay[Minter pays 10000 from the reserve] Pay --> Excess[That is 9900 more than the honest core] Excess --> Admin[An admin-only mint of core 1 pays 100]

Impact#

The reward reserve overpays the proposer by 9900 tokens. 10000 leaves the reserve instead of 100.

Remediation#

Only the admin, or the contract that already checked the proposal, may mint. Do not take core and dataset id from the proposer.

How to reproduce#

BASH
cd evm-hack-registry/61825-h-04-public-contributionnftmint-leads-to-cascading-issues-lo_exp
forge test -vv

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.