The number that matters is not $21,900. It is 0.000055%.
SlowMist flagged the Yapeswap exploit this week. Losses landed near $21,900. The dollar figure is noise. The parameter is the signal. A governance proposal carrying roughly five hundred-thousandths of one percent of the voting supply was sufficient to relocate an administrative permission on YapeFactory — the factory contract that mints and indexes every trading pair on the protocol.
Contrary to popular belief, most protocol losses do not originate in exotic cryptographic breaks. They originate in unset boundary conditions. A require that was never written. A constant that shipped with a permissive default and never got revisited after the fork. Yapeswap is the second kind. No private key was compromised. No signature was forged. The contract executed exactly as specified — and the specification was empty.
To understand the failure you have to understand what a quorum actually is at the EVM level. It is not a security feature. It is a boolean gate. In a Compound-lineage governance module — and WorkersUnion is a thin fork of that lineage — quorumVotes is compared against forVotes before a proposal can be queued. Set quorumVotes to zero and the comparison is trivially satisfied. Any proposal, at any turnout, at any time, passes.
WorkersUnion shipped with that zero. That single storage slot is the entire attack surface.
The permission chain matters here. YapeFactory holds a feeToSetter role — the administrative key that points the protocol at its fee recipient and, transitively, at the registry that resolves which vault a pair should route deposits into. Whoever holds feeToSetter holds the routing table. Not the funds directly. The routing table. That distinction is what made the exploit quiet.
YapePair is the AMM primitive. YapeWrapper is the farm-facing adapter that reads vault state to decide where a user's deposit should land. YapeFactory sits above both, holding the pointer. Three contracts, one administrative slot, zero participation threshold.
Here is the sequence, parsed from the transaction trail.
Step one. The attacker submits a WorkersUnion proposal whose calldata is a single call: YapeFactory.setFeeToSetter(attacker). Voting power behind the proposal: approximately 0.000055% of supply. Not a whale. Not a flash-loan coalition. A rounding error with a wallet address.
Step two. Quorum is zero, so the proposal clears. No timelock worth the name. No veto window with teeth. The role transfers.
Step three. The attacker deploys a malicious Registry contract and repoints the factory's registry reference at it. This is where the design becomes interesting, because the fake registry is not a crude drainer. It is a precision instrument built around an inconsistency that already existed in the interface.
Step four. The malicious registry returns a forged address from latestVault(). Meanwhile vaults() — the enumerable view function — still returns the genuine vault. Two functions, two answers, one registry. The protocol's own read surface disagreed with itself, and nothing in the codebase enforced an invariant between them.
Step five. YapeWrapper trusts latestVault() for routing. Deposits intended for YapePair were routed and counted as farm assets — the accounting layer registered the position — but no redeemable shares were minted against it. Users saw a balance. The balance was a bookkeeping entry, not a claim.
The $21,900 is itself diagnostic. It is not a ceiling set by the attacker's ambition. It is the total value that happened to flow through the wrapper's routing path during the window. Higher TVL would have produced a larger number from the identical transaction. The exploit scales with whatever liquidity shows up.
I have modeled this class of attack before. When I decomposed the Lido stETH oracle proposal in late 2022, the entire exercise was arithmetic: what does it cost to move the reported rate, and what does the movement extract? The model only has meaning if the numerator is finite and the denominator is nonzero.
Yapeswap inverts that. The cost of governance capture was the gas to submit a proposal plus the spot price of a trivial token balance. Call it tens of dollars. The extracted value was roughly $21,900. Any economic security model with a zero denominator does not produce a risk score. It produces a division error.
And note what the attacker did not need. No reentrancy. No price manipulation. No flash loan. No mempool position. The exploit is deterministic, single-block, and reproducible by anyone with a funded wallet — precisely the profile that automated exploit bots scan for. Once the proposal passed, the remainder of the chain was a lookup table.
The contrarian read is that this was not a code vulnerability at all — which is exactly why it survived audit.
Auditors review logic. They trace value flow through functions. They check access control on privileged entry points. Very few audit scopes include a line-item inventory of governance constants, because constants are configuration, not code, and configuration is presumed to have been chosen deliberately. It was not. It was inherited.
The standard is a ceiling, not a foundation. A fork that copies a governance module inherits its permissive defaults along with its bytecode, and the defaults are the part nobody diffs.
There is a second blind spot, quieter. latestVault() and vaults() describing different states is a detectable condition. It is a two-call invariant that any indexer could evaluate every block for a few dollars of RPC. Almost nobody runs that check, because the two functions were never formally specified as required to agree. Code does not lie, but it often omits context — and the omitted context here is the implicit contract between a getter and its enumerator.
Parsing the chaos to find the deterministic core: the Yapeswap loss is $21,900, but the exploitable pattern is a storage slot set to zero, replicated across every WorkersUnion fork that shipped without revisiting the parameter.
The forward-looking question is not who audits Yapeswap next. It is how many deployed factories currently return quorumVotes() == 0 while holding a feeToSetter-equivalent role. That is a one-line eth_call. The answer is probably not one.