All incidents

Luckytiger Lucky Mint Drain

Share
Aug 24, 2022 14:26 UTCAttackLoss: 0.5 ETHPending manual check1 exploit txWindow: Atomic
Estimated Impact
0.5 ETH
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Aug 24, 2022 14:26 UTC → Aug 24, 2022 14:26 UTC

Exploit Transactions

TX 1Ethereum
0x804ff3801542bff435a5d733f4d8a93a535d73d0de0f843fd979756a7eab26af
Aug 24, 2022 14:26 UTCExplorer

Victim Addresses

0x9c87a5726e98f2f404cdd8ac8968e9b2c80c0967Ethereum

Loss Breakdown

0.5ETH

Similar Incidents

Root Cause Analysis

Luckytiger Lucky Mint Drain

1. Incident Overview TL;DR

On Ethereum block 15403431, EOA 0x3392c91403f09ad3b7e7243dbd4441436c7f443c called helper contract 0x880df6cc30bb7934d065498ed9163a6e3b5aa67d, which then executed the exploit transaction 0x804ff3801542bff435a5d733f4d8a93a535d73d0de0f843fd979756a7eab26af against luckytiger at 0x9c87a5726e98f2f404cdd8ac8968e9b2c80c0967. The attacker minted 50 NFTs and drained 0.5 ETH from the contract's prize pool while ending with a net cluster profit of 371902864243275090 wei after gas.

The root cause is a deterministic lottery outcome combined with negative unit economics for the protocol. publicMint() prices one mint at 0.01 ETH, but on a lucky branch it immediately pays 0.019 ETH back to msg.sender and 0.001 ETH to the fee recipient. _getRandom() hashes only block.difficulty and block.timestamp, so every mint in the same transaction sees the same lucky result.

2. Key Background

luckytiger is an ERC721 mint contract with a public mint path and a lucky-token flag. The relevant behavior is:

function publicMint() public payable {
    require(msg.value >= price, "Ether sent is not correct");
    _safeMint(msg.sender, 1);
    bool randLucky = _getRandom();
    uint256 tokenId = _totalMinted();
    emit NEWLucky(tokenId, randLucky);
    tokenId_luckys[tokenId] = lucky;
    if (tokenId_luckys[tokenId] == true) {
        require(payable(msg.sender).send((price * 190) / 100));
        require(payable(withdrawAddress).send((price * 10) / 100));
    }
}

The randomness source is transaction-wide rather than mint-specific:

function _getRandom() private returns (bool) {
    uint256 random = uint256(keccak256(abi.encodePacked(block.difficulty, block.timestamp)));
    uint256 rand = random % 2;
    if (rand == 0) { return lucky = false; }
    else { return lucky = true; }
}

Immediately before the exploit, the collector recorded totalSupply() = 211, price() = 10000000000000000, and the victim ETH balance at 730000000000000000 wei. The helper contract already held one mint price, 0.01 ETH, which was enough to start the repeated mint loop.

3. Vulnerability Analysis & Root Cause Summary

The bug is not just reentrancy. The decisive flaw is that publicMint() makes each lucky mint profitable and _getRandom() makes all mints in a single transaction share the same outcome. Once a transaction lands in the lucky branch, the caller can keep reusing payout proceeds to buy additional mints in the same transaction. That breaks the intended invariant that each 0.01 ETH mint is an independent lottery attempt. The code-level breakpoint is the sequence _getRandom() -> tokenId_luckys[tokenId] = lucky -> send((price * 190) / 100), because it reuses a transaction-constant boolean and transfers more ETH to the caller than the mint cost. The exploit therefore remains permissionless: any unprivileged actor can deploy a helper contract, seed it with one mint price, and loop mints when the block-field predicate is favorable.

4. Detailed Root Cause Analysis

The verified victim source shows that publicMint() first accepts 0.01 ETH, mints an NFT, computes luck via _getRandom(), and then pays rewards on the lucky branch. The reward schedule is economically unsound: the caller gets 0.019 ETH and withdrawAddress gets 0.001 ETH, so the contract sends out 0.02 ETH after collecting only 0.01 ETH. Each lucky mint is therefore a deterministic +0.009 ETH transfer to the caller and a -0.01 ETH drain from the victim.

