Calculated from recorded token losses using historical USD prices at the incident time.
0x189a8dc1e0fea34fd7f5fa78c6e9bdf099a8d575ff5c557fa30d90c6acd0b29f0x4341bdced3908a45835c67a2dbbde2d2daa6645dBSC0x5813d7818c9d8f29a9a96b00031ef576e892def4BSCOn BNB Smart Chain block 34083189, transaction 0x189a8dc1e0fea34fd7f5fa78c6e9bdf099a8d575ff5c557fa30d90c6acd0b29f let an unprivileged adversary drain the MAMO/USDT PancakeSwap pool by abusing Matmo's public buy helper. The attacker EOA 0x829Fe73463cEAE6579973b8bcd1e018976040ec4 called its own contract 0xd7a7d90B63da1B4E7eF79cb36935D38aF0D6D0b4, borrowed 19 WBNB from DODO, unwrapped it to BNB, and then called helper 0xa915Bb6D5C117fB95E9ac2edDaE68AAd5EdB5841::BuyToken(address).
That helper was already whitelisted inside the MAMO token and used its privilege to call giveawayOne twice: once for the attacker contract and once for the MAMO/USDT pair 0x5813d7818c9d8F29A9a96B00031ef576E892DEf4. Because MAMO counts treasury credits inside balanceOf(account), the pair's effective MAMO balance jumped even though no funded user transfer moved MAMO into the pair. PancakePair then treated the pair-side treasury credit as fresh token input and paid out 5683062170081466106194 USDT. The attacker swapped that USDT into WBNB, repaid the flashloan, and kept 5680683707976806110 WBNB profit before gas.
Matmo's MAMO token is not a plain ERC-20 balance ledger. It tracks normal balances in _balances, but it also tracks treasury buckets in _treasury, and returns both together. The transfer path can settle from those treasury buckets, so treasury credits are economically spendable.
balanceOf(account)The critical victim-side code is in the verified MAMO source:
function giveawayOne(
address _addr,
uint _amount,
uint8 times
) external returns (bool) {
require(_liquidity[_msgSender()] == 1, "Error: Operation failed");
if (times == 1) {
_treasury[ANCHOR][_addr].incrFund(_amount);
} else if (times > 1) {
_treasury[BANK][_addr].incrFund(_amount);
}
emit Transfer(address(0), _addr, _amount);
_capacity += _amount;
return true;
}
function balanceOf(address account) public view returns (uint256) {
return _balances[account] + getTreasury(account);
}
This matters because giveawayOne does not debit any existing holder. It only requires that msg.sender is marked in _liquidity. Historical storage at block 34083188 confirms _liquidity[0xa915Bb6D5C117fB95E9ac2edDaE68AAd5EdB5841] = 1, so the helper had mint privilege before the exploit transaction.
The helper itself was not attacker-owned. Historical storage at the same block shows helper slot 0 was 0x92a9a66763b466956ac8eefda8921c8c03715464, not the attacker EOA, and slot 14 was 10000000000000000 wei, meaning the public path only required a 0.01 BNB minimum payment. The MAMO/USDT pair also held live liquidity before the exploit, with reserves:
reserve0 (MAMO): 30503694129561858470407619
reserve1 (USDT): 23976626623894851541714
The root cause is an access-control and accounting failure in the Matmo token system, not a PancakeSwap bug. Matmo exposed a privileged mint path through a public helper contract. That helper could be called by any user who paid the configured minimum BNB amount, and once invoked it called MAMO.giveawayOne(...) under the helper's whitelisted identity.
The decisive accounting flaw is that MAMO counts treasury credits immediately inside balanceOf(account). When the helper minted treasury-backed MAMO to the MAMO/USDT pair, PancakePair observed a higher token balance even though no attacker-funded MAMO transfer had entered the pair. That violated the AMM assumption that reserve-affecting token balances only increase when real inventory is transferred in. The code-level breakpoint is therefore the combination of giveawayOne and balanceOf, reached through the public helper BuyToken(address) entry point.
This incident is ACT. The exploit required no privileged key, no private orderflow, and no attacker-owned protocol role. An unprivileged caller only needed public on-chain state, a flashloan, and the helper's public payable entry point.
Immediately before the exploit transaction, the public pre-state already satisfied all exploit conditions:
0xa915...5841 whitelisted in _liquidity.0.01 BNB, far below the 19 BNB funded by the flashloan.Those conditions are sufficient to make the privileged mint path publicly reachable.
The invariant that should have held is: the pair-side token balance used by PancakePair pricing must only increase when real transferable token inventory is moved into the pair by a funded actor. MAMO breaks that invariant because pair-side treasury accounting is treated as spendable balance:
function _safeTransfer(
address account_,
address recipient,
uint amount
) internal returns (uint) {
uint left = amount;
...
for (uint8 i = 0; left > 0 && i < ROUND; i++) {
left = _treasury[i][account_].settle(left);
}
require(left == 0, "Failed: Invalid balance");
_balances[recipient] += amount;
return amount;
}
Because treasury value can later settle into transfers, balanceOf(pair) is not merely cosmetic. Once the helper mints treasury-backed MAMO to the pair, PancakePair's reserve accounting sees that treasury credit as immediately usable token input.
The historical trace shows the entire exploit path:
0xd7a7...::DVMFlashLoanCall(...)
WBNB::withdraw(19000000000000000000)
0xa915...::BuyToken{value: 19000000000000000000}(PancakePair: [0x5813...DEf4])
MAMO::giveawayOne(0xd7a7..., 95000000000000000000000000, 2)
MAMO::giveawayOne(PancakePair: [0x5813...DEf4], 9500000000000000000000000, 1)
PancakePair::swap(0, 5683062170081466106194, 0xd7a7..., 0x)
The same trace records the pair-side swap accounting:
emit Swap(
sender: 0xd7a7d90B63da1B4E7eF79cb36935D38aF0D6D0b4,
amount0In: 9500000000000000000000000,
amount1In: 0,
amount0Out: 0,
amount1Out: 5683062170081466106194,
to: 0xd7a7d90B63da1B4E7eF79cb36935D38aF0D6D0b4
)
amount0In here is the synthetic MAMO input manufactured by the helper's treasury mint. No attacker-funded MAMO transfer preceded it. That is the precise point where the AMM accounting invariant breaks.
The attacker flow was a single deterministic transaction:
0x829Fe73463cEAE6579973b8bcd1e018976040ec4 called attacker contract 0xd7a7d90B63da1B4E7eF79cb36935D38aF0D6D0b4.19 WBNB from DODO pool 0xD534fAE679f7F02364D177E9D44F1D15963c0Dd7.BuyToken(address) with the MAMO/USDT pair as the argument.9.5e25 MAMO treasury units to the attacker contract and 9.5e24 MAMO treasury units to the MAMO/USDT pair.5683062170081466106194 USDT, approved PancakeRouter 0x10ED43C718714eb63d5aA57B78B54704E256024E, and swapped the USDT through the USDT/WBNB pair 0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE.24680683707976806110 WBNB, from which the attacker repaid the 19 WBNB flashloan and transferred the remaining 5680683707976806110 WBNB to the originating EOA.The profit realization is explicit in the trace:
WBNB::transfer(DVM: [0xD534...0Dd7], 19000000000000000000)
WBNB::transfer(0x829Fe73463cEAE6579973b8bcd1e018976040ec4, 5680683707976806110)
This also explains the adversary cluster: the EOA is the gas payer and final profit recipient, while contract 0xd7a7... is the transient execution harness that performed the public helper call, pair swap, and flashloan settlement.
The MAMO/USDT pair lost 5683062170081466106194 USDT units in the exploit transaction. On BSC USDT uses 18 decimals, so the drained amount is 5683.062170081466106194 USDT.
The attacker EOA received 5680683707976806110 WBNB after settlement and paid 1088997000000000 wei in gas. The exploit path remained permissionless as long as three conditions held simultaneously: the helper stayed whitelisted, the helper's public payable entry point stayed callable, and the MAMO/USDT pair still held counter-asset liquidity.
0x189a8dc1e0fea34fd7f5fa78c6e9bdf099a8d575ff5c557fa30d90c6acd0b29f on BNB Smart Chain block 34083189.0x4341bdCEd3908A45835C67A2DbBDe2d2dAA6645D.0xa915Bb6D5C117fB95E9ac2edDaE68AAd5EdB5841.0x5813d7818c9d8F29A9a96B00031ef576E892DEf4.0xD534fAE679f7F02364D177E9D44F1D15963c0Dd7.34083188 confirming the helper whitelist flag, helper owner, and helper minimum-payment threshold.