Uniswap V4 Liquidity Management
Twin manages concentrated liquidity positions on Uniswap V4 (Base) via BitGo Express async operations.
Overview
Liquidity operations are executed through BitGo Express sendContractCall, which signs and broadcasts on-chain transactions. All operations are async: the API returns a transactionId immediately, and the on-chain work (approvals, TX broadcast, receipt polling) runs in the background.
Token Ordering
Uniswap V4 sorts tokens by address. Since USDC (0x8335...) < ARGT (0xf016...):
| Field | Value |
|---|---|
| token0 | USDC |
| token1 | ARGT |
| Price meaning | "ARGT per USDC" |
| Typical price | ~1466 (1 USDC = 1466 ARGT) |
This ordering affects all calculations, tick directions, and amount assignments.
Action IDs
Uniswap V4 PositionManager encodes operations as action bytes:
| Action | ID | Used In |
|---|---|---|
INCREASE_LIQUIDITY | 0 | Increase |
DECREASE_LIQUIDITY | 1 | Remove |
MINT_POSITION | 2 | Mint |
BURN_POSITION | 3 | (after full remove) |
SETTLE_PAIR | 13 | Increase, Mint |
TAKE_PAIR | 17 | Remove |
SWEEP | 20 | Increase, Mint (refund excess) |
1. Increase Liquidity
Add liquidity to an existing position.
2. Mint Position
Create a new concentrated liquidity position. Same flow as Increase but with tick alignment and tokenId extraction.
Tick Alignment
Ticks must be multiples of the pool's tickSpacing. Misalignment causes reverts.
| Tick | Rounding | Function | Why |
|---|---|---|---|
| Lower | Math.floor (round down) | alignTickToSpacing | Extends range downward |
| Upper | Math.ceil (round up) | alignTickToSpacingCeil | Extends range upward |
Using floor for both ticks creates asymmetric ranges (~1.7% deviation from Uniswap UI).
Out-of-Range Positions
When the entire price range is above or below the current price, only one token is required:
| Range Position | Required Token | Other Token |
|---|---|---|
| Below current price | token0 only | Send "0" |
| Above current price | token1 only | Send "0" |
| In range | Both tokens | Auto-calculated |
Backend validates liquidity > 0n with Errors.badRequest('Calculated liquidity is zero...').
3. Remove Liquidity
Remove a percentage (25/50/75/100%) of liquidity from an existing position.
No approvals needed for remove (tokens flow out, not in).
On-chain Data Reading
All data is read directly via eth_call (no subgraphs).
| Data | Contract | Method |
|---|---|---|
| Current price | StateView | getSlot0(poolId) -> sqrtPriceX96 |
| Pool liquidity | StateView | getLiquidity(poolId) |
| Position info | PositionManager | positionInfo(nftId) |
| Position liquidity | PositionManager | getPositionLiquidity(nftId) |
| Token balances | ERC20 | balanceOf(walletAddress) |
Price Conversion
Code
TVL Calculation
Code
Slippage Handling
All amount0Max / amount1Max values include a slippage buffer to prevent reverts caused by integer math rounding (error 0x31e30ad0).
Code
Approvals must also use slippage-adjusted amounts, not the desired amounts. Without this, the contract may try to transfer 1 wei more than approved.
Applies to: increaseLiquidity, mintPosition, rebalance.
Auto-calc Formula (Decimal.js)
Given a price range and one token amount, calculate the complementary amount using concentrated liquidity math. Uses Decimal.js with 40 digits of precision to avoid catastrophic cancellation.
Code
Why Decimal.js?
The subtraction 1/sqrtP - 1/sqrtPb involves two very close numbers when the range is narrow. Standard floating-point loses significant digits (catastrophic cancellation). Decimal.js with 40 digits of precision eliminates this error.
The effective deposit ratio differs from the spot price because it depends on the position of the current price within the range.
Async Operation Pattern
All liquidity operations follow the same async pattern:
waitForReceipt: 20 attempts x 5s = ~100s max polling.- Frontend shows "Operacion iniciada" on the immediate response.
validateBalances: Pre-checks wallet balances before creating any TX. Prevents wasting gas on TXs that would revert.
BitGo sendContractCall Pattern
All operations use the same BitGo pattern:
Code
Note: coin is baseeth (not baseeth:argt) because these are pure contract calls to Uniswap contracts, not token transfers.