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.
| Metric | Value |
|---|---|
| Token | INT |
| Price at time of exploit | ≈ $0.000328 |
| Minted by attacker | ≈ 925M INT |
| Still held after dump | ≈ 764M INT |
| ETH extracted | 5.85 WETH |
| Contract | LiquidityUnifier (0x837dbabc…2032) |
| Network | Base |
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.
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:
token0() and token1().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.
token0/token1.currentPoolV3 temporary variable was meant to protect the callback, but it only verified the caller was the address the caller itself supplied. Circular trust.validateSupply modifier was a good idea in theory, yet it could be gamed by moving tokens around inside the same transaction before the final supply check.A few practical fixes stand out:
IUniswapV3Pool(pool).factory() returns the official Uniswap V3 factory on Base. A one-line check would have rejected the fake contract.swapAmount (or the amount the outer function requested), never the delta reported by an untrusted pool.MINTER_ROLE.None of these require exotic cryptography. They are just defensive programming habits that many production DeFi contracts already follow.
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.
token0() / token1() alone.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.
UNCX's token vesting contract lost roughly $87.7K when an attacker exploited a share-accounting flaw in release(), topUpVesting(), and cancel(). We break down the root cause, the exact attack sequence, and what builders should take away from it — without the jargon.
Autheo’s THEO token lost 92% after attackers used a Hyperlane TrustedRelayerIsm misconfiguration to mint 19 million unbacked tokens on Base and extract roughly $1.76 million. Here’s how the attack worked and why single-key security models remain dangerous for cross-chain assets.
Uniswap v4 hooks explained: the PoolManager singleton, flash accounting, and what the Cork and Bunni exploits teach us about hook security. A PandaSecurity deep dive.
An attacker forced a Uniswap V4 vault into a zero-liquidity state, redeemed one minimal share unit, and withdrew roughly 14,838.47 DAI. Panda Security breaks down the transaction, vulnerable code, root cause, and practical fixes.