All incidents

Flooring extMulticall Approval Drain

Share
Dec 17, 2023 00:43 UTCAttackLoss: 12.06 WETH, 14 BAYC +1 morePending manual check3 exploit txWindow: 7m 12s
Estimated Impact
12.06 WETH, 14 BAYC +1 more
Label
Attack
Exploit Tx
3
Addresses
1
Attack Window
7m 12s
Dec 17, 2023 00:43 UTC → Dec 17, 2023 00:50 UTC

Exploit Transactions

TX 1Ethereum
0xa329b27fbe0f7b7f92060a9e5370fdf03d60e5c4835f09d7234e5bbecf417ccf
Dec 17, 2023 00:43 UTCExplorer
TX 2Ethereum
0xec8f6d8e114caf8425736e0a3d5be2f93bbea6c01a50a7eeb3d61d2634927b40
Dec 17, 2023 00:49 UTCExplorer
TX 3Ethereum
0xfb9942a119c45adab3980639cd829e57b41449e3b82d610892da4bb921e81d9c
Dec 17, 2023 00:50 UTCExplorer

Victim Addresses

0x49AD262C49C7aA708Cc2DF262eD53B64A17Dd5EEEthereum

Loss Breakdown

12.06WETH
14BAYC
36PPG

Similar Incidents

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 0x49AD262C49C7aA708Cc2DF262eD53B64A17Dd5EE was deployed and delegated to FlooringPeriphery.
  • FlooringPeriphery inherited Multicall and exposed extMulticall publicly.
  • 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 transferFrom or safeTransferFrom.
  • 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 safeTransferFrom calls 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 extMulticall path, now targeting PudgyPenguins transfers
  • Effect: 36 PudgyPenguins NFTs moved from 0xe5442ae87e0fef3f7cc43e507adf786c311a0529 to 0x4d0d...

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 extMulticall with five attacker-supplied WETH.transferFrom calls
  • Effect: 12063212894780222877 wei of WETH moved from approved user addresses to 0x4d0d...

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:

  • 1025400000000000000 wei from 0x2244deba8c0e788a60b30b805ff970e082bdbc5a
  • 788200000000000000 wei from 0xd8AA1a0A09392d03C8829784075edc0dfd17771C
  • 4628100600785714560 wei from 0x1277f5266dEad289eB6Ab3f97a866f5854FEb33d
  • 1650100000000000066 wei from 0xB9fB6a2ef61dd2514d352a9A1252a3A0fF9d4072
  • 3971412293994508251 wei from 0x7e5ccBf79f81baF0430a9eD8208580c7157F143C

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:

  • 12063212894780222877 wei of WETH
  • 14 BAYC NFTs
  • 36 PudgyPenguins 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: tx 0xa329b27fbe0f7b7f92060a9e5370fdf03d60e5c4835f09d7234e5bbecf417ccf
  • [2] Pudgy theft trace: tx 0xec8f6d8e114caf8425736e0a3d5be2f93bbea6c01a50a7eeb3d61d2634927b40
  • [3] WETH theft trace: tx 0xfb9942a119c45adab3980639cd829e57b41449e3b82d610892da4bb921e81d9c
  • [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