Flooring extMulticall Approval Drain
Exploit Transactions
0xa329b27fbe0f7b7f92060a9e5370fdf03d60e5c4835f09d7234e5bbecf417ccf0xec8f6d8e114caf8425736e0a3d5be2f93bbea6c01a50a7eeb3d61d2634927b400xfb9942a119c45adab3980639cd829e57b41449e3b82d610892da4bb921e81d9cVictim Addresses
0x49AD262C49C7aA708Cc2DF262eD53B64A17Dd5EEEthereumLoss Breakdown
Similar Incidents
USDTStaking Approval Drain
41%Vortex approveToken Drain
35%Dexible selfSwap allowance drain
34%0x7CAE Approved-Spender Drain
34%WBTC Drain via Insecure Router transferFrom Path
33%V3Utils Arbitrary Call Drain
32%Root Cause Analysis
Flooring extMulticall Approval Drain
1. Incident Overview TL;DR
Flooring Protocol exposed a public asset-moving surface through proxy 0x49AD262C49C7aA708Cc2DF262eD53B64A17Dd5EE. Users had already granted that proxy ERC20 allowances and ERC721 operator approvals for normal protocol usage. The adversary EOA 0x4d0d746e0f66bf825418e6b3def1a46ec3c0b847 repeatedly invoked the proxy's public extMulticall path and used the proxy's existing approval surface to transfer third-party BAYC, PudgyPenguins, and WETH directly to the attacker without any victim transaction, signature, or privileged role.
The root cause is a missing authorization check on FlooringPeriphery::extMulticall. Because the proxy delegates to FlooringPeriphery, and FlooringPeriphery inherits Multicall, any external caller could force the proxy to execute attacker-chosen external calls as msg.sender = 0x49AD.... Once users had approved that proxy, those approvals became globally stealable.
2. Key Background
The relevant pre-state is Ethereum mainnet immediately before block 18802261, using block 18802260 as the PoC fork point. At that state:
- The Flooring proxy
0x49AD262C49C7aA708Cc2DF262eD53B64A17Dd5EEwas deployed and delegated toFlooringPeriphery. FlooringPeripheryinheritedMulticalland exposedextMulticallpublicly.- Users had already granted the proxy standing ERC721 operator approvals and ERC20 allowances.
The collected pre-state checks confirm this directly:
BAYC isApprovedForAll(0xf15c93562bc3944a68e938ef75d2a3360d98ca57, 0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee) = true
BAYC balanceOf(0xf15c93562bc3944a68e938ef75d2a3360d98ca57) = 2
BAYC tokenOfOwnerByIndex(0xf15c93562bc3944a68e938ef75d2a3360d98ca57, 0) = 6936
WETH allowance(0x1277f5266dead289eb6ab3f97a866f5854feb33d, 0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee) = type(uint256).max
WETH balanceOf(0x1277f5266dead289eb6ab3f97a866f5854feb33d) = 4628100600785714560
This matters because token contracts do not know why the proxy is calling them. They only observe that msg.sender is the already-approved proxy. If a public entrypoint lets arbitrary users trigger external calls from that proxy context, every existing approval becomes a permissionless drain surface.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability class is an authorization failure on a privileged arbitrary-call surface. FlooringPeriphery inherits Multicall and does not add any caller authentication or target filtering around extMulticall. The inherited Multicall.multicall2 function iterates over attacker-supplied call targets and executes calli.target.call(calli.callData) directly. When that logic is reached through proxy 0x49AD..., downstream token contracts observe the proxy, not the external attacker, as msg.sender. That means any prior ERC20 allowance or ERC721 operator approval granted to the proxy can be exercised by any external caller. The safety invariant is straightforward: the Flooring proxy should only spend approvals in flows authorized by the asset owner or an explicitly permissioned protocol action tied to the current caller. The code-level breakpoint is the public extMulticall entrypoint reaching multicall2, which executes attacker-chosen external calls from the proxy context.
From the verified Multicall.sol source used by FlooringPeriphery:
function extMulticall(CallData[] calldata calls) external virtual override returns (bytes[] memory) {
return multicall2(calls);
}
function multicall2(CallData[] calldata calls) internal returns (bytes[] memory) {
bytes[] memory results = new bytes[](calls.length);
CallData calldata calli;
for (uint256 i = 0; i < calls.length;) {
calli = calls[i];
(bool success, bytes memory result) = calli.target.call(calli.callData);
...
}
return results;
}
The ACT conditions are also explicit and deterministic:
- A victim must already have approved the Flooring proxy for ERC20 spending or ERC721 operator control.
- The approved token must expose a transferable function reachable by arbitrary external call, such as
transferFromorsafeTransferFrom. - The adversary only needs public on-chain state to discover approved victims, balances, and token IDs.
4. Detailed Root Cause Analysis
The exploit begins with a regular transaction from attacker EOA 0x4d0d746e0f66bf825418e6b3def1a46ec3c0b847 into helper address 0x7E5433F02F4bf07C4f2a2D341C450E07d7531428, which then reaches the Flooring proxy 0x49AD.... The proxy performs a delegatecall into FlooringPeriphery::extMulticall, so all subsequent token interactions execute from the proxy context.
The WETH drain trace shows the full sequence clearly:
0x7E5433...::6306e50a(...)
├─ ERC1967Proxy::fallback([CallData({ target: WETH, callData: transferFrom(...) }), ...])
│ ├─ FlooringPeriphery::extMulticall([...]) [delegatecall]
│ │ ├─ WETH9::transferFrom(0x2244..., 0x4d0D..., 1025400000000000000)
│ │ ├─ WETH9::transferFrom(0xd8AA..., 0x4d0D..., 788200000000000000)
│ │ ├─ WETH9::transferFrom(0x1277..., 0x4d0D..., 4628100600785714560)
│ │ ├─ WETH9::transferFrom(0xB9fB..., 0x4d0D..., 1650100000000000066)
│ │ └─ WETH9::transferFrom(0x7e5c..., 0x4d0D..., 3971412293994508251)
The critical property is that those transferFrom calls are executed with msg.sender = 0x49AD262C49C7aA708Cc2DF262eD53B64A17Dd5EE, not with msg.sender = 0x4d0d.... Because the victims had already approved the proxy, the token contracts honored the calls.
The BAYC trace shows the same mechanism for ERC721 operator approvals:
ERC1967Proxy::fallback([CallData({ target: BAYC, callData: safeTransferFrom(...) }), ...])
├─ FlooringPeriphery::extMulticall([...]) [delegatecall]
│ ├─ BoredApeYachtClub::safeTransferFrom(0xf15C..., 0x4d0D..., 6936)
│ ├─ BoredApeYachtClub::safeTransferFrom(0xf15C..., 0x4d0D..., 5271)
│ └─ ...
The first observed adversary-crafted transaction in the ACT sequence is 0xa329b27fbe0f7b7f92060a9e5370fdf03d60e5c4835f09d7234e5bbecf417ccf, mined in block 18802261. That transaction alone demonstrates the exploit predicate: an unprivileged external caller invoked the public path and caused third-party approved assets to move to the attacker without any victim transaction in the same sequence. The exact gas cost for that tx is also deterministically known from the sender balance delta: 35433710680158260 wei, or 0.035433710680158260 ETH.
The non-monetary ACT predicate is therefore:
O(sigma_B, sigma_prime) = 1 iff a regular external caller can invoke FlooringPeriphery.extMulticall
through proxy 0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee and cause an ERC20 or ERC721 asset
owned by a third-party EOA and previously approved to that proxy to move from the victim
to an adversary-controlled recipient without any victim transaction in b.
For completeness, the root-cause artifact records ETH as the reference asset only to express the exact fee paid by tx 0xa329.... Because the ACT determination is non-monetary, portfolio-style value_before, value_after, and value_delta fields are intentionally not applicable to the success predicate.
5. Adversary Flow Analysis
The attacker strategy was a repeated single-transaction batch drain. The same EOA 0x4d0d746e0f66bf825418e6b3def1a46ec3c0b847 sent all three observed exploit transactions.
Stage 1: Drain BAYC operator approvals
- Tx:
0xa329b27fbe0f7b7f92060a9e5370fdf03d60e5c4835f09d7234e5bbecf417ccf - Block:
18802261 - Mechanism: attacker-chosen
safeTransferFromcalls delivered through the public proxy entrypoint - Effect: 14 BAYC NFTs moved from six previously approved user addresses to
0x4d0d...
The trace shows repeated ERC1967Proxy::fallback -> FlooringPeriphery::extMulticall [delegatecall] -> BoredApeYachtClub::safeTransferFrom(...) transitions, with BAYC ownership updated to the attacker.
Stage 2: Drain PudgyPenguins operator approval
- Tx:
0xec8f6d8e114caf8425736e0a3d5be2f93bbea6c01a50a7eeb3d61d2634927b40 - Block:
18802289 - Mechanism: the same public
extMulticallpath, now targeting PudgyPenguins transfers - Effect: 36 PudgyPenguins NFTs moved from
0xe5442ae87e0fef3f7cc43e507adf786c311a0529to0x4d0d...
The corresponding balance-diff artifact confirms the transfer of all 36 NFTs:
{
"token": "0xbd3531da5cf5857e7cfaa92426877b022e612cf8",
"holder": "0xe5442ae87e0fef3f7cc43e507adf786c311a0529",
"before": "36",
"after": "0",
"delta": "-36"
}
Stage 3: Drain WETH allowances
- Tx:
0xfb9942a119c45adab3980639cd829e57b41449e3b82d610892da4bb921e81d9c - Block:
18802297 - Mechanism: public
extMulticallwith five attacker-suppliedWETH.transferFromcalls - Effect:
12063212894780222877wei of WETH moved from approved user addresses to0x4d0d...
The WETH trace shows the attacker first reading victim balances and allowances, then calling the proxy, then transferring funds from each approved victim to the attacker. The aggregate effect from the trace and impact artifact is:
1025400000000000000wei from0x2244deba8c0e788a60b30b805ff970e082bdbc5a788200000000000000wei from0xd8AA1a0A09392d03C8829784075edc0dfd17771C4628100600785714560wei from0x1277f5266dEad289eB6Ab3f97a866f5854FEb33d1650100000000000066wei from0xB9fB6a2ef61dd2514d352a9A1252a3A0fF9d40723971412293994508251wei from0x7e5ccBf79f81baF0430a9eD8208580c7157F143C
6. Impact & Losses
The impact is direct unauthorized seizure of already-approved user assets. No victim interaction was required once the approvals existed. The observed exploit cluster drained:
12063212894780222877wei of WETH14BAYC NFTs36PudgyPenguins NFTs
The vulnerable protocol component is the Flooring proxy 0x49AD262C49C7aA708Cc2DF262eD53B64A17Dd5EE, because that is the approved spender/operator address users trusted and that token contracts recognized during the exploit traces.
7. References
[1]BAYC theft trace: tx0xa329b27fbe0f7b7f92060a9e5370fdf03d60e5c4835f09d7234e5bbecf417ccf[2]Pudgy theft trace: tx0xec8f6d8e114caf8425736e0a3d5be2f93bbea6c01a50a7eeb3d61d2634927b40[3]WETH theft trace: tx0xfb9942a119c45adab3980639cd829e57b41449e3b82d610892da4bb921e81d9c[4]Verified FlooringPeriphery source:https://sourcify.dev/server/repository/contracts/partial_match/1/0xc538d17a6aacc5271be5f51b891e2e92c8187edd/sources/src/FlooringPeriphery.sol[5]Verified Multicall source:https://sourcify.dev/server/repository/contracts/partial_match/1/0xc538d17a6aacc5271be5f51b891e2e92c8187edd/sources/src/Multicall.sol[6]Verified proxy code reference:https://etherscan.io/address/0x49AD262C49C7aA708Cc2DF262eD53B64A17Dd5EE#code