Mosca double-withdrawal exploit via helper on BNB
Exploit Transactions
Victim Addresses
0x1962b3356122d6a56f978e112d14f5e23a25037dBSCLoss Breakdown
Similar Incidents
H2O helper-token reward drain from unauthorized claim loop
37%LPMine WTO overvaluation bug enables LP double-counting drain
36%Orion redeemAtomic exploit drains BNB and token reserves
36%SlurpyCoin BuyOrSell flaw drains BNB via flash-loan swaps
35%BNB-chain constructor exploit drains full USDT pool balance
35%AST liquidity-tracking flaw burns AST reserves and yields BNB profit
34%Root Cause Analysis
Mosca double-withdrawal exploit via helper on BNB
1. Incident Overview TL;DR
On BNB Chain, the Mosca subscription/referral protocol (contract 0x1962b3356122d6a56f978e112d14f5e23a25037d) was exploited via a custom helper contract deployed by adversary EOA 0xb7d7240c207e094a9be802c0f370528a9c39fed5. In exploit transaction 0x4e5bb7e3f552f5ee6ee97db9a9fcf07287aae9a1974e24999690855741121aff, the helper used a flash loan and a specific sequence of Mosca buy, join, and exitProgram calls combined with PancakeSwap swaps to extract large quantities of USDT and USDC from Mosca.
The core bug is in Mosca’s withdraw logic. The private function withdrawAll(address) sums three internal accounting fields (balance, balanceUSDT, balanceUSDC) and pays out that combined amount in a single stablecoin without zeroing all of the contributing fields. The public exitProgram() function calls withdrawAll(msg.sender) and only partially resets user state. By carefully setting internal balances and calling exitProgram twice in a single transaction, the helper causes Mosca to pay out the same claim twice, then swaps the duplicated stablecoins to WBNB and sends 27.302344169765420745 WBNB to the adversary EOA, who pays 0.005971938 BNB in gas and nets 27.296372231765420745 BNB profit.
2. Key Background
Mosca is a subscription/referral dApp that tracks per-user balances in an internal ledger. For each user, the contract maintains multiple fields, including:
balance: internal balance representing generic rewards.balanceUSDTandbalanceUSDC: internal balances representing fiat-denominated rewards in USDT and USDC.- referral-related fields such as
refCode,collectiveCode, andinviteCount, plus therewardQueuearray of active users.
Verified Mosca source code is available under artifacts/root_cause/data_collector/iter_1/contract/56/0x1962b3...5037d/source/src/Contract.sol. Mosca integrates with standard BEP20 tokens:
- USDC (
0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d, implementationBEP20TokenImplementationat0xba5fe23f8a3a24bed3236f05f2fcf35fd0bf0b5c), - USDT (
0x55d398326f99059ff775485246999027b3197955,BEP20USDT), - WBNB (
0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c).
Mosca users can:
buyinto the program using USDC,jointo enter the reward queue,withdrawFiatandwithdrawAllto cash out internal balances in USDT or USDC,exitProgramto leave the queue and withdraw.
The exploit routes value through PancakeSwap:
- V3-like pool
0x92b7807bf19b7dddf89b706143896d05228f3121(flash-loan/flash-swap source of USDC), - V2 LP
0x16b9a82891338f9ba80e2d6970fdda79d1eb0daeand router0x10ed43c718714eb63d5aa57b78b54704e256024e, - Router
0xd99c7f6c65857ac913a8f880a4cb84032ab2fc5b.
The adversary-related accounts are:
- EOA
0xb7d7240c207e094a9be802c0f370528a9c39fed5(controls the exploit), - Helper/orchestrator contract
0x851288dcfb39330291015c82a5a93721cc92507a, deployed by the EOA in tx0x39467ca3f7eddaa51104b89d840bc718203f9a97a62aaa6b0d66ae2ef0b78fb8.
The opportunity is permissionless (an ACT opportunity): any EOA can deploy an equivalent helper and execute the same buy/join/exitProgram sequence while Mosca and Pancake liquidity are in a similar state.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is a state-accounting bug in Mosca’s withdraw logic. Conceptually, Mosca intends that each user’s total withdrawals across USDT/USDC never exceed their net deposits plus earned rewards. The contract however maintains three separate internal balance fields (balance, balanceUSDT, balanceUSDC) and uses them inconsistently across withdrawal paths.
The private withdrawAll(address) function computes:
function withdrawAll(address addr) private {
User storage user = users[addr];
require(msg.sender == user.walletAddress, "Wallet addresses do not match");
uint balance = user.balance + user.balanceUSDT + user.balanceUSDC;
if (usdc.balanceOf(address(this)) >= balance) {
usdc.transfer(user.walletAddress, balance);
emit WithdrawAll(user.walletAddress, block.timestamp, balance, 2);
} else {
usdt.transfer(user.walletAddress, balance);
emit WithdrawAll(user.walletAddress, block.timestamp, balance, 1);
}
}
The public exitProgram() function calls withdrawAll(msg.sender) and only partially clears internal state:
function exitProgram() external nonReentrant {
require(!isBlacklisted[msg.sender], "Blacklisted user");
User storage user = users[msg.sender];
address referrer = referrers[user.collectiveCode];
if (referrer != address(0) && users[referrer].inviteCount > 0) {
users[referrer].inviteCount--;
}
for (uint256 i = 0; i < rewardQueue.length; i++) {
address userAddr = rewardQueue[i];
if (userAddr == msg.sender) {
// Perform withdrawal before modifying user state
withdrawAll(msg.sender);
// Remove user from reward queue and reset state
refByAddr[userAddr] = 0;
referrers[user.refCode] = 0x000000000000000000000000000000000000dEaD;
user.balance = 0;
user.enterprise = false;
rewardQueue[i] = rewardQueue[rewardQueue.length - 1];
rewardQueue.pop();
emit ExitProgram(msg.sender, block.timestamp);
}
}
}
Crucially, withdrawAll never zeroes balance, balanceUSDT, or balanceUSDC, and exitProgram only zeroes user.balance, leaving balanceUSDT and balanceUSDC untouched. Combined with other flows that credit these fields, this allows a user (or helper contract) to engineer repeated full cash-outs of the same underlying claim via multiple exitProgram calls in a single transaction.
4. Detailed Root Cause Analysis
This section walks through how the Mosca accounting bug is exploited in seed transaction 0x4e5b... using concrete on-chain evidence.
4.1 Setup and Flash Loan
In transaction 0x3946... (block 45519930), the adversary EOA 0xb7d7... deploys helper contract 0x8512... with constructor arguments tying it to Mosca, USDT/USDC, Pancake routers, and a Pancake V3 pool. This helper is used exactly once in the exploit.
In the exploit transaction 0x4e5b... (block 45519931), the EOA calls the helper with:
from = 0xb7d7...,to = 0x8512...,value = 0,- calldata encoding Mosca address
0x1962...and an amount1000000000000000000000(1e21 units).
From the seed tx_receipt.json and trace.cast.log, the helper obtains USDC via an interaction with the Pancake V3-like pool 0x92b7...:
92b7... (Pancake V3 pool)
... flash-like Swap/Flash event involving 0x8512...
The resulting USDC is then sent into Mosca through its buy and join functions, crediting the helper’s internal balances (in particular, users[0x8512].balanceUSDC) to a large value on the order of 9.8e20 units.
4.2 Double Withdraw via exitProgram / withdrawAll
Once the helper has inflated its Mosca internal balances, it calls exitProgram() twice in the same transaction. Each call traverses the reward queue, finds msg.sender, and executes:
withdrawAll(msg.sender), computingbalance = balance + balanceUSDT + balanceUSDC.- Transfers
balanceworth of USDT or USDC from Mosca’s on-chain token holdings touser.walletAddress(the helper). - Emits a
WithdrawAllevent. - Zeroes only
user.balanceand some referral data;balanceUSDTandbalanceUSDCremain unchanged.
Because the internal fields are not all zeroed, the second exitProgram call sees the same non-zero total and pays it out again. This behavior is observable in Mosca’s events and ERC20 balance diffs.
From balance_diff.json for the seed tx:
{
"erc20_balance_deltas": [
{
"token": "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d",
"holder": "0x1962b3356122d6a56f978e112d14f5e23a25037d",
"delta": "-11366118226600985221667"
},
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x1962b3356122d6a56f978e112d14f5e23a25037d",
"delta": "-8881004926108374384231"
}
]
}
These raw 18-decimal deltas show Mosca losing:
11,366,118,226,600,985,221,667units of USDC,8,881,004,926,108,374,384,231units of USDT.
The matching positive deltas are observed on Pancake router and LP addresses, indicating that these stablecoins are forwarded out of Mosca and into the DEX path orchestrated by the helper.
4.3 Conversion to WBNB and Profit Realization
The helper routes the duplicated stablecoin withdrawals through PancakeSwap, ultimately accumulating WBNB at its own address and then forwarding it to the EOA. The critical transfer is visible in the seed trace:
WBNB::transfer(0xb7D7240c207e094a9Be802C0f370528A9C39Fed5, 27302344169765420745 [2.73e19])
This corresponds to 27.302344169765420745 WBNB being transferred from the helper 0x8512... to the adversary EOA 0xb7d7.... The transaction receipt tx_receipt.json contains the matching WBNB Transfer event from 0x8512... to 0xb7d7... with the same value.
From balance_diff.json, the adversary’s native BNB balance changes by:
{
"address": "0xb7d7240c207e094a9be802c0f370528a9c39fed5",
"delta_wei": "-5971938000000000"
}
-5,971,938,000,000,000 wei is -0.005971938 BNB, representing gas cost for the exploit transaction. Treating WBNB as the reference asset, the opportunity-specific portfolio for the adversary is:
value_before_in_reference_asset = 0(by definition of the opportunity-specific portfolio),value_after_in_reference_asset = 27.302344169765420745BNB (WBNB received),fees_paid_in_reference_asset = 0.005971938BNB (gas),value_delta_in_reference_asset = 27.296372231765420745BNB (after - fees).
These figures are fully determined from on-chain balances and traces with no remaining placeholders.
4.4 Invariant and Breakpoint
An appropriate invariant for Mosca is:
For every user, the sum of all withdrawals from Mosca (across USDT and USDC) must never exceed their net deposits plus earned rewards recorded in internal accounting.
At the code level, the breakpoint is:
withdrawAll(address)computesbalance = user.balance + user.balanceUSDT + user.balanceUSDCand transfers that entire amount from Mosca’s stablecoin balances to the user, without resetting these internal fields.exitProgram()callswithdrawAll(msg.sender)and then setsuser.balance = 0but does not resetbalanceUSDTorbalanceUSDC.
The helper exploits this by:
- Inflating the internal balances via
buy/join. - Calling
exitProgram()twice in the same transaction, each time triggeringwithdrawAllto pay the full sum.
The result is that Mosca’s actual token holdings decrease by more than any legitimate combination of deposits and rewards would justify, violating the invariant.
5. Adversary Flow Analysis
The end-to-end adversary flow consists of a short transaction sequence on BNB Chain:
-
Helper deployment (tx
0x3946..., block45519930):- From
0xb7d7...to a contract-creation address. - Deploys
0x8512...with parameters linking it to Mosca (0x1962...), USDT/USDC, Pancake routers, and a Pancake V3 pool. - This prepares a programmable entry point for the exploit.
- From
-
Exploit transaction (tx
0x4e5b..., block45519931):from = 0xb7d7...,to = 0x8512...,value = 0.- The helper:
- Obtains USDC via an interaction with Pancake V3 pool
0x92b7...(flash swap/loan). - Calls Mosca
buy/jointo record a large internal USDC balance for itself. - Calls
exitProgram()twice, each time triggeringwithdrawAll(msg.sender)and causing Mosca to pay out the same combined internal claim twice in stablecoins. - Routes the duplicated stablecoins through PancakeSwap routers and LPs (
0xd99c7...,0x16b9...) to swap into WBNB. - Sends
27.302344169765420745WBNB to0xb7d7...viaWBNB::transfer.
- Obtains USDC via an interaction with Pancake V3 pool
-
Net result for the adversary:
- Receives
27.302344169765420745WBNB. - Pays
0.005971938BNB in gas. - Nets
27.296372231765420745BNB profit in this opportunity-specific portfolio.
- Receives
All steps use only public contracts and functions (Mosca public entry points, Pancake routers/pools, a standard WBNB token) and a single adversary-controlled helper. No privileged roles or non-public infrastructure are required.
6. Impact & Losses
The primary victim is the Mosca protocol, which suffers large outflows of USDC and USDT from its contract balances. From balance_diff.json for the exploit tx:
- Mosca USDC (
0x8ac76...) balance decreases by11366118226600985221667units. - Mosca USDT (
0x55d398...) balance decreases by8881004926108374384231units.
These amounts correspond to user/protocol funds that are effectively diverted into the adversary’s DEX route and ultimately converted to WBNB. The adversary EOA ends the transaction with a net profit of 27.296372231765420745 BNB in WBNB terms, while Mosca’s internal accounting no longer matches its on-chain token holdings, undermining the integrity of its balances and payouts.
7. References
- Seed transaction trace and metadata for
0x4e5b7e3f552f5ee6ee97db9a9fcf07287aae9a1974e24999690855741121aff(BNB Chain, block45519931), includingtrace.cast.log,balance_diff.json, andmetadata.jsonunderartifacts/root_cause/seed/56/0x4e5b.../. - Mosca contract source at
0x1962b3356122d6a56f978e112d14f5e23a25037d, stored underartifacts/root_cause/data_collector/iter_1/contract/56/0x1962b3...5037d/source/src/Contract.sol. - Stablecoin and WBNB token sources: USDC (
BEP20TokenImplementation), USDT (BEP20USDT), WBNB (WBNB) underartifacts/root_cause/seed/56/*/src/Contract.sol. - PancakeSwap contracts involved in the exploit: V3 pool
0x92b7807bf19b7dddf89b706143896d05228f3121, LP0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae, and router0xd99c7f6c65857ac913a8f880a4cb84032ab2fc5b. - Helper/orchestrator contract
0x851288dcfb39330291015c82a5a93721cc92507adeployment and usage traces underartifacts/root_cause/data_collector/iter_1/contract/56/0x8512.../and address txlists underartifacts/root_cause/data_collector/iter_1/address/56/.