Popsicle Finance Sorbetto Fragola PLP Transfer Fee-Checkpoint Omission / Reward Double-Claim Drain (2021-08-04)
- Name: Popsicle Finance Sorbetto Fragola PLP transfer fee-checkpoint omission / reward double-claim drain
- Date: 2021-08-04
- Protocol: Popsicle Finance Sorbetto Fragola (Uniswap V3 optimizer)
- Chain: Ethereum
- Category: reward-distribution bug / transferable receipt-token checkpoint omission / fee double-claim / flash-loan amplified accounting drain
- Loss: about
$20.7M, with public tallies also rounding the incident to about$20M-$25M - Summary: Popsicle’s Sorbetto Fragola vaults let PLP receipt tokens move between addresses without moving the fee-accounting checkpoints that were supposed to describe how much of the global fee accumulator each holder had already earned.
collectFees()settled rewards from per-address values liketoken0PerSharePaidandtoken1PerSharePaid, but plain PLP transfers left those checkpoints stale. The attacker flash-loaned large balances, deposited into eight Fragola pools, transferred the same PLP position across helper contracts, and calledcollectFees()after each transfer. Each helper address was treated as if its newly received PLP had been present for the full prior fee-accrual window, so the same historical rewards were crystallized multiple times. The attacker then returned the PLP to the original address, withdrew the principal liquidity, repaid Aave, and kept the duplicated fee payouts. - Trigger / entry point:
- Popsicle’s fee system tracked user reward state per address rather than binding it tightly to the transferable PLP balance itself.
collectFees()relied on the current PLP balance plus stored checkpoints such astoken0PerSharePaid/token1PerSharePaid.- PLP
transfer()/transferFrom()did not first settle or reset the receiver’s checkpoints to the current accumulator state. - That let a fresh helper contract receive real PLP and then claim historical fees it never economically earned.
- Root cause:
- Fragola used a global
tokenPerShare-style fee accumulator and per-user paid checkpoints, but only some state-changing paths synchronized the user checkpoints correctly. - The decisive invariant failure was:
historical fee entitlement did not move correctly with PLP ownership. - After a PLP transfer, the receiver inherited the balance but not the economically correct
already-accounted-forcheckpoint, socollectFees()treated the incoming balance as if it had been present since an older accumulator state. - Because rewards were stored in side mappings rather than tied inseparably to the live PLP holder, one helper could claim and keep rewards, pass the same PLP onward, and let the next helper claim again.
- Certora later summarized the bug cleanly: Popsicle updated user accounting on deposit and withdrawal but not on token transfer, so transferring PLP could increase the parties’ total assets instead of merely moving them.
- Fragola used a global
- Exploit path:
- The attacker used Aave flash loans to fund a large multi-asset position, with public reconstructions citing roughly
30M USDT,30M USDC,3M DAI,13k WETH,1.4k WBTC, and200k UNIacross the full attack. - They created three helper contracts / addresses, commonly reconstructed as
A,B, andC. - Using one pool at a time,
Adeposited large balances into a Sorbetto Fragola vault and received PLP shares. Atransferred those PLP shares toB.BcalledcollectFees(), and becauseB’s checkpoints did not reflect the transferred balance’s true prior accounting state, Popsicle creditedBwith fees that belonged to the whole position’s past accrual window.Btransferred the same PLP shares toC, which repeatedcollectFees()and harvested the same historical fee window again under its own stale checkpoint state.- The attacker transferred the PLP back to
A, which withdrew the underlying principal liquidity from the vault. - The cycle was repeated across eight Fragola pools, including a widely cited USDT/WETH example.
- After principal withdrawal and reward harvesting, the attacker repaid the flash loans, swapped much of the stolen inventory to ETH, and sent a large portion through Tornado Cash.
- The attacker used Aave flash loans to fund a large multi-asset position, with public reconstructions citing roughly
- What the attacker needed to be true:
- PLP had to be transferable between helper addresses.
- Reward entitlement had to depend on per-address checkpoints rather than on non-transferable position state.
collectFees()had to trust those checkpoints after transfer without first resetting the receiver to the current accumulator state.- Claimed rewards had to remain with the helper address even after the PLP moved onward.
- Vault fee inventory and underlying liquidity had to be deep enough that duplicated reward claims across several pools were economically meaningful.
- Technical anatomy worth remembering:
- This was not a price-oracle hack, AMM invariant break, or token-callback reentrancy. It was pure reward accounting failure on top of a transferable share token.
- The exploit did not require stealing the underlying LP principal directly. The attacker used honest deposits and honest withdrawals, while the theft happened in the side-pocketed fee distribution math.
- BlockSec described the issue as a double-claiming attack and preserved the core reward formula explicitly:
user.token0Rewards += PLP.balanceOf(account) * (fee0PerShare - user.token0PerSharePaid) / 1e18(with the same structure for token1). The dangerous detail was thattoken0Rewards/token1Rewardswere stored per address and remained withdrawable even after the PLP moved away. - Certora’s later write-up sharpened the design lesson: every yield-bearing receipt token should satisfy the property that plain transfers cannot increase the combined economic value of the sender and receiver, and the concrete fix was to override
transfer()andtransferFrom()rather than inheriting raw ERC-20 behavior. - REKT’s practical exploit loop is worth preserving because it is so compact:
deposit -> transfer -> collectFees -> transfer -> collectFees -> transfer back -> withdraw. - REKT’s USDT/WETH walkthrough also makes the value flow vivid: after depositing about
30M USDTand5.467k WETHfor roughly10.51PLP, helperBreportedly harvested about2.15M USDT + 392 WETHand helperCabout2.15M USDT + 402 WETHbefore the principal liquidity was withdrawn and the loop moved on to the next pool.
- Representative transactions and addresses:
- Main exploit transaction:
0xcd7dae143a4c0223349c16237ce4cd7696b1638d116a72755231ede872ab70fc - Attacker address highlighted by REKT:
0xf9E3D08196F76f5078882d98941b71C0884BEa52 - Publicly highlighted attack functions and accounting surfaces:
collectFees(),updateVault,_fee0Earned,_fee1Earned,token0PerSharePaid,token1PerSharePaid, and PLPtransfer()/transferFrom()behavior - Publicly cited example pool flow: the Sorbetto Fragola USDT/WETH pool, where a large PLP position was moved across helper addresses and claimed twice before principal withdrawal
- Main exploit transaction:
- Response / mitigation:
- Popsicle locked deposits to affected pools and limited withdrawals to pools believed not to be impacted.
- In the same-day postmortem, the team said roughly
85%of Sorbetto Fragola pool funds had been drained and identified the then-withdrawable unaffected set asAXS/ETH,YGG/USDC,LINK/ETH, and theEURtpools. - The team also offered the attacker a
$1Mwhitehat-style return bounty. - The long-term fix is straightforward in retrospect: share transfers must settle or move reward checkpoints, not merely move balances.
- Certora later used the incident as a case study for a generic invariant: token transfers should never increase the total assets of the participating parties.
Pattern tags
- transfer-time-reward-checkpoint-omission-fee-debt-inheritance-windfall
- reward-entitlement-identity-and-batch-canonicalization
- entrypoint-equivalence-under-shared-standards
- The preserved root lesson is not only
reward claim could be repeated, butordinary ERC-20 balance movement reopened the same historical fee window because PLP transfer semantics and fee-entitlement semantics were never one coherent object.
Why the exploit worked
- Popsicle treated
who holds PLP right nowandwho has already consumed the fee history attached to that PLPas separable stories. Deposits and withdrawals updated user-side checkpoints, but plain PLP transfers did not. - That meant the attacker never had to forge liquidity, fake fees, or break Uniswap V3 math. They only had to move one real PLP balance across fresh helper addresses whose
token0PerSharePaid/token1PerSharePaidstate still lagged the balance they had just inherited. collectFees()then converted that stale checkpoint gap into real token payouts, and those payouts stayed behind in the helper even after the PLP moved onward. One economic fee window therefore became several sequentially collectible fee windows.- The flash loans only amplified a transfer-accounting bug that was already balance-sheet fatal. The real control failure was letting inherited ERC-20 mobility outrank the protocol’s own rule for who had already been paid.
Detection and audit pivots
- Replay the exact economic loop
deposit -> transfer to fresh address -> collectFees -> transfer again -> collectFees -> transfer back -> withdrawand assert that aggregateclaimable + withdrawablevalue across all helpers never exceeds what one honest holder could realize. - Diff sender-plus-receiver total assets before and after a pure PLP transfer. If the combined economic value rises without new swap fees entering the vault, the transfer path is already a mint surface.
- Trace every balance-movement path, not only the happy-path
transfer()call:transferFrom(), permit-funded pulls, migration helpers, liquidation flows, rescue paths, or any wrapper that can move reward-bearing shares must settle the same checkpoints. - Check whether a receiver of old shares is initialized to the live accumulator state for those incoming shares or instead inherits zero / stale fee-debt state that can be cashed out immediately.
- Treat reward-bearing ERC-20 compatibility claims skeptically. If deposit and withdraw have custom accounting but inherited token verbs still move the same economic entitlement, reviewers should assume entrypoint drift until transfer invariants are proven explicitly.
Failure-prevention checklist
- Settle or remap fee checkpoints on every share-movement path, not only on deposit and withdrawal. If PLP is transferable, then
transfer()andtransferFrom()must either first realize accrued fees for sender and receiver or move the historical-fee baseline with the shares so the incoming balance starts from the live accumulator state. - Treat
share balanceandalready-paid fee historyas one accounting object. If the protocol cannot keep them fused under ordinary ERC-20 movement, make the receipt token non-transferable or wrap transfers in protocol-owned migration logic rather than inheriting generic token verbs blindly. - Prove the Certora-style balance-sheet invariant directly: a pure transfer of reward-bearing shares must not increase the combined assets of sender and receiver when no new fees entered the vault.
- Reuse one checkpoint-update primitive across mint, burn, transfer,
transferFrom(), permit-assisted pulls, migration helpers, liquidations, rescue paths, and any wrapper that can move yield-bearing shares. Transfer-time omissions are usually entrypoint-drift bugs, not math bugs. - Initialize receivers of transferred shares to the current accumulator for the incoming balance unless the protocol explicitly and atomically transfers the sender’s unclaimed fee state along with the balance.
- Keep claimed fees, pending fees, and withdrawable principal economically coherent under multi-address hop tests such as
deposit -> transfer -> claim -> transfer -> claim -> transfer back -> withdraw; the total outcome should match one honest holder’s outcome, not exceed it. - Stress test with flash-loan-sized positions, because reward-checkpoint bugs that look tiny at ordinary size become treasury-scale once one transaction can transiently own most of the vault.
- Add monitoring for repeated high-value fee claims following recent share transfers; in this family,
fresh holder claims old history immediately after receiving sharesis the onchain smell.
Audit prompts
- When reward-bearing PLP moves from
AtoB, what exact state transition ensuresBcannot immediately claim fee history that economically accrued beforeBowned the shares? - Do
transfer()andtransferFrom()override inherited ERC-20 behavior to synchronize fee checkpoints, or is the protocol assuming token mobility is harmless because the custom accounting lives elsewhere? - If two collaborators alternate custody of the same shares, does the sum of
claimable + already claimed + withdrawable principalacross both addresses stay invariant absent new vault fees? - Are sender and receiver both checkpointed against the same live accumulator on transfer, or can one side keep old rewards while the other side reopens the same historical fee window?
- Does any helper path — migrations, zaps, vault wrappers, liquidations, rescue functions, permit-funded pulls — move PLP without invoking the same checkpoint-settlement logic as the ordinary transfer path?
- Can a receiver with stale or zero
token0PerSharePaid/token1PerSharePaidstate collect against a balance that was minted under a later accumulator value? - If the protocol claims to support transferable yield-bearing shares, where is the explicit proof that plain balance movement cannot mint extra economic value for the participating parties?
- Would a flash loan merely scale the theft, or is there some size threshold above which transfer-time checkpoint drift first becomes economically meaningful? If scale is the only variable, the bug is already fatal.
Lessons that should already be standard
- A yield-bearing ERC-20 is not an ordinary ERC-20 with a side spreadsheet. Once fees, rewards, or rebasing rights ride on the balance, transfer semantics become part of the money-good accounting boundary.
- Historical accrual must either travel with the shares or be settled away before the shares move. Letting balances move while the fee baseline stays behind is just a quiet duplicate-claim rail.
Depositandwithdrawcorrectness is not enough. Many of the most expensive accounting bugs live in inherited standard entrypoints that teams treated as plumbing rather than as custody logic.- The right transfer invariant is economic, not cosmetic: sender plus receiver should not become richer because a token moved.
- Collaborator-hop attacks are often enough. If moving shares across fresh addresses reopens old fee windows, the protocol does not need reentrancy or oracle manipulation to lose treasury-scale value.
- Flash loans are rarely the real bug in this family. They only let the attacker temporarily own enough of the receipt-token supply for a transfer-accounting mistake to matter immediately.
- Reward-bearing share systems should be reviewed as if
balance,historical entitlement, andclaim-consumed statewere one security object, because economically they are.
Why this incident matters
- Popsicle preserves a very clean
transferable receipt token + stale reward checkpoint = repeated historical accrualexploit family. - It is especially useful because the core bug is small and easy to miss: the dangerous path was not deposit, withdraw, or claim in isolation, but the missing accounting logic inside ordinary token transfer behavior.
- The incident is also a reminder that a yield-bearing ERC-20 is not an ordinary ERC-20. Once fee entitlement rides on the token balance, inherited transfer behavior becomes part of the protocol’s solvency boundary.
Internal linkages
- Core recurring pattern: transfer-time-reward-checkpoint-omission-fee-debt-inheritance-windfall.
- Adjacent accumulator family: reward-debt-desynchronization-denominator-collapse-reward-amplification.
- Adjacent claim family: duplicate-claim-idempotency-failure.
- Useful contrast: cover-protocol-blacksmith-stale-pool-cache-rewardwriteoff-desynchronization-infinite-cover-mint-2020-12-28 — both incidents break the tie between a global reward accumulator and user-side cancellation state, but Cover needed a dust-denominator explosion while Popsicle paid out duplicated rewards through transfer-time checkpoint omission alone.
Control surface
-
The leverage sat in Sorbetto Fragola’s reward-distribution state machine: how PLP balances moved, how user fee checkpoints were stored, whether
collectFees()trusted stale per-address history after transfer, and whether claimed rewards remained detached from later PLP ownership. -
The exploit was not
flash loans are dangerousin the abstract. The flash loans only scaled a bookkeeping bug that let token mobility itself mint extra claimable value. -
Sources:
- https://popsiclefinance.medium.com/popsicle-finance-post-mortem-after-fragola-hack-f45b302362e0
- https://blocksecteam.medium.com/the-analysis-of-the-popsicle-finance-security-incident-9d9d5a3045c1
- https://www.certora.com/blog/popsicle-finance-bug-revisited—preventing-the-20m
- https://rekt.news/popsicle-rekt/
-
Last reviewed: 2026-08-04 UTC