
Code4rena · S-171 · Apr 2025
// Frontrunnable Pool Creation Blocks Custom Token Agent Initialization Permanently
_createPair() checks that the Uniswap pair does NOT exist and reverts if it does. Because pool creation is separated from agent registration (initFromToken registers first, pool creation happens in executeTokenApplication), any actor can create the pair in the gap window, permanently blocking the agent.
function _createPair(address tokenAddr) internal returns (address) {
IUniswapV2Factory factory =
IUniswapV2Factory(IUniswapV2Router02(_uniswapRouter).factory());
// @audit Hard reverts if pool already exists — no recovery
require(
factory.getPair(tokenAddr, assetToken) == address(0),
"pool already exists"
);
return factory.createPair(tokenAddr, assetToken);
}
// executeTokenApplication calls _createPair AFTER registration
// giving attackers a window to front-run
function executeTokenApplication(...) external {
// ... token validation ...
lp = _createPair(token); // reverts if Bob already created pair
}▸ INCORRECT ASSUMPTIONS
▸ AFFECTED CONTRACTS
Alice calls initFromToken(AliceToken) to register her custom token agent. Registration succeeds and the application is recorded.
↓Attacker monitors the mempool. Before Alice calls executeTokenApplication(), the attacker calls IUniswapV2Factory.createPair(AliceToken, VirtualToken) directly.
↓Alice calls executeTokenApplication(). Internally, _createPair() checks factory.getPair(AliceToken, assetToken) != address(0) — the pair exists — and reverts with "pool already exists".
↓Alice's agent is permanently uninitialized. She cannot recover: the application is registered but the required pair creation reverts every time. The custom token agent is permanently blocked.
Any custom token agent registration can be permanently blocked by any actor willing to spend the gas to pre-create the Uniswap pair. The attack is cheap and unrecoverable.
There is no mechanism to proceed with an existing pair or cancel and re-register. Affected applications are permanently stuck.
Competitors or protocol adversaries can selectively block specific token agents while allowing others, enabling targeted censorship of custom agent creation.
Move pool creation into initFromToken so it happens atomically with registration, eliminating the window. Alternatively, modify _createPair() to return an existing pair instead of reverting.
// Pool creation is delayed — creates attack window
function initFromToken(address tokenAddr, ...) public {
_tokenApplication[tokenAddr] = id; // register first
// pool created later in executeTokenApplication ← gap here
}// Option A: atomic creation in initFromToken
function initFromToken(address tokenAddr, ...) public {
_createPairOrGet(tokenAddr); // create pair first
_tokenApplication[tokenAddr] = id;
}
// Option B: tolerate existing pair in _createPair
function _createPair(address tokenAddr) internal returns (address) {
address existing = factory.getPair(tokenAddr, assetToken);
if (existing != address(0)) return existing; // reuse if exists
return factory.createPair(tokenAddr, assetToken);
}▸ SECURITY CONSIDERATIONS
Race conditions in multi-step initialization flows are a recurring pattern in DeFi. Whenever I see a protocol split into 'register then execute' steps, I immediately ask: what can happen in the gap? Here, the gap allowed permanent DoS via a permissionless Uniswap factory call.
The severity driver is the absence of a recovery path. If executeTokenApplication could accept a pre-existing pair or if the application could be cancelled and re-submitted, this would be low severity. The combination of 'no error handling + no recovery' is what makes it matter.
The fix is straightforward once you identify the pattern: either make it atomic (create pair in initFromToken) or make it idempotent (accept existing pair in _createPair). Both solutions are one-line changes. The insight is recognizing that the require() check was written with an assumption of exclusive creation rights that Uniswap's permissionless factory doesn't enforce.
I document this class of bug as 'assumed exclusivity over permissionless infrastructure' — protocols that call permissionless external contracts and then rely on being the only caller create exploitable race windows. Always check: can an attacker call this external contract before the protocol does?