Reproduced Exploit
HeroERC721A — free mint wipes the paid counter and doubles the RING cap
OG holders get 2 guaranteed mints per RING plus one free mint. The paid count lives in aux bits 24-63. _ogMint rebuilds aux from (_getAux(to) >> 3) & 0x0fffff, which keeps only the free-mint field, then writes that word back. A holder with 5 RINGs mints the cap of 10, takes the free mint (5 more he…
Chain
Other
Category
logic
Date
Jan 2025
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
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.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 55405-c-02-malicious-users-can-mint-the-double-guaranteed-mints-pe. Standalone Foundry PoC and full write-up: 55405-c-02-malicious-users-can-mint-the-double-guaranteed-mints-pe_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/missing-check · vuln/logic/state-update
Key info#
| Protocol | Onchain Heroes (HeroERC721A) |
| Finding | Pashov Audit Group C-02, AuditVault 55405 |
| Vulnerable code | HeroERC721A._ogMint |
| Harm | 20 paid heroes minted against a cap of 10 (2 per RING, 5 RINGs). Balance is 25 including the 5 free mints. |
| Chain | local synthetic |
TL;DR#
OG holders get 2 guaranteed mints per RING plus one free mint. The paid count lives in aux bits 24-63. _ogMint rebuilds aux from (_getAux(to) >> 3) & 0x0fffff, which keeps only the free-mint field, then writes that word back. A holder with 5 RINGs mints the cap of 10, takes the free mint (5 more heroes, and the paid bits are now 0), then mints 10 again. Paid mints are 20, twice the cap. Preserving the full aux word makes the second batch revert at 10.
The vulnerable code#
function _ogMint(address to, uint64 numOfRings) internal {
// @> VULN: mask keeps only the free-mint field, so the write below deletes paid mints in bits 24-63.
uint64 aux = (_getAux(to) >> 3) & 0x0fffff;
if (aux == numOfRings) revert AlreadyMinted();
_setAux(to, uint64(aux | (numOfRings << 3))); // Set OG free mint flag
_safeMint(to, numOfRings);
}
The report's fix reads the full word once and ORs the flag into it:
uint64 _aux = _getAux(to);
uint64 aux = (_aux >> 3) & 0x0fffff;
if (aux == numOfRings) revert AlreadyMinted();
_setAux(to, uint64(_aux | (numOfRings << 3)));
_safeMint(to, numOfRings);
Aux layout from the report: bits 0-2 other flags, bits 3-23 the free-mint field, bits 24-63 the paid-mint count. 0x0fffff is 20 bits, so everything from bit 24 up is discarded.
Root cause#
The free-mint path treats aux as if it only stored the free-mint field. The paid path stores its counter in the same word, above bit 23. Masking before the write deletes that counter. The cap check already + qty > rings * 2 then sees already == 0 and allows another full batch.
Preconditions#
The caller is the snapshotted holder of 5 RINGs. The cap is 5 * 2 = 10. No second whitelist entry is required. The three ogMint calls fit in one transaction; nothing depends on a later block.
Attack walkthrough#
ogMint(free=false, qty=10, rings=5)stores 10 in bits 24-63 and mints 10.paidMintedis 10.ogMint(free=true, qty=0, rings=5)runs_ogMint. The masked word does not contain the 10. Aux becomes5 << 3and 5 free heroes are minted.aux >> 24is 0.ogMint(free=false, qty=10, rings=5)reads a paid count of 0, so10 <= 10, and mints 10 more.paidMintedis 20.balanceOfis 25.
The control keeps the high bits. After the free mint the paid count is still 10, and the second batch reverts with CapReached. Paid stays 10. Balance stays 15.
Diagrams#
Impact#
A 5-RING holder mints 20 guaranteed heroes instead of 10, plus the 5 free heroes the free-mint path is supposed to mint. The extra 10 heroes are the cap minted a second time. The fixed path stops the second batch at the cap.
Remediation#
When setting the free-mint flag, OR it into the existing aux word. Do not rebuild the word from the masked free-mint field alone.
How to reproduce#
cd 55405-c-02-malicious-users-can-mint-the-double-guaranteed-mints-pe_exp
forge test -vv
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/55405-c-02-malicious-users-can-mint-the-double-guaranteed-mints-pe.md
- Pashov report: https://github.com/pashov/audits/blob/master/team/md/OnchainHeroes-security-review_2025-01-13.md
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 55405-c-02-malicious-users-can-mint-the-double-guaranteed-mints-pe_exp (evm-hack-registry mirror).
- AuditVault finding: 55405-c-02-malicious-users-can-mint-the-double-guaranteed-mints-pe.
Alerts & third-party analyses
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.