On BNB Chain, DxSale’s legacy LP locker at 0xeb3a9c56d963b971d320f889be2fb8b59853e449 was drained through repeat withdrawals after ownership was transferred to 0xc4574ddef299e7e563971e200433e592EeaaFA69 on May 26, 2026 at 01:08:56 UTC. The observable drain phase ran from block 100812090 through block 100898571 and produced 667,465 locker unlock events across 1,362 transactions and 45 Pancake LP token contracts. The primary transaction from the PeckShield alert, 0x16a932b7eb06b9d333c895011a4dceb7e73f0c0151acf75d0e43616335e802a6, prepared one affected LP market by swapping BNB into GWC and adding liquidity, while later transactions repeatedly called the legacy locker to withdraw the same recorded LP amount in batches. Public reporting attributed about $7.3M of drained LP value and about 2,958 BNB routed through two main wallets; on-chain evidence in this analysis confirms the ownership handoff, repeated unlock accounting failure, mass LP withdrawals, and BNB consolidation path.

Root Cause

Vulnerable Contract

The vulnerable component is the DxSale Legacy Locker at 0xeb3a9c56d963b971d320f889be2fb8b59853e449 on BNB Chain. BscScan does not provide verified Solidity for this address, so source confidence is based on runtime bytecode, public selector resolution, revert strings, storage behavior, and execution traces. The contract exposes per-user lock records and an unlockToken(uint256) withdrawal function that transfers the stored LP amount from the locker to msg.sender.

Vulnerable Function

The vulnerable withdrawal entry is unlockToken(uint256) with selector 0xdd2e0ac0. Representative transactions show the attacker repeatedly calling unlockToken(0) or unlockToken(1) hundreds of times in the same transaction, and each call succeeds with a new LP token transfer from the locker. The setup path also includes transferOwnership(address) with selector 0xf2fde38b, which moved legacy locker ownership to the attacker-controlled EIP-7702 account, enabling the attacker’s lock setup and batch execution.

Vulnerable Code

// [pseudocode reconstructed from observed execution]
function unlockToken(uint256 lockIndex) external {
    Lock storage lock = userLocks[msg.sender][lockIndex];
    require(lock.exists, "user doesnt have a locker");
    require(lock.active, "user's tokens are not locked");

    if (block.timestamp >= lock.unlockTime) {
        lock.active = false;
    }

    require(token.balanceOf(address(this)) >= lock.amount, "no more tokens left to refund");
    require(token.transfer(msg.sender, lock.amount), "Token refund to creator failed");
    // <-- VULNERABILITY: the withdrawn amount is not consumed; the same lock index can be reused
    emit TokenUnlocked(msg.sender, lock.token, lock.amount, block.timestamp);
}

Why It’s Vulnerable

Expected behavior: After a lock is unlocked once, the locker must consume the claimable amount before or during the transfer. A second call for the same (user, lockIndex) should either see amount zero, a consumed flag, or a deleted lock and revert.

Actual behavior: The observed unlockToken(uint256) path transfers the stored amount but leaves enough lock state intact for the same caller and same lock index to pass again. In transaction 0x5d51d975ca607d1e51bdf97d40d17ee85893eedd660ece1328b74aa91f43fe52, unlockToken(1) succeeds 500 times and transfers about 0.188504913673793844 Cake-LP on each call. In transaction 0xdbd71d000ee58726ef3e06e4caa9ee046335613c043676cb83a99471bfdf7c79, unlockToken(0) succeeds 500 times against another LP token with the same pattern.

The ownership transfer is an important enabler, not the only defect. It gave the attacker-controlled account the context needed to create or control eligible lock records, but the drain scale comes from the repeat-unlock accounting bug: one recorded lock amount could be replayed until the locker no longer held enough of that LP token.

Attack Execution

High-Level Flow

  1. The legacy locker owner 0x47bacf935066b802eaa0067ec14ab035b24eb78b called transferOwnership(address) and set the owner to 0xc4574DDEF299e7E563971e200433e592EeaaFA69.
  2. The attacker-controlled account used EIP-7702-style delegated code, visible as a 23-byte delegation marker on the handoff account during the attack window.
  3. For each target LP market, the helper code prepared or held a small eligible lock amount under the attacker account.
  4. The helper then batch-called unlockToken(uint256) hundreds of times for the same lock index.
  5. Each replayed unlock transferred another copy of the stored LP amount from the legacy locker to the attacker until the locker balance for that LP was depleted or the batch limit was reached.

Detailed Call Trace

The setup transaction 0x23e331a81e496890c8f6147a672c6bc45cc2d2dd17547e72713b6ee070b38d41 was a direct call from 0x47bacf935066b802eaa0067ec14ab035b24eb78b to the legacy locker with selector 0xf2fde38b. Its only event was OwnershipTransferred, moving ownership from 0x47bacf935066b802eaa0067ec14ab035b24eb78b to 0xc4574DDEF299e7E563971e200433e592EeaaFA69.

The primary alert transaction 0x16a932b7eb06b9d333c895011a4dceb7e73f0c0151acf75d0e43616335e802a6 occurred at block 100853125 on May 28, 2026 at 03:45:50 UTC. It was a self-call from 0xc457...FA69 to 0xc457...FA69 with selector 0x84d3326c. The delegated code called Pancake Router 0x10ed43c718714eb63d5aa57b78b54704e256024e, swapped 0.005 BNB for GWC through pair 0x61fc33cbb5e1b95c9c28f2df9de876ee1d155814, approved the router, and added GWC/WBNB liquidity. The transaction minted about 0.188504913673793844 Cake-LP to 0xc457...FA69.

