All incidents

MicDao Mixed-Price Sale Exploit

Share
Oct 18, 2023 13:09 UTCAttackLoss: 1,601,895.24 MIC, 12,260.25 USDTPending manual check1 exploit txWindow: Atomic
Estimated Impact
1,601,895.24 MIC, 12,260.25 USDT
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Oct 18, 2023 13:09 UTC → Oct 18, 2023 13:09 UTC

Exploit Transactions

TX 1BSC
0x24a2fbb27d433d91372525954f0d7d1af7509547b9ada29cc6c078e732c6d075
Oct 18, 2023 13:09 UTCExplorer

Victim Addresses

0x19345233ea7486c1d5d780a19f0e303597e480b5BSC
0x928902c2499bd6d123f455b1de93f1a139ef9b00BSC

Loss Breakdown

1,601,895.24MIC
12,260.25USDT

Similar Incidents

Root Cause Analysis

MicDao Mixed-Price Sale Exploit

1. Incident Overview TL;DR

On BSC block 32711748, transaction 0x24a2fbb27d433d91372525954f0d7d1af7509547b9ada29cc6c078e732c6d075 used a public DODO flash loan to manipulate the MIC/USDT Pancake spot price and then abused MicDao sale contract 0x19345233ea7486c1d5d780a19f0e303597e480b5. The attacker pumped the pair with 500000 USDT, deployed 80 short-lived helper contracts, and had each helper buy 20000 MIC for 2000 USDT through swap(uint256,address). Because the sale contract used internal sale pricing for the buyer payout but live Pancake reserves for the contract's own liquidity contribution, each helper drained far more MIC than the sale contract economically priced. The attacker then sold the accumulated MIC back into the same pair, repaid the flash loan, and left the adversary profit path with approximately 12235.153972865412371137 USDT net of gas.

The root cause is a mixed-pricing bug in a public sale path. MicDao's sale contract priced the buyer leg with fixed sale math, but priced the contract-side liquidity leg with a flash-loan-manipulable Pancake spot price. That mismatch let an unprivileged actor extract MIC from the sale inventory while contributing only a tiny reserve-derived MIC amount to liquidity on each purchase.

2. Key Background

MicDao token 0xf6876f6ab2637774804b85aecc17b434a2b57168 has an additional transfer rule for sells into listed pairs. Its verified source shows that when the recipient is in pairList and the sender is not in isDelivers, 45% of the transferred MIC is burned before the pair receives the remainder:

function _transfer(address sender, address recipient, uint256 amount) internal override {
    if (pairList[recipient] && !isDelivers[sender]) {
        uint256 toBurn = amount.mul(45).div(100);
        super._transfer(sender, address(1), toBurn);
        amount = amount.sub(toBurn);
    }
    super._transfer(sender, recipient, amount);
}

This matters because the attacker's final dump burned 797280.896553005568769367 MIC and delivered only 974454.429120340139607005 MIC into the pair, exactly matching the balance diff.

The sale contract is unverified, but the trace exposes the relevant public selectors and behavior. The sale path exposes swap(uint256,address), tracks per-caller usage through users(address), and enforces a public maxUSDT() cap of 2000e18. The cap is keyed only by caller address, so a permissionless attacker can bypass it by deploying fresh helper contracts.

The venue used for the manipulated price was Pancake pair 0x928902c2499bd6d123f455b1de93f1a139ef9b00. Its verified source exposes getReserves() and uses the reserve balances as the active price basis for swaps and liquidity operations:

function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
    _reserve0 = reserve0;
    _reserve1 = reserve1;
    _blockTimestampLast = blockTimestampLast;
}

Because those reserves can be moved inside the same transaction, any contract that trusts the instantaneous pair ratio as an oracle is exposed to flash-loan manipulation.

3. Vulnerability Analysis & Root Cause Summary

This is an ATTACK-category ACT exploit against MicDao's public sale path. The vulnerability is not a privileged-access issue and does not rely on private keys or hidden state. It arises because swap(uint256,address) uses two inconsistent price sources for one economic action.

