Fake Pool Callback Let Attackers Mint 925M INT Tokens on Base | PandaSecurity
Web3 Security

Fake Pool Callback Let Attackers Mint 925M INT Tokens on Base

PandaSecurity Team •
#DeFi Exploit#Uniswap V3 Callback#Access Control
Diagram of LiquidityUnifier swapV3 callback mint exploit on Base network

Yesterday (21 September 2026) the INT token on Base lost roughly 764 million tokens plus 5.85 WETH in a single transaction sequence. The root cause was not a classic reentrancy bug or a missing access-control modifier. It was a quiet assumption that any address returning the correct token0/token1 pair must be a legitimate Uniswap V3 pool.

That assumption cost the project dearly.

Quick numbers

MetricValue
TokenINT
Price at time of exploit≈ $0.000328
Minted by attacker≈ 925M INT
Still held after dump≈ 764M INT
ETH extracted5.85 WETH
ContractLiquidityUnifier (0x837dbabc…2032)
NetworkBase

The LiquidityUnifier held the MINTER_ROLE on the RewardToken. Its job was simple: mint a fixed amount of INT, swap it through a Uniswap pool, and send the proceeds to the treasury (minus a small keeper fee). Both swapV2 and swapV3 were public and only protected by nonReentrant and a supply-check modifier.

The vulnerable path

Look at swapV3:

function swapV3(address token, address pool) external nonReentrant validateSupply {
    _validatePool(pool);
    _validatePoolV3Tokens(token, pool);

    bool zeroForOne = IUniswapV3Pool(pool).token0() == rewardToken;
    // ...
    currentPoolV3 = pool;

    IUniswapV3Pool(pool).swap({
        recipient: address(this),
        zeroForOne: zeroForOne,
        amountSpecified: swapAmount.toInt256(),
        sqrtPriceLimitX96: sqrtPriceLimitX96,
        data: ""
    });

    currentPoolV3 = address(0);
    // distribute and burn leftover
}

_validatePool only checks that the address has code and is not on an exclusion list. _validatePoolV3Tokens only checks that token0() and token1() match the expected pair (INT and the output token).

An attacker deployed a contract that:

  1. Returned INT for both token0() and token1().
  2. On swap(), simply called back into uniswapV3SwapCallback with an arbitrarily large positive delta.

Inside the callback:

function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {
    if (msg.sender != currentPoolV3) revert("msg.sender != currentPoolV3");
    int256 amount = amount0Delta > 0 ? amount0Delta : amount1Delta;
    RewardToken(rewardToken).mint(_currentPoolV3, amount.toUint256());
}

Because the fake pool was set as currentPoolV3, the mint went through. The validateSupply modifier only compared total supply before and after the outer call. By routing the newly minted tokens through a Convertor contract in the same transaction, the attacker kept the net supply check happy while still walking away with the tokens.

After minting, a portion was sold into the real INT/WETH Uniswap V3 pool for 5.85 WETH. The rest stayed in the attacker’s wallet.

Why the checks were insufficient

What would have stopped it

A few practical fixes stand out:

  1. Whitelist pools — Only allow pools that have been explicitly approved by governance or a multisig. The exclusion list already existed; an inclusion list would have been stronger.
  2. Hard-code the factory — Verify that IUniswapV3Pool(pool).factory() returns the official Uniswap V3 factory on Base. A one-line check would have rejected the fake contract.
  3. Mint only the expected amount — The callback should mint exactly swapAmount (or the amount the outer function requested), never the delta reported by an untrusted pool.
  4. Separate minter role — Give the LiquidityUnifier a limited mint allowance or a rate-limited minter instead of full MINTER_ROLE.

None of these require exotic cryptography. They are just defensive programming habits that many production DeFi contracts already follow.

Opinion from the trenches

We keep seeing the same pattern: teams correctly protect privileged functions with role checks, then hand an untrusted address the ability to drive those privileged functions through a callback. Callbacks are convenient; they are also one of the easiest places for an attacker to insert themselves.

If your contract mints, burns, or moves value inside a callback that an external party can trigger, treat that external party as hostile by default. Assume the pool, the router, or the oracle will lie to you. Design accordingly.

At Panda Security we have reviewed dozens of similar “liquidity helper” contracts. The ones that survive longest treat every external call as a potential re-entry or value-extraction vector, even when the call looks completely standard.

Take-aways for builders

The INT incident is another reminder that the difference between a working DeFi product and a drained one is often a single missing require statement.

If you are shipping a contract that mints reward tokens through Uniswap (or any AMM), have a second pair of eyes look at the callback path. We are happy to help — that is literally what Panda Security does every day.

Stay safe.

Related articles

← Back to Blog