Reproduced Exploit

Gigaverse — mintUsername charges 0 for a name listed at 0.005 ETH

1. The manager sets the username price with AccountSystem.setMintPrice. mintWithEth reads that table and reverts unless msg.value covers it. 2. GigaNameNFT.mintUsername checks ETH_MINT_PRICE_CID on its own table. Nothing writes that cell, so it is 0. 3. The attacker calls mintUsername(attacker, "al…

Jan 2025•Other•logic•5 min read

Chain

Other

Category

logic

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: 53288-h-05-zero-username-price-when-it-is-minted-with-mintusername. Standalone Foundry PoC and full write-up: 53288-h-05-zero-username-price-when-it-is-minted-with-mintusername_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/fee-calculation · vuln/logic/price-calculation · vuln/logic/wrong-condition

Reproduction: a self-contained Foundry PoC that compiles and runs with only forge-std — no fork, no RPC. Full trace: output.txt. PoC: test/53288-h-05-zero-username-price-when-it-is-minted-with-mintusername_exp.sol.

AuditVault taxonomy: lang/solidity · has/github · severity/high · sector/perpetuals · genome: fee-accounting · logic/fee-calculation · loss-of-funds/fee-theft


Key info#

ImpactHIGH — a username the protocol lists at 0.005 ETH (5000000000000000 wei) is minted for 0 wei, and the caller receives the name
ProtocolGigaverse — AccountSystem.mintWithEth / GigaNameNFT.mintUsername
Vulnerable codeGigaNameNFT.mintUsername — require(msg.value >= getTableUint256Value(ETH_MINT_PRICE_CID))
Bug classFee is read from the name-NFT table, which stays 0, instead of the AccountSystem table the manager writes
FindingPashov Audit Group — Gigaverse, 2025-01-18 · #53288 (H-05)
ReportGigaverse-security-review_2025-01-18
SourceAuditVault
StatusAudit finding, reproduced as a local synthetic
Compilersolc 0.8.27, cancun, via_ir

This is an audit finding, not an on-chain incident. mintUsername is copied verbatim from the report. The two tables are the minimum scaffolding that makes the wrong read observable: AccountSystem's column holds 0.005 ether, GigaNameNFT's column holds 0.


TL;DR#

  1. The manager sets the username price with AccountSystem.setMintPrice. mintWithEth reads that table and reverts unless msg.value covers it.
  2. GigaNameNFT.mintUsername checks ETH_MINT_PRICE_CID on its own table. Nothing writes that cell, so it is 0.
  3. The attacker calls mintUsername(attacker, "alice") with no ETH. The check is 0 >= 0. confirmMint assigns the name.
  4. Harm: listed price 5000000000000000 wei, name-table price 0, ETH charged 0, protocol fees 0, token id 1, owner is the attacker, name is alice.
  5. Control: mintWithEth{value: 0.005 ether} mints bob and records a charge of 5000000000000000. mintWithEth{value: 0} reverts and does not mint.

The vulnerable code#

From the report, preserved verbatim:

SOLIDITY
function mintUsername(address to, string memory username) external payable {
    // @> VULN: price is read from the GigaNameNFT table, which stays 0
    require(msg.value >= getTableUint256Value(ETH_MINT_PRICE_CID), "Insufficient payment");
    confirmMint(to, username);
}

getTableUint256Value reads NAME_TABLE, not ACCOUNT_TABLE:

SOLIDITY
function getTableUint256Value(uint256 columnId) public view returns (uint256) {
    return dataStore.getUint256(NAME_TABLE, columnId);
}

The path that charges the real price:

SOLIDITY
function mintWithEth(address to, string memory username) external payable returns (uint256 id) {
    uint256 price = mintPrice(); // AccountSystem table
    require(msg.value >= price, "Insufficient payment");
    feesCollected += msg.value;
    id = names.mintUsernameByMinter{value: msg.value}(to, username);
}

Recommended fix from the report: both contracts must read the AccountSystem table.

DIFF
- require(msg.value >= getTableUint256Value(ETH_MINT_PRICE_CID), "Insufficient payment");
+ require(msg.value >= accountSystem.mintPrice(), "Insufficient payment");

Root cause#

ETH_MINT_PRICE_CID is a column id, not a global variable. AccountSystem.setMintPrice writes it under the account table. mintUsername loads it under the name-NFT table. The two cells are independent, the name-NFT cell has no setter, and the default is 0. Any caller can satisfy the payment check by sending nothing.

Preconditions#

  • The manager has set a non-zero mint price on AccountSystem. In this PoC that price is 0.005 ether, the figure Gigaverse documents for mintWithEth.
  • mintUsername is externally callable. The report describes it as the alternate mint path.
  • The username is not already taken.

Attack walkthrough#

Numbers from output.txt:

  1. AccountSystem.mintPrice() is 5000000000000000 wei.
  2. The GigaNameNFT table cell for the same column is 0.
  3. The attacker calls mintUsername with msg.value == 0 and username alice.
  4. The require passes. confirmMint sets ownerOf(1) to the attacker and ethPaid(1) to 0.
  5. Protocol feesCollected stays 0. The attacker holds one NAME and the string alice.

The control test pays 5000000000000000 wei through mintWithEth, receives bob, and shows a second call with 0 value does not mint.

Diagrams#

flowchart TD A[AccountSystem lists the name at 0.005 ETH] --> B[Attacker calls mintUsername with 0 ETH] B --> C[GigaNameNFT reads its own price column] C --> D[That column is 0 so the check passes] D --> E[confirmMint assigns alice to the attacker] E --> F[Protocol collected 0 wei and the attacker holds the name]

Impact#

The protocol sells usernames for a manager-set ETH price and also exposes a mint that ignores that price. Every name taken through mintUsername is delivered without the fee. In the PoC the concrete loss is the 0.005 ETH fee on alice: the attacker receives token id 1, and both the name contract and AccountSystem keep 0 wei. Repeating the call takes every unused name for free. mintWithEth still charges, so the bug is the second entry point, not the price setter.

Remediation#

Make mintUsername read AccountSystem.mintPrice() — the same cell setMintPrice writes — or remove the public mint and leave minting to mintWithEth after that check.

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.