The buyer-facing leg is fixed: a 2000 USDT purchase transfers 20000 MIC to the caller. The contract-facing leg is floating: before routing the same 2000 USDT into Pancake liquidity, the sale path reads router.factory(), factory.getPair(token, usdt), pair.token0(), and pair.getReserves(), then computes how much MIC the sale contract should contribute from the live pair ratio. After the attacker pumps the pair, that reserve ratio implies that 2000 USDT only requires 23.6904520522176055 MIC from the sale contract.

The broken invariant is straightforward: the MIC sent to the buyer and the MIC contributed by the sale contract for the same purchase must be priced from the same trusted basis. MicDao instead paid out MIC using fixed sale math while settling the contract-side liquidity leg using a manipulable external spot. That is the code-level breakpoint that makes the exploit profitable.

4. Detailed Root Cause Analysis

The seed metadata shows that tx 0x24a2fbb27d433d91372525954f0d7d1af7509547b9ada29cc6c078e732c6d075 was sent by EOA 0xcd03ed98868a6cd78096f116a4b56a5f2c67757d to orchestrator 0x502b4a51ca7900f391d474268c907b110a277d6f in block 32711748. The trace then shows a public DODO flash loan from pool 0x26d0c625e5f5d6de034495fbde1f6e9377185618:

0x26d0c625e5F5D6de034495fbDe1F6e9377185618::flashLoan(
  0,
  670900962208306651912728,
  0x502b4A51ca7900F391d474268C907B110a277d6F,
  0x30783030
)

Inside the callback, the attacker spent 500000e18 USDT to buy MIC on Pancake. The trace shows the pair paying out 171735.325673345708376372 MIC to the orchestrator, which moved the pair from its normal state to the manipulated reserve state used later by the sale contract.

The first helper purchase shows the bug clearly:

0x19345233ea7486c1D5d780A19F0e303597E480b5::swap(2000000000000000000000, 0x502b4A51...)
  MicDao::transfer(0x8124974bA43E..., 20000000000000000000000)
  PancakeFactory::getPair(MicDao, BEP20USDT)
  PancakePair::getReserves() -> 647813106380890190341074, 7673492667757310999365
  PancakeRouter::addLiquidity(
    MicDao,
    BEP20USDT,
    23690452052217605500,
    2000000000000000000000,
    0,
    0,
    0x3f1AF63823e2FEa40c94Bd016d5a6637c66cae44,
    1697634564
  )

That one call transferred 20000e18 MIC to the helper but only paired 23.6904520522176055 MIC with the helper's 2000e18 USDT on the liquidity side. The sale contract therefore lost almost the full buyer allocation on every call. The same pattern repeated 80 times; the trace contains 80 swap(2000e18, ...) calls and 80 helper SELFDESTRUCT events, confirming that the attacker bypassed the per-address cap with one fresh helper per purchase.

The accounting closes exactly against the balance diff:

  • Sale contract MIC delta: -1601895236164177408440000, or -1601895.236164177408440000 MIC.
  • Pair USDT delta: -12260251676860937689229, matching the adversary extraction from the pool.
  • Sale contract USDT dust: +4800, which equals 80 * 60 raw USDT units, matching the per-call liquidity-rounding remainder.

The MIC loss also matches the mixed-pricing math: 80 * 20000 MIC paid to buyers plus 80 * 23.6904520522176055 MIC paired into liquidity equals 1601895.23616417740844 MIC. This is the explicit invariant break. The sale contract kept issuing the buyer's MIC on fixed sale logic while its own liquidity contribution collapsed to a reserve-derived amount that the attacker had already manipulated upward with the flash loan.

5. Adversary Flow Analysis

