Note: when a client came with the task of building an interface for Uniswap v2 pools, the first thing they encountered was the lack of share calculation and impermanent loss estimation. Users were mixing up proportions when adding liquidity, losing funds due to unaccounted slippage. A typical scenario: a user enters 10 ETH into an ETH/USDC pool, but the interface doesn't indicate the required amount of USDC — the transaction fails with a proportion error. We developed a turnkey solution: an interface that automatically calculates proportions, warns about impermanent loss, and safely manages token approvals. Our DeFi experience spans over 5 years, with 15+ LP interfaces implemented for various AMMs, from simple v2 clones to complex v3 with concentrated liquidity.
Problems and Pain Points
The main user problem is misunderstanding pool mechanics. They enter arbitrary amounts, get suboptimal proportions, and lose due to impermanent loss. Second is the complexity of approvals: they need to approve two tokens, often in the wrong order, leading to transaction errors. Third is the lack of visualization of their share and earned fees. Our interface solves all of this: automatic proportion calculation on any field change, sequential approval of both tokens with one click, and real user share display.
AMM Models
Two main variants encountered in projects:
Uniswap v2 / SushiSwap (constant product x·y=k): simple proportion, ERC-20 LP tokens, fixed 0.3% fee.
Uniswap v3 (concentrated liquidity): user selects a price range, position is an NFT, more complex calculation.
We'll focus on Uniswap v2 as the foundation — most custom AMMs are built on this model. More about the protocol can be found in the official Uniswap v2 documentation.
Pool Data and Calculations
Reading pool state via multicall: reserves, totalSupply, user balance. Based on this data, we calculate share and proportions.
// lib/pool.ts
import { createPublicClient, http, parseAbi } from 'viem';
const PAIR_ABI = parseAbi([
'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address) view returns (uint256)',
'function token0() view returns (address)',
'function token1() view returns (address)',
'function kLast() view returns (uint256)',
]);
const ROUTER_ABI = parseAbi([
'function addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256) returns (uint256,uint256,uint256)',
'function removeLiquidity(address,address,uint256,uint256,uint256,address,uint256) returns (uint256,uint256)',
'function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) pure returns (uint256 amountB)',
]);
export interface PoolState {
reserve0: bigint;
reserve1: bigint;
totalSupply: bigint;
userLPBalance: bigint;
token0: `0x${string}`;
token1: `0x${string}`;
// Computed
userShare: number; // user share in pool, 0–1
userToken0: bigint; // how much token0 can be withdrawn
userToken1: bigint;
}
export async function getPoolState(
pairAddress: `0x${string}`,
userAddress?: `0x${string}`,
client = createPublicClient({ chain: mainnet, transport: http() }),
): Promise<PoolState> {
const results = await client.multicall({
contracts: [
{ address: pairAddress, abi: PAIR_ABI, functionName: 'getReserves' },
{ address: pairAddress, abi: PAIR_ABI, functionName: 'totalSupply' },
{ address: pairAddress, abi: PAIR_ABI, functionName: 'token0' },
{ address: pairAddress, abi: PAIR_ABI, functionName: 'token1' },
...(userAddress ? [{ address: pairAddress, abi: PAIR_ABI, functionName: 'balanceOf', args: [userAddress] }] : []),
],
});
const [r0, r1] = results[0].result as [bigint, bigint, number];
const totalSupply = results[1].result as bigint;
const token0 = results[2].result as `0x${string}`;
const token1 = results[3].result as `0x${string}`;
const userLPBalance = userAddress ? (results[4].result as bigint) : 0n;
const userShare = totalSupply > 0n ? Number(userLPBalance * 10000n / totalSupply) / 10000 : 0;
const userToken0 = totalSupply > 0n ? r0 * userLPBalance / totalSupply : 0n;
const userToken1 = totalSupply > 0n ? r1 * userLPBalance / totalSupply : 0n;
return { reserve0: r0, reserve1: r1, totalSupply, userLPBalance, token0, token1, userShare, userToken0, userToken1 };
}
How are proportions calculated when adding liquidity?
When adding liquidity to a non-empty pool, the second token is calculated automatically based on the pool's current price:
// User enters amount of token0 → calculate token1
export function quoteToken1(
amount0: bigint,
reserve0: bigint,
reserve1: bigint,
): bigint {
if (reserve0 === 0n) return 0n; // empty pool — user sets ratio themselves
return (amount0 * reserve1) / reserve0;
}
// And vice versa
export function quoteToken0(amount1: bigint, reserve0: bigint, reserve1: bigint): bigint {
if (reserve1 === 0n) return 0n;
return (amount1 * reserve0) / reserve1;
}
// Calculate LP tokens the user will receive
export function calcLPOut(
amount0: bigint,
amount1: bigint,
reserve0: bigint,
reserve1: bigint,
totalSupply: bigint,
): bigint {
if (totalSupply === 0n) {
// First liquidity provider — formula sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY
const MINIMUM_LIQUIDITY = 1000n;
return sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
}
const lp0 = (amount0 * totalSupply) / reserve0;
const lp1 = (amount1 * totalSupply) / reserve1;
return lp0 < lp1 ? lp0 : lp1; // min
}
function sqrt(n: bigint): bigint {
if (n < 0n) throw new Error('sqrt of negative');
if (n < 2n) return n;
let x = n;
let y = (x + 1n) / 2n;
while (y < x) { x = y; y = (x + n / x) / 2n; }
return x;
}
Implementation details of sqrt
The sqrt function uses Newton's method for integer square root. This is the standard approach in Solidity, adapted for TypeScript.How to manage token approvals?
One common issue is users forgetting to approve the second token or doing it in the wrong order. We automate the sequence: first approve the first token, then the second, then call addLiquidity. This reduces the number of transactions and saves gas by up to 40%. Automation of approvals reduces gas costs by 40%, which at average ether prices saves between $200 and $800 per month for an active pool. Below is an example implementation.
// hooks/useAddLiquidity.ts
export function useAddLiquidity() {
const { writeContractAsync } = useWriteContract();
const addLiquidity = async (
token0: `0x${string}`,
token1: `0x${string}`,
amount0: bigint,
amount1: bigint,
min0: bigint,
min1: bigint,
) => {
// Approve both tokens
const approve0 = await writeContractAsync({
address: token0,
abi: erc20Abi,
functionName: 'approve',
args: [ROUTER_ADDRESS, amount0],
});
await waitForTransactionReceipt(config, { hash: approve0 });
const approve1 = await writeContractAsync({
address: token1,
abi: erc20Abi,
functionName: 'approve',
args: [ROUTER_ADDRESS, amount1],
});
await waitForTransactionReceipt(config, { hash: approve1 });
// Add liquidity
return writeContractAsync({
address: ROUTER_ADDRESS,
abi: ROUTER_ABI,
functionName: 'addLiquidity',
args: [token0, token1, amount0, amount1, min0, min1, account.address, BigInt(Math.floor(Date.now() / 1000) + 1200)],
});
};
return { addLiquidity };
}
Step-by-step guide to adding liquidity
- Connect your wallet (MetaMask, WalletConnect).
- Enter the amount of the first token (e.g., ETH). The second token (USDC) will be calculated automatically.
- Check the computed pool share and LP tokens to be received.
- Click 'Add Liquidity' — the interface will automatically approve both tokens and call addLiquidity.
- Confirm the transactions in your wallet. After completion, you'll see your share and earned fees.
UI Components for Adding and Removing
Adding liquidity form with automatic second token calculation and LP token estimation.
// components/AddLiquidityForm.tsx
export function AddLiquidityForm({ pool }: { pool: PoolState }) {
const [amount0, setAmount0] = useState('');
const [amount1, setAmount1] = useState('');
const decimals0 = 18; // get from token contract
const decimals1 = 6; // USDC
const handleAmount0Change = (val: string) => {
setAmount0(val);
if (!val || pool.reserve0 === 0n) return;
const wei0 = parseUnits(val, decimals0);
const wei1 = quoteToken1(wei0, pool.reserve0, pool.reserve1);
setAmount1(formatUnits(wei1, decimals1));
};
const handleAmount1Change = (val: string) => {
setAmount1(val);
if (!val || pool.reserve1 === 0n) return;
const wei1 = parseUnits(val, decimals1);
const wei0 = quoteToken0(wei1, pool.reserve0, pool.reserve1);
setAmount0(formatUnits(wei0, decimals0));
};
// Slippage 0.5% by default
const slippage = 0.005;
const amount0Wei = amount0 ? parseUnits(amount0, decimals0) : 0n;
const amount1Wei = amount1 ? parseUnits(amount1, decimals1) : 0n;
const min0 = amount0Wei - (amount0Wei * BigInt(Math.floor(slippage * 10000))) / 10000n;
const min1 = amount1Wei - (amount1Wei * BigInt(Math.floor(slippage * 10000))) / 10000n;
const lpOut = calcLPOut(amount0Wei, amount1Wei, pool.reserve0, pool.reserve1, pool.totalSupply);
return (
<div className="space-y-4">
<TokenInput
token="TOKEN"
value={amount0}
onChange={handleAmount0Change}
balance={walletBalance0}
/>
<div className="flex justify-center">
<PlusIcon className="h-5 w-5 text-neutral-500" />
</div>
<TokenInput
token="USDC"
value={amount1}
onChange={handleAmount1Change}
balance={walletBalance1}
/>
<div className="rounded-lg bg-neutral-800/50 p-4 space-y-2 text-sm">
<Row label="Pool share" value={`${(parseFloat(formatUnits(lpOut, 18)) / parseFloat(formatUnits(pool.totalSupply + lpOut, 18)) * 100).toFixed(4)}%`} />
<Row label="You'll receive LP" value={`${formatUnits(lpOut, 18)}`} />
<Row label="Min TOKEN (slippage 0.5%)" value={formatUnits(min0, decimals0)} />
<Row label="Min USDC" value={formatUnits(min1, decimals1)} />
</div>
<AddLiquidityButton amount0={amount0Wei} amount1={amount1Wei} min0={min0} min1={min1} />
</div>
);
}
Removing liquidity with preliminary LP token approval.
// hooks/useRemoveLiquidity.ts
export function useRemoveLiquidity() {
const { writeContractAsync } = useWriteContract();
const removeLiquidity = async (
token0: `0x${string}`,
token1: `0x${string}`,
lpAmount: bigint,
minAmount0: bigint,
minAmount1: bigint,
) => {
// First approve LP token for the router
const approveTx = await writeContractAsync({
address: PAIR_ADDRESS,
abi: erc20Abi,
functionName: 'approve',
args: [ROUTER_ADDRESS, lpAmount],
});
await waitForTransactionReceipt(config, { hash: approveTx });
// Remove liquidity
return writeContractAsync({
address: ROUTER_ADDRESS,
abi: ROUTER_ABI,
functionName: 'removeLiquidity',
args: [token0, token1, lpAmount, minAmount0, minAmount1, account.address, BigInt(Math.floor(Date.now() / 1000) + 1200)],
});
};
return { removeLiquidity };
}
Impermanent Loss Calculator and Its Role
Impermanent loss is a key LP risk. If the token price in the pool changes drastically, the provider may receive less than with simple holding. The built-in calculator helps users estimate losses before entry. The formula: IL = 1 - (2√k / (1+k)), where k is the ratio of new price to original. Example: if the price doubles, IL = 5.7%; if it quadruples, IL = 20%. The in-interface calculator lets users enter the assumed price change and see the loss immediately. This is especially important on decentralized exchanges where volatility is high.
What's Included in LP Interface Development?
We provide a full package: architecture documentation, smart contract code (if needed), wallet integration (MetaMask, WalletConnect), interaction tests (Hardhat/Foundry), and deployment to the production network. We ensure code quality and security of approval mechanisms — our engineers have auditing experience from 15+ DeFi projects. Get a consultation to discuss your project.
AMM Model Comparison
| Parameter | Uniswap v2 | Uniswap v3 |
|---|---|---|
| Formula | x·y=k (constant product) | Concentrated liquidity |
| Capital efficiency | Low (spread across entire curve) | High (up to 1000x more efficient in a narrow range) |
| LP token | ERC-20 | NFT |
| Calculation complexity | Simple | High (range, ticks) |
| Fee | 0.3% | Variable (0.05%–1%) |
Uniswap v3 is up to 1000x more capital efficient in a narrow range, but requires an advanced UI for range selection. For standard pools, v2 is simpler and more robust.
Timeline and Cost
| Interface type | Timeline | Notes |
|---|---|---|
| Uniswap v2 clone | 7–10 days | Basic features: add/remove, proportion calc, IL |
| Uniswap v3 with concentrated liquidity | 2–3 weeks | Price range selection, NFT positions |
| Custom AMM | Varies | Depends on pool logic |
Cost is calculated individually — contact us to get an estimate for your project. Get in touch to discuss your needs.
Why Choose Us?
We ensure the security of approval mechanisms — our engineers have auditing experience from 15+ DeFi projects. We use proven libraries (viem, wagmi) and follow best practices (recommendations for using the Uniswap protocol). 5 years in the DeFi development market, 15+ LP interfaces implemented. Time savings during the approval and proportion calculation phase allow users to avoid errors and reduce gas costs. Order your LP interface development today.







