Zap into LP (single-sided)
Swap the optimal fraction and add balanced LP in one transaction, with UniswapZap on Uniswap V2 pairs and MotoSwapZap on Motoswap pairs.
A zap takes one token (or ETH), swaps the optimal fraction of it, adds the swap output plus the remainder as balanced liquidity, and refunds the dust. One transaction. The LP lands in your wallet.
There are two zap contracts, one per venue. The deployment status table on the addresses page tracks where each is deployed.
UniswapZap | MotoSwapZap | |
|---|---|---|
| Deployment | Deployed on Ethereum | Deployed on Ethereum |
| Venue | Uniswap V2 pairs | Motoswap pairs |
| Swap leg | Uniswap V2 router. No Motoswap fee. | FeeRouter. Pays the protocol fee, and any creator fee. |
| Address | /config.addresses.uniswapZap | /config.addresses.zap |
Constraints, both zaps:
- Fee-on-transfer tokens are explicitly rejected (
FeeOnTransferNotSupported). The zap compares balance deltas to nominal amounts on both legs. - The pair MUST exist (
PairNotFound).UniswapZapalso revertsEmptyPairwhen the pair has no reserves. - No recipient argument anywhere. LP and refunds go to
msg.sender. - Neither zap holds funds between calls, and neither has an owner.
UniswapZap (deployed)
function getSwapAmount(uint256 amountIn, uint256 reserveIn) pure returns (uint256);
function zapInToken(address tokenIn, uint256 amountIn, address otherToken,
uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline)
returns (uint256 lpAmount);
function zapInETH(address otherToken,
uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline)
payable returns (uint256 lpAmount);
event Zapped(address indexed sender, address indexed pair, address tokenIn, uint256 amountIn, uint256 swapAmount, uint256 lpAmount);ETH into Uniswap V2 LP
import { createPublicClient, createWalletClient, http, parseAbi, parseEther } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
const RPC = process.env.RPC_URL!;
const acct = privateKeyToAccount(process.env.PK as `0x${string}`);
const publicClient = createPublicClient({ chain: mainnet, transport: http(RPC) });
const wallet = createWalletClient({ chain: mainnet, account: acct, transport: http(RPC) });
const cfg = await fetch("https://api.motoswap.org/config").then((r) => r.json());
const { uniswapZap, uniswapV2Factory, weth, motoToken } = cfg.addresses;
const zapAbi = parseAbi([
"function getSwapAmount(uint256 amountIn, uint256 reserveIn) pure returns (uint256)",
"function zapInETH(address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) payable returns (uint256 lpAmount)",
]);
const pairAbi = parseAbi([
"function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)",
"function token0() view returns (address)",
"function totalSupply() view returns (uint256)",
]);
const amountIn = parseEther("0.05");
const otherToken = motoToken;
// 1. The pair.
const pair = await publicClient.readContract({
address: uniswapV2Factory,
abi: parseAbi(["function getPair(address, address) view returns (address)"]),
functionName: "getPair",
args: [weth, otherToken],
});
// 2. Preview the split so the floors mean something.
const [[r0, r1], token0, totalSupply] = await Promise.all([
publicClient.readContract({ address: pair, abi: pairAbi, functionName: "getReserves" }),
publicClient.readContract({ address: pair, abi: pairAbi, functionName: "token0" }),
publicClient.readContract({ address: pair, abi: pairAbi, functionName: "totalSupply" }),
]);
const [reserveIn, reserveOut] = token0.toLowerCase() === weth.toLowerCase() ? [r0, r1] : [r1, r0];
const swapAmount = await publicClient.readContract({
address: uniswapZap, abi: zapAbi, functionName: "getSwapAmount", args: [amountIn, reserveIn],
});
const swapOut = (swapAmount * 997n * reserveOut) / (reserveIn * 1000n + swapAmount * 997n); // Uniswap V2, 0.30%
const lpExpected = ((amountIn - swapAmount) * totalSupply) / (reserveIn + swapAmount);
// 3. Floors at 1% slippage, then send. msg.value is the input.
const floor = (x: bigint, bps: bigint) => (x * (10_000n - bps)) / 10_000n;
const deadline = BigInt(Math.floor(Date.now() / 1000) + 600);
await wallet.writeContract({
address: uniswapZap,
abi: zapAbi,
functionName: "zapInETH",
args: [otherToken, floor(swapOut, 100n), 0n, 0n, floor(lpExpected, 100n), deadline],
value: amountIn,
});The guide's fetch calls are simple GETs, which work from any origin. A request that triggers a CORS preflight (a script-set If-None-Match, a JSON POST) only passes for the Motoswap app origin, so make those server-side.
For a token input, approve tokenIn to the zap first and call
zapInToken(tokenIn, amountIn, otherToken, ...).
MotoSwapZap
function getSwapAmount(uint256 amountIn, uint256 reserveIn, address tokenIn, address otherToken) view returns (uint256);
function creatorSkimBps(address tokenIn, address otherToken) view returns (uint256 bps);
function zapInToken(address tokenIn, uint256 amountIn, address otherToken,
uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline)
returns (uint256 lpAmount);
function zapInETH(address otherToken,
uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline)
payable returns (uint256 lpAmount);
import { parseAbi } from "viem";
// 1. Approve tokenIn to the ZAP (not to the router or the FeeRouter).
await wallet.writeContract({
address: tokenIn,
abi: parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]),
functionName: "approve",
args: [zap, amountIn],
});
// 2. Ask the zap for the split it will use. It reads the live fees itself.
const swapAmount = await publicClient.readContract({
address: zap,
abi: parseAbi(["function getSwapAmount(uint256 amountIn, uint256 reserveIn, address tokenIn, address otherToken) view returns (uint256)"]),
functionName: "getSwapAmount",
args: [amountIn, reserveIn, tokenIn, otherToken],
});
// 3. Quote the swap leg through the FeeRouter rules, then set the floors.
// quoteExactIn is the function from the aggregators page.
const blockNumber = await publicClient.getBlockNumber();
const swapOut = await quoteExactIn(publicClient, feeRouter, swapAmount, [tokenIn, otherToken], blockNumber);
// 4. Execute.
await wallet.writeContract({
address: zap,
abi: parseAbi([
"function zapInToken(address tokenIn, uint256 amountIn, address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) returns (uint256 lpAmount)",
]),
functionName: "zapInToken",
args: [tokenIn, amountIn, otherToken, amountSwapOutMin, amountTokenMin, amountOtherMin, amountLpMin, deadline],
});The swap leg goes through the FeeRouter, so it pays the protocol fee and
any creator fee on the quote side. See
the quote recipe for the quote
function.
The zap ends at adding liquidity. Liquidity providers on Motoswap earn the 0.30% trading fee on every swap in their pool.
Slippage floors
amountSwapOutMin
Floor on the swap-leg output.
amountTokenMin / amountOtherMin
Floors passed to addLiquidity. Both are opt-in; 0n accepts whatever ratio
the pool takes.
amountLpMin
Floor on the final LP minted. Set this. It protects the whole zap from being sandwiched by a bot.
The optimal-swap formula
For an input amount A and pool reserve R of the input token, the amount
to swap so that the remainder and the output add as balanced liquidity is:
f = 10_000 - total fee on the swap leg, in bps
fp = f + 10_000
toSwap = (sqrt(R * (R * fp^2 + A * 4 * f * 10_000)) - R * fp) / (2 * f)UniswapZap:f = 9_970, the fixed Uniswap V2 0.30%.MotoSwapZap:f = 10_000 - factory.swapFeeBps() - feeRouter.protocolFeeBps() - creatorSkimBps(tokenIn, otherToken), all three read live inside the call.
Call getSwapAmount on the zap instead of re-implementing this. It is the
number the zap will use.
Common errors
| Error | Zap | Cause | Fix |
|---|---|---|---|
PairNotFound() | both | No pair for (tokenIn, otherToken). | Create it and seed it with a normal addLiquidity. |
EmptyPair() | UniswapZap | The pair exists with zero reserves. | Seed it with a normal addLiquidity. |
IdenticalTokens() | both | tokenIn == otherToken. | Fix your params. |
ZeroAmount() | UniswapZap | amountIn or msg.value is zero. | Send an amount. |
Expired() | both | block.timestamp > deadline. | Later deadline. |
FeeOnTransferNotSupported() | both | One of the tokens taxes transfers. | Add liquidity through the router directly. |
InsufficientLp() | both | Final LP < amountLpMin. | Re-quote, or widen the floor. |
EthTransferFailed() | both | The ETH dust refund to msg.sender failed. | Call from an address that can receive ETH. |