Calculated from recorded token losses using historical USD prices at the incident time.
0x7c5a909b45014e35ddb89697f6be38d08eff30e7c3d3d553033a6efc3b444fdd0xddadf1bf44363d07e750c20219c2347ed7d826b9Ethereum0x89cb997c36776d910cfba8948ce38613636cbc3cEthereumPandorasNodes404, an ERC404-style token on Ethereum mainnet, was drained through an unauthorized ERC20-unit transferFrom path in transaction 0x7c5a909b45014e35ddb89697f6be38d08eff30e7c3d3d553033a6efc3b444fdd at block 19184578. An unprivileged EOA, 0x096f0f03e4be68d7e6dd39b22a3846b8ce9849a3, called helper contract 0xcc5159b5538268f45afda7b5756fa8769ce3e21f, which moved almost the full BLOCK balance out of the PandorasNodes404/WETH Uniswap V2 pair without pair approval, manipulated pair reserve accounting, restored BLOCK to the pair, and swapped out nearly all WETH reserve.
The root cause is in the ERC20 branch of PandorasNodes404's inherited ERC404.transferFrom. The code subtracts amountOrId from allowance[from][msg.sender] before checking that the allowance is sufficient. Because the contract is compiled with Solidity ^0.7.0, the subtraction wraps on underflow instead of reverting, so a zero allowance becomes a very large allowance and _transfer(from, to, amountOrId) executes.
The measurable loss was 7548019801980198018 WETH wei from the pair. The adversary EOA's native balance increased by 5678084122534054097 wei after gas, while the helper also paid 490621287128712871 wei to 0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5.
PandorasNodes404 is deployed at 0xddadf1bf44363d07e750c20219c2347ed7d826b9. Its verified source shows that PandorasNodes404 inherits ERC404 and is compiled under Solidity ^0.7.0, which does not include Solidity 0.8 checked-arithmetic behavior by default.
pragma solidity ^0.7.0;
contract PandorasNodes404 is ERC404 {
constructor(address _owner) ERC404("Pandora's Nodes 404", "BLOCK", 18, 200, _owner) {
balanceOf[_owner] = totalSupply;
setWhitelist(_owner, true);
}
}
The token combines ERC20-like fractional balances with ERC721-like native token accounting. In transferFrom, the branch is selected by comparing amountOrId to the current minted NFT id. If amountOrId <= minted, the function treats the input as an NFT id and enforces owner/operator approval. If amountOrId > minted, the function treats the input as an ERC20 amount and should enforce ERC20 allowance before moving balances.
The affected liquidity venue was the PandorasNodes404/WETH Uniswap V2 pair at 0x89cb997c36776d910cfba8948ce38613636cbc3c. Immediately before the exploit transaction, that pair held a substantial PandorasNodes404 balance and a WETH reserve that could be withdrawn by satisfying the pair's swap accounting.
The vulnerability class is an authorization bypass in ERC20 transferFrom. The ERC20-unit branch reads allowance[from][msg.sender], skips only the unlimited allowance case, subtracts amountOrId, and then calls _transfer. There is no require(allowed >= amountOrId) before the subtraction. Under Solidity 0.7 wrapping arithmetic, a caller with zero allowance can subtract a positive amount and produce a large stored allowance instead of reverting.
The violated invariant is: for ERC20-unit transferFrom(from, to, amount), if msg.sender != from and allowance[from][msg.sender] < amount, then balances must remain unchanged and the call must revert. The code-level breakpoint is the ERC20 branch of ERC404.transferFrom, where allowance[from][msg.sender] = allowed - amountOrId executes before _transfer(from, to, amountOrId).
function transferFrom(
address from,
address to,
uint256 amountOrId
) public virtual {
_preTransferCheck(from, to);
if (amountOrId <= minted) {
...
} else {
uint256 allowed = allowance[from][msg.sender];
if (allowed != type(uint256).max)
allowance[from][msg.sender] = allowed - amountOrId;
_transfer(from, to, amountOrId);
}
}
The internal _transfer function then mutates balances directly and performs ERC404 mint/burn side effects. That makes the missing authorization check a direct path to moving another account's ERC20 units.
function _transfer(
address from,
address to,
uint256 amount
) internal returns (bool) {
uint256 unit = _getUnit();
uint256 balanceBeforeSender = balanceOf[from];
uint256 balanceBeforeReceiver = balanceOf[to];
_preTransferCheck(from, to);
balanceOf[from] -= amount;
balanceOf[to] += amount;
...
emit ERC20Transfer(from, to, amount);
return true;
}
The attacker selected an amountOrId of 185520527453721770724, which was greater than the current minted NFT id and therefore forced the ERC20-unit branch of transferFrom. The caller did not need the pair's ERC20 approval because the missing precondition let the allowance subtraction wrap.
The trace shows the helper first reading the pair's BLOCK balance as 185520527453721770725, then calling:
PandorasNodes404::transferFrom(
0x89CB997C36776D910Cfba8948Ce38613636CBc3c,
0xddaDF1bf44363D07E750C20219C2347Ed7D826b9,
185520527453721770724
)
That call moved nearly all BLOCK units from the pair to the token contract and emitted ERC20Transfer for the same amount. The helper then called the pair's sync(). The trace records WETH balance 7548019801980198019 and PandorasNodes404 balance 1, and the pair emitted Sync(7548019801980198019, 1). This pushed the pair's reserve accounting into an exploitable state.
Next, the helper called the same vulnerable transferFrom path in the opposite direction:
PandorasNodes404::transferFrom(
0xddaDF1bf44363D07E750C20219C2347Ed7D826b9,
0x89CB997C36776D910Cfba8948Ce38613636CBc3c,
185520527453721770724
)
After the restoration, the pair's balanceOf returned 185520527453721770725, while its reserves still reflected the manipulated prior sync. The helper then called swap(7548019801980198018, 0, helper, 0x), causing WETH9 to transfer 7548019801980198018 WETH wei from the pair to the helper. The pair's remaining WETH balance was 1 wei.
The balance-diff artifact confirms the economic impact. WETH9's native backing decreased by 7548019801980198018 wei, matching the WETH withdrawn from the pair. The adversary EOA's native balance increased from 78437427929653870209 wei to 84115512052187924306 wei, a net gain of 5678084122534054097 wei after gas.
The exploit was a single Ethereum mainnet transaction:
chainid: 1
block: 19184578
tx: 0x7c5a909b45014e35ddb89697f6be38d08eff30e7c3d3d553033a6efc3b444fdd
from: 0x096f0f03e4be68d7e6dd39b22a3846b8ce9849a3
to: 0xcc5159b5538268f45afda7b5756fa8769ce3e21f
value: 65 wei
First, the EOA called the helper contract with calldata that encoded the pair address and PandorasNodes404 token address. Second, the helper used the vulnerable token function to move 185520527453721770724 PandorasNodes404 units from the pair to the token contract without approval. Third, the helper called the pair sync() so the pair recorded a BLOCK reserve of 1 while WETH remained high. Fourth, the helper moved the same BLOCK amount from the token contract back to the pair. Fifth, the helper called the pair swap to withdraw 7548019801980198018 WETH wei. Finally, the helper called WETH9.withdraw, paid 490621287128712871 wei to 0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5, and paid 7057398514851485212 wei to the adversary EOA before gas accounting.
The adversary-related accounts are defensible from the trace and balance effects. 0x096f0f03e4be68d7e6dd39b22a3846b8ce9849a3 is the transaction sender and final net-profit recipient. 0xcc5159b5538268f45afda7b5756fa8769ce3e21f is the helper contract that executed the unauthorized token transfers, the pair sync and swap, the WETH withdrawal, and ETH payouts.
This is an anyone-can-take opportunity under the stated adversary model. The exploit uses public contract addresses, public on-chain state, and public callable functions. It does not require a private key, privileged role, private protocol state, or attacker-only deployed bytecode from the original incident; a new unprivileged actor can reproduce the strategy by deploying equivalent helper logic and calling the same public contracts on the same pre-state.
The direct venue impact was the loss of 7548019801980198018 WETH wei from the PandorasNodes404/WETH Uniswap V2 pair. After the swap, the trace shows the pair's WETH balance at 1 wei.
The native balance deltas are:
[
{
"address": "0x096f0f03e4be68d7e6dd39b22a3846b8ce9849a3",
"before_wei": "78437427929653870209",
"after_wei": "84115512052187924306",
"delta_wei": "5678084122534054097"
},
{
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"before_wei": "3254544007352623710817138",
"after_wei": "3254536459332821730619120",
"delta_wei": "-7548019801980198018"
},
{
"address": "0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5",
"before_wei": "14880067895153484740",
"after_wei": "15370689182282197611",
"delta_wei": "490621287128712871"
}
]
The root cause category is ATTACK, not benign arbitrage, because the profit depends on violating the ERC20 transferFrom authorization invariant in the victim token.
Primary transaction trace: /workspace/session/artifacts/collector/seed/1/0x7c5a909b45014e35ddb89697f6be38d08eff30e7c3d3d553033a6efc3b444fdd/trace.cast.log.
Transaction metadata: /workspace/session/artifacts/collector/seed/1/0x7c5a909b45014e35ddb89697f6be38d08eff30e7c3d3d553033a6efc3b444fdd/metadata.json.
Balance-diff evidence: /workspace/session/artifacts/collector/seed/1/0x7c5a909b45014e35ddb89697f6be38d08eff30e7c3d3d553033a6efc3b444fdd/balance_diff.json.
PandorasNodes404 ERC404 source: /workspace/session/artifacts/collector/seed/1/0xddadf1bf44363d07e750c20219c2347ed7d826b9/src/ERC404.sol.
PandorasNodes404 wrapper source: /workspace/session/artifacts/collector/seed/1/0xddadf1bf44363d07e750c20219c2347ed7d826b9/src/pandorasblock404.sol.