All incidents

Minto Fake-Token Purchase Exploit

Share
Jul 23, 2023 12:40 UTCAttackLoss: 14,724.1 BTCMTPending manual check2 exploit txWindow: 1m 3s
Estimated Impact
14,724.1 BTCMT
Label
Attack
Exploit Tx
2
Addresses
2
Attack Window
1m 3s
Jul 23, 2023 12:40 UTC → Jul 23, 2023 12:41 UTC

Exploit Transactions

TX 1BSC
0x4ec561ce249ae87359f1958e64572deb33d608c1aea531b5155fe1eb956fc1c5
Jul 23, 2023 12:40 UTCExplorer
TX 2BSC
0x53be95dc8ffbc80060215133f76f48df35deef3cd7e1803e24b1e2f8aa53440b
Jul 23, 2023 12:41 UTCExplorer

Victim Addresses

0xdbf1c56b2ad121fe705f9b68225378aa6784f3e5BSC
0x410a56541bd912f9b60943fcb344f1e3d6f09567BSC

Loss Breakdown

14,724.1BTCMT

Similar Incidents

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 USDT 0x55d398326f99059fF775485246999027B3197955 and BUSD 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56.
  • The referral link hash 0xc69c51e039668f688f28f427c63cd60aa986f8ce1546039e6a302fb721473814 was already enabled and owned by 0x602abFbBcF537714ba845681f842F868852589A0.

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:

  1. Called ReferralCrowdsale.buyTokens() using the public enabled referral link hash.
  2. Set paymentToken = 0xBA91..., usdtAmount = 12100000000000000000000, and relied on the manual-price branch.
  3. Received 14724096385542168674698 BTCMT from the crowdsale despite sending no approved stablecoin.
  4. Approved Pancake V3 router 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4 and swapped the BTCMT for 9646445252364728917322 USDT through pool 0x11bd737757B86c16646313FdF9e86681dd3F065F.
  5. 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, BUSD 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56
  • Pancake V3 router: 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4
  • Evidence used for validation: verified ReferralCrowdsale source, collector pre-state observations, the seed execution trace, and the seed balance-diff artifact