October 20, 2026
|
Exploit Postmortem
Cover image for "DIP Token's $111K Loss and How Olympix Would Have Prevented It"

DIP Token's $111K Loss and How Olympix Would Have Prevented It

Date: June 16, 2026

Chain: BNB Chain

Loss: 111,097.6 USDC, about $111K

Class: a missing return in a token's transfer function that made router transfers run twice

At 16:28:38 UTC on June 16, 2026, a PancakeSwap pool on BNB Chain sent 111,097.596667856001191208 USDC to a wallet that had brought almost nothing into the transaction. The attacker didn't borrow from a lending market or touch a price oracle, and the only borrowed capital was 19 million AIC tokens flash-swapped from another PancakeSwap pair and returned a few steps later.

What made the drain possible was one line that wasn't there. The DIP token's transfer function had a special branch for transfers involving the PancakeSwap router, and after that branch moved the tokens, the function kept going and moved them a second time. Any time DIP went to or from the router, it moved twice, and the attacker found a way to make a liquidity pool pay that double bill.

A Token With a Special Case for the Router

DIP is a token deployed directly on BNB Chain at 0x6c60...db50, without a proxy and with verified source code, according to DarkNavy's analysis. It traded in an AIC-DIP pool on PancakeSwap, and AIC in turn traded against USDC in a separate pool. Those two pools are where the money moved.

Like many BNB Chain tokens, DIP layered custom logic on top of the standard transfer. Its internal _transfer() function checked whether the transfer involved PancakeSwap Router V2, and if it did, it ran an if (isRouter) branch that performed the transfer. That branch had no return statement at the end. Execution fell out of the branch and into the ordinary transfer code below it, which performed the same transfer again unconditionally.

Why a Missing Return Breaks a Pool

A token transfer has one job: take an amount from the sender and give the same amount to the recipient, exactly once. When a function has a special-case branch, that branch has to finish the job and leave, the way a cashier who rings up an item at the express lane shouldn't also ring it up again at the main register. DIP's router branch rang it up twice.

In normal trading that duplicate went largely unnoticed. It became dangerous because of how PancakeSwap pairs keep their books. A pair stores a reserve figure for each token, separate from its actual balance, and exposes two housekeeping functions. skim(address) sends any balance above the stored reserve to an address the caller picks, and sync() overwrites the stored reserves with whatever the actual balances are. Both assume a transfer out of the pair removes exactly the amount requested.

That assumption is the lesson of this case. A token's transfer function is part of every pool's accounting, so if a single call can move a balance more than once, the pool's reserves can be pushed anywhere the attacker likes. Calling skim() with the router as the recipient hit DIP's router branch, so the pair paid out its surplus twice, and sync() then wrote the shrunken balance into the books as the new reserve.

The Attack, Step by Step

The attacker's wallet, 0x0d40...5ba3, called a helper contract at 0xddef...83c7. The helper flash-swapped 19,000,000 AIC from an AIC-NEX pair, which triggered a callback into the helper where the rest of the attack ran.

Inside that callback, the helper swapped AIC for DIP through the AIC-DIP pair and received 17,564,897.93 DIP, leaving the pair with 29,037,659 AIC and 9,302,754.56 DIP in reserve. It then sent 9,896,547.4 DIP straight to the pair. After DIP's 6% sell fee, that left 9,302,754.56 DIP sitting in the pair above its recorded reserve.

Next came skim(router). The pair tried to send that 9.3 million DIP surplus to the router, and because of the missing return, it sent it twice. The pair's real DIP balance fell far below where it should have been, and the helper immediately called sync(). The pair recorded a DIP reserve of 1,045 wei, a tiny fraction of one token, against 29 million AIC.

To the pool's pricing formula, DIP had become almost infinitely scarce. The helper sold DIP into the broken pair and received 29,037,659 AIC, leaving 4,221 wei of AIC behind. It repaid 19,060,000 AIC to the AIC-NEX pair to settle the flash swap, then sold the remaining 9,977,659 AIC into the AIC-USDC pair. That final swap sent 111,097.6 USDC directly to the attacker's wallet, all inside transaction 0x1c09...fb29 in block 104598279.

After the Drain

SlowMist flagged the exploit the next day and reported the loss at 111,097.6 USDC. Neither SlowMist nor DarkNavy identified the attacker or reported any recovery, and the public analyses don't include a statement from the DIP team.

The remediation DarkNavy describes is small: remove the second unconditional transfer so a router transfer executes once. It also recommends fuzz-testing tokens with custom transfer logic against pair invariants, especially the rule that skim() can only ever remove a pool's true surplus.

How Olympix Catches a Transfer That Runs Twice

Olympix reads a token the way an attacker does, as a set of states, the transitions between them and the entry points that trigger those transitions. For DIP, the state that matters is every holder's balance, and every path through _transfer() is a transition worth mapping. The engine also takes in the context around the code, here the fact that this token lives inside AMM pairs, to infer the invariants the system depends on without ever writing them down.

Most paths through a token are harmless, so the engine uses signals to decide which ones are worth attacking. The signal in DIP is a conditional branch inside _transfer() that moves balances and then falls through into code that moves them again. That narrows the search from every path in the token to the feasible ones where the recipient is the router, and from there to the ones that can reach a pair's skim() and sync().

The invariant in plain words is that one transfer call moves an amount out of the sender exactly once, so a pair can never pay out more than its true surplus. Olympix attacks the router branch from both ends. Counterexample-Guided Invariant Refutation starts from the token as a whole, infers that exactly-once rule from how DIP sits inside its pairs, and executes paths until one breaks it. Bounded Adversarial Verification starts from the fall-through branch itself and runs known attack strategies against it, skim-and-sync reserve manipulation among them, until one lands. Symbolic execution sits underneath both as an engine component.

The output is a counterexample: transfer surplus DIP into the pair, call skim() with the router as recipient, watch the pair pay twice, then call sync() and swap against the collapsed reserve. It comes back with the minimal conditions that trigger it and a generated Forge test that replays it, which is the June 16 drain produced in development instead of on mainnet. Because the engine runs automatically during development, at every merge and before release, a router special case added late in a token's life gets the same scrutiny as the original code.

One Line Between a Fee Rule and a Drained Pool

Every branch a team adds to a transfer function changes the accounting of every pool that holds the token, and the rule that a transfer moves value exactly once is the one those pools quietly rely on. DIP's router branch did its job and then kept going, and two standard PancakeSwap housekeeping calls turned that into a 111,097.6 USDC payout. To see how Olympix turns the rules your token's integrations depend on into runnable counterexamples before launch, book a technical walkthrough.

On-Chain References

Protocol: DIP token, AIC-DIP and AIC-USDC PancakeSwap pools

Chain: BNB Chain

Date: June 16, 2026, 16:28:38 UTC

Loss: 111,097.596667856001191208 USDC

Attacker EOA: 0x0d40...5ba3

Attack contract: 0xddef...83c7

DIP token (vulnerable contract): 0x6c60...db50

AIC-DIP PancakeSwap pair (manipulated): 0xf7d8...19ef

AIC-USDC PancakeSwap pair (cash-out): 0x4737...a9bd

Exploit tx: 0x1c09...fb29

Block: 104598279

AIC-NEX pair (flash-swap source): 0xF833...CAE1

AIC token: 0x524c...4405

Sources

DarkNavy: DIP Token Double-Transfer Reserve Manipulation Exploit

SlowMist: TI Alert on the DIP token exploit

DeFiHackLabs: DIP exploit PoC

More from Olympix:

No items found.

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