Minto Fake-Token Purchase Exploit
Exploit Transactions
Victim Addresses
0xdbf1c56b2ad121fe705f9b68225378aa6784f3e5BSC0x410a56541bd912f9b60943fcb344f1e3d6f09567BSCLoss Breakdown
Similar Incidents
Biswap Migrator Token Substitution
33%Public mint flaw drains USDT from c3b1 token pool
33%GymRouter Arbitrary Approved Token Spend
33%PHIL Public Mint Drain
32%DexToken BEP20USDT pool drain from token-logic exploit
32%UN Burn-Skim Exploit
32%Root Cause Analysis
Minto Fake-Token Purchase Exploit
1. Incident Overview TL;DR
On BSC block 30214353, an unprivileged adversary exploited the Minto ReferralCrowdsale proxy at 0xDbF1C56b2aD121Fe705f9b68225378aa6784f3e5 by calling buyTokens() with an attacker-controlled contract as purchaseParams.paymentToken. Because the sale was operating with a non-zero manual price, the code path skipped the payment-token allowlist, transferred 14724096385542168674698 BTCMT to the attacker contract, then accepted the fake token's transferFrom(...) return value as if real payment had been received.
The attacker immediately swapped the stolen BTCMT for 9646445252364728917322 USDT on Pancake V3 and forwarded the USDT to EOA 0xc5001F60DB92aFcC23177A6c6B440A4226cb58Bf. After accounting for the exploit sender's BNB gas cost, the adversary cluster remained net positive by 9646087238958229401981 USDT units. The root cause is a concrete contract bug: buyTokens() enforces the payment-token whitelist only in the final branch, while _buy() pays out BTCMT before collecting and validating payment.
2. Key Background
The incident centers on the Minto ReferralCrowdsale implementation behind proxy 0xDbF1C56b2aD121Fe705f9b68225378aa6784f3e5, with verified implementation code at 0x0D116ed40831FEF8E21EcE57C8455ae3B1e4041B. The sale distributes BTCMT token 0x410a56541bD912F9B60943fcB344f1E3D6F09567 in exchange for approved payment assets.
Collector-backed pre-state reads at block 30214352 show the conditions that made the exploit possible:
dexInfo.manualPrice = 830000000000000000, so purchases entered the manual-price branch.getAllPaymentTokens()returned only USDT0x55d398326f99059fF775485246999027B3197955and BUSD0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56.- The referral link hash
0xc69c51e039668f688f28f427c63cd60aa986f8ce1546039e6a302fb721473814was already enabled and owned by0x602abFbBcF537714ba845681f842F868852589A0.
Those facts matter because buyTokens() required only public on-chain state: an enabled referral link readable through links(bytes32) and a caller-supplied paymentToken address. No privileged signature, private key, or hidden state was needed to reach the vulnerable path.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is an access-control and settlement-integrity failure in ReferralCrowdsale.buyTokens(address,...). The contract maintains an allowlist of approved payment tokens, but it checks paymentsTokens.contains(purchaseParams.paymentToken) only in the final fallback branch. When dexInfo.manualPrice > 0, execution instead enters the manual-price branch and forwards the caller-chosen paymentToken into _buy() with no allowlist enforcement. _buy() then calls _give() first, transferring BTCMT to the buyer and updating purchase accounting before it attempts to collect payment. Payment collection relies on TransferHelper.safeTransferFrom(), which treats any successful low-level call returning true as valid settlement. An attacker can therefore deploy a fake ERC-20-like contract whose transferFrom simply returns true, pass that contract as paymentToken, and receive BTCMT for free. The exploit is ACT because every required precondition was publicly observable and every required action was permissionless.
4. Detailed Root Cause Analysis
The critical victim-side code is the buyTokens() branch structure. In the implementation source, the manual-price branch sends the attacker-supplied token directly into _buy(), while the allowlist check appears only later in the final else branch:
} else if (dexInfo.manualPrice > 0) {
_buy(
purchaseParams.paymentToken,
purchaseParams.usdtAmount,
getPrice(purchaseParams.usdtAmount, false, 0),
0,
bonusPercent,
linkParams.linkHash
);
} else {
require(
paymentsTokens.contains(purchaseParams.paymentToken),
"Wrong paymentToken"
);
...
}
The second half of the bug is in _buy() and TransferHelper.safeTransferFrom(). The sale pays out BTCMT before trying to collect payment, and the helper checks only call success plus a boolean return value:
_give(
_msgSender(),
payAmount,
buyAmount,
lockedDuration,
bonusPercent,
link
);
TransferHelper.safeTransferFrom(
payToken,
_msgSender(),
address(this),
payAmount
);
(bool success, bytes memory data) =
token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
"TransferHelper::transferFrom: transferFrom failed"
);
The seed trace for exploit transaction 0x53be95dc8ffbc80060215133f76f48df35deef3cd7e1803e24b1e2f8aa53440b shows the mechanism end to end. The attacker contract 0xBA91dB0B31D60c45e0b03E6d515E45fcabC7b1CD called buyTokens() with itself as paymentToken, received BTCMT, and then returned true from transferFrom:
TransparentUpgradeableProxy::fallback(..., (false, false, 0xBA91..., 12100000000000000000000, 0, 0, 0, 0x))
0x0D116ed40831FEF8E21EcE57C8455ae3B1e4041B::buyTokens(...)
BTCMT::transfer(0xBA91..., 14724096385542168674698)
emit PurchaseWithBonuses(..., 14578313253012048192771, ..., 145783132530120481927, 0xBA91..., ...)
0xBA91...::transferFrom(0xBA91..., TransparentUpgradeableProxy: [0xDbF1...], 12100000000000000000000)
<- [Return] true
The balance diff corroborates the economic effect. The crowdsale proxy lost 14724096385542168674698 BTCMT, and no USDT or BUSD increase was recorded for the crowdsale. The profit EOA 0xc5001F60DB92aFcC23177A6c6B440A4226cb58Bf gained 9646445252364728917322 USDT, while the exploit sender paid only BNB gas. That is the precise invariant break: BTCMT inventory left the sale without receipt of any whitelisted payment asset.
5. Adversary Flow Analysis
The attack used two adversary-crafted transactions. First, tx 0x4ec561ce249ae87359f1958e64572deb33d608c1aea531b5155fe1eb956fc1c5 deployed contract 0xBA91dB0B31D60c45e0b03E6d515E45fcabC7b1CD. Runtime observations tie that deployment to EOA 0xc5001F60DB92aFcC23177A6c6B440A4226cb58Bf, which later received the stolen USDT.
Second, tx 0x53be95dc8ffbc80060215133f76f48df35deef3cd7e1803e24b1e2f8aa53440b executed the exploit. EOA 0x547fb3db0f13eed5d3ff930a0b61ae35b173b4b5 called the freshly deployed contract, which then:
- Called
ReferralCrowdsale.buyTokens()using the public enabled referral link hash. - Set
paymentToken = 0xBA91...,usdtAmount = 12100000000000000000000, and relied on the manual-price branch. - Received
14724096385542168674698BTCMT from the crowdsale despite sending no approved stablecoin. - Approved Pancake V3 router
0x13f4EA83D0bd40E75C8222255bc855a974568Dd4and swapped the BTCMT for9646445252364728917322USDT through pool0x11bd737757B86c16646313FdF9e86681dd3F065F. - Transferred the full USDT proceeds to
0xc5001F60DB92aFcC23177A6c6B440A4226cb58Bf.
The monetization leg is explicit in the seed trace:
0x13f4EA83D0bd40E75C8222255bc855a974568Dd4::exactInputSingle((BTCMT, USDT, 100, 0xBA91..., 14724096385542168674698, 0, 0))
BEP20USDT::transfer(0xBA91..., 9646445252364728917322)
...
BEP20USDT::transfer(0xc5001F60DB92aFcC23177A6c6B440A4226cb58Bf, 9646445252364728917322)
This sequence requires no privileged actor. Any unprivileged adversary could deploy a fake token contract with a permissive transferFrom, reuse a public enabled referral link, and route the received BTCMT into a liquid market.
6. Impact & Losses
The direct protocol loss was 14724096385542168674698 BTCMT removed from the crowdsale inventory. That is the measurable victim-side depletion recorded in the seed balance diff.
The attacker converted those tokens into 9646445252364728917322 USDT. After subtracting the exploit sender's gas cost, valued at 358013406499515341 USDT units using the recorded PancakeRouterV2 conversion, the adversary cluster retained a net gain of 9646087238958229401981 USDT units. The affected party is the Minto ReferralCrowdsale inventory holder, represented on-chain by proxy 0xDbF1C56b2aD121Fe705f9b68225378aa6784f3e5.
7. References
- Exploit transaction:
0x53be95dc8ffbc80060215133f76f48df35deef3cd7e1803e24b1e2f8aa53440b - Attacker helper deployment:
0x4ec561ce249ae87359f1958e64572deb33d608c1aea531b5155fe1eb956fc1c5 - Victim proxy:
0xDbF1C56b2aD121Fe705f9b68225378aa6784f3e5 - Victim implementation source:
0x0D116ed40831FEF8E21EcE57C8455ae3B1e4041B - BTCMT token:
0x410a56541bD912F9B60943fcB344f1E3D6F09567 - Payment tokens in pre-state allowlist: USDT
0x55d398326f99059fF775485246999027B3197955, BUSD0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56 - Pancake V3 router:
0x13f4EA83D0bd40E75C8222255bc855a974568Dd4 - Evidence used for validation: verified
ReferralCrowdsalesource, collector pre-state observations, the seed execution trace, and the seed balance-diff artifact