TheNFTV2 Stale Burn Approval
Exploit Transactions
0xd5b4d68432cbbd912130bbb5b93399031ddbb400d8f723c78050574de7533106Victim Addresses
0x79a7d3559d73ea032120a69e59223d4375deb595EthereumLoss Breakdown
Similar Incidents
USDTStaking Approval Drain
39%Flooring extMulticall Approval Drain
35%TIME ERC2771 Burn Exploit
32%PLNTOKEN transferFrom burn hook drains WETH reserves
31%Sandbox Land public burn flaw enables unauthorized NFT destruction
30%NOON Pool Drain via Public transfer
30%Root Cause Analysis
TheNFTV2 Stale Burn Approval
1. Incident Overview TL;DR
In Ethereum mainnet transaction 0xd5b4d68432cbbd912130bbb5b93399031ddbb400d8f723c78050574de7533106 at block 18647465, helper contract 0x85301f7b943fd132c8dbc33f8fd9d77109a84f28 exploited TheNFTV2 at 0x79a7d3559d73ea032120a69e59223d4375deb595 by repeatedly redeeming the same NFT token. The helper already owned token #1071, self-approved that token, burned it for 1 DAO, then immediately pulled the burned token back out of DEAD_ADDRESS and repeated the cycle 173 times. The drained 173 DAO was sold through the public DAO/WETH Uniswap V2 pair at 0xe1ecadb5fec254c2c893c230b935db30b8fff0db, and the resulting 1.647137809460155184 ETH was forwarded to EOA 0x2f746bc70f72aaf3340b8bbfd254fd91a3996218.
The root cause is a stale single-token approval in TheNFTV2. burn(uint256) transfers ownership to DEAD_ADDRESS but does not clear approval[tokenId]. Because transferFrom(address,address,uint256) still honors approval[tokenId] == msg.sender, a previously approved spender can recover a burned token and redeem it again. After gas (0.174586325 ETH), the EOA realized 1.472551484460155184 ETH net profit.
2. Key Background
TheNFTV2 is the upgraded TheDAO NFT wrapper. Each NFT is backed by 1 DAO token (oneDao = 1e16 raw units), mint(uint256) wraps DAO into a new NFT, burn(uint256) returns 1 DAO and moves the NFT to DEAD_ADDRESS, and restore(uint256) is the intended path to recover a burned NFT after paying a 4 DAO fee. The protocol therefore relies on a simple safety invariant: one NFT should only be able to redeem its 1 DAO backing once unless it is explicitly restored through the paid restore path.
At pre-state block 18647464, the exploit conditions were fully permissionless:
- TheNFTV2 held
174 DAObacking. - Token
#1071was owned by the attacker helper and had no active approval. - TheNFTv1 inventory still had
728mintable NFTs, so any unprivileged actor could have acquired a fresh exploitable NFT even though the incident helper already owned one. - The DAO/WETH pair held deep WETH liquidity, allowing immediate monetization of drained DAO.
The helper contract itself was deployed one block earlier in transaction 0xe05b287871f06463d16547c91e174fa2c29497803983100cd0e0fed90f62c6b8, and the same creator EOA submitted the exploit transaction.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-class logic flaw in TheNFTV2’s approval lifecycle. The contract correctly records single-token approvals in approval[tokenId], but it does not revoke that approval when a burn transfers the NFT to DEAD_ADDRESS. The burn path therefore violates the protocol’s intended terminal-state semantics: a burned token is no longer owned by the prior user, yet that same user can remain an authorized spender. The follow-up transferFrom path checks approval[tokenId] == msg.sender and does not care that ownership has already moved to DEAD_ADDRESS, so the stale approved spender can recover the token without using the paid restore path. Once the attacker regains ownership, the contract is back in the same state as before the burn, except that another 1 DAO has already been redeemed. Repeating the cycle drains TheNFTV2’s backing while consuming only one NFT. Because public minting and public DAO/WETH liquidity were both available, the opportunity was anyone-can-take.
4. Detailed Root Cause Analysis
The verified TheNFTV2 mainnet source shows the exact breakpoint:
function approve(address _to, uint256 _tokenId) external {
address o = ownership[_tokenId];
require(o == msg.sender || isApprovedForAll(o, msg.sender), "action not token permitted");
approval[_tokenId] = _to;
emit Approval(msg.sender, _to, _tokenId);
}
function burn(uint256 id) external {
require(msg.sender == ownership[id], "only owner can burn");
if (theDAO.transfer(msg.sender, oneDao)) {
_transfer(msg.sender, DEAD_ADDRESS, id);
emit Burn(msg.sender, id);
}
}
function transferFrom(address _from, address _to, uint256 _tokenId) external regulated(_to) {
address o = ownership[_tokenId];
require(o == _from, "_from must be owner");
address a = approval[_tokenId];
require(o == msg.sender || (a == msg.sender) || (approvalAll[o][msg.sender]), "not permitted");
_transfer(_from, _to, _tokenId);
if (a != address(0)) {
approval[_tokenId] = address(0);
emit Approval(msg.sender, address(0), _tokenId);
}
}
The invariant is: after a burn, the prior owner or any prior delegate must not be able to move the token again unless the explicit restore path is taken. The breakpoint is that burn(uint256) updates ownership but never clears approval[tokenId]. That leaves a stale approved spender active across a burn.
The seed transaction trace shows the exploit at the call level:
TheNFTV2::approve(0x85301f7b943fd132c8dBc33f8FD9d77109A84f28, 1071)
emit Approval(owner: 0x85301f7b943fd132c8dBc33f8FD9d77109A84f28, approved: 0x85301f7b943fd132c8dBc33f8FD9d77109A84f28, tokenId: 1071)
TheNFTV2::burn(1071)
emit Transfer(from: 0x85301f7b943fd132c8dBc33f8FD9d77109A84f28, to: 0x000000000000000000000000000000000074eda0, tokenId: 1071)
emit Burn(owner: 0x85301f7b943fd132c8dBc33f8FD9d77109A84f28, tokenId: 1071)
TheNFTV2::transferFrom(0x000000000000000000000000000000000074eda0, 0x85301f7b943fd132c8dBc33f8FD9d77109A84f28, 1071)
emit Transfer(from: 0x000000000000000000000000000000000074eda0, to: 0x85301f7b943fd132c8dBc33f8FD9d77109A84f28, tokenId: 1071)
The first burn pays out 1 DAO to the helper, but because the helper remained the approved spender, it could transfer the burned NFT back from DEAD_ADDRESS. That recreated the same exploitable state and allowed the helper to repeat the loop 173 times in the same transaction. The collected balance diff confirms that TheNFTV2’s DAO balance dropped from 174 DAO to 1 DAO, which is exactly 173 DAO drained from one reusable token.
5. Adversary Flow Analysis
The adversary flow had three stages.
First, the controlling EOA 0x2f746bc70f72aaf3340b8bbfd254fd91a3996218 deployed helper 0x85301f7b943fd132c8dbc33f8fd9d77109a84f28 in transaction 0xe05b287871f06463d16547c91e174fa2c29497803983100cd0e0fed90f62c6b8. By block 18647464, the helper already owned token #1071.
Second, the EOA called run(uint256,uint256) on the helper in transaction 0xd5b4d68432cbbd912130bbb5b93399031ddbb400d8f723c78050574de7533106. Inside the helper’s Uniswap V2 flash-swap callback, the helper executed the repeated approval-reuse sequence against TheNFTV2. The trace records this pattern many times; the first two iterations already demonstrate the exploit, and the same sequence continues until 173 DAO has been redeemed.
Third, the helper monetized the drained DAO through the public DAO/WETH pair and forwarded the proceeds back to the creator EOA:
DAO::transfer(0xE1eCaDb5FEC254c2c893C230b935Db30b8FfF0db, 1730000000000000000)
WETH9::withdraw(1647137809460155184)
0x2F746bC70f72aAF3340B8BbFd254fd91a3996218::fallback{value: 1647137809460155184}()
This sequence is important for the ACT classification. The helper contract was only execution tooling. The exploit itself required no privileged roles, no attacker secrets, and no private protocol hooks. Any unprivileged user could have obtained a TheNFTV2 token, self-approved it, reused the stale approval after burn, and sold the redeemed DAO into public liquidity.
6. Impact & Losses
The measurable protocol loss was 173 DAO, encoded as raw on-chain units 1730000000000000000 with 16 decimals. TheNFTV2’s DAO backing fell from 174 DAO to 1 DAO in a single transaction, meaning almost the entire reserve behind outstanding NFTs was removed.
The adversary converted the drained DAO into 1.647137809460155184 ETH and paid 0.174586325 ETH in gas, leaving 1.472551484460155184 ETH net profit. The primary directly affected contract was TheNFTV2, while the DAO token and DAO/WETH pair were used as the backing asset and monetization venue rather than as the vulnerability source.
7. References
- Exploit transaction:
0xd5b4d68432cbbd912130bbb5b93399031ddbb400d8f723c78050574de7533106 - Helper deployment transaction:
0xe05b287871f06463d16547c91e174fa2c29497803983100cd0e0fed90f62c6b8 - Victim contract: TheNFTV2
0x79a7d3559d73ea032120a69e59223d4375deb595 - Supporting protocol contracts: TheNFTv1
0x266830230bf10a58ca64b7347499fd361a011a02, DAO0xbb9bc244d798123fde783fcc1c72d3bb8c189413, DAO/WETH pair0xe1ecadb5fec254c2c893c230b935db30b8fff0db, WETH0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - Verified TheNFTV2 mainnet source inspected during validation for
approve(address,uint256),burn(uint256), andtransferFrom(address,address,uint256) - Collected seed metadata, balance diffs, and verbose trace for the exploit transaction, which show the pre-state, the repeated approval-reuse loop, and the final ETH payout