LP Shares
Liquidity providers in a DualPool do not receive ERC-20 share tokens and do not hold v4 positions. Shares are internal, non-transferable accounting entries kept per pool by the hook — a proportional claim on the pool's total assets, earning swap fees and vault yield with no further action.
Bootstrap
Every pool must be seeded by the owner before it can trade:
bootstrap(PoolKey key, uint256 amount0, uint256 amount1) returns (uint256 shares)Bootstrap pulls both tokens, mints sqrt(received0 * received1) shares to the owner, and flips the pool live. The owner-supplied amounts set the initial share/asset ratio, which matters for pairs with asymmetric decimals. Amounts below a per-pool floor revert with BootstrapTooSmall, so the bootstrapper's economic claim is not meaningfully diluted by the virtual position described below.
Virtual-shares inflation defense
Share conversion uses the EIP-4626 virtual-offset pattern. Conversions add one virtual asset and 10 ** decimalsOffset virtual shares to the ratio:
amount = shares * (totalAssets + 1) / (totalShares + 10 ** offset)The offset is derived per pool from the pair's token decimals:
offset = clamp((decimals0 + decimals1) / 2 - 6, 6, 12)For an 18/18-decimal pair the offset is 12; for a 6/6-decimal stablecoin pair it is 6. The virtual position makes post-bootstrap donation attacks uneconomic, at the cost of permanently retaining a dust amount of assets — roughly one token unit per side on a 6/6 pool, measured at about $2.00 of the live pool's $2,000 seed.
totalShares + 10 ** offset denominators with the rounding below — when displaying redeemable amounts. Naive pro-rata math will not match the contract. This app cross-checks its local math against previewWithdraw on every render and prefers the contract's answer when they differ.Deposits and withdrawals
After bootstrap:
addLiquiditymints a requested number of shares and pulls token0/token1 proportional to current total assets. Deposits round up, so new LPs cannot dilute existing LPs.removeLiquidityburns shares and returns proportional token0/token1. Withdrawals round down, so exiting LPs cannot over-withdraw.
Both entry points take caller slippage bounds (maxAmount0/maxAmount1 on deposit, minAmount0/minAmount1 on withdrawal) and a deadline, protecting LPs from ratio changes and vault share-price moves between preview and execution.
Use previewDeposit(key, shares) and previewWithdraw(key, shares) for offchain sizing, and sharesOf(key, account) / totalShares(poolId) to read positions.
Deposit lock
Each pool has a minDepositBlocks parameter, fixed at initialization, that controls how many blocks must elapse after an account's last deposit before it may withdraw:
0: no lock; same-block deposit-then-withdraw is allowed1: same-block withdrawals revert, preventing single-block fee sniping- Larger values enforce longer holding periods
A withdrawal inside the lock window reverts with DepositLocked. The positions page reads the lock live and shows a countdown when one is active.
External deposits
addLiquidity is owner-only by default. The owner can open a pool to third-party LPs with setExternalDeposits(key, true); the current policy is readable via externalDepositsEnabled(poolId).
Keep reading
For the end-to-end deposit and withdrawal flow, read Provide Liquidity.