Uniswap v4 expliqué : ce qui change par rapport à v3, comment fonctionnent les hooks, et un tutoriel complet de gestion de liquidité

Photo de Thomas CosiallsThomas Cosialls

Uniswap v4 est en production depuis janvier 2025, et ce n'est pas une v3 avec un meilleur gas. C'est une refonte de la façon dont on construit un AMM : chaque pool vit désormais dans un contrat unique, les jetons ne bougent qu'une fois par transaction, et les pools sont devenus programmables grâce aux hooks. Pour les traders, la différence est presque invisible. Pour les fournisseurs de liquidité, et pour quiconque écrit du code qui gère de la liquidité, presque toute la mécanique quotidienne a changé, à commencer par le fait qu'il n'existe plus de fonction collect().

Cet article est en deux parties. D'abord une revue structurée de ce qui a réellement changé depuis v3, et pourquoi cela compte. Ensuite un tutoriel complet et pratique : gérer une vraie position v4 depuis TypeScript, en couvrant toutes les opérations du cycle de vie — créer, augmenter la liquidité, réduire la liquidité, collecter les frais et brûler — avec les encodages d'actions exacts attendus par le protocole.

Partie 1 : ce qui change par rapport à v3

Un singleton plutôt qu'un contrat par pool

En v3, UniswapV3Factory.createPool() déploie un contrat complet pour chaque pool, et chaque contrat de pool détient ses propres soldes ERC-20. En v4, un unique contrat PoolManager héberge tous les pools sous forme d'état interne : créer un pool n'est plus qu'une écriture d'état, ce qui la rend environ 99,99 % moins coûteuse. Un pool est identifié par sa PoolKey, la structure que vous utiliserez dans chaque opération du tutoriel ci-dessous :

struct PoolKey { Currency currency0; // lower-sorted token; address(0) for native ETH Currency currency1; uint24 fee; // static fee in pips, or the dynamic-fee flag int24 tickSpacing; IHooks hooks; // hook contract, or address(0) for none }
La factory Uniswap v3 déployant un contrat par pool, face au PoolManager singleton d'Uniswap v4 qui héberge les pools sous forme d'état, avec des hooks attachés
v3 déploie un contrat par pool ; v4 stocke chaque pool dans un unique PoolManager.

Deux conséquences en découlent. La même paire de jetons peut désormais exister avec n'importe quels frais et n'importe quel tick spacing, car les paliers de frais ne sont plus limités au classique 0,05 % / 0,30 % / 1 % : tout frais statique jusqu'à 100 % est valide, ou un frais dynamique qu'un hook peut mettre à jour. Et l'ETH natif redevient une monnaie de premier rang : plus d'enveloppement WETH obligatoire, ce qui économise du gas et une étape pour chaque paire ETH.

La comptabilité flash

v4 règle les soldes avec une comptabilité flash bâtie sur le stockage transitoire d'EIP-1153. Pendant une transaction, les opérations n'accumulent que des deltas (qui doit quoi au PoolManager) ; les transferts ERC-20 réels n'ont lieu qu'une fois, en net, à la fin du déverrouillage. Un swap multi-sauts qui aurait déplacé des jetons entre trois contrats de pool en v3 les déplace exactement une fois en v4. La règle qui fait tenir l'ensemble : à la fin de la transaction, tous les deltas doivent être ramenés à zéro, sinon tout est annulé. Cette règle façonne l'API de gestion de position que vous utiliserez plus bas : chaque opération se termine par une action de règlement (SETTLE_PAIR quand vous devez des jetons, TAKE_PAIR quand on vous en doit).

Les hooks : les pools deviennent programmables

La fonctionnalité phare. Un hook est un contrat externe attaché à un pool au moment de sa création (il fait partie de la PoolKey : la même paire avec deux hooks différents donne deux pools différents). Le PoolManager l'appelle à une dizaine de points du cycle de vie : avant et après l'initialisation, l'ajout de liquidité, le retrait de liquidité, le swap et le don, plus des variantes qui peuvent renvoyer des deltas de solde et prélever une part ou subventionner une opération.

Un détail d'implémentation élégant : les permissions d'un hook sont encodées dans sa propre adresse. Les 14 bits de poids faible de l'adresse du contrat déclarent quels callbacks il implémente ; les déployeurs minent donc un sel CREATE2 jusqu'à ce que l'adresse porte les bons drapeaux, et le PoolManager sait quoi appeler sans le moindre registre.

Pour les fournisseurs de liquidité, les hooks sont à la fois l'opportunité et les petits caractères :

