Reproduced Exploit

Gigaverse — anyone can retarget a name and take its 100 ACCT escrow

GigaNameNFTBeforeUpdateHandler.update is what GameNFT._update calls when a GigaNameNFT is minted, transferred, or burned. It clears the previous owner's account row and writes the new owner. The function is external and does not require the caller to be GigaNameNFT. Anyone can pass an already-minte…

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: 53285-h-02-unauthorized-access-to-update-function-pashov-audit-gro. Standalone Foundry PoC and full write-up: 53285-h-02-unauthorized-access-to-update-function-pashov-audit-gro_exp in the evm-hack-registry mirror.


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

Key info#

ProtocolGigaverse
FindingPashov Audit Group H-02, AuditVault 53285
Vulnerable codeGigaNameNFTBeforeUpdateHandler.update
HarmCaller takes 100 ACCT that was escrowed on the victim's name row
Chainlocal synthetic

TL;DR#

GigaNameNFTBeforeUpdateHandler.update is what GameNFT._update calls when a GigaNameNFT is minted, transferred, or burned. It clears the previous owner's account row and writes the new owner. The function is external and does not require the caller to be GigaNameNFT. Anyone can pass an already-minted token id and a recipient. In this run the victim's row holds 100 ACCT of mint escrow. The attacker calls update, the 100 moves onto the attacker's row, and withdraw pays it out. The victim's username id is 0 and their escrow is 0.

The vulnerable code#

The report quotes the unguarded signature. The body below is the rewrite that finding describes and that M-04 spells out: if the token exists, drop the previous owner's record; then, if the recipient has no username, store this token id for them. There is no msg.sender check.

SOLIDITY
function update(
        address tokenContract,
        address to,
        uint256 tokenId,
        address //auth
    ) external override {
        NameNFT gigaNameNFT = NameNFT(tokenContract);
        if (gigaNameNFT.exists(tokenId)) {
            address prevOwner = gigaNameNFT.ownerOf(tokenId);
            if (prevOwner != address(0) && accountSystem.getPlayerUsernameId(prevOwner) == tokenId) {
                // @> VULN: update() does not require msg.sender == GigaNameNFT, so this clears the real owner's escrow.
                accountSystem.removeUsername(prevOwner);
            }
        }
        if (accountSystem.getPlayerUsernameId(to) == 0) {
            accountSystem.setUsername(to, tokenId);
        }
    }

removeUsername zeroes the player's escrow and parks it in pending. setUsername writes the token id and pays pending to the new address. On a real transfer that is what the name NFT is supposed to do. Called by a stranger, it is a theft of the row.

DIFF
 function update(address tokenContract, address to, uint256 tokenId, address) external {
+    require(msg.sender == address(nameNft), "not name nft");
     // existing removeUsername / setUsername body
 }

Root cause#

The handler is trusted because AccountSystem grants it GAME_LOGIC_CONTRACT_ROLE, and only that role may edit username rows. update itself never checks that the caller is the NFT that owns the hook. The auth argument is unnamed and unused. The deployed handler later added require(registeredSystem == msg.sender), which is the fix; the reviewed code did not have it.

Preconditions#

A GigaNameNFT id is already minted and the account row for its owner holds a non-zero escrow. No role, signature, or approval is required. The call is one transaction.

Attack walkthrough#

  1. Name token 7 is minted to the victim.
  2. 100 ACCT is seeded onto that row. escrow(victim) is 100 and usernameId(victim) is 7.
  3. The attacker calls update(nameNft, attacker, 7, address(0)).
  4. removeUsername sees that the victim's username id is 7 and clears the row. The 100 sits in pending.
  5. setUsername writes token 7 and the 100 onto the attacker, because the attacker had no username.
  6. withdraw pays the attacker 100 ACCT. The victim's escrow and username id are both 0.

The control is the same body with require(msg.sender == address(nameNft)). A stranger's call reverts with not name nft and the victim still has 100. The name NFT itself can still call update, and that authorized call does move the 100 to the transfer recipient.

Diagrams#

flowchart TD A["Victim holds name 7 and 100 ACCT"] --> B["Stranger calls update"] B --> C["VULN: removeUsername clears the victim"] C --> D["setUsername writes the 100 onto the stranger"] D --> E["withdraw pays the stranger 100 ACCT"] E --> F["Fixed update requires the name NFT and the 100 stays"]

Impact#

The account row is the claim on the escrow collected when the name was minted. After the call the victim cannot withdraw that 100 ACCT, and the attacker can. The same unguarded write is what the report says blocks a later mintWithEth with "User already has an account": the recipient is now recorded as owning a name they do not. This reduction shows the balance that moves with that record: 100 ACCT, from the victim to the caller.

Remediation#

Require msg.sender == GigaNameNFT at the start of GigaNameNFTBeforeUpdateHandler.update, and the same check on GigaNoobNFTBeforeUpdateHandler.update against GigaNoobNFT.

How to reproduce#

BASH
cd 53285-h-02-unauthorized-access-to-update-function-pashov-audit-gro_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.