Reproduced Exploit

Heurist — partnerFreeMint reenters before lastMinted is set

partnerFreeMint calls _safeMint and only then writes lastMinted. _safeMint calls onERC721Received. The receiver calls partnerFreeMint again. lastMinted is still zero, so the 24h check passes. Two NFTs are minted and 2 reward tokens are paid.

Jul 2024•Other•reentrancy•3 min read

Chain

Other

Category

reentrancy

Date

Jul 2024

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: 37689-reentrance-in-partnerfreemint-zokyo-none-heurist-markdown. Standalone Foundry PoC and full write-up: 37689-reentrance-in-partnerfreemint-zokyo-none-heurist-markdown_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/reentrancy/single-function

Reproduction: a self-contained Foundry PoC that compiles and runs offline with only forge-std — no fork and no RPC. Trace: output.txt. PoC: test/37689-reentrance-in-partnerfreemint-zokyo-none-heurist-markdown_exp.sol.


Key info#

ImpactHIGH — one call mints 2 NFTs and pays 2 reward tokens. The 24h limit allows 1
ProtocolHeurist partnerFreeMint
Vulnerable codelastMinted[msg.sender] = ... after _safeMint
Bug classChecks-effects-interactions. The stamp is the effect and it is last
FindingZokyo — Heurist · id 37689
ReportZokyo Heurist, 1 Jul 2024
SourceAuditVault finding
StatusThe report says the client moved the lastMinted write before _safeMint and later added a reentrancy guard. Reproduced as a local synthetic.
Compilersolc 0.8.27, cancun, via IR

TL;DR#

partnerFreeMint calls _safeMint and only then writes lastMinted. _safeMint calls onERC721Received. The receiver calls partnerFreeMint again. lastMinted is still zero, so the 24h check passes. Two NFTs are minted and 2 reward tokens are paid.

Writing lastMinted before _safeMint makes the reentered call revert. Only 1 token is paid and token id 2 is never created. run() does not warp.

The vulnerable code#

SOLIDITY
_safeMint(to);
// @> VULN: lastMinted is written after _safeMint, so the receiver reenters inside the 24h window
lastMinted[msg.sender] = block.timestamp == 0 ? 1 : block.timestamp;

Root cause#

The report says _safeMint runs before lastMinted is updated, so a contract receiver can reenter and bypass the 24 hour limit.

Preconditions#

  • The caller holds a partner NFT.
  • lastMinted for that caller is zero.
  • The treasury holds 2 reward tokens.
  • The mint recipient is a contract.

Attack walkthrough#

  1. partnerFreeMint passes the 24h check and calls _safeMint.
  2. The first reward token is transferred. onERC721Received runs.
  3. The receiver calls partnerFreeMint again. The stamp is still zero.
  4. A second NFT and a second reward token are paid.
  5. Both frames then write lastMinted. The caller holds 2 tokens.

Diagrams#

flowchart TD A["partnerFreeMint passes the 24h check"] --> B["_safeMint pays 1 token"] B --> C["onERC721Received reenters"] C --> D["Second mint also sees a zero stamp"] D --> E["lastMinted is written after both mints"] E --> F["Caller holds 2 tokens"]

Impact#

One partner holder mints twice and takes 1 extra reward token. The 24h limit never gets a chance to reject the second mint.

Remediation#

Update lastMinted before _safeMint, as the report's fix does.

How to reproduce#

BASH
cd evm-hack-registry/37689-reentrance-in-partnerfreemint-zokyo-none-heurist-markdown_exp
forge test -vv

Sources#

  • AuditVault finding 37689
  • Zokyo, Heurist, 1 July 2024, partnerFreeMint, fix commit noted as a27af000

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.