  • Les hooks de frais dynamiques ajustent le frais de swap par bloc ou par swap, en fonction de la volatilité ou de l'inventaire, ce qui change l'APR de frais que vous percevez réellement.
  • Les hooks de stratégie implémentent des ordres limites on-chain, du TWAMM (time-weighted average market making), de l'auto-capitalisation, ou de véritables vaults de rééquilibrage au-dessus d'un pool.
  • Les petits caractères : un hook peut aussi prélever une part des frais de swap ou de votre retrait, bloquer des opérations, ou transmettre votre hookData à une logique arbitraire. Lire le contrat du hook avant de fournir de la liquidité à un pool hooké n'est pas optionnel : cela fait partie de la due diligence. Chaque opération de position en v4 porte un paramètre hookData, précisément parce qu'un hook peut attendre une entrée de votre part.

Les positions existent toujours, mais leur gestion passe par des commandes

Comme en v3, une position v4 est un ERC-721 émis par un PositionManager de périphérie. Contrairement à v3, ce contrat n'expose plus une fonction par opération. Il a un point d'entrée unique :

function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable;

unlockData est une paire (actions, params) encodée en ABI : une liste compacte d'octets d'action et un blob de paramètres par action. Vous regroupez ce que vous voulez faire (par exemple réduire la liquidité, puis récupérer les deux jetons) et le PositionManager exécute le lot de façon atomique dans un seul déverrouillage. C'est plus bas niveau qu'en v3, effectivement, mais c'est aussi strictement plus puissant : toute combinaison d'opérations se règle en une transaction, avec un unique mouvement net de jetons.

Les différences v3 → v4 qui comptent pour un LP, côte à côte :

Uniswap v3Uniswap v4
Déploiement du poolUn contrat par poolÉtat dans le singleton PoolManager
ETH natifWETH uniquementPaires en ETH natif prises en charge
Paliers de fraisEnsemble fixe (0,01 à 1 %)Tout frais statique, ou dynamique via un hook
PersonnalisationAucuneHooks sur plus de 10 points du cycle de vie
API de positionUne fonction par opérationUn modifyLiquidities() avec des actions encodées
Collecte des fraiscollect()DECREASE_LIQUIDITY avec une liquidité nulle
Règlement des jetonsTransferts à chaque opérationComptabilité flash, règlement net (EIP-1153)
ApprobationsApprouver le position managerApprouver via Permit2

Partie 2 : le tutoriel de gestion de liquidité

Tout ce qui suit s'exécute sur le pool canonique ETH/USDC 0,05 % sur Base, depuis TypeScript avec viem. Le même code fonctionne sur n'importe quelle chaîne v4, une fois les adresses remplacées depuis la page officielle des déploiements. Nous parcourrons tout le cycle de vie, dans l'ordre :

Les cinq étapes d'une position Uniswap v4 : créer, augmenter, collecter, réduire et brûler, chacune sous forme de lot d'actions via modifyLiquidities
Un point d'entrée, cinq opérations. La collecte est la plus surprenante : c'est une réduction de zéro.

Préparation : adresses, clients et clé de pool

npm install viem @uniswap/v3-sdk @uniswap/sdk-core jsbi

Nous n'utilisons @uniswap/v3-sdk que pour ses calculs (conversion tick ↔ prix et calcul de liquidité) ; l'arithmétique des ticks est identique entre v3 et v4.

config.ts
import { createPublicClient, createWalletClient, http, parseAbi } from 'viem'
import { base } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'

// Uniswap v4 on Base (docs.uniswap.org/contracts/v4/deployments)
export const POSITION_MANAGER = '0x7c5f5a4bbd8fd63184577525326123b519429bdc'
export const STATE_VIEW = '0xa3c0c9b65bad0b08107aa264b0f3db444b867a71'
export const PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3'
export const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
export const NATIVE_ETH = '0x0000000000000000000000000000000000000000'

export const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
export const publicClient = createPublicClient({ chain: base, transport: http() })
export const walletClient = createWalletClient({ account, chain: base, transport: http() })

export const posmAbi = parseAbi([
  'function modifyLiquidities(bytes unlockData, uint256 deadline) payable',
  'function nextTokenId() view returns (uint256)',
  'function getPositionLiquidity(uint256 tokenId) view returns (uint128)',
])

export const stateViewAbi = parseAbi([
  'function getSlot0(bytes32 poolId) view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)',
])

// The ETH/USDC 0.05% pool. currency0 < currency1, and native ETH is address(0),
// so ETH is always currency0 in its pairs.
export const poolKey = {
  currency0: NATIVE_ETH,
  currency1: USDC,
  fee: 500,
  tickSpacing: 10,
  hooks: '0x0000000000000000000000000000000000000000',
} as const

L'aide à l'encodage

Chaque opération est un couple (actions, params) encodé dans un seul blob bytes. Les valeurs d'octets d'action proviennent d'Actions.sol dans v4-periphery :

encoding.ts
import { encodeAbiParameters, encodePacked, keccak256 } from 'viem'
import { poolKey } from './config'

export const Actions = {
  INCREASE_LIQUIDITY: 0x00,
  DECREASE_LIQUIDITY: 0x01,
  MINT_POSITION: 0x02,
  BURN_POSITION: 0x03,
  SETTLE_PAIR: 0x0d,
  TAKE_PAIR: 0x11,
  SWEEP: 0x14,
} as const

export const POOL_KEY_ABI = {
  type: 'tuple',
  components: [
    { name: 'currency0', type: 'address' },
    { name: 'currency1', type: 'address' },
    { name: 'fee', type: 'uint24' },
    { name: 'tickSpacing', type: 'int24' },
    { name: 'hooks', type: 'address' },
  ],
} as const

export function encodeUnlockData(actions: number[], params: `0x${string}`[]) {
  return encodeAbiParameters(
    [{ type: 'bytes' }, { type: 'bytes[]' }],
    [encodePacked(actions.map(() => 'uint8'), actions), params],
  )
}

// A pool's id is the hash of its key
export const poolId = keccak256(encodeAbiParameters([POOL_KEY_ABI], [poolKey]))

export const deadline = () => BigInt(Math.floor(Date.now() / 1000) + 600)

Les approbations initiales, via Permit2

Le PositionManager de v4 ne tire pas les jetons via une allowance ERC-20 directe : il utilise Permit2. Chaque ERC-20 demande donc deux approbations, une fois pour toutes : le jeton approuve Permit2, et Permit2 approuve le PositionManager. L'ETH natif ne demande aucune approbation : il voyage en msg.value.

approve.ts
import { maxUint160, maxUint256, parseAbi } from 'viem'
import { PERMIT2, POSITION_MANAGER, USDC, walletClient } from './config'

const erc20Abi = parseAbi(['function approve(address spender, uint256 amount) returns (bool)'])
const permit2Abi = parseAbi([
  'function approve(address token, address spender, uint160 amount, uint48 expiration)',
])

await walletClient.writeContract({
  address: USDC, abi: erc20Abi, functionName: 'approve',
  args: [PERMIT2, maxUint256],
})

await walletClient.writeContract({
  address: PERMIT2, abi: permit2Abi, functionName: 'approve',
  args: [USDC, POSITION_MANAGER, maxUint160, 2 ** 48 - 1],
})

Créer une position

Créer une position, c'est trois actions : MINT_POSITION crée la position, SETTLE_PAIR paie les deux jetons et, parce qu'un côté est de l'ETH natif, SWEEP nous rend l'excédent d'ETH envoyé. Nous calculons le montant de liquidité à partir des jetons que nous acceptons de déposer, avec les calculs du SDK v3 (un pas de tick vaut 0,01 %, donc ±500 ticks correspond à une fourchette d'environ ±5 %) :

