This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0x80a0d7a6fd2a22982ce282933b384568e5c852bfBSC0x8a4aa176007196d48d39c89402d3753c39ae64c1BSCAn unprivileged attacker repeatedly exploited MinterProxyV2 on BSC to steal ERC20 balances from address 0x8A4AA176007196D48d39C89402d3753c39AE64c1, a holder that had previously approved MinterProxyV2 at 0x80a0D7A6FD2A22982Ce282933b384568E5c852bF as spender. In transactions 0x051276afa96f2a2bd2ac224339793d82f6076f76ffa8d1b9e6febd49a4ec11b2, 0x407e09faabf7072cd10dc86b7fa3180ccc1701f52f7fdca29464568498c30997, and 0xd348b5fc00b26fc1457b70d02f9cb5e5a66a564cc4eba2136a473866a47dac08, attacker EOAs used helper contracts to call the public swap() entrypoint with attacker-chosen target, receiveToken, receiver, and raw calldata.
The root cause is an arbitrary-call approval drain. MinterProxyV2.swap() forwards attacker-controlled calldata to an attacker-chosen target and treats any increase in an attacker-chosen receiveToken balance as valid swap output. The seed traces show the attacker using a helper token to create a synthetic 1-unit output while the forwarded call executes transferFrom(victim, attackerRecipient, victimBalance) on victim-approved tokens. The result is direct theft of approved balances, not slippage, pricing error, or MEV.
is a public swap router. Its function accepts the input token address, amount, target contract, output token, receiver, minimum output amount, and arbitrary from the external caller. That design is only safe if the router constrains those fields so the external call can spend only the caller's intended input and can produce only the expected output asset.
0x21d8b164f0cb8beb1ed27d164ed986c3fc26b33655ce18226b05b9cfcf6cd93c0xd348b5fc00b26fc1457b70d02f9cb5e5a66a564cc4eba2136a473866a47dac08MinterProxyV2swap()callDataThat binding is absent here. ERC20 approvals let an approved spender invoke transferFrom(owner, recipient, amount) without a new signature from the owner. Once a victim has approved MinterProxyV2, any unrestricted path inside MinterProxyV2 that can make arbitrary external calls becomes a spend primitive over the victim's allowance. The attacker exploited exactly that condition.
The relevant on-chain actors are:
MinterProxyV2: 0x80a0D7A6FD2A22982Ce282933b384568E5c852bF0x8A4AA176007196D48d39C89402d3753c39AE64c10x6eec0f4c017afe3dfadf32b51339c37e9fd59dfb0x69795d09aa99a305b4fc2ed158d4944bcd91d59a0x791c6542bc52efe4f20df0ee672b88579ae3fd9a0xacdbe7b770a14ca3bc34865ac3986c0ce771fd680x52b19de39476823d33ab4b1edbec91e29dadba38The vulnerability class is an unrestricted arbitrary external call in a public router combined with an unbound success predicate. The verified MinterProxyV2 code shows that swap() first snapshots the router's balance of the caller-chosen receiveToken, then transfers in the caller-chosen tokenAddr, optionally approves the caller-chosen target, and finally executes the caller-chosen callData against that target. After the call returns, the function checks only that the router's balance of receiveToken increased and then transfers that balance delta to the caller-chosen receiver.
That means the router never verifies that the external call represents a legitimate swap, never verifies that the spent asset belongs to the caller, and never verifies that the observed output token is economically related to the external call. The attacker therefore chose a malicious helper token as both tokenAddr and receiveToken, causing MinterProxyV2 to observe a trivial 1-unit helper-token increase when it pulled the helper token in. In the same swap() call, the attacker chose a victim token contract as target and encoded transferFrom(victim, attackerRecipient, victimBalance) as callData. Because the victim had already approved MinterProxyV2, the target token honored the forwarded transferFrom. The helper-token delta then satisfied the router's output check, so the call completed successfully while the real economic effect was theft from the victim.
The code-level breakpoint is the target.functionCall(callData, "MP: FunctionCall failed") statement followed by the new_balance > old_balance and _amountOut >= minAmount checks on a caller-selected receiveToken. The violated invariant is: a public swap router must not expose an arbitrary-call path that can spend third-party approvals unless the output asset and spent asset are tightly bound to the initiating user and validated against a real swap route.
The vulnerable path in the verified MinterProxyV2 source is:
function swap(
address tokenAddr,
uint256 amount,
address target,
address receiveToken,
address receiver,
uint256 minAmount,
bytes calldata callData,
bytes calldata order
) external payable nonReentrant whenNotPaused {
uint256 old_balance = _balanceOfSelf(receiveToken);
IERC20(tokenAddr).safeTransferFrom(_msgSender(), address(this), amount);
if (IERC20(tokenAddr).allowance(address(this), target) < amount) {
IERC20(tokenAddr).safeApprove(target, MAX_UINT256);
}
target.functionCall(callData, "MP: FunctionCall failed");
uint256 new_balance = _balanceOfSelf(receiveToken);
require(new_balance > old_balance, "MP: receive amount should above zero");
require(new_balance - old_balance >= minAmount, "MP: receive amount not enough");
IERC20(receiveToken).safeTransfer(receiver, new_balance - old_balance);
}
This logic appears in the verified source at swap() lines 683-737. The only success condition is a positive balance delta in receiveToken, and receiveToken is fully attacker-controlled.
The first seed trace shows the exploit concretely on BTCB. Inside transaction 0x407e09faabf7072cd10dc86b7fa3180ccc1701f52f7fdca29464568498c30997, the helper contract calls:
MinterProxyV2::swap(
0x791c6542...,
1,
0x7130d2A1...,
0x791c6542...,
0x791c6542...,
1,
0x23b872dd...(victim, attackerRecipient, victimBalance),
0x00
)
0x791c6542...::transferFrom(helper, MinterProxyV2, 1)
BEP20Token::transferFrom(victim, 0x69795D09..., 761239692987924742)
0x791c6542...::balanceOf(MinterProxyV2) => 21
0x791c6542...::transfer(helper, 1)
This trace proves four critical facts in one execution path:
MinterProxyV2 accepts a 1-unit helper-token movement as success.transferFrom against the victim-approved token.MinterProxyV2.The balance-diff artifact for the same transaction confirms the economic effect: BTCB balance at 0x8A4AA176007196D48d39C89402d3753c39AE64c1 fell from 761239692987924742 to 0, and BTCB at 0x69795d09aa99a305b4fc2ed158d4944bcd91d59a rose by the same amount.
The broader drain transaction 0xd348b5fc00b26fc1457b70d02f9cb5e5a66a564cc4eba2136a473866a47dac08 repeats the same structure across many tokens. Representative trace segments show repeated loops of:
0x52b19De3...::transferFrom(helper, MinterProxyV2, 1)
<victim token>::transferFrom(victim, 0xacDBE7..., victimBalance)
0x52b19De3...::balanceOf(MinterProxyV2) => prior + 1
0x52b19De3...::transfer(helper, 1)
emit LogVaultOut(... amount: 1 ...)
The collected balance diffs for that transaction show full drains from the same victim holder into 0xacdbe7b770a14ca3bc34865ac3986c0ce771fd68 for assets including USDC, XRP, MATIC, SHIB, and 1INCH. Those outputs are consistent with the reported exploit conditions:
MinterProxyV2 a sufficient allowance;receiveToken delta;transferFrom calldata.The ACT framing is supported by the execution model. No privileged key, admin path, or private protocol state was required. Any unprivileged attacker could deploy a helper token/contract, inspect public victim allowances and balances, and submit the same swap() pattern directly on BSC.
The adversary flow is deterministic and repeated.
First, attacker EOA 0x6eec0f4c017afe3dfadf32b51339c37e9fd59dfb sent transactions directly to helper contract 0x791c6542bc52efe4f20df0ee672b88579ae3fd9a. The helper then called MinterProxyV2.swap() with itself as tokenAddr and receiveToken, with the victim token as target, and with transferFrom(victim, 0x69795d09..., victimBalance) as raw calldata. Transaction 0x051276... drained multiple tokens, 0x407e09... drained BTCB, and 0x21d8b164... reused the same pattern but produced no ERC20 balance delta.
Second, attacker EOA 0xacdbe7b770a14ca3bc34865ac3986c0ce771fd68 repeated the same method through helper contract 0x52b19de39476823d33ab4b1edbec91e29dadba38 in transaction 0xd348b5fc00b26fc1457b70d02f9cb5e5a66a564cc4eba2136a473866a47dac08. The trace shows the helper iterating over multiple victim-approved tokens and invoking the same swap() pattern for each asset. Each successful iteration moved one token balance from the victim to the attacker EOA while consuming only a synthetic 1-unit helper-token movement to satisfy MinterProxyV2.
The decision points are straightforward:
allowance(victim, MinterProxyV2) < balanceOf(victim), the drain attempt would fail or only partially succeed.swap() would revert on new_balance > old_balance.transferFrom for MinterProxyV2, the drain would fail.In the successful traces, all three conditions were satisfied, so the attack path completed end-to-end.
The impact is direct theft of victim-approved ERC20 balances from 0x8A4AA176007196D48d39C89402d3753c39AE64c1. The loss overview recorded in the validated root-cause input is:
USDT: 20580187820908676022964BTCB: 761239692987924742USDC: 12860437261686192189809XRP: 110775895260635480709999MATIC: 3339743837585002326098SHIB: 1397050837707373056074417651INCH: 3114421514767735651283TLOS: 10763844656650669551555All amounts are raw on-chain integer amounts in smallest units. The seed balance diffs show the victim balances going to zero or decreasing sharply and the corresponding attacker-controlled recipients increasing by the same raw amounts. This is not accounting drift inside the protocol. The loss mechanism is the protocol acting as an unrestricted spender over victim approvals.
MinterProxyV2 source at 0x80a0D7A6FD2A22982Ce282933b384568E5c852bF, especially swap() at lines 683-737.0x407e09faabf7072cd10dc86b7fa3180ccc1701f52f7fdca29464568498c30997, showing helper-token transferFrom, victim-token transferFrom, and helper-token output check.0xd348b5fc00b26fc1457b70d02f9cb5e5a66a564cc4eba2136a473866a47dac08, showing the same pattern repeated across a larger token basket.0x407e09faabf7072cd10dc86b7fa3180ccc1701f52f7fdca29464568498c30997, confirming the BTCB drain from the victim to 0x69795d09aa99a305b4fc2ed158d4944bcd91d59a.0xd348b5fc00b26fc1457b70d02f9cb5e5a66a564cc4eba2136a473866a47dac08, confirming multi-token drains from the same victim to 0xacdbe7b770a14ca3bc34865ac3986c0ce771fd68.