NFTX Doodles collateral accounting flaw enables flash-loan ETH extraction
Exploit Transactions
0x05d65e0adddc5d9ccfe6cd65be4a7899ebcb6e5ec7a39787971bcc3d6ba73996Victim Addresses
0xEBe72cdafEbc1AbF26517dd64B28762Df77912A9Ethereum0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2EthereumLoss Breakdown
Similar Incidents
AirdropGrapesToken ApeCoin Claim via NFTX BAYC Vault
37%P2Controller/XNFT BAYC Over-Borrow via Per-Order-Only Collateral
34%Indexed Finance DEFI5 gulp/reindex bug enables SUSHI flash-swap drain
34%Revest TokenVault withdrawFNFT accounting flaw drains RENA vault reserves
34%SilicaPools decimal-manipulation bug drains WBTC flashloan collateral
33%WIFStaking claimEarned bug enables repeated WIF reward extraction
33%Root Cause Analysis
NFTX Doodles collateral accounting flaw enables flash-loan ETH extraction
1. Incident Overview TL;DR
An unprivileged Ethereum mainnet attacker used an orchestrator contract to compose Balancer WETH flash loans, NFTX Doodles vault flash-mint/redeem operations, Uniswap trades, and an Omni-style lending Pool in a single block. In the profit transaction 0x05d65e0adddc5d9ccfe6cd65be4a7899ebcb6e5ec7a39787971bcc3d6ba73996 (block 15114362), the orchestrator caused the lending Pool to mint WETH debt against 20 Doodles NFTs that never actually left the NFTX vault’s custody.
Because the Pool’s Doodles-collateral path trusted cross-protocol representations (via NFTX vault tokens and internal NToken accounting) without enforcing that ERC721 ownerOf(tokenId) moved into Pool-controlled escrow, it credited full collateral value for NFTs that remained owned by the NFTX Doodles vault. The attacker repaid all flash loans and exited with approximately 145.353220738541920700 ETH net profit, extracted from protocol-side WETH liquidity, while canonical Doodles ownership for all 20 NFTs stayed with the NFTX vault.
2. Key Background
The incident involves a cross-protocol interaction between:
- NFTX Doodles Vault (BeaconProxy) at
0x2F131C4DAd4Be81683ABb966b4DE05a549144443, implemented by NFTXVaultUpgradeable at0x0fa0fD98727C443dd5275774C44D27cFf9D279ed, which custodies underlying Doodles ERC721 tokens and issues ERC20 vault tokens with flash-mint and redeem functionality. - Doodles ERC721 at
0x8a90cAB2b38dBA80c64b7734E58Ee1DB38b8992e, a standard enumerable ERC721 collection that maintains canonical ownership via the inheritedownerOfmapping at storage slot 2. - Omni-style Lending Pool proxy at
0xEBe72cdafEbc1AbF26517dd64B28762Df77912A9, integrated with an NToken wrapper for Doodles and a VariableDebtToken at0x2d51f3040ada50d9dbf0efa737fc0ff0c104d4e8to track WETH borrow positions. - OmniOracle at
0x08Eaf1C8c270a485DD9c8Aebb2EDE3FcAe72e04f, which provides a floor price for Doodles that the Pool uses to value NFT collateral. - WETH9 at
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, which supplies WETH liquidity to the Pool and participates in Balancer flash loans and final ETH withdrawal.
NFTXVaultUpgradeable behaves as a custody vault: users deposit NFTs and receive fungible vault tokens; they redeem vault tokens to withdraw specific NFTs. The relevant parts of the verified NFTXVaultUpgradeable source, captured in seed/1/0x0fa0fd98.../src/solidity/NFTXVaultUpgradeable.sol, show that for ERC721 vaults it keeps a holdings set and, on withdraw, transfers out ERC721 tokens:
function withdrawNFTsTo(
uint256 amount,
uint256[] memory specificIds,
address to
) internal virtual returns (uint256[] memory) {
bool _is1155 = is1155;
address _assetAddress = assetAddress;
uint256[] memory redeemedIds = new uint256[](amount);
...
if (_is1155) {
quantity1155[tokenId] -= 1;
...
} else {
holdings.remove(tokenId);
transferERC721(_assetAddress, to, tokenId);
}
...
}
The corresponding Doodles ERC721 contract in seed/1/0x8a90cab2.../src/Doodles.sol is a straightforward ERC721/Ownable/Enumerable implementation with no special hooks or non-standard transfer semantics. Canonical ownership is therefore fully determined by ownerOf(tokenId) on this contract, as reflected in the decoded storage.
The Omni-style lending Pool integrates with NFTX and treats certain NFTs, including Doodles, as collateral via an internal NToken representation. It uses OmniOracle to obtain a Doodles floor price (13.5 ETH per NFT in this incident) and then allows WETH borrowing against the implied collateral value. Critical safety for such an integration depends on a cross-protocol custody invariant: any NFT counted as collateral must actually be under Pool-controlled on-chain custody (e.g., held by an NToken or escrow contract), not left in an unrelated vault.
3. Vulnerability Analysis & Root Cause Summary
The root cause is a ghost-collateral design flaw in the lending protocol’s Doodles market, integrated via NFTX. The Pool’s collateral-handling logic accepts NFTX- and NToken-based representations of Doodles NFTs as sufficient for collateral credit, without enforcing that canonical ERC721 ownership has moved to a Pool-controlled escrow or NToken contract.
Formally, the intended invariant is:
For every Doodles tokenId
idthat the lending Pool counts as active collateral for any account, canonical ERC721 ownership must satisfyDoodles.ownerOf(id) == NToken_or_escrow, whereNToken_or_escrowis a Pool-controlled contract that actually holds the NFT on-chain; no tokenId may simultaneously remain owned by an external protocol (such as an NFTX vault) while being credited as fully available collateral in the Pool.
During the profit tx 0x05d65e0a..., this invariant is violated: the Pool credits 20 Doodles NFTs as collateral and mints WETH debt via the VariableDebtToken and Pool borrow path, even though decoded Doodles storage shows that for all 20 tokenIds, ownerOf remains the NFTX Doodles vault 0x2F131C4DAd4Be81683ABb966b4DE05a549144443 before and after the transaction. The same NFTs simultaneously back NFTX vault tokens and the attacker’s WETH loan, enabling under-collateralized borrowing without ever transferring canonical ownership into Pool custody.
This is a code-level integration failure in the lending protocol’s Doodles/NFTX collateral path, not a bug in NFTXVaultUpgradeable or Doodles. NFTX behaves as a normal custody vault, and Doodles is a standard ERC721; it is the Pool’s failure to couple collateral recognition to canonical ERC721 custody that creates the ACT opportunity.
4. Detailed Root Cause Analysis
This section walks through the full attack path from pre-state to exploit, tying each step to concrete on-chain evidence.
4.1 Pre-state and ACT opportunity
The ACT opportunity is defined with respect to Ethereum mainnet state immediately before block 15114362, reconstructed from:
seed/index.jsonand seed artifacts for the profit transaction.debug_prestateTracer_diff.jsonindata_collector/iter_2/state_diff/1, which captures native and ERC20 balance deltas.doodles_owners_storage_decoded.jsonindata_collector/iter_3/state_diff/1/0x8a90cab2..., which decodes Doodles owner mappings.
The decoded Doodles storage shows that, for all 20 tokenIds used in the attack, canonical ownership is the NFTX Doodles vault ERC20 token contract 0x2F131C4DAd4Be81683ABb966b4DE05a549144443 both before and after the profit tx:
{
"contract_address": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
"owner_slots": {
"mapping_slot_index": 2,
"entries": [
{
"tokenId": 2595,
"before_owner": "0x2f131c4dad4be81683abb966b4de05a549144443",
"after_owner": "0x2f131c4dad4be81683abb966b4de05a549144443"
},
{
"tokenId": 7418,
"before_owner": "0x2f131c4dad4be81683abb966b4de05a549144443",
"after_owner": "0x2f131c4dad4be81683abb966b4de05a549144443"
}
// ...remaining tokenIds have the same before/after owner
]
}
}
This demonstrates that, regardless of what the lending Pool believes, the Doodles NFTs are never transferred out of the NFTX vault in canonical ERC721 terms. Any protocol that credits these NFTs as collateral for a borrower while ownerOf(tokenId) remains the NFTX vault is necessarily counting ghost collateral.
4.2 Adversary setup: orchestrator deployment and approvals
Using txlist_15113862-15114372.json for EOA 0x627a22ff70cb84e74c9c70e2d5b0b75af5a1dcb9, we can see the adversary’s preparatory transactions:
- Orchestrator deployment (
0xec06fee5..., block15114325):- From:
0x627a22ff70cb84e74c9c70e2d5b0b75af5a1dcb9 - To: contract creation
- Resulting contract:
0x5992f10a5b284be845947a1ae1694f8560a89fa8
- From:
- Doodles approval (
0xf6b498c9..., block15114342):- From: attacker EOA.
- To: Doodles
0x8a90cab2.... - Calldata:
setApprovalForAll(0x5992f10a5b284be845947a1ae1694f8560a89fa8, true).
- Orchestrator configuration (
0x2ede0167..., block15114354):- From: attacker EOA.
- To: orchestrator
0x5992f10a5b284be845947a1ae1694f8560a89fa8. - Encodes addresses for Balancer Vault, NFTX Doodles vault BeaconProxy, Doodles, a UniswapV2 router, lending Pool proxy
0xEBe72cd..., OmniOracle0x08Eaf1C8..., and NFTX staking proxies.
All of these transactions are standard contract deployments and approvals callable by any unprivileged EOA; they require no special permissions or private state, satisfying the ACT model’s inclusion feasibility.
4.3 Profit transaction: cross-protocol sequence
The profit tx 0x05d65e0a... (block 15114362) has:
{
"from": "0x627a22ff70cb84e74c9c70e2d5b0b75af5a1dcb9",
"to": "0x5992f10a5b284be845947a1ae1694f8560a89fa8",
"input": "0x11fe65fa..." // orchestrator::attack selector and parameters
}
The cast trace in data_collector/iter_1/tx/1/.../trace.cast.log shows the orchestrator driving a deep call stack involving:
- Balancer Vault (WETH flash loan),
- NFTX Doodles vault BeaconProxy (flash-mint/redeem and NFT flows),
- UniswapV2 router (swaps),
- Lending Pool proxy
0xEBe72cd...(deposit and borrow), - VariableDebtToken
0x2d51f304...(debt minting), - OmniOracle
0x08Eaf1C8...(price query), - WETH9
0xC02aaA39...(WETH withdrawal to ETH).
For example, a mid-trace segment shows the Pool delegating into the VariableDebtToken implementation:
SM Address: 0x2d51f3040ada50d9dbf0efa737fc0ff0c104d4e8,
caller: 0xebe72cdafebc1abf26517dd64b28762df77912a9,
target: 0x42f6339b5e4bc225090428a913992f466160f681
is_static: true, transfer: Apparent(0), input_size: 4
depth:13, ... OPCODE: "CALLDATASIZE"
and repeated non-static calls with larger input sizes, consistent with the Pool invoking VariableDebtToken::mint and related accounting functions during a WETH borrow.
4.4 Custody mismatch at the breakpoint
The key breakpoint is when the lending Pool updates the attacker’s collateral state and mints WETH debt while the 20 Doodles NFTs remain canonically owned by the NFTX vault.
The decoded Doodles owner mappings (see the snippet above) show no change in owner for any of the 20 tokenIds across the profit transaction. At the same time, debug_prestateTracer_diff.json shows a large positive ETH delta for the attacker EOA and a corresponding negative delta for WETH9:
{
"native_balance_deltas": [
{
"address": "0x627a22ff70cb84e74c9c70e2d5b0b75af5a1dcb9",
"before_wei": "938506870892549170",
"after_wei": "146291727609434469870",
"delta_wei": "145353220738541920700"
},
{
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"delta_wei": "-145494697969606694076"
}
]
}
The attacker’s ETH balance increases by approximately 145.35 ETH, while the WETH9 contract’s balance decreases by roughly the same magnitude (plus gas fees paid to the miner). This confirms that:
- The Pool successfully minted WETH debt and transferred WETH (later withdrawn to ETH) to the attacker.
- All flash loans and other obligations were repaid within the transaction, leaving a net extraction from WETH liquidity.
Because the transaction completes successfully while all 20 NFTs’ canonical ownerOf values remain the NFTX vault, the Pool’s deposit/collateral update logic for the Doodles/NFTX market cannot be enforcing Doodles.ownerOf(id) == NToken_or_escrow. If it did, either:
- The NFTs would have had to move into a Pool-controlled escrow (contradicted by the decoded storage), or
- The borrow would have reverted (contradicted by the successful tx and positive profit).
The vault-side code further supports this interpretation. NFTXVaultUpgradeable’s transferFromERC721 helper ensures the vault does not double-count NFTs it already owns:
function transferFromERC721(address assetAddr, uint256 tokenId) internal virtual {
...
// Default.
// Allow other contracts to "push" into the vault, safely.
// If we already have the token requested, make sure we don't have it in the list to prevent duplicate minting.
if (IERC721Upgradeable(assetAddress).ownerOf(tokenId) == address(this)) {
require(!holdings.contains(tokenId), "Trying to use an owned NFT");
return;
} else {
data = abi.encodeWithSignature(
"safeTransferFrom(address,address,uint256)",
msg.sender,
address(this),
tokenId
);
}
(bool success, bytes memory resultData) = address(assetAddr).call(data);
require(success, string(resultData));
}
This logic, together with the storage diffs, shows that NFTX correctly tracks its own custody and does not accidentally relinquish or duplicate NFTs during flash operations. The ghost collateral arises because the lending Pool credits collateral based on its internal representation of the NFTs and oracle price, without checking canonical Doodles ownership.
4.5 Conclusion on the root cause
The exploit leverages a deterministic mismatch between:
- What the lending Pool believes (that 20 Doodles NFTs are under its control and can back a WETH loan), and
- What canonical on-chain state records (that all 20 NFTs remain owned by the NFTX vault).
This mismatch exists independently of the attacker’s specific orchestrator implementation and is reproducible by any unprivileged account that can:
- Use flash loans and NFTX to route Doodles NFTs through the same integration path, and
- Trigger the Pool’s Doodles collateralization path and WETH borrow while canonical ownership remains external.
The root cause is therefore a violation of the cross-protocol custody invariant in the Doodles/NFTX market of the lending protocol, not a transient or privileged condition.
5. Adversary Flow Analysis
The adversary-related cluster and lifecycle stages are fully captured on-chain and are reproducible by any unprivileged account.
5.1 Adversary-related cluster accounts
- EOA (attacker):
0x627a22ff70cb84e74c9c70e2d5b0b75af5a1dcb9- Sender of all attacker-crafted transactions: orchestrator deployment, Doodles approval, orchestrator configuration, and the profit attack.
- Receives the net ETH profit, as confirmed by
debug_prestateTracer_diff.json.
- Orchestrator contract:
0x5992f10a5b284be845947a1ae1694f8560a89fa8- Deployed directly by the attacker EOA in tx
0xec06fee5.... - Used exclusively to coordinate Balancer, NFTX, Uniswap, lending Pool, OmniOracle, and WETH9 calls in the profit tx.
- Deployed directly by the attacker EOA in tx
No privileged or governance-controlled accounts participate in the exploit path; all calls are made from these adversary-related accounts to permissionless contracts.
5.2 Lifecycle stages and transactions
-
Orchestrator deployment
- Tx:
0xec06fee586aea310b31600f3c53f9e4a6eef9aa8311863f4cd4160523676683a - Block:
15114325 - Mechanism: contract deploy from attacker EOA, resulting in orchestrator
0x5992f1....
- Tx:
-
Approval and configuration
- Tx:
0xf6b498c9209987f2128ac2f527b5ceceffc41a59b0a20b5a03f3568ce5b46e2e- Doodles
setApprovalForAll(orchestrator, true)from the attacker EOA.
- Doodles
- Tx:
0x2ede0167334d572d1bf9ddc594b918cb8fbfa9dfdcbb5c1c9f341d68f519a94b- Configuration call to the orchestrator with Balancer Vault, NFTX Doodles vault, Doodles, Uniswap router, lending Pool proxy, OmniOracle, and NFTX staking proxies.
- Tx:
-
Cross-protocol ghost-collateral attack
- Tx:
0x05d65e0adddc5d9ccfe6cd65be4a7899ebcb6e5ec7a39787971bcc3d6ba73996 - Block:
15114362 - Mechanism:
- Obtain a large WETH flash loan from Balancer.
- Use NFTXVaultUpgradeable functions via the Doodles vault to flash-mint vault tokens and redeem 20 specific Doodles tokenIds (2595, 7418, 4571, 3968, 9403, 4407, 8980, 179, 8234, 4510, 5251, 7894, 7425, 9582, 9069, 1067, 8252, 8883, 3995, 4314).
- Route those NFTs through helper proxies into the lending protocol’s Doodles/NFTX collateral path, causing the Pool to count them as collateral at a 13.5 ETH floor price per NFT.
- The Pool, via VariableDebtToken and its internal position accounting, mints WETH debt to the attacker.
- The orchestrator repays NFTX and Balancer flash loans.
- Remaining WETH is withdrawn to ETH via WETH9 and left in the attacker’s EOA.
- Tx:
Throughout this sequence, canonical Doodles ownership for all 20 tokenIds remains the NFTX vault, as shown by the decoded storage. The lending Pool’s internal perspective diverges from this canonical state, enabling the ghost-collateral exploit.
5.3 ACT criteria and reproducibility
The exploit satisfies ACT criteria:
- Permissionless inputs: All contracts invoked (Balancer Vault, NFTX Doodles vault, Doodles, Uniswap router, lending Pool, OmniOracle, WETH9) are public, permissionless contracts callable by any EOA with sufficient gas.
- No privileged roles: The attacker uses only their own EOA and orchestrator contract; no governance or admin roles are involved.
- Public information: All required addresses, ABIs, and calldata are derivable from public on-chain data and contract metadata.
An independent unprivileged adversary can reproduce the attack by:
- Deploying an equivalent orchestrator,
- Sending analogous configuration and approval transactions, and
- Triggering the same
attack-style entrypoint with appropriate parameters, under the same economic conditions (Doodles floor price, NFTX liquidity, and lending Pool parameters).
6. Impact & Losses
The impact is a direct extraction of WETH/ETH value from protocol-side liquidity providers, without any corresponding change in Doodles NFT ownership.
From debug_prestateTracer_diff.json:
- Attacker EOA
0x627a22...:- Before:
0.938506870892549170ETH. - After:
146.291727609434469870ETH. - Delta:
+145.353220738541920700ETH.
- Before:
- WETH9 contract
0xC02aaA39...:- Delta: approximately
-145.494697969606694076wei.
- Delta: approximately
- Miner address:
- Gains
0.028110272ETH in gas fees.
- Gains
The net effect is that roughly 145.353220738541920700 ETH of value is transferred from protocol-side liquidity (WETH reserves and lending Pool WETH liquidity) to the attacker EOA, after accounting for gas. The 20 Doodles NFTs remain fully owned by the NFTX Doodles vault; no Doodles holder loses their NFT, and NFTX’s custody is unchanged.
In summary:
- Loss asset: ETH (via WETH).
- Magnitude: ~145.35 ETH net to the attacker.
- Victims: Liquidity providers and protocol stakeholders whose WETH liquidity backs the lending Pool and WETH9 reserves, rather than Doodles or NFTX token holders.
7. References
Key artifacts and on-chain references used in this analysis:
- Seed tx metadata and balance diffs
artifacts/root_cause/seed/1/0x05d65e0adddc5d9ccfe6cd65be4a7899ebcb6e5ec7a39787971bcc3d6ba73996/metadata.jsonartifacts/root_cause/seed/1/0x05d65e0adddc5d9ccfe6cd65be4a7899ebcb6e5ec7a39787971bcc3d6ba73996/balance_diff.json
- Profit tx trace
artifacts/root_cause/data_collector/iter_1/tx/1/0x05d65e0adddc5d9ccfe6cd65be4a7899ebcb6e5ec7a39787971bcc3d6ba73996/trace.cast.log
- Orchestrator configuration tx trace
artifacts/root_cause/data_collector/iter_2/tx/1/0x2ede0167334d572d1bf9ddc594b918cb8fbfa9dfdcbb5c1c9f341d68f519a94b/trace.cast.log
- Decoded state diffs
- Doodles owner mappings:
artifacts/root_cause/data_collector/iter_3/state_diff/1/0x8a90cab2b38dba80c64b7734e58ee1db38b8992e/doodles_owners_storage_decoded.json - NFTX Doodles vault proxy storage:
artifacts/root_cause/data_collector/iter_3/state_diff/1/0xe0fbc366b704d0fcbcd752bfdded8382e93700b9/vault_proxy_storage_decoded.json
- Doodles owner mappings:
- Verified contract sources
- NFTXVaultUpgradeable:
artifacts/root_cause/seed/1/0x0fa0fd98727c443dd5275774c44d27cff9d279ed/src/solidity/NFTXVaultUpgradeable.sol - Doodles ERC721:
artifacts/root_cause/seed/1/0x8a90cab2b38dba80c64b7734e58ee1db38b8992e/src/Doodles.sol
- NFTXVaultUpgradeable:
- Lending and oracle contracts
- Lending Pool proxy (Omni-style):
0xEBe72cdafEbc1AbF26517dd64B28762Df77912A9 - VariableDebtToken:
0x2d51f3040ada50d9dbf0efa737fc0ff0c104d4e8 - OmniOracle:
0x08Eaf1C8c270a485DD9c8Aebb2EDE3FcAe72e04f
- Lending Pool proxy (Omni-style):
- Core ERC20/721 assets
- NFTX Doodles Vault ERC20 token:
0x2F131C4DAd4Be81683ABb966b4DE05a549144443 - Doodles ERC721 collection:
0x8a90cAB2b38dBA80c64b7734E58Ee1DB38b8992e - WETH9:
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
- NFTX Doodles Vault ERC20 token: