Calculated from recorded token losses using historical USD prices at the incident time.
0x670da209fb1168941c4565a9a86f87d1011b24b857ea64f658b126a43f031fa00xe85a08cf316f695ebe7c13736c8cc38a7cc3e944EthereumOn Ethereum mainnet block 15084458, a helper contract at 0xb314fd4ac6e10a7e27929cbc8db96743739c82b6, controlled by EOA 0x0000000a5aab7e0b99e8b30028d790de05da0f09, invoked the public function ownerWithdrawAllTo(address) on the FlippazOne NFT auction contract at 0xe85a08cf316f695ebe7c13736c8cc38a7cc3e944. In a single transaction (0x670da209fb1168941c4565a9a86f87d1011b24b857ea64f658b126a43f031fa0), FlippazOne sent its entire 1.15 ETH balance to the helper, which immediately forwarded the same amount to the attacker EOA. Using on-chain balance deltas and gas data, the attacker’s net ETH balance increased by approximately 1.1453 ETH after paying transaction fees.
The root cause is a missing access-control check on FlippazOne’s withdrawal functions. The functions ownerWithdraw(), ownerWithdrawTo(address), ownerWithdrawAll(), and ownerWithdrawAllTo(address) are declared public and lack any onlyOwner-style restriction, allowing any caller to withdraw proceeds or even the contract’s full ETH balance. The adversary exploited this by calling ownerWithdrawAllTo(helper) via a helper contract, turning a latent “anyone-can-drain” condition into a realized theft of funds.
FlippazOne is a verified ERC721-based NFT auction contract deployed at 0xe85a08cf316f695ebe7c13736c8cc38a7cc3e944. It manages bidding, buy-now functionality, and settlement for a single NFT, holding ETH from bidders and forwarding proceeds to the project owner at the end of the auction. The contract inherits standard OpenZeppelin ERC721 components and adds auction-specific state such as highestBid, , , and associated withdrawal utilities.
highestBidderauctionEndedThe contract exposes the following owner withdrawal functions, all marked public:
// Extract from FlippazOne Contract.sol (verified source)
function ownerWithdraw() public {
require(auctionEnded || block.timestamp > auctionEndTimestamp, "Cannot withdraw until auction is ended");
(bool success, ) = owner().call{value: highestBid}("");
require(success, "Failed to withdraw funds.");
}
function ownerWithdrawTo(address toAddress) public {
require(auctionEnded || block.timestamp > auctionEndTimestamp, "Cannot withdraw until auction is ended");
(bool success, ) = toAddress.call{value: highestBid}("");
require(success, "Failed to withdraw funds.");
}
function ownerWithdrawAll() public {
(bool success, ) = owner().call{value: address(this).balance}("");
require(success, "Failed to withdraw funds.");
}
function ownerWithdrawAllTo(address toAddress) public {
(bool success, ) = toAddress.call{value: address(this).balance}("");
require(success, "Failed to withdraw funds.");
}
None of these functions enforce msg.sender == owner() or any equivalent access-control condition. As a result, any unprivileged caller can trigger a transfer of ETH from the contract balance either to the configured owner or to an arbitrary recipient address.
The adversary uses a separate helper contract deployed at 0xb314fd4ac6e10a7e27929cbc8db96743739c82b6. The seed transaction metadata and call trace show that:
0x84097393, which, based on the verified ABI, corresponds to ownerWithdrawAllTo(address).toAddress parameter, receiving the full contract balance and then forwarding it to the EOA.The relevant portion of the seed transaction’s call trace is:
// Excerpt from callTracer trace for tx 0x670d…fa0
{
"from": "0x0000000a5aab7e0b99e8b30028d790de05da0f09",
"to": "0xb314fd4ac6e10a7e27929cbc8db96743739c82b6",
"value": "0x0",
"input": "0x442d9a20…", // helper entrypoint
"gasUsed": "0xb725",
"calls": [
{
"from": "0xb314fd4ac6e10a7e27929cbc8db96743739c82b6",
"to": "0xe85a08cf316f695ebe7c13736c8cc38a7cc3e944",
"type": "CALL",
"input": "0x84097393 000000000000000000000000b314fd4ac6e10a7e27929cbc8db96743739c82b6",
"value": "0x0",
"calls": [
{
"from": "0xe85a08cf316f695ebe7c13736c8cc38a7cc3e944",
"to": "0xb314fd4ac6e10a7e27929cbc8db96743739c82b6",
"type": "CALL",
"value": "0xff59ee833b30000" // 1.15 ETH
}
]
},
{
"from": "0xb314fd4ac6e10a7e27929cbc8db96743739c82b6",
"to": "0x0000000a5aab7e0b99e8b30028d790de05da0f09",
"type": "CALL",
"value": "0xff59ee833b30000" // forward 1.15 ETH to EOA
}
]
}
This trace confirms the execution path: EOA → helper → FlippazOne → helper → EOA, with exactly 1.15 ETH moving out of the victim contract and ultimately into the attacker’s externally owned account.
The vulnerability is a classic “missing access control on privileged withdrawal functions.” FlippazOne’s owner withdrawal functions are implemented as ordinary public functions, with no restriction tying invocation to the contract owner or any authorized role. In particular, ownerWithdrawAllTo(address toAddress) blindly sends address(this).balance to the supplied toAddress for any caller, guarded only by a simple success check on the low-level call.
The intended invariant for a single-NFT auction like FlippazOne is that only the auction owner or a formally designated recipient can withdraw proceeds or the entire ETH balance accumulated in the contract. By failing to assert msg.sender == owner() (or equivalent) in ownerWithdraw* and ownerWithdrawAll*, the contract allows arbitrary, unprivileged accounts to drain its ETH holdings at any time the contract holds funds. This design mistake converts a privileged action into an “anyone-can-take” opportunity.
The adversary did not need to exploit any subtle reentrancy or price-manipulation behavior. Instead, they simply constructed a transaction via a helper contract that invoked ownerWithdrawAllTo(helper), collected the full contract balance, and forwarded it to their EOA, turning the latent access-control bug into a realized theft.
From an invariant perspective, the protocol should satisfy:
The concrete breakpoint in the code is the family of unguarded, public withdrawal functions shown earlier. None of these functions enforce ownership or role checks. The most severe of them, ownerWithdrawAllTo(address toAddress), performs:
// Core breakpoint: anyone can drain full balance to any address
function ownerWithdrawAllTo(address toAddress) public {
(bool success, ) = toAddress.call{value: address(this).balance}("");
require(success, "Failed to withdraw funds.");
}
Because this function is public, any account — including arbitrary EOAs and contracts — can call it. When the contract holds ETH (e.g., from auctions or bids), a single call to ownerWithdrawAllTo(attacker) will send the entire balance to the attacker-controlled address, regardless of who the contract owner is.
The on-chain state immediately before the exploit transaction (block 15084458) satisfies the preconditions of the vulnerability:
// Native balance changes around the exploit transaction
{
"address": "0xe85a08cf316f695ebe7c13736c8cc38a7cc3e944",
"before_wei": "1150000000000000000",
"after_wei": "0",
"delta_wei": "-1150000000000000000"
}
In the exploit transaction 0x670da209fb1168941c4565a9a86f87d1011b24b857ea64f658b126a43f031fa0:
0x84097393 (ownerWithdrawAllTo(address)), passing the helper’s address as the parameter.ownerWithdrawAllTo is public and unguarded, the call succeeds and transfers the entire contract balance (1.15 ETH) to the helper.There is no reliance on special permissions, governance actions, mempool manipulation, or non-standard infrastructure. The exploit leverages a straightforward, permissionless call into a misconfigured public function.
The adversary-related cluster consists of:
The end-to-end flow for the seed ACT transaction is:
0x442d9a20….ownerWithdrawAllTo(address) with the helper’s own address as parameter.to = 0xe85a08cf316f695ebe7c13736c8cc38a7cc3e944input = 0x84097393 000000000000000000000000b314fd4ac6e10a7e27929cbc8db96743739c82b60x84097393 maps to ownerWithdrawAllTo(address) in the verified FlippazOne ABI, so this call invokes ownerWithdrawAllTo(helper).ownerWithdrawAllTo(helper) and sends address(this).balance (1.15 ETH) to the helper.CALL from the victim to the helper with value = 0xff59ee833b30000 wei.CALL from helper to EOA with value = 0xff59ee833b30000 wei.The net effect is a single helper-orchestrated, adversary-crafted transaction that drains the victim’s entire ETH balance to the attacker’s EOA without any privileged rights or governance actions. Because the vulnerable function is public and unprotected, any unprivileged account could replicate this strategy, satisfying the “anyone-can-take” (ACT) condition.
On profitability, the attacker’s balance and fee profile are:
// Attacker EOA native balance deltas and gas/fee computation
{
"attacker_balance": {
"before_wei": "12246898012606991696",
"after_wei": "13392209512606851041",
"delta_wei": "1145311499999859345"
},
"tx_fee": {
"gasUsed": 46885,
"effectiveGasPrice_wei": 100000000003,
"fee_wei": "4688500000140655"
},
"derived_profit_ETH": {
"before": "12.246898012606991696",
"after": "13.392209512606851041",
"delta": "1.145311499999859345",
"fees": "0.004688500000140655"
}
}
These figures confirm that the adversary’s net portfolio value in ETH strictly increases after fees, satisfying a clear profit-based success predicate.
The direct on-chain impact is:
Measured in ETH:
Because the vulnerable function withdraws the entire contract balance, the scale of loss is bounded only by how much ETH the contract happens to hold at call time. In this concrete incident, that amount is 1.15 ETH; under different conditions, the same bug could be used to drain larger balances as they accumulate.
Contract.sol) including public ownerWithdraw* and ownerWithdrawAll* functions without access control.debug_traceTransaction callTracer output showing ownerWithdrawAllTo(helper) and subsequent ETH forwarding.gasUsed = 46885 and effectiveGasPrice = 100000000003 wei, from which the gas fee and profit figures are computed.Collectively, these artifacts demonstrate that the incident is a genuine, reproducible ACT opportunity and that the root cause is a missing access-control guard on public withdrawal functions in the FlippazOne contract.