Why aggregator routes destroy objects
This is the most important page in the docs. If you skip everything else, read this.
The short version
If you buy or sell PEG through 1inch, CowSwap, ParaSwap, Matcha, Uniswap v3, the Uniswap widget, or any other aggregator, you receive the ERC-20 balance but zero objects. The art is forfeited at the contract level. There is no off-chain remedy. There is no refund.
Always use the native swap UI on the token's detail page at peg.fun/t/<address>.
Why it happens
peg.fun's object-mint logic fires on the boundary between the Uniswap v4 PoolManager and the PEG token contract. The mint requires two conditions in the same transaction:
- The recipient receives PEG directly from the PoolManager (via the v4 hook).
- The recipient was "armed" by a prior
armForExternalSwapcall binding their address to the swap.
peg.fun's native swap UI calls armForExternalSwap before routing through the v4 PoolManager, the binding is set and the mint fires.
Aggregators do not. When you swap through 1inch, the PEG arrives at the aggregator's address first, then gets transferred to your wallet via a second ERC-20 transfer. The token contract sees from == aggregator_contract, not from == PoolManager, so the mint guard does not fire.
You walk away with ERC-20 balance and no objects. Worse, the PEG you hold is then permanently unmintable, even if you transfer it to another wallet later, the mint signal is single-shot per swap and the moment has passed.
Enforced at the contract level
The object-mint guard lives inside PegToken.sol. It checks the from address of every incoming transfer and only fires the mint when from == pool. This check cannot be bypassed, patched, or worked around, the contract is renounced after graduation. No party, including peg.fun itself, can mint objects retroactively for a buyer who routed through an aggregator.
What to do instead
Use peg.fun's native swap interface, the Buy / Sell form embedded on every token detail page. The form calls DirectSwap, peg.fun's own router, which is the only routing path that satisfies the contract guard.
DirectSwap is:
- A peg.fun-deployed router contract.
- Permissionless, anyone can call it.
- Calls
armForExternalSwapthen routes through Uniswap v4's PoolManager. - Verifies the mint fired in the same transaction.
There is no setup, no allowlist, no special wallet required. If you swap from peg.fun/t/<address>, you are using DirectSwap by default.
Why peg.fun ships its own router
Aggregators are outside peg.fun's control. The mint binding is enforced at the contract level, no aggregator protocol supports the required armForExternalSwap pre-call.
DirectSwap is the only routing path that satisfies the contract guard. It is not a limitation or technical debt; it is a deliberate design boundary that ensures every buy through peg.fun's UI produces the correct object mint.
Can I get a refund?
No. The contract has no refund mechanism for object-less purchases.
If you bought through an aggregator and want to recover your ETH, you can sell the orphan PEG back through peg.fun's swap interface. The sell route also goes through DirectSwap; you'll receive ETH for the PEG balance, but no object burns because no object was minted in the first place.
Identifying an aggregator route
If you are looking at PEG on any of these surfaces:
- Uniswap's app (
app.uniswap.org) - 1inch, CowSwap, ParaSwap, Matcha
- A wallet's built-in swap (some wallets route through aggregators by default)
- A "DEX aggregator" or "best price finder"
…you are about to lose objects. Cancel and use the swap form on peg.fun/t/<address> instead.
For aggregators and integrators
This page tells users not to route through aggregators. This section tells aggregators how to route correctly — the path exists, it is permissionless, and no allowlist or partnership is required.
Use DirectSwap.buyExactInFor
DirectSwap is peg.fun's router. It has no owner, no allowlist and no access control — any contract or EOA may call it. It exposes a buy-on-behalf entry point built for pass-through callers:
function buyExactInFor(
address hook, // the token's PegHook (one per collection)
address recipient, // the END USER — objects and tokens both land here
uint256 minTokensOut, // ERC-20 slippage guard
uint256 minObjects // OBJECT guard — see below
) external payable;
ETH comes from msg.sender; tokens and objects both go to recipient, and any leftover ETH refunds to recipient rather than the caller, so an integrating router stays a clean pass-through.
Route the user's buy through this call with the end user's address as recipient, and the object mint fires for them correctly. That is the whole integration.
Set minObjects — it is an object-level slippage guard
This section was rewritten (Round 11 finding 2). The two rules below were true before peg.fun's held-credit accounting shipped and are corrected here.
minObjects reverts the transaction if fewer objects than expected are minted. Treat it exactly like minTokensOut: it is the difference between "the swap succeeded" and "the swap succeeded and the user actually received their art." It remains a minimum — the realised mint count is always greater than or equal to a fee-only estimate of the ETH in, so a conservative minObjects never over-reverts an honest buy.
Two things determine how many objects a buy mints, corrected:
- A sub-1-token buy CAN mint an object — held credit accumulates across transactions. peg.fun tracks each wallet's accumulated hook-routed purchase amount (
hookCredit, a public getter on the token contract) separately from its ERC-20 balance. A wallet that buys 0.3 tokens four times through the hook mints its first object on the FOURTH buy, not zero forever. ReadhookCredit(address)before the buy to predict the count exactly:newPegs = floor((hookCredit(recipient) + expectedTokensOut) / 1e18), bounded by the recipient's spare whole-token capacity. - The mint follows the recipient's whole-token count, and only hook-routed purchases accrue credit — at any spacing, across any number of transactions. If your router receives the tokens itself and forwards them in a second ERC-20 transfer, the credit accrues to your router's address, not the user's — pass the user as
recipientand this problem disappears. Tokens acquired outside the hook (a v3 pool, a gift, a marketplace purchase) never accrue credit no matter how they are combined with hook-routed buys.
Fee, for your quote math: the pre-graduation curve and the post-graduation Uniswap v4 pool both charge 2.5% each way (buy and sell) — 2.25% hook-dispatched plus the pool's native 0.25% post-graduation.
Size the order yourself — there is a per-transaction mint cap
One transaction may mint at most 350 objects. This applies to every buy, pre- and post-graduation, and it is the one thing most likely to make an integration's quote disagree with its fill.
What happens depends entirely on whether you route through DirectSwap:
-
Through
DirectSwap— your order is clamped, not rejected. The router sizes the swap to the cap before unlocking the pool, buys that much, and refunds the remainder. Note the refund follows the same rule as any other leftover on this page: it goes torecipient, not to your router. So a 10 ETH order on a fresh curve does not fail — it fills partially and the rest lands with the end user. Quote accordingly, or size the order yourself using the formula below. -
Bypassing
DirectSwap, before graduation — the swap reverts withMintCapExceeded(uint256 requestedPegs, uint256 capPegs). Nothing is taken; you pay gas. -
Bypassing
DirectSwap, after graduation — there is no cap check on that path at all, not even a revert. The bound post-graduation is enforced entirely byDirectSwapsizing the input before the swap; a caller that goes straight to the PoolManager is not bounded by it. Do not read the pre-graduation revert as a guarantee that applies everywhere — and note this is the case that will actually apply to you, since a token has no public Uniswap liquidity to route against until it graduates.Being direct about what that means: bypassing us post-graduation will not stop you, but the object-mint arming still will (
armForExternalSwaprevertsNotDirectSwap), so you get the ERC-20 and your user gets no art. That is the failure this whole page is about.
Everything you need to compute the ceiling yourself is public on the hook. Pre-graduation:
reserveToken = R_tok_v() - realTokensOut()
reserveEth = R_eth_v() + realEthCollected()
if (reserveToken <= 350e18) -> no bound applies
netForCap = 350e18 * reserveEth / (reserveToken - 350e18)
maxGrossEth = netForCap * 10_000 / (10_000 - 250) // add back the 2.5% fee
A threshold-crossing buy is bounded a second time, by the ETH still needed to reach
migrationThreshold() — take the smaller of the two ceilings.
Post-graduation the ceiling comes from the pool rather than the curve: read
slot0.sqrtPriceX96 and getLiquidity() for poolKey().toId(), then solve for the ETH
that moves the price by 350e18 of currency1 and gross it up by the same 2.5%. Pool
liquidity is full-range and constant across ticks, so the single-range v4 formulas are
exact here rather than an approximation.
Why a cap exists at all: minting is the leg nearest the chain's 32,000,000 per-transaction gas cap. Without the bound, an oversized buy died with an out-of-gas revert that nothing on chain explained. A named error and a clamped fill are both strictly better failures.
What does NOT work
- Routing through the Uniswap v4 PoolManager directly with your own
hookData. Post-graduation, the mint arming is gated:PegHook.armForExternalSwapreverts withNotDirectSwapfor any caller other thanDirectSwap. - A generic v3/v2-style route, or any path that lands PEG at your contract first and forwards it afterwards.
- Backfilling later. The mint signal is bound to a single transaction; once that transaction ends, no party — including peg.fun — can mint the objects retroactively. The contracts are renounced.
Chain support
peg.fun runs on Robinhood Chain (Arbitrum Orbit L2; mainnet chain id 4663, testnet 46630). An aggregator that does not index this chain cannot list peg.fun tokens at all — chain support comes first, routing second.
Why this is intentional
The trade-off is routing certainty over routing convenience, peg.fun deliberately controls the buy path so that every object mint, every fee accumulation, and every balance-weight calculation is consistent and predictable. The alternative, supporting aggregator routes, would require either off-chain mint backfilling (which breaks the "everything on-chain" promise) or a contract upgrade path (which breaks the renounced posture).
DirectSwap is the answer. Always swap there.