# EST BNBDeposit Claim Abuse and Pair Reserve Manipulation
On 2026-03-27, the EST / BNBDeposit system on BNB Smart Chain was exploited through a **flash-loan-assisted reward-accounting flaw** in `BNBDeposit`, amplified by **fee-exempt routing and pair-state manipulation** in EST. The attacker borrowed `250,000 WBNB`, built a temporary claim-bearing share in `BNBDeposit` via `34` deposits of `0.3 BNB`, routed `400 WBNB` worth of EST into the fee-exempt deposit contract, and triggered `onTokenReceived` with exactly `1 EST`. `BNBDeposit` then paid out `20,569,915.273855479094886078 EST` from its live balance. After routing another `245,000 WBNB` worth of EST into the same sink and running `150` `skim()` cycles against the EST/WBNB pair, the attacker exited `19,730,833.203602432219877940 EST` back to WBNB. Over the full transaction, the EST/WBNB pair lost **154.571495162546593567 WBNB** net, while the attacker realized **150.689451187958247246 WBNB** in liquid profit before gas, excluding any residual value of the temporary internal LP position.
## Root Cause
### Vulnerable Contracts
– **Contract**: `BNBDeposit`
– **Address**: `0xe71547170c5ad5120992b85cf1288fab23d29a61`
– **Proxy**: No
– **Source type**: Verified
– **Contract**: `ESTToken`
– **Address**: `0xd4524be41cd452576ab9ff7b68a0b89af8498a91`
– **Proxy**: No
– **Source type**: Verified
The observed profit path in this transaction relied on both contracts:
1. `BNBDeposit` incorrectly treats its entire live EST balance as immediately claimable reward inventory.
2. `ESTToken` provides the deterministic callback path used in this transaction: an exact `1 EST` transfer to `depositContract` triggers `BNBDeposit.onTokenReceived(from)` when the sender also satisfies `tx.origin == user`.
3. `ESTToken` also exempts the deposit contract from transfer fees and mutates pair reserves with `sync()` during sells, which makes the attacker’s exit materially more favorable.
### Vulnerable Functions
– `BNBDeposit.onTokenReceived(address user)`
– `BNBDeposit._claimToken(address user)`
– `ESTToken._transfer(address from, address to, uint256 amount)`
The primary accounting flaw is in `BNBDeposit._claimToken`. In this transaction, the attacker used `ESTToken._transfer` as a convenient trigger path because it calls `onTokenReceived` when exactly `1 EST` is transferred to the configured `depositContract`.
### Vulnerable Code
“`
// BNBDeposit.sol function onTokenReceived(address user) external { require(!_locked, “Reentrant”); require(msg.sender == address(token), “Only token”); require(tx.origin == user, “Only EOA”); require(userInfo[user].lpAmount > 0, “No LP”); require(userInfo[user].claimedValueInUSDT < userInfo[user].lpValueInUSDT * 5, “Already reached 5x limit”); _locked = true; _claimToken(user); _locked = false; } function _claimToken(address user) internal { UserInfo storage info = userInfo[user]; require(block.timestamp >= info.lastClaimTime + claimInterval, “Claim too frequent”); uint256 contractBalance = token.balanceOf(address(this)); // <– VULNERABILITY: uses full live token balance uint256 claimable = contractBalance * info.lpAmount / totalLP; // <– VULNERABILITY: arbitrary inbound EST becomes claimable require(claimable > 0, “Nothing to claim”); uint256 claimValueUSDT = _getTokenValueInUSDT(claimable); // … token.transfer(user, claimable); }
“`
**Snippet 1 Explained**
`onTokenReceived` is the externally reachable claim callback. It can only be called by the EST token contract, and it also requires `tx.origin == user`, so the eventual claimant must be the transaction origin and must already hold a positive `lpAmount`. Once those checks pass, `_claimToken` calculates rewards from `token.balanceOf(address(this))`, which is the contract’s entire live EST balance, not a separately tracked reward bucket. That means any EST routed into `BNBDeposit` immediately becomes part of the claim base and can be extracted pro rata by any address that first acquires temporary LP share.
“`
// ESTToken.sol address public burnReceiver = address(0xE71547170c5ad5120992B85Cf1288FAb23d29A61); address public depositContract = address(0xE71547170c5ad5120992B85Cf1288FAb23d29A61); constructor() { _isExcludedFromFee[burnReceiver] = true; // <– fee exemption also applies to depositContract // … } function _transfer(address from, address to, uint256 amount) private { // … if (takeFee) { uint256 feeAmount = amount.mul(totalFee).div(100); uint256 transferAmount = amount.sub(feeAmount); // … if (isSell) { _pendingSellBurn += transferAmount; } } // … if (to == depositContract && depositContract != address(0) && amount == 1 * 10 ** uint256(_decimals)) { IBNBDeposit(depositContract).onTokenReceived(from); // <– exact 1 EST transfer triggers claim path } }
“`
**Snippet 2 Explained**
This snippet wires EST directly into the vulnerable claim path. `burnReceiver` and `depositContract` are configured to the same address, and the constructor excludes `burnReceiver` from fees, so transfers into `BNBDeposit` also become fee-exempt. In `_transfer`, an exact `1 EST` transfer to `depositContract` calls `onTokenReceived(from)`. Together, these rules give the attacker two useful properties at once: large EST inflows can be parked inside `BNBDeposit` without normal transfer tax, and a deterministic `1 EST` transfer can trigger the claim at the exact moment the injected balance is most favorable.
“`
// ESTToken.sol if (_pendingSellBurn > 0 && !ammPairs[from] && uniswapV2Pair != address(0) && _tTotal > minSupply) { uint256 burnAmount = _pendingSellBurn; _pendingSellBurn = 0; // … _tOwned[uniswapV2Pair] = _tOwned[uniswapV2Pair].sub(burnAmount); _tTotal = _tTotal.sub(burnAmount); emit Transfer(uniswapV2Pair, address(0), burnAmount); IUniswapV2Pair(uniswapV2Pair).sync(); // <– pair reserves are mutated mid-exit }
“`
**Snippet 3 Explained**
This is not the root cause of the theft, but it makes the exit materially better for the attacker. During sell-side flow, EST burns tokens from the pair balance and then immediately calls `sync()`, which updates the pair’s stored reserves to the reduced EST side. Combined with the attacker’s repeated `skim()` loop, this lets the pair sit in a distorted state before the final EST-to-WBNB swap, so the attacker unwinds the claimed EST position at a more favorable price than would be possible in a normal reserve flow.
### Why It’s Vulnerable
**Expected behavior**: `BNBDeposit` should pay rewards only from an explicitly tracked reward pool or from value accrued through a controlled emission mechanism. Arbitrary EST sent into the contract should not become instantly claimable by any address that temporarily acquires LP share. The token hook should not expose a one-transfer trigger that bypasses a normal user-facing claim flow. Fee exemptions should not create a privileged sink that can receive large token inflows without the normal transfer tax.
**Actual behavior**:
1. **Live-balance accounting**: `BNBDeposit._claimToken` uses `token.balanceOf(address(this))` as the reward base. Any EST transferred into the contract immediately inflates the reward pool.
2. **Hook-triggered claim**: EST’s `_transfer` calls `BNBDeposit.onTokenReceived(from)` whenever exactly `1 EST` is sent to `depositContract`. The hook is not universally triggerable by an arbitrary helper contract because `BNBDeposit` also requires `tx.origin == user`; in this exploit that check passed because the transaction sender itself was the same address as the attack contract.
3. **Fee-exempt sink**: `depositContract` and `burnReceiver` are the same address, and `burnReceiver` is excluded from fees in the constructor. Large EST buys can therefore be routed into `BNBDeposit` without paying the token’s 5% transfer fee.
4. **Exit amplifier**: During subsequent sells, EST’s delayed sell-burn and `sync()` logic, combined with repeated `skim()` calls, lets the attacker keep the pair’s EST side artificially low before the final EST-to-WBNB exit.
**What this means economically**:
– The attacker first buys EST into the fee-exempt deposit contract.
– The attacker then claims a proportional slice of that injected EST by sending exactly `1 EST`.
– After claiming EST into their own balance, the attacker uses a second large fee-exempt buy plus repeated pair perturbations to exit the claimed EST position at an inflated WBNB price.
The core flaw is therefore not a standard `skim()` bug in PancakeSwap itself. The true bug is that `BNBDeposit` exposes its entire live EST balance as claimable inventory, while EST simultaneously provides:
– a deterministic `1 EST` trigger for claims when the LP holder is also the transaction origin,
– a fee-exempt deposit sink,
– and pair-state mutation during sells.
## Attack Execution
### High-Level Flow
01. The local transaction artifact records the top-level sender and callee as the same address, `0xcf300de6f177ec10db0d7f756ced3ae2d2203bfd`, which executes `start(…)`.
02. It flash-loans `250,000 WBNB` from `0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c`.
03. Inside `onMoolahFlashLoan`, it unwraps `15 WBNB` to native BNB.
04. It sends `0.3 BNB` into `BNBDeposit` `34` times, creating a temporary internal LP share. Total BNB sent to `BNBDeposit`: `10.2 BNB`.
05. It calls PancakeRouter to swap `400 WBNB -> EST` with recipient `BNBDeposit`. This routes `822,411,955.122453151617438286 EST` into the deposit contract.
06. It transfers exactly `1 EST` to `BNBDeposit`, triggering `onTokenReceived(attacker)`. This succeeds because the top-level transaction sender is also `0xcf300de6…`, so `tx.origin == user` passes.
07. `BNBDeposit` transfers `20,569,915.273855479094886078 EST` to the attacker. From the traced `EST.balanceOf(BNBDeposit)` return of `832,455,814.625476530077095246 EST` immediately before the transfer, this is approximately `2.4709918427454223%` of the deposit contract’s EST balance at claim time.
08. It then performs a second fee-exempt `245,000 WBNB -> EST` buy into `BNBDeposit`, routing `330,866,039.650425264248436964 EST` into the deposit contract and heavily pumping the EST/WBNB price.
09. The attacker enters a `150`-iteration loop:
– transfer small amounts of EST into the EST/WBNB pair,
– let EST’s sell-path logic queue or execute burns and `sync()`,
– call `skim(BNBDeposit)`.
10. Across those loops, approximately `797,145.066740394531257707 EST` is redirected from the pair to `BNBDeposit`.
11. After the loop, the attacker sends one additional dust-sized `100 wei EST` transfer into the pair, triggering one more sell-path `sync()` before exit.
12. The attacker finally swaps `19,730,833.203602432219877940 EST -> WBNB`; due to EST’s 5% fee, `18,744,291.543422310608884043 EST` reaches the pair and `986,541.660180121610993897 EST` is sent to the fee wallet.
13. The attacker receives `245,560.889451187958247246 WBNB`, re-wraps to WBNB, repays the `250,000 WBNB` flash loan, and keeps the remainder.
### Detailed Call Trace
The trace below is reconstructed directly from `decoded_calls.json` and `trace_callTracer.json`. One important artifact caveat: `tx.json` records `from == to == 0xcf300de6…` for this legacy transaction. The trace and receipt are internally consistent with that, and it explains why `tx.origin == user` passes in `BNBDeposit.onTokenReceived`, but this sender model is unusual enough that it should be treated as an observed artifact rather than a fully explained environmental property.
“`
AttackerContract (0xcf300de6…) CALL AttackerContract.start(…) [0x707a4e96] CALL FlashLoanProvider (0x8f73b65b…).flashLoan(WBNB, 250000 WBNB, “”) [0xe0232b42] CALL WBNB.transfer(attacker, 250000 WBNB) [0xa9059cbb] CALL AttackerContract.onMoolahFlashLoan(250000 WBNB, “”) [0x13a1a562] CALL WBNB.approve(router, max) [0x095ea7b3] CALL EST.approve(router, max) [0x095ea7b3] CALL WBNB.approve(flashLoanProvider, max) [0x095ea7b3] CALL WBNB.withdraw(15 WBNB) [0x2e1a7d4d] [34x] CALL BNBDeposit.receive() with 0.3 BNB -> distribute fee splits -> swap 0.093 BNB -> EST -> add liquidity -> credit internal LP share CALL PancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens( 400 WBNB, [WBNB, EST], BNBDeposit, … ) [0x5c11d795] -> Pair.swap(…) -> EST transferred from pair to BNBDeposit: 822,411,955.122453… EST CALL EST.transfer(BNBDeposit, 1 EST) [0xa9059cbb] CALL BNBDeposit.onTokenReceived(attacker) [0x6c069868] CALL EST.balanceOf(BNBDeposit) [0x70a08231] CALL router.getAmountsOut(…) [0xd06ca61f] CALL EST.transfer(attacker, 20,569,915.273855… EST) [0xa9059cbb] CALL PancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens( 245000 WBNB, [WBNB, EST], BNBDeposit, … ) [0x5c11d795] -> Pair.swap(…) -> EST transferred from pair to BNBDeposit: 330,866,039.650425… EST [150-cycle loop] CALL EST.transfer(Pair, small amount) [0xa9059cbb] -> EST _transfer() -> delayed sell burn / sync path may execute CALL Pair.skim(BNBDeposit) [0xbc25cf77] CALL EST.transfer(Pair, 100 wei) [0xa9059cbb] -> final dust sell / sync trigger before exit CALL PancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens( 19,730,833.203602432219877940 EST, [EST, WBNB], attacker, … ) [0x5c11d795] -> EST.transferFrom(attacker, pair, 19,730,833.203602432219877940 EST) -> final post-fee EST delivered to pair: 18,744,291.543422… EST -> Pair.swap(…) -> WBNB.transfer(attacker, 245,560.889451… WBNB) CALL WBNB.deposit() with 4.8 BNB [0xd0e30db0] CALL WBNB.transferFrom(attacker, flashLoanProvider, 250000 WBNB) [0x23b872dd]
“`
## Financial Impact
**Direct on-chain loss**: the EST/WBNB PancakeSwap V2 pair ( `0x74986cd86caf54961dd70eedcaf7cb3fe813c0b9`) lost a net **154.571495162546593567 WBNB** over the full transaction.
This can be verified directly from the pair’s WBNB transfers:
– WBNB sent into the pair during the `34` setup deposits: `6.317956025411653679 WBNB`
– WBNB sent into the pair by the two large attacker buys: `400 + 245000 = 245,400 WBNB`
– Total WBNB sent into the pair during the transaction: `245,406.317956025411653679 WBNB`
– WBNB sent out from the pair to the attacker on the final exit: `245,560.889451187958247246 WBNB`
– Net pair WBNB loss: `154.571495162546593567 WBNB`
**Gas cost**: `0.0017449786 BNB`
**Realized attacker profit before gas**:
“`
245,560.889451187958247246 – 245,400 – 10.2 = 150.689451187958247246 WBNB
“`
This realized-profit figure excludes any residual value of the temporary internal LP share created by the `34` deposits.
**Attacker profit after gas**:
“`
150.689451187958247246 – 0.0017449786 = 150.687706209358247246 WBNB
“`
**Who lost funds**:
– Primary direct victims: liquidity providers in the EST/WBNB pair
**Important nuance**:
– The attacker also extracted `20,569,915.273855479094886078 EST` from `BNBDeposit`.
– However, that EST was not a clean protocol loss in the same sense as the WBNB drained from the pair, because the attacker had just routed `400 WBNB` worth of EST into the deposit contract immediately before claiming it.
– The economically meaningful profit was realized when the attacker later exited to WBNB at a manipulated price, but the realized liquid profit is lower than the pair’s net WBNB loss because the attacker also spent `10.2 BNB` to create the temporary LP share used for claiming.
## Evidence
– Transaction hash: `0x2f1c33eaaaace728f6101ff527793387341021ef465a4a33f53a0037f5bd1626`
– Block: `89060337`
– Timestamp: `2026-03-27T15:40:34Z`
– Status: success
– Gas used: `17,449,786`
– Local artifact caveat: `tx.json` records the top-level sender as `0xcf300de6…`, the same address as the attack contract. This is self-consistent with the trace, but unusual enough that it remains a residual confidence caveat unless independently explained from a live explorer or chain-specific execution model.
**Validated call counts and amounts**:
– `34` calls into `BNBDeposit.receive()` with `0.3 BNB` each
– `150` calls to `skim(address)`
– `151` direct attacker `EST.transfer(…)` calls into the pair, plus the final router-driven `EST.transferFrom(…)` on exit
– `822,411,955.122453151617438286 EST` bought into `BNBDeposit` before the claim trigger
– `20,569,915.273855479094886078 EST` transferred from `BNBDeposit` to the attacker after the `1 EST` trigger
– `330,866,039.650425264248436964 EST` bought into `BNBDeposit` after the claim
– `797,145.066740394531257707 EST` redirected from the pair to `BNBDeposit` across the skim loop
– Final exit input: `19,730,833.203602432219877940 EST`
– Final exit output: `245,560.889451187958247246 WBNB`
**Selector verification**:
– `0x707a4e96` = `start(uint256,uint256,uint256,uint256)`
– `0xe0232b42` = `flashLoan(address,uint256,bytes)`
– `0x13a1a562` = `onMoolahFlashLoan(uint256,bytes)`
– `0x5c11d795` = `swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256,uint256,address[],address,uint256)`
– `0x6c069868` = `onTokenReceived(address)`
– `0xbc25cf77` = `skim(address)`
– `0x022c0d9f` = `swap(uint256,uint256,address,bytes)`
**Code-path evidence**:
– `BNBDeposit.receive()` auto-deposits on qualifying BNB sends.
– `BNBDeposit._claimToken()` calculates rewards from `token.balanceOf(address(this))`.
– `ESTToken._transfer()` triggers `onTokenReceived(from)` when `amount == 1e18` and `to == depositContract`.
– `BNBDeposit.onTokenReceived()` additionally requires `tx.origin == user`; this exploit satisfies that because the transaction sender in `tx.json` is the same `0xcf300de6…` address that holds the LP share and sends the `1 EST`.
– `depositContract` is equal to `burnReceiver`.
– `burnReceiver` is explicitly excluded from transfer fees in the EST constructor.
These points together match the executed trace exactly:
– the attacker routes EST into the fee-exempt deposit contract,
– triggers the hook with `1 EST`,
– receives a pro-rata claim payout,
– then uses a second fee-exempt buy and repeated `skim()` calls to keep the pair favorable for the final exit.
## Related URLs
– Transaction: https://bscscan.com/tx/0x2f1c33eaaaace728f6101ff527793387341021ef465a4a33f53a0037f5bd1626
– BNBDeposit: https://bscscan.com/address/0xe71547170c5ad5120992b85cf1288fab23d29a61
– ESTToken: https://bscscan.com/address/0xd4524be41cd452576ab9ff7b68a0b89af8498a91
– EST/WBNB Pair: https://bscscan.com/address/0x74986cd86caf54961dd70eedcaf7cb3fe813c0b9
– Flash-loan provider: https://bscscan.com/address/0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c
– Attacker contract: https://bscscan.com/address/0xcf300de6f177ec10db0d7f756ced3ae2d2203bfd