mint.ts
import { TickMath, maxLiquidityForAmounts, nearestUsableTick } from '@uniswap/v3-sdk'
import JSBI from 'jsbi'
import { encodeAbiParameters, parseEther } from 'viem'
import { account, poolKey, posmAbi, publicClient, walletClient,
         POSITION_MANAGER, STATE_VIEW, stateViewAbi, NATIVE_ETH } from './config'
import { Actions, POOL_KEY_ABI, deadline, encodeUnlockData, poolId } from './encoding'

// 1. Where is the pool trading right now?
const [sqrtPriceX96, tick] = await publicClient.readContract({
  address: STATE_VIEW, abi: stateViewAbi, functionName: 'getSlot0', args: [poolId],
})

// 2. A ±5% range around the current tick, aligned to the pool's tick spacing
const tickLower = nearestUsableTick(tick - 500, poolKey.tickSpacing)
const tickUpper = nearestUsableTick(tick + 500, poolKey.tickSpacing)

// 3. How much liquidity do 0.05 ETH + 120 USDC buy in that range?
const amount0 = parseEther('0.05')
const amount1 = 120_000_000n // USDC has 6 decimals
const liquidity = maxLiquidityForAmounts(
  JSBI.BigInt(sqrtPriceX96.toString()),
  TickMath.getSqrtRatioAtTick(tickLower),
  TickMath.getSqrtRatioAtTick(tickUpper),
  amount0.toString(), amount1.toString(), true,
)

