Reproduced Exploit
Gigaverse: `GameNFT._update` bricks minting and burning of soulbound tokens
Chain
Other
Category
untagged
Date
Jan 1970
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: Gigaverse-security-review_2025-01-18. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.
Vulnerability classes: vuln/dos · vuln/logic
Reproduction: a faithful minimal reproduction of the vulnerable finding — the vulnerable
_updateoverride ofGameNFTis reproduced verbatim (marked@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
OpenZeppelin's ERC721 routes every mint, transfer and burn through _update. GameNFT overrides it with a blanket require(!isSoulbound, ...) that also fires on mints (prevOwner == 0) and burns (to == 0), not just transfers — so a soulbound token can never be minted and, once soulbound, can never be burnt. The vulnerable override, reproduced verbatim:
function _update(
address to,
uint256 tokenId,
address auth
)
internal
virtual
override(
ERC721
) returns (address)
{
if (beforeUpdateHandler != address(0)) {
IERC721UpdateHandler(
beforeUpdateHandler
).update(
address(this),
to,
tokenId,
auth
);
}
address prevOwner = _ownerOf(tokenId);
//...
bool isSoulbound = getDocBoolValue(tokenId, IS_SOULBOUND_CID);
@> require(!isSoulbound, "GameNFT: Token is soulbound");
//...
}
The check makes no attempt to distinguish a mint (prevOwner == address(0)) or a burn (to == address(0)) from a real owner-to-owner transfer. Because _mint and _burn both funnel through _update, the guard intercepts the two operations it should never block.
Why it's exploitable here#
Following the synthetic reproduction, which mirrors how GigaNoobNFT / GigaNameNFT expose GameNFT:
- Control:
mintNormal(USER, 1)mints a non-soulbound token —_updateruns,isSoulboundisfalse, therequirepasses, andownerOf(1) == USER. The mint path itself is healthy. - Harm 1 — unmintable:
mintSoulbound(USER, 2)first sets token 2'sIS_SOULBOUNDdoc value totrue, then calls_mint. Inside_update,prevOwner == address(0)(a mint) yetisSoulbound == true, sorequire(!isSoulbound)reverts.rawOwnerOf(2)staysaddress(0)— the soulbound token can never come into existence. - Harm 2 — unburnable:
mintNormal(USER, 3)mints token 3 normally, thensetSoulbound(3, true)marks it soulbound. Callingburn(3)routes_burn→_update(address(0), 3, ...);to == address(0)(a burn) but the samerequirereverts.ownerOf(3)remainsUSERforever — the token is permanently locked.
Both reverts are recorded on the SBLCK DoS marker (2 units minted to the sink), quantifying the permanent denial of service on the soulbound feature.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0x8ea53755…:
- L62 — ERC721 core reads previous owner: Setup: ERC721's core
_updatereads the token's previous owner — every mint, transfer and burn is funneled through this single primitive. - L80 — Burn routes through _update: Setup:
_burninvokes the same_updatewithto = address(0), so burns hit the identical soulbound check as transfers and mints. - L89 — GameNFT overrides _update: GameNFT overrides
_update, inheriting the mint/transfer/burn funnel and inserting its own soulbound guard directly into that shared path. - L129 — Soulbound guard reverts mint and burn: Root cause:
require(!isSoulbound, ...)reverts for any soulbound token, but mint (prevOwner==0) and burn (to==0) are not transfers, so both revert. - L135 — Public mint entry points: The public entry points
mintNormalandmintSoulboundforward into_mint, which routes through the buggy_updateoverride before completing. - L150 — Marking a token soulbound:
setSoulboundflips a token's IS_SOULBOUND doc value to true, so an already-minted token becomes permanently unburnable via the same guard. - L173 — DoS marker records harm: The
SBLCKDoS marker records the harm magnitude — one unit minted to the sink for every soulbound token permanently bricked. - L185 — Exploit driver proves the DoS: The exploit driver proves a normal token mints fine while the soulbound mint and burn both revert — a permanent DoS of the soulbound feature.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 53286-h-03-soulbound-tokens-cannot-be-minted-or-burnt-due-to-an-in_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: a normal token mints fine, but the soulbound mint and the soulbound burn both revert on the require(!isSoulbound) guard — 2 tokens permanently bricked. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- No executable Forge reproduction is claimed; the historical source/toolchain was unavailable for this finding.
- AuditVault finding: Gigaverse-security-review_2025-01-18.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Gigaverse:
GameNFT._updatebricks minting and burning of soulbound tokens". - 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.