Immediately afterward, repeated self-calls with selector 0x11b432b4 batched locker withdrawals. For the GWC/WBNB pair, transaction 0x5d51d975ca607d1e51bdf97d40d17ee85893eedd660ece1328b74aa91f43fe52 called unlockToken(1) 500 times against the legacy locker, and each call transferred about 0.188504913673793844 Cake-LP from the locker to 0xc457...FA69. A second transaction, 0xdb55ae5691e4787d808d2e38b90fc328de89150c173c66b852fc911648273612, repeated the same 500-call batch for the same LP token. Earlier BLS-related batches show the same pattern for 0x88da6bc38d5bfef6e332f87e06a310a9e5f768e2, including one 500-call transaction 0xdbd71d000ee58726ef3e06e4caa9ee046335613c043676cb83a99471bfdf7c79.

Across the measured attack window, the locker emitted 667,465 unlock events to 0xc457...FA69 across 1,362 transactions. The top affected LP contracts by event count were:

  • 0x9ed6a3faf7058f282bd31cec0773ca512f42ac40 (Cake-LP): 117,740 unlock events, about 6,170.95 LP tokens withdrawn
  • 0x61fc33cbb5e1b95c9c28f2df9de876ee1d155814 (Cake-LP): 51,690 unlock events, about 9,743.82 LP tokens withdrawn
  • 0x2fb498caa29015e067a20c4183dd2db343ca4ccc (Cake-LP): 49,706 unlock events, about 214.708 LP tokens withdrawn
  • 0xaf845e2fa005abe70bb66fbe1d63350ff6ce9675 (Cake-LP): 34,209 unlock events, about 75.6748 LP tokens withdrawn
  • 0x0721159d54741b5e0754a094e23da5efad16ac9a (Cake-LP): 28,647 unlock events, about 2.62682e+08 LP tokens withdrawn
  • 0x519e37defe5c154fbdd37e5512c9250a6a392f21 (Cake-LP): 25,098 unlock events, about 4,781.68 LP tokens withdrawn
  • 0x63b1e2e6b2d0c2219aac9e84e5873840c9d45e63 (Cake-LP): 21,044 unlock events, about 268.223 LP tokens withdrawn
  • 0x82d86b481f8d9d01c6ad3d979a7b4c36e5d397d5 (Cake-LP): 18,609 unlock events, about 116,530 LP tokens withdrawn
  • 0x9bd1fa8cd8e37f4713198365f499dfc92267bec9 (Cake-LP): 17,408 unlock events, about 4,131.48 LP tokens withdrawn
  • 0xd827461744be8ef03e4d63a06dd87b0dfabb6c3c (Cake-LP): 17,382 unlock events, about 7.07128e+07 LP tokens withdrawn

Financial Impact

Public incident reporting stated that roughly $7.3M was drained from about 1,400 BNB Chain LP lockers. This analysis confirms the mass LP withdrawal mechanics and observes 667,465 attacker-directed unlock events across 45 LP token contracts. On the BNB consolidation path, 0xc457...FA69 sent 712.5 BNB and 1 BNB to 0x4c5ee9703653c8e7725c65593bff372655e0453c, and 713.5 BNB plus 104 BNB to 0xb71c1c2a0cd7a88f1317f9a996e4d121e7db5e92; those two wallets later sent at least 1,438 BNB and 1,542.281 BNB respectively onward, totaling about 2,980.281 BNB of observed outbound movement from the two wallets. The final USD-equivalent impact depends on per-LP reserves and liquidation timing, so the report should treat the $7.3M figure as externally reported rather than independently priced from every LP reserve.

Evidence

  • Transaction: 0x16a932b7eb06b9d333c895011a4dceb7e73f0c0151acf75d0e43616335e802a6 on BNB Chain, block 100853125, status success.
  • Attacker: 0xc4574DDEF299e7E563971e200433e592EeaaFA69.
  • Vulnerable contract: 0xeb3a9c56d963b971d320f889be2fb8b59853e449.
  • Impacted pool/token/protocol component: DxSale legacy LP locker custody for Pancake LP tokens.
  • Key on-chain fact: 0x23e331a81e496890c8f6147a672c6bc45cc2d2dd17547e72713b6ee070b38d41 emitted OwnershipTransferred from 0x47bacf935066b802eaa0067ec14ab035b24eb78b to 0xc4574DDEF299e7E563971e200433e592EeaaFA69.
  • Key on-chain fact: 0x5d51d975ca607d1e51bdf97d40d17ee85893eedd660ece1328b74aa91f43fe52 made 500 successful unlockToken(1) calls that transferred GWC/WBNB Cake-LP from the locker to the attacker.
  • Key on-chain fact: 0xdbd71d000ee58726ef3e06e4caa9ee046335613c043676cb83a99471bfdf7c79 made 500 successful unlockToken(0) calls against another LP token, demonstrating the same repeat-withdraw behavior.
  • Key on-chain fact: The legacy locker emitted 667,465 unlock events to the attacker over 1,362 transactions in the measured window.

Remediation

DxSale should retire the legacy locker and migrate users to a custody design that consumes each lock exactly once. unlockToken(uint256) should set the amount to zero, mark the lock consumed, or delete the record before transferring tokens, and it should use a non-reentrant pattern around token calls. Add invariant tests that repeatedly call unlock for the same lock index and assert that total transferred value cannot exceed the recorded amount. Administrative ownership should also be hardened with multisig, timelock, emergency pause, and monitoring because the owner handoff was a prerequisite for the observed attacker-controlled execution context.