// 4. Slippage caps: revert if the pool asks for more than +0.5%
const amount0Max = (amount0 * 1005n) / 1000n
const amount1Max = (amount1 * 1005n) / 1000n

// The next minted position will get this id
const tokenId = await publicClient.readContract({
  address: POSITION_MANAGER, abi: posmAbi, functionName: 'nextTokenId',
})

const params = [
  encodeAbiParameters(
    [POOL_KEY_ABI, { type: 'int24' }, { type: 'int24' }, { type: 'uint256' },
     { type: 'uint128' }, { type: 'uint128' }, { type: 'address' }, { type: 'bytes' }],
    [poolKey, tickLower, tickUpper, BigInt(liquidity.toString()),
     amount0Max, amount1Max, account.address, '0x'],
  ),
  encodeAbiParameters(
    [{ type: 'address' }, { type: 'address' }],
    [poolKey.currency0, poolKey.currency1],
  ),
  encodeAbiParameters([{ type: 'address' }, { type: 'address' }], [NATIVE_ETH, account.address]),
]

const hash = await walletClient.writeContract({
  address: POSITION_MANAGER, abi: posmAbi, functionName: 'modifyLiquidities',
  args: [
    encodeUnlockData([Actions.MINT_POSITION, Actions.SETTLE_PAIR, Actions.SWEEP], params),
    deadline(),
  ],
  value: amount0Max, // currency0 is native ETH; SWEEP refunds the unused part
})

console.log(`Position ${tokenId} minted in tx ${hash}`)

Deux détails méritent qu'on s'y arrête. hookData vaut '0x' parce que ce pool n'a pas de hook ; sur un pool hooké, c'est là que se glisse l'entrée attendue par le hook. Et nous lisons nextTokenId avant la création : modifyLiquidities ne renvoie pas l'identifiant, c'est donc la façon standard de savoir quel ERC-721 vous venez de recevoir.

Augmenter la liquidité

Ajouter à une position existante reprend le même schéma avec INCREASE_LIQUIDITY, qui prend le tokenId au lieu d'une clé de pool et d'une fourchette. Une subtilité de v4 : les revenus de frais sont automatiquement crédités lors d'une augmentation. Si la position a accumulé des frais, ceux-ci compensent une partie de ce que vous devez, et le règlement ne tire que la différence.

increase.ts
const params = [
  encodeAbiParameters(
    [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint128' },
     { type: 'uint128' }, { type: 'bytes' }],
    [tokenId, BigInt(addedLiquidity.toString()), amount0Max, amount1Max, '0x'],
  ),
  encodeAbiParameters(
    [{ type: 'address' }, { type: 'address' }],
    [poolKey.currency0, poolKey.currency1],
  ),
  encodeAbiParameters([{ type: 'address' }, { type: 'address' }], [NATIVE_ETH, account.address]),
]

