Floor DAO Stale Epoch Harvesting
Exploit Transactions
0x1274b32d4dfacd2703ad032e8bd669a83f012dde9d27ed92e4e7da0387adafe4Victim Addresses
0x759c6De5bcA9ADE8A1a2719a31553c4B7DE02539Ethereum0xf59257e961883636290411c11ec5ae622d19455eEthereumLoss Breakdown
Similar Incidents
TheNFTV2 Stale Burn Approval
36%OxODex Stale Withdrawal Drain
33%QUATERNION Pair-Rebase Accounting Drift Enables Permissionless Drain
31%Conic crvUSD Oracle Exploit
29%SorraV2 staking withdraw bug enables repeated SOR reward drain
29%bZx/Fulcrum WBTC market manipulation drains ETH liquidity
28%Root Cause Analysis
Floor DAO Stale Epoch Harvesting
1. Incident Overview TL;DR
On Ethereum mainnet, transaction 0x1274b32d4dfacd2703ad032e8bd669a83f012dde9d27ed92e4e7da0387adafe4 exploited Floor DAO's staking system by flash-borrowing nearly all available FLOOR from the public FLOOR/WETH Uniswap V3 pool, cycling that capital through FloorStaking, and forcing a backlog of overdue rebases to be processed inside one transaction. The attacker then repaid the flash leg and sold the extracted surplus FLOOR for 40.146353823753349478 WETH, while the originating EOA paid 0.046925585061332004 ETH in gas. The protocol-side root cause is that FloorStaking::rebase() only advances one epoch per call even when many epochs are overdue, and both stake() and unstake(..., true, ...) expose that path permissionlessly.
2. Key Background
Floor DAO's staking system centers on FloorStaking at 0x759c6De5bcA9ADE8A1a2719a31553c4B7DE02539, the rebasing token sFLOOR, and the non-rebasing wrapper gFLOOR at 0xb1Cc59Fc717b8D4783D41F952725177298B5619d. FloorStaking stores epoch state as (length, number, end, distribute) and triggers rebases through user-facing functions rather than through a privileged keeper-only path. The verified gFLOOR code converts balances through the live staking index, so a holder that remains exposed across a rebase can later redeem more FLOOR via balanceFrom().
The reward side is handled by Distributor at 0x9e3CEe6cA6E4C11fC6200a17545003b6cf6635d0. Its verified source shows that only the staking contract can call distribute() and retrieveBounty(), so every permissionless rebase call in staking can also trigger fresh reward minting into the staking system. That means delayed epoch processing is economically dangerous: if many epochs are overdue, any user who can keep a large position staked while repeatedly triggering rebases can capture value that should have been amortized over time.
The incident also depended on public liquidity. The FLOOR/WETH Uniswap V3 pool at 0xB386c1d831eED803F5e8F274A59C91c4C22EEAc0 held enough FLOOR for the attacker to borrow almost the entire pool balance in one flash operation, making the attacker the dominant staking participant during catch-up.
3. Vulnerability Analysis & Root Cause Summary
The bug is an attack-class accounting flaw, not a pure arbitrage. Verified FloorStaking source shows that stake() always calls rebase(), and unstake(..., _trigger=true, ...) also calls rebase(). Inside rebase(), if epoch.end <= block.timestamp, the contract performs exactly one overdue epoch step, calls sFLOOR.rebase(epoch.distribute, epoch.number), advances epoch.end by one epoch.length, increments epoch.number, and then invokes Distributor::distribute(). There is no loop that catches the system up to the current time and no guard preventing one position from remaining exposed while repeatedly forcing stale epochs to process.
That creates the violated invariant: once the staking system is far behind schedule, one position must not be able to alternate in and out across repeated overdue-epoch processing inside the same transaction. The code-level breakpoint is the single-step branch in FloorStaking::rebase():
if (epoch.end <= block.timestamp) {
sFLOOR.rebase(epoch.distribute, epoch.number);
epoch.end = epoch.end.add(epoch.length);
epoch.number++;
if (address(distributor) != address(0)) {
distributor.distribute();
bounty = distributor.retrieveBounty();
}
}
Because gFLOOR tracks redeemable FLOOR through the live index, the attacker could first convert borrowed FLOOR into gFLOOR, then repeatedly call unstake(..., true, false) and stake(..., false, true) while the contract remained overdue. Each successful rebase increased the amount of FLOOR that the same gFLOOR position could redeem, and each call to Distributor::distribute() compounded the available value.
4. Detailed Root Cause Analysis
The pre-state immediately before block 18068773 already exposed the ACT opportunity: FloorStaking was overdue, the public pool still held FLOOR liquidity, and the staking/unstaking entrypoints were permissionless. The PoC pre-checks reflect the same conditions:
assertLe(epochEnd, block.timestamp, "epoch must be overdue before exploit");
assertGt(poolFloorBefore, 0, "pool must hold FLOOR for the flash leg");
The on-chain trace shows the exploit starting with a helper contract call and then a public Uniswap V3 flash borrow:
0x6cE5...::flash(0, 17)
0xB386...::flash(0x6cE5..., 0, 152089813098498, 0x00)
FloorStaking::stake(0x6cE5..., 152089813098498, false, true)
That initial stake() pushes the borrowed FLOOR into staking and mints gFLOOR exposure. After that, the same transaction repeatedly alternates between unstaking with _trigger=true and staking again:
FloorStaking::unstake(0x6cE5..., ..., true, false)
sFLOOR::rebase(133319016399463, 1323)
Distributor::distribute()
FloorStaking::stake(0x6cE5..., 165360032398453, false, true)
...
FloorStaking::unstake(0x6cE5..., ..., true, false)
sFLOOR::rebase(882089258242, 1355)
Distributor::distribute()
The trace proves this was not a normal single maintenance rebase. The seed transaction advanced epoch numbers from the initial state into the mid-1300s inside one transaction, with many consecutive sFLOOR::rebase and Distributor::distribute() calls. That is exactly the stale-epoch harvesting behavior described in the root cause.
The financial effect is visible in the balance diff. The staking contract's FLOOR balance fell from 1356712874641176 to 1354038984742699, a loss of 2673889898477 raw FLOOR units:
{
"holder": "0x759c6de5bca9ade8a1a2719a31553c4b7de02539",
"before": "1356712874641176",
"after": "1354038984742699",
"delta": "-2673889898477"
}
After repaying the flash borrow plus fee, the helper sold the remaining extracted FLOOR back into the same pool:
0xB386...::swap(0xD3b85111c974c350f95532E3f4210A41c4874BB6, false, 14606145072279, ...)
That swap delivered 40.146353823753349478 WETH to the attacker-side recipient, matching the success predicate in the root cause artifact.
5. Adversary Flow Analysis
The adversary lifecycle began with helper deployment. The root cause artifact ties EOA 0x4453aed57c23a50d887a42ad0cd14ff1b819c750 to helper contract 0x6ce5a85cff4c70591da82de5eb91c3fa38b40595 through the immediately preceding nonce-0 deployment transaction 0xbc863c288a5eaa6bd0bcc411b546828a2b89deb786063b45aaf32ba280a21501.
In the exploit transaction itself, the EOA called the helper using selector 0x828cc5ce, and the helper initiated a public flash borrow from the FLOOR/WETH Uniswap V3 pool. The helper approved staking, staked the borrowed FLOOR, then entered the rebase-harvesting loop. Each loop iteration followed the same decision pattern:
- Burn current gFLOOR exposure back into FLOOR terms through
unstake(..., true, false). - Force one more overdue epoch to process via the
_trigger=truerebase path. - Receive a larger FLOOR amount because the gFLOOR index had increased.
- Re-stake the enlarged FLOOR balance back into gFLOOR unless the loop had reached the final profitable boundary.
The root cause identifies 17 profitable loop iterations in the reproduction, and the seed trace shows a long series of repeated unstake and stake calls consistent with that design. Once the helper had extracted the profitable stale reward backlog, it transferred poolFloorBefore + fee1 back to the pool, then swapped the residual FLOOR for WETH. The final WETH recipient recorded in the incident was 0xD3b85111c974c350f95532E3f4210A41c4874BB6.
6. Impact & Losses
The immediate attacker-side output was 40.146353823753349478 WETH, with approximately 40.099428238692017474 ETH-equivalent net profit after the originating EOA's gas spend. The root cause artifact also records a protocol-side depletion of 2673889898477 raw FLOOR units from the staking contract. In practical terms, the exploit accelerated historical reward distribution into one transaction and converted that stale value into attacker profit, while forcing the FLOOR/WETH pool to absorb the final sale of extracted FLOOR.
The affected protocol components were:
FloorStakingat0x759c6De5bcA9ADE8A1a2719a31553c4B7DE02539Distributorat0x9e3CEe6cA6E4C11fC6200a17545003b6cf6635d0gFLOORat0xb1Cc59Fc717b8D4783D41F952725177298B5619d- The public FLOOR/WETH Uniswap V3 pool at
0xB386c1d831eED803F5e8F274A59C91c4C22EEAc0
7. References
- Seed exploit transaction:
0x1274b32d4dfacd2703ad032e8bd669a83f012dde9d27ed92e4e7da0387adafe4 - Related helper deployment transaction:
0xbc863c288a5eaa6bd0bcc411b546828a2b89deb786063b45aaf32ba280a21501 - Victim staking contract:
0x759c6De5bcA9ADE8A1a2719a31553c4B7DE02539 - Distributor contract:
0x9e3CEe6cA6E4C11fC6200a17545003b6cf6635d0 - gFLOOR contract:
0xb1Cc59Fc717b8D4783D41F952725177298B5619d - FLOOR token:
0xf59257e961883636290411c11ec5ae622d19455e - FLOOR/WETH Uniswap V3 pool:
0xB386c1d831eED803F5e8F274A59C91c4C22EEAc0 - Evidence artifacts used for validation:
- seed transaction metadata
- seed transaction execution trace
- seed transaction balance diff
- verified source for
FloorStaking,Distributor, andgFLOOR