# Yapeswap: zero-quorum governance capture drains pair reserves

Yapeswap's zero-quorum DAO let an attacker buy YAPE, pass a proposal moving feeToSetter, install a malicious Yearn registry, and drain six pairs' reserves (~$21.9K).

- Payout: Real-world hack
- Severity: critical
- Program: Yapeswap
- Analysis by: DeFiHackLabs
- Disclosed: 2026-10-25
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-10/Yapeswap_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20261025-yapeswap
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained technique:source-review vuln:access-control vuln:business-logic vuln:governance

**Target:** YapeFactory / WorkersUnion DAO / YapePair Yearn registry
**Root cause:** DAO voting has zero quorum (minimumVotes and minimumVotesForProposing both 0) so a dust veVISION lock passes any proposal; feeToSetter (held by timelock) can re-point the factory's Yearn registry/operator, and YapePair credits farmed tokens back to farming[token] so keepBalance holds even after real tokens leave.
**Steps:**
1. Buy dust YAPE from the live YAPE/WETH pair and lock it via `createLockUntil` for sqrt-weighted veVISION voting power.
2. Submit proposal `setFeeToSetter(address(this))` via `proposeTx` on WorkersUnion.
3. `manualVote` with the lock's rightId, then `schedule` and `execute` through the timelock so factory.feeToSetter becomes the attacker contract.
4. Deploy MaliciousRegistry + per-token MaliciousVault whose `deposit` pulls approved tokens to the beneficiary.
5. Call `setRegistry`/`setOperator` on the factory to install the malicious registry.
6. Force a 100% rebalance (`setFarmingRatio(0,0)` + `rebalance`) so each pair ships reserves to the attacker vault.
**How found:** Noticed the DAO's minimumVotes/minimumVotesForProposing were both 0 and that feeToSetter could re-point the Yearn registry/operator, while YapePair's keepBalance invariant credits farmed tokens back so drained reserves still look accounted for.
**Exploitation:** Attacker acquired YAPE from the on-chain pool, locked it for voting power, passed a governance proposal through the real DAO and timelock to seize feeToSetter, installed a malicious Yearn registry whose latestVault returns attacker vaults, then forced rebalances so each pair's `_toYearn` deposit pulled reserves to the attacker. On-chain also withdrew Yearn-farmed balances first via setFarmingRatio(0,0)+rebalance.
**Escalation:** Chained governance capture (zero quorum) with registry re-pointing to convert a single dust vote into full reserve theft across six pairs.
**Insight:** When a pair's accounting credits tokens to a farming mapping on deposit, the invariant survives even if the vault is malicious — so any privileged setter that controls the vault/registry address is effectively a drain primitive; check governance quorum thresholds for zero values that let dust locks pass proposals.

## Proof of concept (copied exactly from the original report)

**Governance proposal payload to seize feeToSetter**

```
proposalData = abi.encodeWithSignature("setFeeToSetter(address)", address(this));
```

**Malicious vault deposit pulling approved tokens**

```
function deposit(uint256 amount) external returns (uint256) {
        // tolerate non-standard tokens (USDT) that return no bool
        (bool ok, bytes memory ret) =
            token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, msg.sender, beneficiary, amount));
        require(ok && (ret.length == 0 || abi.decode(ret, (bool))), "transferFrom failed");
        return amount;
    }
```

**Swap WETH for YAPE to acquire voting power**

```
uint256 wethIn = IERC20(WETH).balanceOf(address(this));
        (uint112 r0, uint112 r1,) = YAPE_WETH.getReserves();
        uint256 amountInWithFee = wethIn * 997;
        uint256 yapeOut = (amountInWithFee * r0) / (uint256(r1) * 1000 + amountInWithFee);
        IERC20(WETH).transfer(address(YAPE_WETH), wethIn);
        YAPE_WETH.swap(yapeOut, 0, address(this), "");
```

