On 25 October 2026 at 02:19:23 UTC, an attacker drained the Yearn-farmed reserves of Yapeswap, a small Uniswap V2-style DEX on Ethereum. SlowMist put the loss at about $21.9K. That is not a headline number, but DefiLlama data cited on X suggested Yapeswap held only around $25K in total value, so most of the protocol was gone in one transaction.
The mechanics are worth studying because nothing about them depends on size. A governance vote with almost no support transferred the factory’s top admin role. From there, the attacker never had to touch a user’s tokens directly. The pairs moved their own reserves into attacker contracts, because the factory told them to.
setFeeToSetter on the YapeFactory.updateRegistry() that copied the factory’s registry address without validation, and rebalance() deposited reserves into whatever vault that registry returned.Yapeswap pairs work like Uniswap V2 pairs with one addition: idle reserves are “farmed” in Yearn. Each pair asks a registry for the latest Yearn vault for a token, and rebalance() moves reserves between the pair and that vault up to a configured farming ratio. Yield and fees are split, with a fee leg paid to the factory’s feeTo address.
Control sits in the YapeFactory. As in Uniswap V2, the factory has a feeToSetter admin. In Yapeswap that admin can also set the factory’s operator, registry and feeTo. Ownership of the factory was meant to belong to the Yapeswap DAO, which votes with VISION tokens locked in a voting-escrow contract and executes through a governance timelock.
In other words, the security of every pair’s reserves reduced to one question: who can win a DAO vote?
SlowMist’s alert states that WorkersUnion, Yapeswap’s governance module, had a quorum of zero. A proposal therefore did not need a meaningful share of voting power, only more “for” than “against” votes. With no one else voting, a dust-sized lock was enough. SlowMist estimates the attacker’s proposal carried about 0.000055% of voting power.
Oculr’s trace reconstruction lays out the timeline. We did not re-derive the earlier setup transactions ourselves, so treat these dates as Oculr’s:
createLockUntil, delegated to itself, and called proposeTx targeting the YapeFactory.schedule on the timelock.execute ran the payload a2e74af6(attacker EOA).We confirmed that 0xa2e74af6 is the selector for setFeeToSetter(address). A read of the factory after the attack returns the attacker EOA as feeToSetter, the attack contract as operator and feeTo, and the attacker-deployed contract 0xc620...b5a7 as registry.
A timelock existed, and it ran for days. It did not help, because nobody was watching the queue and the vote itself could not fail.
Owning the factory gave the attacker the right to change settings. Turning settings into stolen funds needed one more weakness, which Oculr locates in YapePair.sol:
updateRegistry() is public and simply copies factory.registry() into the pair’s storage.setFarmingRatio() is restricted to the factory operator, which the attacker now controlled.rebalance() withdraws from the real Yearn vault and deposits into registry.latestVault(token).The fake registry was built to look normal. SlowMist notes that vaults() still listed the genuine Yearn vaults, while latestVault() returned attacker vaults. Any off-chain check that only listed vaults would have seen nothing wrong.
The sketch below is our own simplified illustration of the pattern, not verbatim Yapeswap code:
// Simplified illustration of the vulnerable pattern (not verbatim Yapeswap code)
contract YapePairSketch {
IYapeFactory public factory;
IRegistry internal _registry;
uint256 public maxFarmingRatio; // e.g. 8000 = 80%
// Anyone can sync the registry from the factory, with no validation
function updateRegistry() public {
_registry = IRegistry(factory.registry());
}
function setFarmingRatio(uint256 minR, uint256 maxR) external {
require(msg.sender == factory.operator(), "not operator");
maxFarmingRatio = maxR; // attacker raised this to 100%
}
function rebalance(address token) external {
// 1. Pull the pair's assets out of the genuine Yearn vault
_withdrawFromCurrentVault(token);
// 2. Deposit into whatever the registry calls "latest"
IVault target = IVault(_registry.latestVault(token));
IERC20(token).approve(address(target), type(uint256).max);
target.deposit(_farmAmount(token), address(this));
// 3. Pay the fee leg to factory.feeTo()
_payFee(token, factory.feeTo());
}
}
Each piece looks reasonable alone. Syncing configuration from a factory is common. Letting an operator tune a ratio is common. Depositing into the “latest” vault is how Yearn integrations are usually written. Together, they mean whoever controls the factory controls every pair’s reserves.
We confirmed the main transaction, its sender, block 26,266,653 and timestamp against a public Ethereum node. The step details below come from SlowMist and Oculr.
The timelock executed the attacker’s proposal, making the attacker EOA the factory’s feeToSetter.
In block 26,266,649, the attacker deployed the attack contract 0x327b...be01, which deployed a fake registry and four fake vaults for DAI, USDC, WETH and WBTC. The EOA then set the factory’s operator, registry and feeTo to its own contracts.
Four blocks later, the attack contract iterated through allPairs. For each pair it called the public updateRegistry(), which, according to Oculr’s storage diff, switched the pair’s registry slot from the real Yearn registry to the attacker’s. It then called setFarmingRatio as operator, raising the maximum from 8,000 to 10,000 basis points.
Each rebalance() redeemed the pair’s real Yearn positions (unwinding through Yearn v2 and v3 strategies, Sky and Morpho along the way) and deposited the freed DAI, USDC, WETH and WBTC into the fake vaults. Fee legs went to feeTo, also the attacker. The fake vaults minted no redeemable shares, so the pairs’ accounting still counted those assets as farmed while they were in fact gone.
Oculr counts 1,613 DAI, 1,607 USDC, 7,705 USDT, 4.205 WETH and 0.0141 WBTC reaching the attack contract in this transaction. A follow-up transaction repeated the loop for the remaining pairs and a third swept the vaults. The proceeds ended as 10.52 ETH in the attacker EOA, which still held them when we checked.
Earlier we wrote about the X100 Senate takeover, where a flash loan rented a governance majority for one transaction. Yapeswap is the slow, cheap version of the same lesson. The attacker did not need to rent anything. It needed governance that would accept a proposal no one supported, and a protocol where governance could rewire custody of funds.
Small and older protocols are the most exposed. Token holders drift away, votes go quiet, and parameters that were “fine at launch” stay untouched. A zero quorum might have been convenient when the team was the only voter. Years later it is an open admin key.
Quorum should be a meaningful share of circulating voting power, and it should be revisited as participation changes. Pair it with a minimum proposal threshold so that dust locks cannot even submit a proposal.
Any proposal that changes an owner, admin or feeToSetter should face a higher quorum, a longer delay, or a guardian veto. Changing who holds the keys is different from tuning a fee.
A timelock only helps if someone reads it. Alert on every queued call that targets core contracts, especially role setters, and make sure the alert reaches people who can act.
Pairs should not blindly accept a new registry. Restrict updateRegistry() to a trusted role, and check that returned vaults are on an allowlist or match the expected underlying asset and a known vault factory. Better still, pin the vault per token and require a delayed migration to change it.
After rebalance(), verify that the pair actually received vault shares worth the deposited assets. A deposit that returns zero shares should revert.
For a broader pre-launch list, see our smart contract security checklist and our overview of top smart contract security risks.
0x4c741568...179e1e0x6e523866424220b3cb2cab88ba2195cfa5636b120x327bf6dfc38a79393f78866c705193c39b04be010xc620ca5b2a63cdb792d462898010d2be4421b5a70x46adc1c052fafd590f56c42e379d7d16622835a20x23c1653e9e13eefe2e1b5954bfef00a2db0483b6YapePair functions and per-token amountsYapeswap lost a small amount, but the pattern is the one that empties large protocols too: governance that cannot fail, controlling configuration that funds trust without checking. A quorum, a watched timelock or a registry allowlist would each have broken this chain.
Running a DAO-governed protocol, or integrating external vaults? Contact Panda Security for a governance and access-control review, or see our audit pricing.
Flash-loaned X tokens bought two of three X100 Senate seats, and one transaction later the Freezer treasury sold 400,000 X for about 36 USDT on Ethereum.
A missing sign check on max_fee in WAX's eth.token withdraw action let an attacker bridge out ~$90K in USDC, USDT, DAI, WETH and WBTC for free.
A stale sell counter in NemTokenV2 let an attacker burn the PancakeSwap pair's own NEM 42 times and sell into a pumped price for $822K on BNB Chain.
A wrapped recovery deadline and a replayable signed payload cost an Argent-style smart wallet system about $59.8K on Ethereum. How it worked and how to fix it.