The randomness is equally broken for repeated execution inside the same transaction. _getRandom() uses only block.difficulty and block.timestamp, both constant for all calls within one transaction. That means the first lucky result is not local to one mint; it applies to every subsequent mint executed before the transaction ends.

The collector trace for 0x804ff3801542bff435a5d733f4d8a93a535d73d0de0f843fd979756a7eab26af shows repeated calls into publicMint() from the helper contract and repeated ETH transfers of 19000000000000000 wei to the caller and 1000000000000000 wei to withdrawAddress. The balance-delta artifact confirms the aggregate result: the victim contract dropped from 0.73 ETH to 0.23 ETH, the helper rose from 0.01 ETH to 0.46 ETH, and the fee recipient gained 0.05 ETH.

Because the helper contract receives the payout directly, it can recycle the returned ETH into the next mint without requiring off-chain funding for every iteration. The attacker only needs enough initial capital to fund the first mint. After that, the payout path itself bankrolls the remaining loop. The incident helper used a helper contract and recursive execution, but the ACT condition is broader: any helper logic that repeats publicMint() during one favorable transaction reproduces the same drain because the luck predicate and payout economics remain unchanged for the whole transaction.

5. Adversary Flow Analysis

The adversary flow is fully contained in the single seed transaction.

  1. The EOA 0x3392c91403f09ad3b7e7243dbd4441436c7f443c submitted a type-2 transaction to helper contract 0x880df6cc30bb7934d065498ed9163a6e3b5aa67d with calldata 0x25565cdd...00000032, corresponding to hack(50).
  2. The helper invoked luckytiger.publicMint() repeatedly. The trace shows the victim call path recurring with the same payout amounts on each successful lucky mint.
  3. Each mint created one new token for the helper. Starting from pre-state supply 211, the exploit minted token IDs 212 through 261.
  4. Each of those tokens was marked lucky and each mint transferred 0.019 ETH back to the helper plus 0.001 ETH to the fee recipient.
  5. By the end of the transaction, the helper held all 50 newly minted NFTs and a 0.45 ETH balance increase, while the protocol had lost 0.5 ETH.

Representative code and transaction evidence:

tx hash: 0x804ff3801542bff435a5d733f4d8a93a535d73d0de0f843fd979756a7eab26af
from:    0x3392c91403f09ad3b7e7243dbd4441436c7f443c
to:      0x880df6cc30bb7934d065498ed9163a6e3b5aa67d
input:   0x25565cdd0000000000000000000000000000000000000000000000000000000000000032
trace evidence highlights:
- repeated calls into the victim mint path
- repeated transfers of 19000000000000000 wei to the helper
- repeated transfers of 1000000000000000 wei to withdrawAddress

6. Impact & Losses

The direct measured loss was 500000000000000000 wei (0.5 ETH) from the victim contract. The helper contract gained 450000000000000000 wei, and withdrawAddress received 50000000000000000 wei in fee transfers. The attacker cluster's combined ETH value increased from 105398547139915705 wei to 477301411383190795 wei, producing a net profit of 371902864243275090 wei after paying 78097135756724910 wei in gas.

The exploit also transferred 50 NFTs, token IDs 212 through 261, to the adversary helper. Those NFTs are secondary impact; the measured primary financial loss in the artifacts is the 0.5 ETH drained from contract reserves.

7. References

  • Victim contract: luckytiger at 0x9c87a5726e98f2f404cdd8ac8968e9b2c80c0967
  • Seed transaction: 0x804ff3801542bff435a5d733f4d8a93a535d73d0de0f843fd979756a7eab26af
  • Helper contract: 0x880df6cc30bb7934d065498ed9163a6e3b5aa67d
  • Gas-paying EOA: 0x3392c91403f09ad3b7e7243dbd4441436c7f443c
  • Fee recipient: 0x511604E18d63D32ac2605B5f0aF0cF580D21FA49
  • Supporting evidence: verified victim source code, execution trace, transaction metadata, and balance-delta artifacts collected under the session collector outputs