BNB Chain
Reserve manipulation

YSDAO's $19.5K Loss and How Olympix Would Have Prevented It

Loss
Loss
Exploit date
May 29, 2026

Date: May 29, 2026

Chain: BNB Chain

Loss: 19,490.91 USDT

Class: liquidity detection that compared a pair's live balance against its reserves, letting trades skip the token's taxes and limits

The transaction that drained YSDAO's PancakeSwap pool on May 29, 2026 turned on two tiny amounts. The first was 1 wei of USDT, the smallest unit the token has, which the attacker asked the pool to pay out alongside 611,320 YSDAO. The second was a single USDT, sent straight into the pool a few steps later. Each one changed how YSDAO's own code labeled the trade that followed, and between them the attacker bought far more YSDAO than the token allowed, sold it back without the 25% tax meant for that sale, and left with 19,490.91 USDT.

The trade ran at 09:12:43 UTC in BNB Chain block 101088361. TenArmor's alert flagged it on June 1 at about $19.5K, and the DeFiHackLabs reproduction traces the full sequence.

A Token That Taxed Its Own Pool

YSDAO is a BEP-20 token paired against USDT on PancakeSwap V2, and like many tokens on BNB Chain, it polices its own market. The verified YSDAO source on BSCTrace shows the rules its transfer function applies whenever tokens move in or out of the YSDAO/USDT pair. A buy can't take more than a tenth of the pool's YSDAO reserve, pays a 3% fee, and records how much USDT the buyer spent. A sell is also capped at a tenth of the reserve, has to wait out a one-minute cooldown after the seller's last buy, pays the 3% fee, and then pays an extra profit tax. If the seller has no recorded buy cost, that extra tax is a quarter of the amount sold.

Liquidity providers get gentler treatment. Removing liquidity moves YSDAO out of the pair with no fee at all, and adding liquidity pays only the 3% fee, with no cap and no cooldown. That's a reasonable design, as long as the token can reliably tell a liquidity change apart from a trade.

How YSDAO Decided What Kind of Transfer It Was

The token made that call in two small helpers inherited from its BaseUSDTWA contract. Both compare the USDT the pair actually holds right now with the USDT reserve the pair last recorded.

    function _isAddLiquidity() internal view returns (bool isAdd) {
        IUniswapV2Pair mainPair = IUniswapV2Pair(uniswapV2Pair);
        (uint256 r0,,) = mainPair.getReserves();
        uint256 bal = IUniswapV2Pair(USDT).balanceOf(address(mainPair));
        isAdd = bal >= (r0 + 1 ether);
    }

    function _isRemoveLiquidity() internal view returns (bool isRemove) {
        IUniswapV2Pair mainPair = IUniswapV2Pair(uniswapV2Pair);
        (uint256 r0,,) = mainPair.getReserves();
        uint256 bal = IUniswapV2Pair(USDT).balanceOf(address(mainPair));
        isRemove = r0 > bal;
    }

If the pair is holding at least 1 USDT more than its reserve when YSDAO arrives, the token assumes someone is adding liquidity. If the pair is holding less USDT than its reserve when YSDAO leaves, it assumes liquidity is being removed. The trouble is that a pair's live balance is a number any caller can move. Anyone can send USDT to the pair, and a swap can ask the pair to pay USDT out before YSDAO moves. Neither helper checks who's calling, whether LP tokens were minted or burned, or whether the router is involved at all.

The Buy That Looked Like a Withdrawal

The attacker's contract borrowed about 8,000,002 USDT in a flash loan from a PancakeSwap V3 pool and then called swap on the YSDAO/USDT pair, asking for 611,320.33 YSDAO plus 1 wei of USDT. A V2 pair sends the requested tokens before it collects payment, so it paid out the 1 wei of USDT first. When the YSDAO transfer then ran through the token's code, the pair's USDT balance sat 1 wei below its reserve, and _isRemoveLiquidity() returned true.

Treated as a liquidity removal, the buy skipped every buy-side rule. On-chain, the pool held about 3,732,040 USDT and 897,219 YSDAO before the trade, so the one-tenth cap would have stopped any buy above about 89,722 YSDAO. The attacker took 611,320 YSDAO, about 68% of the pool's supply, with no fee and no recorded purchase cost. Inside the swap callback the contract paid the pair 8,000,000 USDT plus 2 wei, enough to satisfy PancakeSwap's pricing check.

Raising the Price With Someone Else's USDT

