Post-graduation routing
Once a launch reaches phase 2, PLOUTO trades in a canonical Uniswap v4 pool. Buybacks must route there instead of the curve.
No Universal Router
At the time of writing there is no canonically documented Universal Router deployment on Robinhood Chain. Inventing an address, or guessing at one from another chain, would be unsafe — a wrong router address is a lost buyback at best.
So Plouto does not use one. BuybackExecutor settles directly against the verified Uniswap v4 PoolManager at 0x8366a39CC670B4001A1121B8F6A443A643e40951, read from factory.poolManager() and verified on Blockscout.
Reconstructing the PoolKey
The pool is identified by a PoolKey, rebuilt from the factory's own launch record:
function poolKeyFor(address token) public view returns (PoolKey memory key) { PonsLaunchedToken memory record = ponsFactory.getLaunchedToken(token); if (!record.exists) revert RouteMismatch(); if (record.pairToken != address(0)) revert RouteMismatch(); // Native ETH is address(0) and therefore always currency0. key = PoolKey({ currency0: address(0), currency1: record.token, fee: record.poolFee, tickSpacing: record.tickSpacing, hooks: ponsMemeHook });}Nothing here is hardcoded except the hook address, which is itself read from factory.memeHook() at construction.
A fork test confirms the reconstructed key resolves to a pool the PoolManager has actually initialized.
The swap
Uniswap v4 uses a lock-and-settle model. The executor implements IUnlockCallback and does the whole exchange inside one callback:
function unlockCallback(bytes calldata data) external override returns (bytes memory) { if (msg.sender != address(poolManager)) revert NotPoolManager(); (PoolKey memory key, uint256 ethIn, uint256 minTokensOut) = abi.decode(data, (PoolKey, uint256, uint256)); int256 delta = poolManager.swap( key, SwapParams({ zeroForOne: true, amountSpecified: -int256(ethIn), // negative == exact input sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 }), "" ); int128 delta0 = int128(delta >> 128); int128 delta1 = int128(delta); uint256 owed = delta0 < 0 ? uint256(uint128(-delta0)) : 0; uint256 received = delta1 > 0 ? uint256(uint128(delta1)) : 0; if (received < minTokensOut) revert InsufficientOutput(received, minTokensOut); if (owed != 0) poolManager.settle{value: owed}(); if (received != 0) poolManager.take(key.currency1, address(this), received); return abi.encode(received);}unlockCallback is callable only by the PoolManager. Anyone else gets NotPoolManager.
Price impact, bounded on chain
The executor reads the pool's sqrtPriceX96 straight out of PoolManager storage rather than trusting a quote:
function poolSqrtPriceX96(address token) public view returns (uint160) { bytes32 stateSlot = keccak256(abi.encode(poolId(token), POOLS_SLOT)); bytes32 slot0 = poolManager.extsload(stateSlot); return uint160(uint256(slot0));}That spot price gives an ideal output, and the realised output must land within maxPriceImpactBps of it:
uint256 idealOut = Math.mulDiv(Math.mulDiv(ethIn, sqrtPriceX96, Q96), sqrtPriceX96, Q96);uint256 impactFloor = Math.mulDiv(idealOut, BPS - maxPriceImpactBps, BPS);The two mulDiv steps avoid overflowing sqrtPrice².
Verified against a live pool
A fork test reconstructs the PoolKey for a real graduated Pons launch, confirms the pool is initialized, and executes a 0.005 ETH swap through it — receiving and retiring tokens against genuine liquidity.