The adversary flow is a single-transaction ACT sequence:

  1. The gas-paying EOA 0xcd03ed98868a6cd78096f116a4b56a5f2c67757d calls orchestrator 0x502b4a51ca7900f391d474268c907b110a277d6f.
  2. The orchestrator borrows 670900.962208306651912728 USDT from the public DODO pool.
  3. The orchestrator spends 500000 USDT on Pancake to push the MIC/USDT spot price sharply upward.
  4. The orchestrator funds 80 ephemeral helper contracts with 2000 USDT each. Each helper:
    • approves the sale contract,
    • calls swap(2000e18, ...),
    • receives 20000 MIC,
    • returns the MIC to the orchestrator,
    • self-destructs.
  5. During each swap, the sale contract re-reads the manipulated Pancake reserves and adds liquidity with only 23.6904520522176055 MIC plus roughly 2000 USDT, leaving the buyer-side payout unchanged.
  6. After all helpers finish, the orchestrator holds 1771735.325673345708376372 MIC.
  7. The orchestrator dumps that MIC back into Pancake. Because of MicDao's sell-to-pair burn rule, 797280.896553005568769367 MIC is burned and 974454.429120340139607005 MIC reaches the pair.
  8. Pancake pays out 672260.251676860937684429 USDT. The orchestrator repays the DODO principal and transfers the residual 12260.251676860937684429 USDT to immediate profit recipient 0xa5b92a7abebf701b5570db57c5d396622b6ed348.

Representative trace evidence for the liquidation leg is:

MicDao::transferFrom(0x502b4A51..., PancakePair, 1771735325673345708376372)
  emit Transfer(... to 0x0000000000000000000000000000000000000001, 797280896553005568769367)
  emit Transfer(... to PancakePair, 974454429120340139607005)
PancakePair::swap(672260251676860937684429, 0, 0x502b4A51..., 0x)

Everything in this sequence is permissionless and publicly callable, which is why the case is correctly classified as ACT.

6. Impact & Losses

The direct protocol-side inventory loss was 1601895.236164177408440000 MIC from sale contract 0x19345233ea7486c1d5d780a19f0e303597e480b5. The public MIC/USDT Pancake liquidity lost 12260.251676860937684429 USDT to the adversary cluster. The attacker also triggered a collateral token burn of 797280.896553005568769367 MIC during the terminal liquidation because MicDao burns 45% of sells into listed pairs from non-deliver addresses.

The loss summary used for session metadata is:

  • MIC: raw amount "1601895236164177408440000", decimal 18
  • USDT: raw amount "12260251676860937684429", decimal 18

The measurable profit predicate is also satisfied. The immediate profit-recipient address gained 12260.251676860937684429 USDT, and after converting the seed transaction's 0.118065926125134867 BNB gas cost into USDT using same-block Pancake reserves, the adversary cluster remained net positive by approximately 12235.153972865412371137 USDT.

7. References

  1. Seed transaction metadata: 0x24a2fbb27d433d91372525954f0d7d1af7509547b9ada29cc6c078e732c6d075, block 32711748, sender 0xcd03ed98868a6cd78096f116a4b56a5f2c67757d, recipient 0x502b4a51ca7900f391d474268c907b110a277d6f.
  2. Seed opcode trace for the full exploit sequence, including the DODO flash loan, Pancake buy, repeated swap(2000e18, ...) calls, reserve reads, addLiquidity, helper self-destruction, final dump, and repayment.
  3. Seed balance diff showing the sale contract MIC loss, pair USDT loss, burn-address MIC gain, sale-contract USDT dust, and profit-recipient USDT gain.
  4. Verified MicDao token source at 0xf6876f6ab2637774804b85aecc17b434a2b57168, especially _transfer, pairList, and isDelivers.
  5. Verified PancakePair source at 0x928902c2499bd6d123f455b1de93f1a139ef9b00, especially getReserves() and liquidity accounting.
  6. Public DODO pool 0x26d0c625e5f5d6de034495fbde1f6e9377185618, which exposes the flash-loan primitive used in the seed transaction.
  7. MicDao sale contract 0x19345233ea7486c1d5d780a19f0e303597e480b5, whose trace-visible swap(uint256,address) path mixed fixed sale pricing with manipulated reserve pricing.