October 20, 2026
|
Exploit Postmortem
Cover image for "Set Protocol's $50K Loss and How Olympix Would Have Prevented It"

Set Protocol's $50K Loss and How Olympix Would Have Prevented It

Date: October 6, 2026

Chain: Ethereum

Loss: about 18.35 WETH across two legacy Set products, roughly $50K

Class: a public fee function that rounded a share price up even when no fee was charged

At 07:36:47 UTC on October 6, 2026, a fresh address used Uniswap v4's flash accounting to front about 25,045 WETH, bought into a legacy Set Protocol product called ETH 26 EMA Crossover Yield II, called one public function, and cashed straight back out with 5.08 WETH more than it had put in. The function charged no fee, yet it nudged the number that decides how much collateral each token is worth from 4927 to 4928, and the redemption paid out at the higher number.

Seventeen minutes later the same move started on a sibling product, ETH 20 MA Crossover Yield II, which runs on identical bytecode. Over the next two hours, three senders ran it seven times and took another 13.27 WETH, the last two runs landing two blocks apart. Nobody broke a key or bent an oracle. A fee function that anyone could call recalculated each token's share of the collateral every time it ran, and it rounded that share up even when no fee was charged.

Two Trend-Following Sets Running the Same Code

Set Labs launched the ETH 20 Day MA Crossover Yield Set, ticker ETHMACOAPY, in December 2019. The strategy held ETH while its price sat above the 20-day moving average and switched into USDC lent on Compound when it dropped below, so the cash side kept earning. Rebalances were spaced at least 48 hours apart and needed the signal to hold for two hours before triggering. ETH 26 EMA Crossover Yield II, ticker ETHEMAAPY, is a sibling product from the same family of Sets.

Both contracts hit on October 6 are RebalancingSetTokenV3 contracts, and the DeFiHackLabs researcher who reproduced the second attack found the two share the same bytecode hash. Their collateral sits in Set's shared Vault as a base Set token called BaseAssetCollateral, and holders issue or redeem through Set Core. Each token's claim on that collateral is tracked by a single number, unitShares, and every issue and redeem is priced off it.

A Public Function With a One-Way Round

RebalancingSetTokenV3 has a function called actualizeFee(), and Set's own code comments say anyone can call it. Its only requirement is that the Set isn't mid-rebalance. It asks a fee calculator what's owed, mints that fee to the fee recipient only if the amount is above zero, and then recalculates unitShares by dividing the collateral in the Vault by the token supply.

That last step runs every time, fee or no fee, and it rounds the result up. Issuing tokens sets unitShares with rounding down, so a fresh issue followed by actualizeFee() can tick the number up by one even when nothing was charged. With nothing charged, the recalculation should leave every holder exactly where they were. Rounding up nudges each token's claim slightly higher than the collateral really supports, and whoever redeems right after collects that nudge out of everyone else's share. A shared pot that rounds every slice up will eventually hand out more than it holds, and the person who takes the biggest slice right after the rounding gets most of the difference.

For contracts that track a share price, the rule is that every rounding step should favor the pool over the caller, and a recalculation that charges nothing should change nothing. actualizeFee() broke both halves of that rule in one line.

Borrow, Issue, Round, Redeem

The first attack, reproduced in DeFiHackLabs PR #1294, ran entirely inside a Uniswap v4 unlock callback. The attack contract took ETH from the PoolManager, issued ETHEMAAPY at a unitShares of 4927, called actualizeFee() to move it to 4928, redeemed the same quantity, and settled the borrowed ETH before the callback closed. The Vault received 25,044.67 WETH and paid back 25,049.76 WETH, and the 5.08 WETH difference matches SlowMist's alert.

The ETHMACOAPY runs followed the same loop with bigger borrowed sums from Morpho, Balancer, Aave and Uniswap v4, the largest about 116,000 WETH from an Aave v3 fork pool. Holding most of the supply is what turned a rounding remainder into real money, since the extra collateral per token gets paid out on every token redeemed. Measured by the WETH the Vault paid out above what came in, the seven runs took 1.18, 0.05, 0.18, 3.49, 0.25, 4.05 and 4.06 WETH.

The sixth run added a twist that DeFiHackLabs PR #1296 reconstructs. Before calling actualizeFee(), the attacker donated about 1.49 units of the base Set into ETHMACOAPY's backing without minting any new tokens, which raised the collateral side of the division while the supply stayed put, so the round-up moved unitShares from 12253 to 12264 instead of by one.

The first sender, 0xfc3fAcD67138966aB0c841E905B0C4BCA1AbE92F, ran five of the seven ETHMACOAPY transactions, starting with the 07:54 trial and finishing at 09:43:23 UTC with about 4.06 WETH. A second sender, 0xcccc640018f8c2b00fa45F456017AD2378Eb3447, took about 3.49 WETH at 09:22, and a third, 0x3A15Cf422307e922645d671Cb247e76e8b1eC50d, took about 4.05 WETH at 09:42:59, two blocks before the first sender's final run. ExVul's report of 8.11 WETH lost covers those last two transactions. The ETHEMAAPY token's transfer history on Blockscout shows that single issue and redeem as its only movements since a redemption on December 31, 2025, so 5.08 WETH is that Set's full loss.