Next, according to DeFiHackLabs, the attacker called sync() on YSDAO's staking contract, the address YSDAO's constructor names as its staking partner. DeFiHackLabs reports that this function has no access control and moves all of the staking contract's USDT into the YSDAO/USDT pair before syncing the pair's reserves. On-chain, 164,833.57 USDT moved from the staking contract into the pair at that point, followed by a Sync event that lifted the pair's USDT reserve from about 11,732,040 to about 11,896,874. With more USDT behind the same YSDAO, every YSDAO in the pool was suddenly worth more.

The Sell That Looked Like a Deposit

Then came the single USDT. The attack contract sent exactly 1 USDT to the pair, leaving the pair's balance 1 USDT above its freshly synced reserve. When the contract sold its YSDAO through the PancakeSwap router a moment later, _isAddLiquidity() saw that surplus and classified the sale as someone adding liquidity.

That label waved the sale past the cooldown, the one-tenth sell cap and the profit tax. The token took only its 3% fee, 18,339.61 YSDAO split between the burn address and the token contract, and 592,980.72 YSDAO went into the pair. A seller with no recorded buy cost would normally have paid a quarter of the amount on top of that fee, and a sale that size would have failed the cap outright. The pair paid out 8,020,291.91 USDT. The attack contract returned 8,000,802 USDT to the V3 pool to close the flash loan and sent the remaining 19,490.91 USDT to the attacker's wallet.

A Classification Anyone Could Forge

The whole exploit came down to letting a balance stand in for an intention. YSDAO's taxes and caps were real protections, and each of them depended on the token correctly naming what kind of transfer it was looking at. Because that naming came from a number the caller could set, the caller got to choose which rules applied. About a week later, one wei of USDT sent to a pair let an attacker sell DTXT fee-free for about $35K, because that token also compared a balance anyone could change.

For builders, the fix is to classify transfers from facts a trader can't fake, such as whether the router is the sender and LP tokens were actually minted or burned, and to put any function that moves pooled funds, like the staking contract's sync(), behind access control.

How Olympix Would Have Exposed the Mislabeled Trades

Olympix reads a token like YSDAO as a set of states, transitions and entry points. The state that matters is the split of value inside the YSDAO/USDT pair and the fees and caps the token is supposed to enforce on every trade against it. The transitions are buys, sells, liquidity changes and the staking contract's calls into the pair. Olympix's static analysis would have surfaced the signal in BaseUSDTWA: transfer classification built on a pair's current balanceOf compared with getReserves, inputs that any caller can shift with a direct transfer or a swap's output, deciding whether tax, cooldown and size limits apply.

That signal points straight at the invariant YSDAO was written around. Every trade against the pair should pay the fees and respect the caps its direction calls for, and no sequence of calls in one transaction should let a trader take more USDT out of the pool than they put in. BugPoCer would have run Counterexample-Guided Invariant Refutation from the token, its pair and the staking contract outward, inferred that invariant from the transfer rules themselves, and searched for a sequence that breaks it.

The counterexample it would have returned matches May 29 step for step. A buy that requests a dust amount of USDT alongside the YSDAO so the token reads it as a removal, an unrestricted call that moves staking USDT into the pair, a 1 USDT transfer to the pair, and a sell the token reads as a liquidity deposit, ending with more USDT than the flash loan required. BugPoCer would have handed back that sequence with the minimal conditions that trigger it and a generated Forge test that replays it, which is the 19,490.91 USDT drain produced in development instead of on BNB Chain.

Taxes Only Work If the Label Is Honest

YSDAO wrote careful rules for buyers and sellers and then let the trader decide which rules applied by nudging a balance. Any token that treats liquidity providers differently from traders needs that distinction tested against someone trying to forge it. To see how Olympix maps the transfer paths in your token and returns a runnable counterexample whenever a trade can be made to look like something else, book a technical walkthrough.

On-Chain References

Protocol: YSDAO token and YSDAO/USDT PancakeSwap V2 pair

Chain: BNB Chain

Date: May 29, 2026, 09:12:43 UTC

Loss: 19,490.91 USDT

Attacker EOA: 0x8114...b460

Attack contract: 0xafbf...16af

YSDAO token: 0xc036...7918

YSDAO/USDT pair: 0x24df...5937

YSDAO staking contract: 0x3E13...c7CA

Flash loan source (PancakeSwap V3 pool): 0x172f...f849

Exploit tx: 0x91f2...1907

Block: 101088361

See what Olympix finds on your own code

Send us a repo that's already been audited, and we'll run Olympix at the same commit and show you what comes back.