Skip to content
Lattice

UniswapV3AdapterLib

Git Source

Title: UniswapV3AdapterLib

Authors: David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80), Modified from Uniswap V3 (https://github.com/Uniswap/v3-periphery)

Logic for a custom, full-range Uniswap V3 LP strategy. A v3 LP is a two-token, NFT-wrapped concentrated-liquidity position that does not fit the single-asset IStrategy surface cleanly, so this adapter makes three deliberate simplifications: (1) the position is pinned to full-range (min/max usable ticks) — no active range management; (2) the adapter is swap-free — the keeper funds both token0 and token1; (3) the vault-facing asset is token0 and all NAV is denominated in token0.

NAV is token0 only (#271). Being swap-free, the adapter can only ever hand the vault token0, and the vault counts only its asset. NAV is therefore idle token0 plus the position's token0 leg. token1 is never NAV, whether it sits idle in the adapter (keeper-funded, or left over from a deploy) or in the position's token1 leg. Counting it would put value in the share price that no redeemer can receive.

Valuation — TWAP, never spot (the central risk). NAV reads the pool's TWAP via pool.observe(twapWindow), converts the arithmetic-mean tick to a sqrt price, and derives the position's token0 leg at that price. It never reads slot0: the spot tick is single-block manipulable (a flash swap can push it arbitrarily), and pricing vault shares off spot would let an attacker mint/redeem at a skewed NAV. The TWAP averages the tick over the whole window, so a one-block spike barely moves it. Uncollected fees are NOT counted in NAV (they are the yield distribution, forwarded raw on harvest).

Deploys happen at spot, so the guard bounds them. The pool prices a mint or increase at spot, while NAV counts the new token0 leg at the TWAP. A deploy therefore steps NAV by about consumed0 × (√(P_spot / P_twap) − 1), and deploy runs inside the permissionless rebalance(), where anyone can move spot first. deploy refuses any step larger than slippageBps of the token0 it consumed (UniswapV3AdapterDeployOffTwap); the StrategyManager then reports the failed deploy and the token0 stays idle and counted. Within the bound the step is real: an attacker choosing spot can move NAV by up to slippageBps of each deploy, so keep slippageBps tight.

Withdraw pays idle token0 only. withdraw(amount, to) is token0-denominated and sends at most the adapter's idle token0; it never removes liquidity. A decrease pays out at spot while NAV counts the token0 leg at the TWAP, so with spot above the TWAP it frees less token0 than NAV drops by, and the StrategyManager's value-loss check reverts the whole rebalance(); anyone who can move spot could force that. Deciding from spot would make the recall manipulable, and the token1 a decrease frees would sit idle with nothing to pair it. It returns the REAL token0 delta and never over-reports; anything beyond the idle token0 is an honest partial recall, which the StrategyManager accepts.

Exit needs the admin. rebalance() can allocate into the position but never recall from it, so capital allocated here reaches redeemers only after the admin's emergencyWithdraw; size the target with that in mind. At a target of 0 a rebalance recalls the idle token0, the position's token0 leg stays counted, and removeStrategy refuses the adapter. To retire it: set the target to 0 and rebalance, then the admin calls emergencyWithdraw, which removes the position and sends both tokens to the vault; then removeStrategy. The token0 replaces the token0 leg NAV already counted, but the pool pays it at spot, so NAV steps by about leg0 × (√(P_twap / P_spot) − 1) with no slippage floor: the admin should exit while spot sits near the TWAP (and through a private transaction where one is available). The token1 lands in the vault outside NAV, and VaultCore has no sweep for a non-asset token, so it stays there unless the vault's admin cuts one in.

All heavy math lives in UniswapV3FullRangeMath to keep the facet under the 24KB limit.

Functions

uniswapV3AdapterStorage

function uniswapV3AdapterStorage() internal pure returns (UniswapV3AdapterStorage storage $);

Returns

NameTypeDescription
$UniswapV3AdapterStorage

__UniswapV3Adapter_init

function __UniswapV3Adapter_init(
        address positionManager_,
        address pool_,
        address vault_,
        address recipient_,
        uint32 twapWindow_,
        uint256 slippageBps_
    ) internal;

Parameters

NameTypeDescription
positionManager_address
pool_address
vault_address
recipient_address
twapWindow_uint32
slippageBps_uint256

_initPoolConfig

Reads the pool's token0/token1/fee/tickSpacing, validates the spacing, computes the full-range ticks, and writes them to storage. Split out of the initializer for stack room.

function _initPoolConfig(UniswapV3AdapterStorage storage $, address pool_) private;

Parameters

NameTypeDescription
$UniswapV3AdapterStorage
pool_address

registerInterface

function registerInterface() internal;

asset

function asset() internal view returns (address);

Returns

NameTypeDescription
<none>address

positionManager

function positionManager() internal view returns (address);

Returns

NameTypeDescription
<none>address

pool

function pool() internal view returns (address);

Returns

NameTypeDescription
<none>address

token0

function token0() internal view returns (address);

Returns

NameTypeDescription
<none>address

token1

function token1() internal view returns (address);

Returns

NameTypeDescription
<none>address

fee

function fee() internal view returns (uint24);

Returns

NameTypeDescription
<none>uint24

tokenId

function tokenId() internal view returns (uint256);

Returns

NameTypeDescription
<none>uint256

vault

function vault() internal view returns (address);

Returns

NameTypeDescription
<none>address

twapWindow

function twapWindow() internal view returns (uint32);

Returns

NameTypeDescription
<none>uint32

slippageBps

function slippageBps() internal view returns (uint256);

Returns

NameTypeDescription
<none>uint256

rewardRecipient

function rewardRecipient() internal view returns (address);

Returns

NameTypeDescription
<none>address

operator

function operator() internal view returns (address);

Returns

NameTypeDescription
<none>address

_checkOperator

Reverts ProtocolAdapterUnauthorized unless the caller is the wired operator. Placed at the very top of deploy/withdraw/harvest — BEFORE the reentrancy guard — so an unauthorized call never leaves the guard latched. Zero operator ⇒ always reverts.

function _checkOperator() private view;

minHealthFactor

function minHealthFactor() internal pure returns (uint256);

Returns

NameTypeDescription
<none>uint256

healthFactor

function healthFactor() internal pure returns (uint256);

Returns

NameTypeDescription
<none>uint256

isPaused

function isPaused() internal view returns (bool);

Returns

NameTypeDescription
<none>bool

_twapSqrtPriceX96

The pool's time-weighted-average sqrt price (Q64.96) over twapWindow.

Reads pool.observe([twapWindow, 0]) and converts the arithmetic-mean tick to a sqrt price. This is the only price source for NAV. observe is a view and is resistant to single-block manipulation (it averages the tick across the window), unlike slot0, which is never read here. Rounds the mean tick toward negative infinity to match Uniswap's OracleLibrary.consult (matters only for negative remainders).

function _twapSqrtPriceX96(UniswapV3AdapterStorage storage $) private view returns (uint160);

Parameters

NameTypeDescription
$UniswapV3AdapterStorage

Returns

NameTypeDescription
<none>uint160

_positionLiquidity

The live liquidity of the adapter's position (0 if none minted).

positions() returns a flat 12-word tuple; decoding all 12 just to keep one blows the stack under the via-ir-disabled CI profile, so we staticcall and read only the liquidity word (index 7 → returndata offset 224). Reverts if the call fails or returns short data.

function _positionLiquidity(UniswapV3AdapterStorage storage $) private view returns (uint128 liquidity);

Parameters

NameTypeDescription
$UniswapV3AdapterStorage

Returns

NameTypeDescription
liquidityuint128

totalAssetsManaged

Total assets managed, in token0 = idle token0 + the position's token0 leg at the TWAP price. token1 is excluded, idle or in the position: the swap-free adapter can never hand it to the vault as token0 (see the library's "NAV is token0 only"). Uncollected fees are intentionally excluded too (forwarded raw on harvest, not part of principal NAV).

Manipulation-resistance: the position's token0 leg is derived from the TWAP sqrt price (see _twapSqrtPriceX96), NEVER slot0. The TWAP averages the tick over twapWindow, so a flash-loan spike of the spot price within one block barely moves the reported NAV — an attacker cannot mint/redeem vault shares against a skewed price. view (observe is a view). The adapter's state-changing ops are all nonReentrant, and VaultCore blocks share-price-sensitive vault entries while a rebalance is in flight.

token1 (#271): keeper-funded token1 does not move NAV when it lands. A deploy turns idle token0 into the token0 leg, consumed at spot and counted at the TWAP, so it leaves NAV flat at spot == TWAP and otherwise steps it by the gap, which deploy caps at slippageBps of the token0 consumed (see the library's "Deploys happen at spot"). An adapter holding only token1 reports zero, so addStrategy accepts it. The token1 is recovered only by the admin's emergencyWithdraw, which sends it to the vault, outside NAV.

function totalAssetsManaged() internal view returns (uint256);

Returns

NameTypeDescription
<none>uint256

_positionAmount0

The token0 that liquidity of the full-range position holds at the TWAP price.

The price source is the TWAP sqrt price (_twapSqrtPriceX96) — never slot0. Excludes uncollected fees.

function _positionAmount0(UniswapV3AdapterStorage storage $, uint128 liquidity)
        private
        view
        returns (uint256 amount0);

Parameters

NameTypeDescription
$UniswapV3AdapterStorage
liquidityuint128

Returns

NameTypeDescription
amount0uint256

setTwapWindow

function setTwapWindow(uint32 twapWindow_) internal;

Parameters

NameTypeDescription
twapWindow_uint32

setSlippageBps

function setSlippageBps(uint256 slippageBps_) internal;

Parameters

NameTypeDescription
slippageBps_uint256

setRewardRecipient

function setRewardRecipient(address recipient) internal;

Parameters

NameTypeDescription
recipientaddress

setOperator

Sets the authorized operator for deploy/withdraw/harvest (admin-only). Rejects address(0) so the trio cannot be opened to an unauthenticated default.

function setOperator(address operator_) internal;

Parameters

NameTypeDescription
operator_address

deploy

Adds the adapter's held token0 + token1 to a full-range position: mint the first time, increaseLiquidity thereafter. Swap-free — consumes whatever the keeper funded.

Slippage floors amount0Min/amount1Min are the desired amounts haircut by slippageBps (Uniswap enforces them; a thin/imbalanced pool that would consume too little reverts). The floors check the keeper's funding ratio, not the price, so _checkDeployAtTwap also refuses a deploy whose new token0 leg, counted at the TWAP, differs from the token0 it consumed at spot by more than slippageBps (#271). "deployed" is reported in token0 units (the amount0 actually consumed) for parity with the single-asset adapters; token1 consumption is incidental to building the position.

function deploy() internal returns (uint256 deployed);

Returns

NameTypeDescription
deployeduint256

_mintPosition

Mints the initial full-range position from (bal0, bal1); stores the new id and returns the token0 actually consumed. Slippage floors are the balances haircut by slippageBps.

function _mintPosition(UniswapV3AdapterStorage storage $, uint256 bal0, uint256 bal1)
        private
        returns (uint128, uint256);

Parameters

NameTypeDescription
$UniswapV3AdapterStorage
bal0uint256
bal1uint256

Returns

NameTypeDescription
<none>uint128
<none>uint256

_increasePosition

Adds (bal0, bal1) to the existing position; returns the liquidity added and the token0 actually consumed.

function _increasePosition(UniswapV3AdapterStorage storage $, uint256 bal0, uint256 bal1)
        private
        returns (uint128, uint256);

Parameters

NameTypeDescription
$UniswapV3AdapterStorage
bal0uint256
bal1uint256

Returns

NameTypeDescription
<none>uint128
<none>uint256

_checkDeployAtTwap

Refuses a deploy that would step NAV by more than slippageBps of the token0 it consumed (#271). The pool prices a mint or increase at spot, so it takes consumed0 token0, while NAV counts the added liquidity's token0 leg at the TWAP. NAV therefore moves by the difference, about consumed0 × (√(P_spot / P_twap) − 1), and anyone who can move spot inside the permissionless rebalance() could choose it. Reverting rolls the deploy back, and the StrategyManager reports the failure and leaves the token0 idle (and counted), so the worst case is a skipped deploy. Compares the two amounts the deploy produced and never reads slot0. The 2 wei allowance absorbs the pool rounding the consumed amount up and the TWAP leg rounding down, so slippageBps == 0 still deploys at spot == TWAP.

function _checkDeployAtTwap(UniswapV3AdapterStorage storage $, uint128 added, uint256 consumed0) private view;

Parameters

NameTypeDescription
$UniswapV3AdapterStorage
addeduint128
consumed0uint256

withdraw

Recalls up to amount of token0 to to from the adapter's idle token0. Shortfall-honest: the returned value is the REAL token0 sent to to, never over-reported.

Never removes liquidity (#271): a decrease pays out at spot against a TWAP-counted token0 leg, so with spot above the TWAP the StrategyManager would see a value loss and revert the whole rebalance() (see the library's "Withdraw pays idle token0 only"). Asking for more than the idle token0 is therefore an honest partial recall: the unpaid part stays in the counted token0 leg. The position is unwound only by the admin's emergencyWithdraw (see the library's "Exit needs the admin").

function withdraw(uint256 amount, address to) internal returns (uint256 withdrawn);

Parameters

NameTypeDescription
amountuint256
toaddress

Returns

NameTypeDescription
withdrawnuint256

harvest

Collects accrued swap fees (token0 + token1) and forwards them RAW to the reward recipient. Fees are the yield distribution; they are NOT counted in NAV.

Collects with tokensOwed* maxima straight to the recipient. Graceful on a zero-fee position (collect returns 0). Because principal (live liquidity) is never decreased here, a non-zero collect can only be accrued fees — never principal — so it cannot drain the LP position. Idle balances are never forwarded: idle token0 is NAV, and idle token1 waits for the next deploy.

function harvest() internal;

emergencyWithdraw

Fully exits: removes ALL liquidity, collects everything, and sends both tokens to the vault. Admin-gated; runs even when paused/stopped (the emergency path).

No slippage floor (min == 0): an emergency prioritizes getting funds out. Both token0 and token1 (principal + any accrued fees, indistinguishable once collected) go to the vault. This is the only way the position is unwound and the only way token1 leaves the adapter (see "Exit needs the admin"). The token0 replaces the token0 leg and idle token0 NAV already counted, but the decrease pays at spot, so the vault's NAV steps by about leg0 × (√(P_twap / P_spot) − 1) (plus any collected token0 fees), with no floor: call it while spot sits near the TWAP. The token1 was never NAV and lands in the vault outside it.

function emergencyWithdraw() internal returns (uint256 recovered);

Returns

NameTypeDescription
recovereduint256

_decreaseAndCollect

Decreases liquidityToRemove from the position and collects the freed token0 + token1 (plus any accrued fees) to the adapter. Used only by emergencyWithdraw.

function _decreaseAndCollect(UniswapV3AdapterStorage storage $, uint128 liquidityToRemove) private;

Parameters

NameTypeDescription
$UniswapV3AdapterStorage
liquidityToRemoveuint128