How Olympix Catches a Round-Up That Pays the Caller

Olympix reads a contract the way an attacker does, as states, transitions and entry points, and works out from the code and its context what the system has to keep true. In a rebalancing Set, unitShares and the collateral behind it are the state every holder's money depends on, and issue, redeem and actualizeFee() are the three doors that touch them. The engine maps every path through those doors and uses signals, patterns in the code that mark a path as dangerous, to decide which to attack. Olympix's static analysis surfaces the signal here: the number that decides each token's share of the collateral, recalculated with rounding toward the caller inside a function anyone can call, with nothing tying the recalculation to a fee actually being minted.

BugPoCer then tests that signal against the Set's accounting as a whole. Counterexample-Guided Invariant Refutation infers the rule a share-accounting contract can't live without, which here is that issuing a quantity of tokens and redeeming the same quantity should never hand back more collateral than went in, and then runs call sequences through the three functions until one hands back more. Issue, actualizeFee(), redeem does it in three calls, and adding a donation to the backing before the fee call makes the gap larger.

The output is a counterexample: the three-call sequence with concrete amounts, the minimal conditions that trigger it, including how much of the supply the caller has to hold for the round-up to pay, and a generated Forge test that replays it. That test is the October 6 attack on both Sets, produced in development instead of on mainnet.

Olympix has flagged this rounding class before in other codebases. Because the engine runs automatically in development, at every merge and before release, a team keeps a standing view of every path that can move its share price, including maintenance functions that look harmless.

One Bytecode, Two Drains

actualizeFee() was written to collect fees, and on October 6 it collected nothing while still moving the number every redemption depends on, first in one Set and then, seventeen minutes later, in its twin. Any public function that touches an exchange rate deserves the same scrutiny as deposit and withdraw, and when the same code is deployed more than once, one finding covers every copy. To see how Olympix tests the paths that move your share price and returns a runnable counterexample for each one that leaks, book a technical walkthrough.

On-Chain References

Protocol: Set Protocol V1, ETH 26 EMA Crossover Yield II (ETHEMAAPY) and ETH 20 MA Crossover Yield II (ETHMACOAPY)

Chain: Ethereum

Date: October 6, 2026, 07:36:47 to 09:43:23 UTC

Loss: about 18.35 WETH: 5.08 WETH from ETHEMAAPY and 13.27 WETH from ETHMACOAPY

Victim Set 1 (ETHEMAAPY, RebalancingSetTokenV3): 0x54e8...a6bE

Victim Set 2 (ETHMACOAPY, RebalancingSetTokenV3): 0xB647...9525

Set Vault: 0x5B67...86Dc

Set Core: 0xf551...20C8

PerformanceFeeCalculator: 0x20B0...aD8A

ETHEMAAPY attacker EOA: 0x5064...c5B6

ETHEMAAPY attack contract: 0x016F...2F6E

ETHEMAAPY exploit tx (block 26131887, 07:36:47 UTC, 5.08 WETH): 0xaca5...ba65

ETHMACOAPY sender 1: 0xfc3f...E92F

ETHMACOAPY sender 2: 0xcccc...3447

ETHMACOAPY sender 3: 0x3A15...C50d

ETHMACOAPY tx 1 (block 26131974, 07:54:11 UTC, 1.18 WETH): 0x6001...308f

ETHMACOAPY tx 2 (block 26132321, 09:04:11 UTC, 0.05 WETH): 0x979b...00de

ETHMACOAPY tx 3 (block 26132355, 09:10:59 UTC, 0.18 WETH): 0x8dad...745c

ETHMACOAPY tx 4 (block 26132411, 09:22:11 UTC, 3.49 WETH): 0x5811...28f2

ETHMACOAPY tx 5 (block 26132471, 09:34:23 UTC, 0.25 WETH): 0xd7ba...1b3d

ETHMACOAPY tx 6 (block 26132514, 09:42:59 UTC, 4.05 WETH): 0xb54c...8fc2

ETHMACOAPY tx 7 (block 26132516, 09:43:23 UTC, 4.06 WETH): 0x1dc6...e08a

Sources

SlowMist: Set Protocol alert, October 6, 2026

DeFiHackLabs PR #1294: Set Protocol actualizeFee() rounding bump via Uniswap v4 flash accounting

DeFiHackLabs PR #1296: Set Protocol ETHMACOAPY vault drained via actualizeFee() rounding bump

KuCoin News: Old Set Protocol Product ETHMACOAPY Loses 8.11 WETH in Attack (citing ExVul)

Set Protocol: RebalancingSetTokenV3 source

Set Labs: Introducing the ETH 20 Day MA Crossover Yield Set

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.