October 20, 2026
|
Exploit Postmortem
Cover image for "Internet Token's $16K Loss and How Olympix Would Have Prevented It"

Internet Token's $16K Loss and How Olympix Would Have Prevented It

Date: September 21, 2026

Chain: Base

Loss: 5.847 WETH realized, about $16K, plus about 764 million freshly minted INT kept by the attacker

Class: privileged contract trusting a caller-supplied pool, letting a fake callback mint tokens

The pool the attacker handed to Internet Token's LiquidityUnifier contract on September 21, 2026 had never traded anything. It was a small contract deployed moments earlier in the same transaction on Base, and when anyone asked which tokens it held, it answered INT for both sides. LiquidityUnifier accepted it, swapped through it, and minted about 925 million INT to it.

The attacker sold about 161 million of those tokens into the real INT/WETH pool for 5.847 WETH, worth about $16,000 by SlowMist's valuation, and kept the other 764 million. It was possible because a contract that held the right to mint INT let any caller choose which pool it trusted, and checked that choice only by asking the pool to describe itself.

A Contract With the Power to Mint

Internet Token is a DAO-governed protocol on Base built around a perpetual lottery, where buying INT generates lottery ticket NFTs for a weekly prize pot and INT holders control the treasury. Within its system, a contract called LiquidityUnifier held MINTER_ROLE on the INT token, the permission to create new INT out of nothing.

LiquidityUnifier exposed a function, swapV3(token, pool), that took the pool address from the caller. Before using it, the contract ran two checks, _validatePool and _validatePoolV3Tokens, which confirmed that the address had deployed code, wasn't on an exclusion list, and returned INT and the requested token from its token0() and token1() functions. According to the DeFiHackLabs proof of concept, which was checked line by line against the verified source on Basescan, nothing compared the address against the Uniswap V3 factory or any registry of real pools.

A Pool That Vouches for Itself

Any contract can return whatever it likes from token0() and token1(), so those checks confirmed what the pool said about itself and nothing more. A real Uniswap V3 pool's address is computed by the factory from its two tokens and its fee tier, which is the one thing a fake can't reproduce.

The trust then carried into the callback. When swapV3 accepted the pool, it stored it as currentPoolV3 and called swap() on it. The pool called back into LiquidityUnifier's uniswapV3SwapCallback, whose only check was that the caller matched currentPoolV3, and the callback minted whichever of amount0Delta or amount1Delta was positive to that pool. The fake pool picked both numbers.

That's the takeaway for builders: when a contract holds a privilege like minting, every address it lets a caller supply becomes part of who can use that privilege. Asking a stranger at the door whether they live in the building will always get a yes.

One Constructor, 925 Million Tokens

The attack ran in transaction 0xed62...b08b at block 51,593,878. The attacker's wallet deployed a throwaway contract whose constructor carried out the whole sequence, and DeFiHackLabs reconstructs the steps. The contract deployed a fake pool that reported INT for both token slots, called swapV3 with it, and when the fake pool's swap() called back into LiquidityUnifier, the callback minted about 925.41 million INT.

LiquidityUnifier did have a supply guard, validateSupply, that checked total INT supply hadn't grown across the swapV3 call. The attacker routed around it through the protocol's Convertor contract, which swaps INT for a bridged token and back. During the guarded call, part of the newly minted INT was converted out and burned, so the supply check passed, and after swapV3 returned the attacker converted back and re-minted it outside the guard's view.

With the INT in hand, the attacker sold 161,265,976 INT into the real INT/WETH Uniswap V3 pool for 5.846988 WETH and kept about 764.15 million INT.

The same wallet then turned to the DAO. On September 21, the attacker's address created a governance proposal on Tally titled "nope" with no on-chain actions attached, and canceled it the same day.

How Olympix Finds a Callback Anyone Can Steer

Olympix reads a contract the way an attacker does, as states, transitions and entry points, and it takes in the context around the code, including which roles a contract holds on other contracts. Here the state that matters is INT's total supply, and LiquidityUnifier is an entry point that can change it because it carries MINTER_ROLE. The engine maps every path that reaches a mint and narrows them to the feasible ones a caller can drive.

Olympix's static analysis surfaces the signal: a function in a contract holding a mint role that calls into an address the caller supplies, validated only by values that address reports about itself, with a callback whose sole access check is that it comes back from that same address. Olympix has flagged arbitrary external contract interaction before in other codebases.

BugPoCer picks up the flagged swapV3 path and treats INT's supply as the thing to defend. Through Counterexample-Guided Invariant Refutation it infers the rule the minter role is meant to uphold and then executes paths, including ones that deploy helper contracts, until one breaks it. The invariant is that INT supply must only grow through a mint the protocol intends, and never through a call where the caller controls both the pool and the amounts. The breaking path is short: deploy a contract that passes the token checks, call swapV3 with it, and return the mint amounts the attacker wants from the callback.

The output is a counterexample that shows the mint, the Convertor round trip that keeps validateSupply quiet, and the supply left over after the call, with the minimal conditions that trigger it and a generated Forge test that replays it. That's the September 21 transaction produced in development, and because the engine runs at every merge and before release, it appears the moment a privileged function starts accepting addresses from callers.

Privilege Follows the Address

LiquidityUnifier's checks described a pool, and the attacker wrote the description. A contract that can mint should decide for itself which addresses it trusts, and that rule is one a tool can infer from the roles in a codebase and test on every change. If your contracts hold mint, burn or treasury rights and call out to addresses a user can choose, book a technical walkthrough.

On-Chain References

Protocol: Internet Token (INT)

Chain: Base

Date: September 21, 2026

Loss: 5.846988 WETH (about $16K per SlowMist), plus about 764.15M INT retained

Attacker EOA (also the proposer of the "nope" DAO proposal): 0x5F7c...Ec68

LiquidityUnifier (holds INT minter role): 0x837D...2032

INT token: 0x968D...eC19

Convertor: 0x6b82...5516

Real INT/WETH Uniswap V3 pool: 0xDEc6...969d

Exploit tx: 0xed62...b08b

Block: 51,593,878

Sources

DeFiHackLabs: InternetToken_exp.sol

Defimon Alerts: Internet Token exploit alert

SlowMist Hacked: Internet Token entry

KalCrypto: Internet Token (INT) protocol overview

DefiLlama: Hacks database

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.