On June 16, 2026 at 16:28:38 UTC, an attacker exploited DIP on BNB Chain by abusing a transfer-logic bug in the DIP token at 0x6c60bf5db0670ae94489d3dde2c60f271625db50. The bug caused any transfer involving Pancake Router V2 to execute twice, which let the attacker weaponize skim() and sync() on the AIC-DIP pool to erase the pool’s DIP reserve and rewrite its price. Using a flash-swapped 19,000,000 AIC position, the attacker drained 29,037,659.001700876485419035 AIC from the manipulated AIC-DIP pair, repaid the flash swap, and sold the remaining 9,977,659.001700876485419035 AIC into the AIC-USDC pool. The final cash-out transferred 111,097.596667856001191208 USDC directly to the attacker EOA.

Root Cause

Vulnerable Contract

DIP token (0x6c60bf5db0670ae94489d3dde2c60f271625db50). This was a direct, non-proxy deployment with verified source. The vulnerable behavior was in the token itself, while the Pancake pairs only supplied the execution surface that made the bug exploitable.

Vulnerable Function

Internal transfer path _transfer(address from, address to, uint256 amount) (internal, no external selector). This path is reached by DIP transfer and transferFrom, including the AIC-DIP pair’s skim(address) call to Pancake Router V2. Because the function executes a router-path transfer inside if (isRouter) and then always executes the same transfer again at the end, every DIP transfer to or from the router moves twice.

Vulnerable Code

function _transfer(
    address from,
    address to,
    uint256 amount
) internal override {
    require(from != address(0), "ERC20: transfer from the zero address");
    require(to != address(0), "ERC20: transfer to the zero address");

    if (amount == 0) {
        super._transfer(from, to, 0);
        return;
    }

    bool isSell = automatedMarketMakerPairs[to];
    bool isRouter = (from == uniswapV2Router || to == uniswapV2Router);

    if (isRouter) {
        super._transfer(from, to, amount); // <-- VULNERABILITY: router-path transfer already executes here
    }
    else if (isSell) {
        uint256 feeAmt = amount.mul(sellFee).div(100);
        amount = amount.sub(feeAmt);

        if (feeAmt > 0) {
            super._transfer(from, feeAddress, feeAmt);
        }
    }

    super._transfer(from, to, amount); // <-- VULNERABILITY: router-path transfer executes a second time
}

Why It’s Vulnerable

Expected behavior: A token transfer initiated by a router, or sent to a router, should debit the sender once and credit the receiver once for the exact requested amount. If an AMM pair calls skim(to), only the true excess balance above the stored reserves should leave the pool.

Actual behavior: DIP treats any transfer where from == router or to == router as a special branch, but it does not stop after that branch runs. The function therefore performs super._transfer(from, to, amount) inside if (isRouter) and then performs the same super._transfer(from, to, amount) again unconditionally.

That bug turns skim(router) into a reserve-draining primitive. The pair intends to send only its temporary DIP surplus to the router, but DIP debits the pair twice and credits the router twice. After that double debit, sync() commits an almost-zero DIP reserve to the pair state, so a small follow-up DIP input can buy out nearly the entire AIC side of the pool at a fabricated price.

Attack Execution

High-Level Flow

  1. The attacker called a helper contract that flash-swapped 19,000,000 AIC from the AIC-NEX Pancake pair.
  2. The helper swapped the borrowed AIC through the AIC-DIP pair and received 17,564,897.925642142489089334 DIP, moving the pair to 29,037,659.001700876485423256 AIC and 9,302,754.558885577321486581 DIP.
  3. The helper sent 9,896,547.403069763107963336 DIP gross into the AIC-DIP pair; 593,792.8441841857864778 DIP was diverted as the 6% sell fee, while 9,302,754.558885577321485536 DIP reached the pair as excess balance.
  4. The helper called skim(router) and then sync(). Because DIP transfers to the router execute twice, the pair sent 9,302,754.558885577321485536 DIP to the router twice and ended the sync with only 1,045 wei of DIP reserve against 29,037,659.001700876485423256 AIC.
  5. The helper sold its remaining DIP back into the broken AIC-DIP pool, drained 29,037,659.001700876485419035 AIC, repaid 19,060,000 AIC to the flash-swap pair, and dumped the remaining 9,977,659.001700876485419035 AIC into the AIC-USDC pair for 111,097.596667856001191208 USDC sent directly to the attacker EOA.