await walletClient.writeContract({
  address: POSITION_MANAGER, abi: posmAbi, functionName: 'modifyLiquidities',
  args: [
    encodeUnlockData([Actions.INCREASE_LIQUIDITY, Actions.SETTLE_PAIR, Actions.SWEEP], params),
    deadline(),
  ],
  value: amount0Max,
})

Si vous voulez que les frais accumulés financent eux-mêmes l'augmentation, v4 propose des actions de règlement dédiées : CLOSE_CURRENCY décide, monnaie par monnaie, s'il faut régler ou récupérer le reliquat, et CLEAR_OR_TAKE abandonne la poussière en dessous d'un seuil plutôt que de payer plus de gas qu'elle ne vaut. Ces deux actions existent précisément parce que « réinvestir mes frais dans la position » est le geste de gestion de liquidité le plus courant qui soit.

Collecter les frais

Voici l'opération qui surprend tous les développeurs venus de v3 : v4 n'a pas de fonction de collecte. Les frais se collectent en réduisant la liquidité de zéro. L'action DECREASE_LIQUIDITY crédite les frais accumulés comme effet de bord : une réduction nulle crédite donc uniquement les frais, et TAKE_PAIR les envoie à votre adresse.

collect-fees.ts
const params = [
  // liquidity = 0, amount mins = 0: a pure fee collection.
  // Zero mins are safe here because fee collection cannot be front-run.
  encodeAbiParameters(
    [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint128' },
     { type: 'uint128' }, { type: 'bytes' }],
    [tokenId, 0n, 0n, 0n, '0x'],
  ),
  encodeAbiParameters(
    [{ type: 'address' }, { type: 'address' }, { type: 'address' }],
    [poolKey.currency0, poolKey.currency1, account.address],
  ),
]

await walletClient.writeContract({
  address: POSITION_MANAGER, abi: posmAbi, functionName: 'modifyLiquidities',
  args: [
    encodeUnlockData([Actions.DECREASE_LIQUIDITY, Actions.TAKE_PAIR], params),
    deadline(),
  ],
})

Aucun événement ni valeur de retour ne vous indique directement les montants collectés : lisez vos soldes de jetons avant et après l'appel. Si vous exécutez cela sur une planification (et vous devriez : des frais qui dorment ne rapportent rien), cet écart de solde constitue aussi votre comptabilité de revenus de frais.

Réduire la liquidité

Une vraie réduction, c'est le même lot avec un montant de liquidité non nul, et là les paramètres de montant minimal cessent d'être optionnels : ils constituent votre protection contre le slippage si le pool bouge (ou est bougé) entre l'estimation et l'exécution. Calculez hors chaîne les montants attendus pour la liquidité retirée, puis passez une petite décote comme plancher :

decrease.ts
// Current liquidity, straight from the position manager
const currentLiquidity = await publicClient.readContract({
  address: POSITION_MANAGER, abi: posmAbi, functionName: 'getPositionLiquidity',
  args: [tokenId],
})

const liquidityToRemove = currentLiquidity / 4n // trim 25%

// expected0/expected1: quote them off-chain from sqrtPriceX96 and your range
// (the SDK's Position class does this), then floor them at -0.5%:
const amount0Min = (expected0 * 995n) / 1000n
const amount1Min = (expected1 * 995n) / 1000n

const params = [
  encodeAbiParameters(
    [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint128' },
     { type: 'uint128' }, { type: 'bytes' }],
    [tokenId, liquidityToRemove, amount0Min, amount1Min, '0x'],
  ),
  encodeAbiParameters(
    [{ type: 'address' }, { type: 'address' }, { type: 'address' }],
    [poolKey.currency0, poolKey.currency1, account.address],
  ),
]

await walletClient.writeContract({
  address: POSITION_MANAGER, abi: posmAbi, functionName: 'modifyLiquidities',
  args: [
    encodeUnlockData([Actions.DECREASE_LIQUIDITY, Actions.TAKE_PAIR], params),
    deadline(),
  ],
})

Comme les revenus de frais sont crédités automatiquement à chaque réduction, les jetons que vous recevez correspondent au principal plus tous les frais accumulés. Une réduction est donc toujours implicitement une collecte.

Brûler la position

