Movie Token (MT) — Deflationary Burn Manipulation via extractFromPoolForLpMining
On 2026-02-28 (BSC block 85677691), the Movie Token ($MT) project was exploited for approximately 381.75 WBNB (~$242K USD) in a single transaction. The attacker abused the MT token’s extractFromPoolForLpMining function, which burns tokens directly from the PancakeSwap MT/WBNB LP pair’s balance without the pair’s knowledge (i.e., without a prior reserve-matching transfer). By pairing this with a PancakeSwap flash swap callback (pancakeCall), the attacker caused the pair to sync() after the pool’s MT balance had been drastically reduced by the burn, collapsing the MT reserve from ~6.76M MT to 21,000 MT and allowing a subsequent sell of MT tokens at a massively inflated effective price.
Root Cause
Vulnerable Contract
- Name: Movie Token (MT)
- Address:
0xb32979f3a5b426a4a6ae920f2b391d885abf4c18 - Source type: Verified (Etherscan)
- Proxy: No
Vulnerable Function
- Name:
extractFromPoolForLpMining - Signature:
extractFromPoolForLpMining(uint256 amount) - Selector:
0x962b2809 - File:
contracts/core/MT.sol
Vulnerable Code
// contracts/core/MT.sol (verified)
function extractFromPoolForLpMining(uint256 amount) external returns (uint256) {
require(msg.sender == lpMiningAddr, "Only LP mining"); // <-- VULNERABILITY (1)
// ... day-limit and amount-cap checks ...
// Extract from pool balance (similar to BTdaoToken)
balanceOf[extractionPool] -= amount; // <-- VULNERABILITY (2)
balanceOf[address(this)] += amount;
emit Transfer(extractionPool, address(this), amount);
// Transfer to LP mining contract
balanceOf[address(this)] -= amount;
balanceOf[lpMiningAddr] += amount;
emit Transfer(address(this), lpMiningAddr, amount);
// Sync pair reserves
try IUniswapV2Pair(extractionPool).sync() {} catch {} // <-- VULNERABILITY (3)
lastPoolExtractionTime = todayMidnight;
return amount;
}
And the closely related internal burn called within the same function path:
function _executePendingBurn(address extractionPool) internal {
// ...
// Burns tokens from pool balance directly, then calls sync()
balanceOf[extractionPool] -= burnAmount; // <-- VULNERABILITY (2)
balanceOf[DEAD_ADDRESS] += burnAmount;
emit Transfer(extractionPool, DEAD_ADDRESS, burnAmount);
// ...
try IUniswapV2Pair(extractionPool).sync() {} catch {} // <-- VULNERABILITY (3)
}
Caller access guard (MT_LP_RewardDistributor, distributeDailyRewards, selector 0x501788af):
// MT_LP_RewardDistributor (0x139bd2ecfde76f5311d7beeb2e05cba6fede26d6) [recovered — approximation]
// distributeDailyRewards() is callable by anyone with no access control guard.
// It reads pair reserves, computes a 1% extraction amount, then calls
// MT.extractFromPoolForLpMining(amount), which is gated only on msg.sender == lpMiningAddr.
// The MT token has lpMiningAddr set to this distributor contract — so calling
// distributeDailyRewards() from inside a PancakeSwap flash swap callback is permitted.
Why It’s Vulnerable
Expected behavior: extractFromPoolForLpMining SHOULD only be triggerable under safe conditions where the pair’s reserves are known to be accurate (e.g., outside an active swap or after a sync initiated by the pair itself). The pair’s swap() function holds a reentrancy lock (Pancake: LOCKED) to prevent reserve manipulation during a swap.
Actual behavior: The function directly subtracts tokens from balanceOf[extractionPool] (the pair’s token balance as tracked in the MT token contract’s own mapping), and then calls IUniswapV2Pair(extractionPool).sync(). This sync() call updates the pair’s reserve state to match its actual token balances, meaning that any reduction in balanceOf[pair] — whether by a legitimate transfer or by this direct subtraction — is absorbed by sync() as a real reserve decrease.
This creates a two-part flaw:
The authorization check is insufficient.
msg.sender == lpMiningAddronly restricts direct calls; it does not prevent callingdistributeDailyRewards()on0x139bd2ecfde76f5311d7beeb2e05cba6fede26d6(the LP reward distributor, which is thelpMiningAddr) from inside a flash swap callback. The pair’s reentrancy lock blocksswap(),mint(), andburn()on the pair itself, but does NOT block calls to the MT token contract or the reward distributor. There is no lock onextractFromPoolForLpMiningpreventing it from being called while a PancakeSwap swap is in progress.Reserve manipulation via direct balance subtraction + sync. When
extractFromPoolForLpMiningreducesbalanceOf[pair]directly in the MT token’s storage (not via atransferthat the pair can observe) and subsequently callspair.sync(), the pair setsreserve0(MT) to the new, lower balance. The key effect: after the attacker had already used flash-loan capital to buy ~10M MT twice from the pool (depleting MT reserves to ~6.76M MT and pushing WBNB reserves to ~1,201 WBNB), callingdistributeDailyRewards()insidepancakeCalltriggers the burn of ~6.74M MT frombalanceOf[pair], followed bysync(). The pair’s MT reserve collapses from ~6.76M MT to ~21,000 MT (theMIN_LP_SUPPLYfloor), while WBNB reserves remain at ~1,201 WBNB. This artificial skew — 1,201 WBNB backing only 21,000 MT — is what yields the profit when the attacker sells MT back into the pool.
Normal vs. Attack flow (reserves in the MT/WBNB pair, reserve0=MT as token0, reserve1=WBNB as token1):
| Scenario | MT reserve (reserve0) | WBNB reserve (reserve1) | MT price |
|---|---|---|---|
| Pre-attack initial | ~17,756,517 MT | ~384.27 WBNB | fair (~21.6 MT per WBNB) |
| After attacker buys 10M MT (first swap) | ~7,756,517 MT | ~880.93 WBNB | MT price up |
| After attacker buys 10M MT (second swap) | ~6,756,517 MT | ~1,201.15 WBNB | MT price up further |
After extractFromPoolForLpMining + sync() | ~21,000 MT | ~1,201.15 WBNB (unchanged) | MT “worth” ~0.057 WBNB each — 46× distortion |
| Attacker sells 10M MT into depleted pool | drains remaining WBNB | — | attacker profits massively |
On-chain confirmation from SYNC events in receipt.json:
- Log #42: reserve0(MT)=17,756,516.74, reserve1(WBNB)=384.27 — initial state before attack ops
- Log #79: reserve0(MT)=6,756,516.94, reserve1(WBNB)=1,201.15 — after both 10M MT purchases
- Log #86: reserve0(MT)=6,756,516.90, reserve1(WBNB)=1,201.15 — just before
distributeDailyRewardscall (tiny LP burn) - Log #92: reserve0(MT)=21,000.00, reserve1(WBNB)=1,201.15 — after
extractFromPoolForLpMining+sync(): burn confirmed
Attack Execution
High-Level Flow
- Attacker EOA (
0xdb0901a3) callsexploit(MT, PancakeRouter, MT_WBNB_Pair, LPRewardDistributor)on pre-deployed contract0xdf7ed22d. - Attacker checks the WBNB balance of the Moolah flash loan pool, then takes a flash loan of the full amount (~358,681 WBNB) from Moolah (
0x8f73b65b). - Inside
onMoolahFlashLoan, the attacker seeds the pair with a tiny WBNB amount (~0.000004 WBNB) and 0.2 MT, mints a small LP position, then uses the flash-loan capital to buy ~10M MT from the pool via PancakeRouter. - A small LP removal is performed. The attacker then initiates a PancakeSwap flash swap requesting WBNB out, triggering the
pancakeCallcallback. - Inside
pancakeCall, the attacker sends ~10M MT back to the pair to partially settle the flash swap (triggering MT fee logic), then buys a second batch of ~10M MT from the pool, depleting MT reserves to ~6.76M MT and raising WBNB reserves to ~1,201 WBNB. - The attacker calls
distributeDailyRewards()on the LP reward distributor. This reads the current reserves, computes a ~1% daily extraction amount (~6.74M MT), and callsMT.extractFromPoolForLpMining(~6.74M MT). This burns ~6.74M MT directly frombalanceOf[pair]and callspair.sync()— collapsing the MT reserve to theMIN_LP_SUPPLYfloor of 21,000 MT while WBNB stays at ~1,201 WBNB. - The attacker sells 10M MT into the now-depleted pool, receiving ~1,198.6 WBNB (at the artificially distorted price).
- The attacker performs additional LP removals to redeem remaining WBNB from LP positions.
- The flash swap callback returns. The attacker repays the Moolah flash loan (358,681 WBNB) and transfers ~381.75 WBNB profit to the EOA.
Detailed Call Trace
EOA (0xdb0901a3)
└── CALL AttackerContract (0xdf7ed22d).exploit(0x279f9e46)
├── STATICCALL WBNB (0xbb4cdb9c).balanceOf(Moolah) → 358681.54 WBNB
└── CALL Moolah Proxy (0x8f73b65b).flashLoan(WBNB, 358681.54e18, calldata) (0xe0232b42)
└── DELEGATECALL Moolah Impl (0x9321587e).flashLoan(...)
├── CALL WBNB.transfer(AttackerContract, 358681.54e18) → flash loan disbursed
└── CALL AttackerContract.onMoolahFlashLoan(358681.54e18, data) (0x13a1a562)
├── CALL WBNB.transfer(Pair, ~0.000004 WBNB) [seed WBNB for LP mint]
├── CALL MT.transfer(Pair, 0.2 MT) [seed MT for LP mint]
├── CALL Pair (0x037e6eb2).mint(AttackerContract) → receive LP tokens
│ (SYNC log#42: MT=17,756,516.74, WBNB=384.27 — initial state)
│ (SYNC log#49: MT=17,756,516.94, WBNB=384.27 — after mint)
├── CALL WBNB.approve(PancakeRouter, ~497 WBNB)
├── CALL PancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
│ ~497 WBNB, 0, [WBNB, MT], AttackerContract, deadline)
│ [BUY ~10M MT from pool; pair sends MT to router then to AttackerContract]
│ ├── CALL WBNB.transferFrom(AttackerContract, Pair, ~497 WBNB)
│ └── CALL Pair.swap(10M MT out, 0 WBNB out, PancakeRouter, "")
│ └── CALL MT.transfer(PancakeRouter, 10M MT) → log#53
│ (SYNC log#54: MT=7,756,516.94, WBNB=880.93)
├── [AttackerContract receives 10M MT from router — log#64]
├── CALL Pair.approve(PancakeRouter, LP_small)
├── CALL PancakeRouter.removeLiquidityETHSupportingFeeOnTransferTokens(MT, LP_small, ...)
│ └── CALL Pair.burn(PancakeRouter) → returns tiny MT + WBNB
│ (SYNC log#62: MT=7,756,516.90, WBNB=880.93 — minor LP burn)
│
├── CALL Pair.swap(0, ~158.8 WBNB, AttackerContract, calldata) (0x022c0d9f)
│ [Flash swap: request WBNB out, pass callback data]
│ ├── CALL WBNB.transfer(AttackerContract, ~158.8 WBNB) → log#66
│ └── CALL AttackerContract.pancakeCall(sender, 0, ~158.8 WBNB, data) (0x84800812)
│ │
│ ├── CALL MT.transfer(Pair, ~9M MT) [settle flash swap partially]
│ │ └── [MT._transfer triggers fee → CALL MT_FeeDistributor.distributeFees()
│ │ ├── swapExact... → REVERTS "Pancake: LOCKED"
│ │ └── CALL MT.transfer(0x9edce171, eco_fee_MT)]
│ │ (SYNC log#74: MT=16,756,516.94, WBNB=483.60)
│ │
│ ├── CALL WBNB.approve(PancakeRouter, ~718 WBNB)
│ ├── CALL PancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
│ │ ~718 WBNB, 0, [WBNB, MT], AttackerContract, deadline)
│ │ [BUY second batch of ~10M MT from pool]
│ │ (SYNC log#79: MT=6,756,516.94, WBNB=1,201.15)
│ │
│ ├── CALL Pair.approve(PancakeRouter, LP_small)
│ ├── CALL PancakeRouter.removeLiquidityETHSupportingFeeOnTransferTokens(...)
│ │ [small LP removal, burns LP → tiny MT + WBNB]
│ │ (SYNC log#86: MT=6,756,516.90, WBNB=1,201.15)
│ │
│ ├── CALL LPRewardDistributor (0x139bd2ec).distributeDailyRewards() (0x501788af)
│ │ [*** CORE EXPLOIT STEP ***]
│ │ ├── STATICCALL Pair.getReserves() → MT=6,756,516.90, WBNB=1,201.15
│ │ ├── STATICCALL Pair.token0() → MT address (confirms reserve0=MT)
│ │ └── CALL MT.extractFromPoolForLpMining(~6,735,516 MT) (0x962b2809)
│ │ ├── [_executePendingBurn: burns ~6,735,517 MT from balanceOf[Pair]
│ │ │ Transfer(Pair, 0xdead, ~6,735,517 MT) — log#90]
│ │ └── CALL Pair.sync() (0xfff6cae9)
│ │ [Pair reserves updated to actual balances]
│ │ ├── STATICCALL MT.balanceOf(Pair) → ~21,000 MT
│ │ └── STATICCALL WBNB.balanceOf(Pair) → 1,201.15 WBNB
│ │ (SYNC log#92: MT=21,000.00, WBNB=1,201.15 — CONFIRMED)
│ │
│ ├── CALL MT.approve(PancakeRouter, ~10M MT)
│ ├── CALL PancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
│ │ ~10M MT, 0, [MT, WBNB], AttackerContract, deadline)
│ │ [SELL 10M MT into depleted pool → receives ~1,198.6 WBNB — log#96]
│ │ (SYNC log#97: MT=10,021,000.04, WBNB=2.52 — most WBNB drained)
│ │
│ └── [additional LP removal for remaining WBNB; WBNB deposit/withdraw]
│
├── CALL WBNB.approve(Moolah, 358681.54 WBNB) [prepare flash loan repayment]
├── CALL WBNB.transfer(Moolah, 358681.54 WBNB) [repay flash loan — log#100]
└── CALL WBNB.transfer(EOA, 381.75 WBNB) [profit — log#102]
Financial Impact
Based on funds_flow.json (primary evidence):
| Party | Change |
|---|---|
Attacker EOA (0xdb0901a3) | +381.75 WBNB profit |
Attacker Contract (0xdf7ed22d) | net ~0 WBNB (flash loan borrowed and repaid), net 0 MT |
MT/WBNB PancakeSwap Pair (0x037e6eb2) | -381.75 WBNB, -7,735,517 MT (reserves drained) |
0xdead (burn address) | +6,735,517 MT permanently burned by extractFromPoolForLpMining |
Fee Distributor (0x048f012b) | +250,000 MT (accumulated fee MT, illiquid) |
Ecosystem address (0x9edce171) | +750,000 MT (eco fee MT, illiquid) |
| Moolah flash loan pool | net 0 (repaid in full) |
Total attacker profit: ~381.75 WBNB.
At ~$635/WBNB (approximate market rate at time of attack), this equates to approximately $242,100 USD.
Protocol solvency impact: The MT/WBNB PancakeSwap V2 pair was severely damaged. The MT reserve was burned from ~6.76M MT (at the moment of extraction) down to the MIN_LP_SUPPLY floor of 21,000 MT — a reduction of ~6.74M MT tokens, permanently burned to 0xdead. The pair’s WBNB reserve of ~1,201 WBNB was then drained to near zero (~2.52 WBNB) by the attacker’s final sell. Legitimate LP holders suffered near-total loss of WBNB liquidity. The initial pre-attack pool state (17.76M MT, 384 WBNB) had been inflated to 1,201 WBNB by the attacker’s own WBNB injections before the burn, which is how the profit ratio was achieved.
Evidence
Key Log Entries (from receipt.json / funds_flow.json)
| Log Index | Event | From | To | Amount |
|---|---|---|---|---|
| 38 | WBNB Transfer | Moolah (0x8f73b6) | AttackerContract | 358,681.54 WBNB |
| 53 | MT Transfer | Pair | PancakeRouter | 10,000,000 MT (first buy) |
| 78 | MT Transfer | Pair | PancakeRouter | 10,000,000 MT (second buy) |
| 90 | MT Transfer | Pair (0x037e6e) | 0xdead | 6,735,516.9 MT (burn) |
| 96 | WBNB Transfer | Pair | AttackerContract | 1,198.63 WBNB (sell proceeds) |
| 100 | WBNB Transfer | AttackerContract | Moolah | 358,681.54 WBNB (repayment) |
| 102 | WBNB Transfer | AttackerContract | EOA | 381.75 WBNB (profit) |
SYNC Event Reserve Progression (from receipt.json)
| Log Index | Event | MT Reserve (reserve0) | WBNB Reserve (reserve1) | Stage |
|---|---|---|---|---|
| 42 | SYNC | 17,756,516.74 MT | 384.27 WBNB | Pre-attack initial state |
| 49 | SYNC | 17,756,516.94 MT | 384.27 WBNB | After LP mint (tiny seed) |
| 54 | SYNC | 7,756,516.94 MT | 880.93 WBNB | After first 10M MT purchase |
| 62 | SYNC | 7,756,516.90 MT | 880.93 WBNB | After first small LP burn |
| 74 | SYNC | 16,756,516.94 MT | 483.60 WBNB | After ~9M MT transferred back to pair |
| 79 | SYNC | 6,756,516.94 MT | 1,201.15 WBNB | After second 10M MT purchase |
| 86 | SYNC | 6,756,516.90 MT | 1,201.15 WBNB | After second small LP burn |
| 92 | SYNC | 21,000.00 MT | 1,201.15 WBNB | After extractFromPoolForLpMining + sync() |
| 97 | SYNC | 10,021,000.04 MT | 2.52 WBNB | After attacker sells 10M MT |
Note: MT address (0xb32979f3...) < WBNB address (0xbb4cdb9c...), therefore MT = token0 = reserve0 and WBNB = token1 = reserve1 in the PancakeSwap V2 pair. The pre-attack slot8 prestate value 0x69aec27d000000000014d4d22a569a015cf80000000eb016b65c79cf077f5585 decodes to: reserve0(MT) = 0xeb016b65c79cf077f5585 = 17,756,516.94 MT; reserve1(WBNB) = 0x14d4d22a569a015cf8 = 384.27 WBNB.
Selector Verification
All selectors verified with cast sig:
extractFromPoolForLpMining(uint256)→0x962b2809✓distributeDailyRewards()→0x501788af✓pancakeCall(address,uint256,uint256,bytes)→0x84800812✓onMoolahFlashLoan(uint256,bytes)→0x13a1a562✓exploit(address,address,address,address)→0x279f9e46✓flashLoan(address,uint256,bytes)→0xe0232b42✓
Related URLs
- Transaction: https://bscscan.com/tx/0xfb57c980286ea8755a7b69de5a74483c44b1f74af4ab34b7c52e733fc62dfca6
- MT Token: https://bscscan.com/address/0xb32979f3a5b426a4a6ae920f2b391d885abf4c18
- LP Reward Distributor: https://bscscan.com/address/0x139bd2ecfde76f5311d7beeb2e05cba6fede26d6
- MT/WBNB Pair: https://bscscan.com/address/0x037e6eb26275dbfe3a5244239bbe973f1a56b449