Reproduced Exploit

GachaToken — anyone can burn another holder's Gotcha tokens

GachaToken exposes Solady ERC1155's internal _burn(address by, address from, uint256 id, uint256 amount). That function skips the approval check when by is the zero address, because internal callers are trusted. burnFrom lets any external caller choose by. Passing address(0) burns 100 tokens of id…

Jan 2025•Other•access-control•4 min read

Chain

Other

Category

access-control

Date

Jan 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: 55404-c-01-unauthorized-access-to-burn-gotcha-token-pashov-audit-g. Standalone Foundry PoC and full write-up: 55404-c-01-unauthorized-access-to-burn-gotcha-token-pashov-audit-g_exp in the evm-hack-registry mirror.


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

Key info#

ProtocolOnchain Heroes (GachaToken)
FindingPashov Audit Group C-01, AuditVault 55404
Vulnerable codeGachaToken.burnFrom / GachaToken._burn(address by, ...)
Harm100 Gotcha tokens of id 1 burned; the holder's balance goes from 100 to 0
Chainlocal synthetic

TL;DR#

GachaToken exposes Solady ERC1155's internal _burn(address by, address from, uint256 id, uint256 amount). That function skips the approval check when by is the zero address, because internal callers are trusted. burnFrom lets any external caller choose by. Passing address(0) burns 100 tokens of id 1 from a holder who never approved the caller. The balance is 0 afterwards. The fixed burn passes msg.sender as by, so a non-owner call reverts and the 100 tokens stay.

The vulnerable code#

SOLIDITY
/// - If `by` is not the zero address, it must be either `from`,
///   or approved to manage the tokens of `from`.
function burnFrom(address by, address from, uint256 id, uint256 amount) external {
    _burn(by, from, id, amount);
}

function _burn(address from, uint256 id, uint256 amount) internal {
    _burn(address(0), from, id, amount);
}

function _burn(address by, address from, uint256 id, uint256 amount) internal {
    // @> VULN: by == address(0) skips this approval check, so the debit below hits any holder.
    if (by != address(0)) {
        if (by != from && !isApprovedForAll[from][by]) revert NotOwnerNorApproved();
    }
    balanceOf[from][id] = balanceOf[from][id] - amount;
}

burn(from, id, amount) reaches the same skip by calling the three-argument _burn, which hardcodes by = address(0). The auditor's fix deletes burnFrom and changes burn to _burn(msg.sender, from, id, amount).

Root cause#

The by == address(0) exception is an internal privilege: Solady uses it so _burn(from, id, amount) can burn without a second authorization check. Publishing that parameter on burnFrom, and calling the three-argument wrapper from the external burn, turns the privilege into a caller input. The caller does not need to be from, an operator, or Endgame.

Preconditions#

The holder has a non-zero Gotcha balance. No approval, signature, or minter role is required to burn it. Endgame is the only minter; that role is not the attacker.

Attack walkthrough#

  1. The holder starts at 0 of id 1.
  2. Endgame mints 100 of id 1 to the holder. The balance is 100.
  3. A stranger calls burnFrom(address(0), holder, 1, 100).
  4. by is the zero address, so NotOwnerNorApproved is not evaluated.
  5. The holder's balance is written to 0 and burned is 100.

The control uses the recommended burn: by is msg.sender. The same stranger reverts with NotOwnerNorApproved and the balance stays 100. The holder can still burn their own tokens.

Diagrams#

flowchart TD A["Endgame mints 100 of id 1 to the holder"] --> B["Stranger calls burnFrom with by = address 0"] B --> C["VULN: zero by skips the approval check"] C --> D["Holder balance of id 1 falls from 100 to 0"] D --> E["Fixed burn passes msg.sender and the stranger reverts"]

Impact#

Anyone can destroy another user's Gotcha tokens. In this run the holder loses all 100 of id 1. The tokens are burned, not stolen, so the attacker has no token balance to show; the harm is the holder's balance going to 0.

Remediation#

Delete burnFrom. Change burn so it calls _burn(msg.sender, from, id, amount).

How to reproduce#

BASH
cd 55404-c-01-unauthorized-access-to-burn-gotcha-token-pashov-audit-g_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.