Detailed Call Trace

The attacker EOA 0x0d4024cd27538350a911d9b7ee90811fa4875ba3 called test(address,address,address,address) (0x785e4ba6) on the helper contract at 0xddef10a85a5c67a9af8398d297aa51f8716383c7. The helper immediately called the AIC-NEX pair’s swap(uint256,uint256,address,bytes) (0x022c0d9f) and borrowed 19,000,000 AIC, which triggered pancakeCall(address,uint256,uint256,bytes) (0x84800812) back into the helper.

Inside the callback, the helper approved Pancake Router V2 and called swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256,uint256,address[],address,uint256) (0x5c11d795) on the AIC -> DIP path. The router pulled 19,000,000 AIC into the AIC-DIP pair and the pair returned 17,564,897.925642142489089334 DIP to the helper. The pair then emitted a Sync with reserves of 29,037,659.001700876485423256 AIC and 9,302,754.558885577321486581 DIP, followed by the corresponding Swap.

The helper next transferred DIP directly to the AIC-DIP pair and invoked skim(address) (0xbc25cf77) with Pancake Router V2 as the recipient. The pair attempted to transfer the 9,302,754.558885577321485536 DIP surplus once, but DIP’s router branch executed twice, producing two equal DIP transfers from the pair to the router. The helper then called sync() (0xfff6cae9), and the pair emitted a Sync showing 29,037,659.001700876485423256 AIC against only 1,045 wei DIP.

With the price corrupted, the helper approved DIP again and called the router on the DIP -> AIC path. The router delivered 7,208,249.491218036618258439 DIP net into the AIC-DIP pair, and the pair paid out 29,037,659.001700876485419035 AIC to the helper, leaving only 4,221 wei AIC in reserve. The helper then transferred 19,060,000 AIC back to the AIC-NEX pair to settle the flash swap and used the remaining 9,977,659.001700876485419035 AIC in the AIC-USDC pair. That final router call caused the AIC-USDC pair to transfer 111,097.596667856001191208 USDC directly to the attacker EOA.

Financial Impact

The attacker realized 111,097.596667856001191208 USDC in final profit, paid directly to the EOA 0x0d4024cd27538350a911d9b7ee90811fa4875ba3. The immediate price-manipulation surface was the AIC-DIP pair, whose reserves were forced from a healthy post-swap state of 29,037,659.001700876485423256 AIC and 9,302,754.558885577321486581 DIP to an artificial state of 29,037,659.001700876485423256 AIC and 1,045 wei DIP before the drain. The cash-out liquidity came from the AIC-USDC pair, which transferred the recoverable dollar-denominated proceeds.

Evidence

  • Transaction: 0x1c09395848a87069c9d6ddbe5adc6249510aba7a2a83479a74b4280cafb5fb29 on BNB Chain, block 104598279, status success.
  • Attacker: 0x0d4024cd27538350a911d9b7ee90811fa4875ba3.
  • Vulnerable contract: DIP token 0x6c60bf5db0670ae94489d3dde2c60f271625db50.
  • Impacted pool/token/protocol component: the AIC-DIP Pancake pair 0xf7d8267d01d1104da2dd30828aa9c0e1647919ef, with the AIC-USDC pair 0x473715a3ad10e6ea8e8d549cc0c0b1059b2ba9bd used as the cash-out venue.
  • Key on-chain fact: skim(router) from the AIC-DIP pair produced two identical DIP transfers of 9,302,754.558885577321485536 each from the pair to Pancake Router V2, proving that router-involved DIP transfers execute twice.
  • Key on-chain fact: after the forced sync(), the AIC-DIP pair recorded only 1,045 wei of DIP reserve; the next swap then delivered 29,037,659.001700876485419035 AIC to the attacker flow and the final AIC-USDC swap transferred 111,097.596667856001191208 USDC to the attacker EOA.

Remediation

Remove the unconditional second super._transfer from DIP’s router path so a router-involved transfer executes exactly once. Rework sell-fee handling so AMM accounting is explicit and does not depend on side effects in transfer/transferFrom, and test skim, sync, swap, and router-assisted paths against the token’s actual transfer semantics. Any token with custom transfer logic should be fuzz-tested against Pancake/Uniswap pair invariants, especially the invariant that skim() can only remove true excess balances and can never drain booked reserves.