Sortir complètement ne nécessite pas de réduire au préalable. BURN_POSITION retire toute la liquidité restante (et les frais), supprime l'ERC-721, et TAKE_PAIR verse le tout :

burn.ts
const params = [
  encodeAbiParameters(
    [{ type: 'uint256' }, { type: 'uint128' }, { type: 'uint128' }, { type: 'bytes' }],
    [tokenId, amount0Min, amount1Min, '0x'],
  ),
  encodeAbiParameters(
    [{ type: 'address' }, { type: 'address' }, { type: 'address' }],
    [poolKey.currency0, poolKey.currency1, account.address],
  ),
]

await walletClient.writeContract({
  address: POSITION_MANAGER, abi: posmAbi, functionName: 'modifyLiquidities',
  args: [
    encodeUnlockData([Actions.BURN_POSITION, Actions.TAKE_PAIR], params),
    deadline(),
  ],
})

L'aide-mémoire

OpérationActionsSens du règlement
CréerMINT_POSITION + SETTLE_PAIR (+ SWEEP pour l'ETH)Vous payez
AugmenterINCREASE_LIQUIDITY + SETTLE_PAIR (+ SWEEP pour l'ETH)Vous payez (moins les frais accumulés)
Collecter les fraisDECREASE_LIQUIDITY (liquidité = 0) + TAKE_PAIRVous recevez
RéduireDECREASE_LIQUIDITY + TAKE_PAIRVous recevez (principal + frais)
BrûlerBURN_POSITION + TAKE_PAIRVous recevez tout

Et les habitudes qui se transposent depuis v3, avec une nuance : les approbations passent par Permit2, pas par le position manager ; les bornes de slippage sont des paramètres par action (amountMax à l'entrée, amountMin à la sortie, zéro uniquement pour une collecte de frais pure) ; l'ETH natif implique msg.value et un SWEEP ; et sur un pool hooké, vérifiez toujours ce que fait le hook avant que votre capital ne l'approche.

D'un script à une stratégie

Tout ce qui précède gère une position, une fois. Une vraie stratégie de liquidité, c'est cette boucle exécutée sans fin : surveiller le tick du pool par rapport à votre fourchette, collecter et réinvestir les frais quand ils couvrent le gas, rééquilibrer quand le prix sort de la fourchette, retirer la liquidité quand la volatilité ou le comportement d'un hook se retourne contre vous, et comptabiliser chaque opération. Entre v4 sur les chaînes EVM, Slipstream sur Aerodrome, les forks d'Algebra, et Raydium ou Orca sur Solana, chaque place parle un dialecte différent du même cycle de vie que vous venez d'apprendre.

Cette boucle, c'est ce que nous construisons chez Etherwave Labs : une automatisation sans garde qui exécute création, augmentation, réduction, collecte et rééquilibrage en continu, avec simulation avant chaque transaction et surveillance après, sur Uniswap v3/v4, Slipstream, Algebra V1, Raydium et Orca. Nous expliquons notre approche sur notre page de service dédiée à l'automatisation de la gestion de liquidité et, si vous préférez que vos positions se gèrent toutes seules, parlons-en.

Plus d’articles

Image de couverture de Construire des bots de trading crypto autonomes avec ElizaOS : le guide technique complet

Construire des bots de trading crypto autonomes avec ElizaOS : le guide technique complet

Apprenez à construire des bots de trading IA autonomes avec le framework ElizaOS. Guide complet : analyse de marché, algorithmes de décision, déploiement sécurisé en TEE et mise en œuvre concrète. Rédigé par les experts blockchain d'Etherwave Labs.

Lire la suite
Image de couverture de Le guide pour collecter frais et récompenses sur les positions Orca Whirlpool

Le guide pour collecter frais et récompenses sur les positions Orca Whirlpool

Apprenez à collecter les frais de trading et les récompenses de vos positions Orca Whirlpool avec Anchor et la crate whirlpool_cpi. Y compris l'étape de mise à jour critique que la plupart des développeurs oublient.

Lire la suite

Prêt à faire passer votre projet au niveau supérieur ?

Contactez-nous dès aujourd’hui pour voir comment nous pouvons vous aider à atteindre vos objectifs dans la blockchain.