Pools de liquidite#

Les pools de liquidite constituent le mecanisme central de la finance decentralisee. Avant leur apparition, les echanges decentralises tentaient de reproduire le modele des carnets d’ordres (order books) utilise par les bourses traditionnelles, avec un succes limite : sans market makers professionnels, la liquidite restait insuffisante et les spreads prohibitifs. Les pools de liquidite ont resolu ce probleme en remplacant les market makers humains par des algorithmes mathematiques, permettant a n’importe qui de fournir de la liquidite et de percevoir des frais en retour.

Sur Solana, cet ecosysteme est particulierement riche. Raydium implemente le modele classique a produit constant, Orca propose de la liquidite concentree, et Meteora a introduit le modele DLMM (Dynamic Liquidity Market Maker) avec ses bins discrets. La rapidite du reseau (temps de bloc ~400 ms) et le faible cout des transactions (fractions de centimes) rendent viables des strategies de fourniture de liquidite qui seraient impraticables sur des chaines plus lentes ou plus couteuses.

Ce chapitre explore en profondeur les mecanismes mathematiques qui gouvernent les pools de liquidite. Nous partirons du modele fondamental a produit constant, puis nous examinerons les differentes courbes de bonding, le modele DLMM, le phenomene d’impermanent loss, les LP tokens, et les oracles de prix. Chaque concept est accompagne de simulations Python executables que le lecteur pourra modifier et experimenter.

Pourquoi les pools de liquidite#

Un marche financier ne fonctionne que s’il y a de la liquidite : des acheteurs et des vendeurs disposes a echanger a tout moment. Dans la finance traditionnelle, ce role est assure par des market makers — des institutions qui affichent en permanence des prix d’achat et de vente. Reproduire ce modele sur une blockchain decentralisee pose un probleme fondamental : qui joue le role du market maker lorsqu’il n’y a pas d’intermediaire ?

Définition 113 (Pool de liquidite)

Une pool de liquidite (liquidity pool) est un smart contract qui detient des reserves de deux (ou plus) tokens, permettant des echanges (swaps) entre ces tokens sans recourir a un carnet d’ordres traditionnel. Le prix d’echange est determine par un algorithme mathematique — l”Automated Market Maker (AMM) — en fonction des reserves courantes de la pool. Toute personne peut echanger des tokens contre la pool, a tout moment, tant que les reserves ne sont pas epuisees.

Définition 114 (Fournisseur de liquidite (LP))

Un fournisseur de liquidite (liquidity provider, LP) est un utilisateur qui depose des tokens dans une pool de liquidite. En echange de son depot, il recoit des LP tokens representant sa part proportionnelle des reserves. Les frais de transaction preleves sur chaque swap sont redistribues aux fournisseurs de liquidite, proportionnellement a leur part. Lorsqu’un LP souhaite se retirer, il brule ses LP tokens et recupere sa part des reserves, augmentee des frais accumules.

Le modele des pools de liquidite resout trois problemes simultanement. Premierement, il elimine le besoin de contrepartie specifique : un trader echange contre la pool elle-meme, pas contre un autre trader. Deuxiemement, il democratise la fourniture de liquidite : n’importe qui peut devenir LP en deposant des tokens, sans avoir besoin d’infrastructure de trading sophistiquee. Troisiemement, il fonctionne de maniere entierement autonome : le smart contract execute les echanges et distribue les frais sans intervention humaine.

Remarque 81 (L’innovation des AMM)

Le concept d’AMM pour les echanges decentralises a ete pionnier par Bancor en 2017, qui a introduit l’idee de reserves algorithmiques. Mais c’est Uniswap, lance en novembre 2018 sur Ethereum, qui a popularise le modele a produit constant \(x \cdot y = k\) avec une elegance et une simplicite remarquables. Le contrat Uniswap v1 ne faisait que quelques centaines de lignes de code Solidity, mais il a engendre un ecosysteme de plusieurs dizaines de milliards de dollars de valeur verrouillee. Sur Solana, Raydium (2021) a ete le premier AMM majeur, suivi d”Orca et de Meteora.

AMM a produit constant#

Le modele le plus simple et le plus repandu d’AMM repose sur la formule du produit constant. Malgre sa simplicite, il possede des proprietes mathematiques elegantes et suffit pour la grande majorite des paires de tokens.

Définition 115 (Automated Market Maker (AMM))

Un Automated Market Maker (AMM) est un algorithme qui determine le prix des actifs dans une pool de liquidite en fonction d’une formule mathematique appliquee aux reserves. Contrairement a un carnet d’ordres ou le prix resulte de la rencontre entre offres et demandes, un AMM calcule le prix de maniere deterministe a partir de l’etat courant des reserves. Il n’y a pas de contrepartie humaine : le trader echange directement avec le smart contract.

Définition 116 (Formule du produit constant)

La formule du produit constant stipule que le produit des reserves de deux tokens dans une pool reste constant apres chaque echange :

\[x \cdot y = k\]

ou \(x\) et \(y\) sont les quantites de tokens X et Y dans la pool, et \(k\) est l”invariant. Si un trader souhaite acheter \(\Delta y\) tokens Y en deposant \(\Delta x\) tokens X, la relation suivante doit etre respectee :

\[(x + \Delta x)(y - \Delta y) = k\]

Le prix marginal (instantane) du token Y en termes de token X est alors :

\[P = \frac{x}{y}\]

Ce prix evolue continuement avec chaque echange, assurant que la pool ne peut jamais etre completement drainee d’un cote.

Implementation Python#

L’implementation suivante fournit une classe ConstantProductAMM complete. Le lecteur peut l’utiliser pour simuler des swaps, observer l’evolution des prix et comprendre le fonctionnement interne d’un AMM.

import numpy as np

class ConstantProductAMM:
    """Simulation d'un AMM a produit constant (x * y = k)."""

    def __init__(self, reserve_x: float, reserve_y: float, fee: float = 0.003):
        """
        Parametres
        ----------
        reserve_x : float
            Reserve initiale du token X.
        reserve_y : float
            Reserve initiale du token Y.
        fee : float
            Fraction des frais preleves sur chaque swap (defaut : 0.3 %).
        """
        self.reserve_x = reserve_x
        self.reserve_y = reserve_y
        self.fee = fee
        self.k = reserve_x * reserve_y
        self.total_lp_shares = 1000.0  # parts LP initiales
        self.history = [{
            "action": "init",
            "reserve_x": reserve_x,
            "reserve_y": reserve_y,
            "price": reserve_x / reserve_y,
        }]

    def price(self) -> float:
        """Prix marginal de Y en termes de X (combien de X pour 1 Y)."""
        return self.reserve_x / self.reserve_y

    def swap_x_for_y(self, amount_x: float) -> float:
        """Echange amount_x tokens X contre des tokens Y.

        Retourne la quantite de tokens Y recus.
        """
        amount_x_after_fee = amount_x * (1 - self.fee)
        new_reserve_x = self.reserve_x + amount_x_after_fee
        new_reserve_y = self.k / new_reserve_x
        amount_y = self.reserve_y - new_reserve_y

        self.reserve_x = self.reserve_x + amount_x  # la pool garde les frais
        self.reserve_y = new_reserve_y
        self.k = self.reserve_x * self.reserve_y  # k augmente grace aux frais

        self.history.append({
            "action": f"swap {amount_x:.2f} X -> {amount_y:.2f} Y",
            "reserve_x": self.reserve_x,
            "reserve_y": self.reserve_y,
            "price": self.price(),
        })
        return amount_y

    def swap_y_for_x(self, amount_y: float) -> float:
        """Echange amount_y tokens Y contre des tokens X."""
        amount_y_after_fee = amount_y * (1 - self.fee)
        new_reserve_y = self.reserve_y + amount_y_after_fee
        new_reserve_x = self.k / new_reserve_y
        amount_x = self.reserve_x - new_reserve_x

        self.reserve_y = self.reserve_y + amount_y
        self.reserve_x = new_reserve_x
        self.k = self.reserve_x * self.reserve_y

        self.history.append({
            "action": f"swap {amount_y:.2f} Y -> {amount_x:.2f} X",
            "reserve_x": self.reserve_x,
            "reserve_y": self.reserve_y,
            "price": self.price(),
        })
        return amount_x

    def add_liquidity(self, amount_x: float, amount_y: float) -> float:
        """Ajoute de la liquidite. Retourne le nombre de parts LP emises."""
        share_x = amount_x / self.reserve_x
        share_y = amount_y / self.reserve_y
        share = min(share_x, share_y)
        lp_tokens = share * self.total_lp_shares

        self.reserve_x += amount_x
        self.reserve_y += amount_y
        self.k = self.reserve_x * self.reserve_y
        self.total_lp_shares += lp_tokens

        self.history.append({
            "action": f"add liquidity (+{amount_x:.0f} X, +{amount_y:.0f} Y)",
            "reserve_x": self.reserve_x,
            "reserve_y": self.reserve_y,
            "price": self.price(),
        })
        return lp_tokens

    def remove_liquidity(self, lp_tokens: float) -> tuple[float, float]:
        """Retire de la liquidite. Retourne (amount_x, amount_y)."""
        share = lp_tokens / self.total_lp_shares
        amount_x = share * self.reserve_x
        amount_y = share * self.reserve_y

        self.reserve_x -= amount_x
        self.reserve_y -= amount_y
        self.k = self.reserve_x * self.reserve_y
        self.total_lp_shares -= lp_tokens

        self.history.append({
            "action": f"remove liquidity (-{amount_x:.2f} X, -{amount_y:.2f} Y)",
            "reserve_x": self.reserve_x,
            "reserve_y": self.reserve_y,
            "price": self.price(),
        })
        return amount_x, amount_y

    def __repr__(self) -> str:
        return (
            f"AMM(X={self.reserve_x:.2f}, Y={self.reserve_y:.2f}, "
            f"k={self.k:.2f}, price={self.price():.4f})"
        )


# --- Demonstration ---
pool = ConstantProductAMM(reserve_x=10_000, reserve_y=100)

print(f"Etat initial : {pool}")
print(f"Prix de Y en X : {pool.price():.2f} (1 Y = {pool.price():.2f} X)\n")

# Swaps successifs
for amount in [100, 500, 1000, 2000]:
    y_received = pool.swap_x_for_y(amount)
    print(f"Swap {amount:>5} X -> {y_received:>8.4f} Y | "
          f"Prix effectif : {amount/y_received:.2f} X/Y | "
          f"Nouveau prix marginal : {pool.price():.2f} X/Y")

print(f"\nEtat final : {pool}")
Etat initial : AMM(X=10000.00, Y=100.00, k=1000000.00, price=100.0000)
Prix de Y en X : 100.00 (1 Y = 100.00 X)

Swap   100 X ->   0.9872 Y | Prix effectif : 101.30 X/Y | Nouveau prix marginal : 102.01 X/Y
Swap   500 X ->   4.6571 Y | Prix effectif : 107.36 X/Y | Nouveau prix marginal : 112.34 X/Y
Swap  1000 X ->   8.1118 Y | Prix effectif : 123.28 X/Y | Nouveau prix marginal : 134.50 X/Y
Swap  2000 X ->  12.6505 Y | Prix effectif : 158.10 X/Y | Nouveau prix marginal : 184.80 X/Y

Etat final : AMM(X=13600.00, Y=73.59, k=1000871.53, price=184.7989)

On observe que le prix augmente apres chaque swap : plus on achete de Y, plus il devient cher. C’est le mecanisme fondamental qui empeche la pool d’etre drainee.

Courbe du produit constant et slippage#

La visualisation suivante trace la courbe hyperbolique \(xy = k\), montre la position actuelle des reserves, et illustre un swap comme un deplacement le long de la courbe.

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Pool initiale
x0, y0 = 10_000, 100
k = x0 * y0

# Apres un swap de 3000 X
dx = 3000
x1 = x0 + dx
y1 = k / x1

# Courbe xy = k
x_curve = np.linspace(2000, 25000, 500)
y_curve = k / x_curve

# Prix ideal (sans slippage)
price_ideal = x0 / y0  # prix marginal initial
dy_ideal = dx / price_ideal

fig, ax = plt.subplots(figsize=(10, 7))

# Courbe
ax.plot(x_curve, y_curve, color="#4C72B0", linewidth=2.5, label=f"$xy = {k:,.0f}$")

# Points avant et apres
ax.plot(x0, y0, "o", color="#55A868", markersize=12, zorder=5, label=f"Avant swap ({x0:,}, {y0})")
ax.plot(x1, y1, "o", color="#C44E52", markersize=12, zorder=5, label=f"Apres swap ({x1:,}, {y1:.2f})")

# Fleche du swap
ax.annotate(
    "", xy=(x1, y1), xytext=(x0, y0),
    arrowprops=dict(arrowstyle="-|>", color="#333333", lw=2,
                    connectionstyle="arc3,rad=-0.15")
)
ax.text((x0 + x1) / 2 - 400, (y0 + y1) / 2 + 5,
        f"Swap +{dx:,} X\n$\\rightarrow$ -{y0 - y1:.2f} Y",
        fontsize=10, ha="center", color="#333333",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="wheat", alpha=0.8))

# Zone de slippage
y_ideal = y0 - dy_ideal
ax.fill_between([x1, x1 + 200], [y1, y1], [y_ideal, y_ideal],
                alpha=0.3, color="#C44E52", label="Slippage")
ax.annotate(
    f"Slippage\n{abs(y_ideal - y1):.2f} Y",
    xy=(x1 + 100, (y1 + y_ideal) / 2),
    xytext=(x1 + 2500, (y1 + y_ideal) / 2 + 10),
    fontsize=9, color="#C44E52",
    arrowprops=dict(arrowstyle="-|>", color="#C44E52", lw=1.2),
    bbox=dict(boxstyle="round,pad=0.3", facecolor="#FFEBEE", alpha=0.8)
)

ax.set_xlabel("Reserve X")
ax.set_ylabel("Reserve Y")
ax.set_title("Courbe du produit constant $xy = k$ — swap et slippage")
ax.legend(loc="upper right", fontsize=10)

plt.show()
_images/a0b4bd9bdacf2a22447a38c492b14fc4c29696c9a5536dff021372b44ff71c27.png

Définition 117 (Slippage)

Le slippage (glissement de prix) est la difference entre le prix attendu d’un echange et le prix effectivement obtenu. Dans un AMM a produit constant, le slippage augmente avec la taille de la transaction par rapport aux reserves de la pool. Pour un swap de \(\Delta x\) tokens X dans une pool de reserves \((x, y)\), le prix effectif est :

\[P_{\text{eff}} = \frac{\Delta x}{\Delta y} = \frac{\Delta x}{y - \frac{k}{x + \Delta x}}\]

Le slippage relatif est alors :

\[\text{slippage} = \frac{P_{\text{eff}} - P_{\text{marginal}}}{P_{\text{marginal}}} = \frac{P_{\text{eff}} - x/y}{x/y}\]

Plus la transaction est grande par rapport aux reserves, plus le slippage est important. C’est pourquoi les pools profondes (avec beaucoup de liquidite) offrent de meilleurs prix.

La visualisation suivante montre l’impact du slippage pour differentes tailles de transactions.

Hide code cell source

import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Pool : 10 000 X / 100 Y
x0, y0 = 10_000, 100
k = x0 * y0
prix_marginal = x0 / y0

# Tailles de trade en % de la reserve X
trade_pcts = [0.01, 0.02, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.50]
slippages = []
labels = []

for pct in trade_pcts:
    dx = pct * x0
    dy = y0 - k / (x0 + dx)
    prix_eff = dx / dy
    slip = (prix_eff - prix_marginal) / prix_marginal * 100
    slippages.append(slip)
    labels.append(f"{pct*100:.0f}%")

fig, ax = plt.subplots(figsize=(10, 5))

colors = sns.color_palette("YlOrRd", n_colors=len(slippages))
bars = ax.bar(labels, slippages, color=colors, edgecolor="white", linewidth=1.5)

# Annoter les barres
for bar, slip in zip(bars, slippages):
    ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5,
            f"{slip:.1f}%", ha="center", va="bottom", fontsize=9, fontweight="bold")

ax.set_xlabel("Taille du swap (% de la reserve X)")
ax.set_ylabel("Slippage (%)")
ax.set_title("Slippage en fonction de la taille du swap\n(pool 10 000 X / 100 Y)")

plt.show()
_images/c273c748f31eb73c8a217a1eca76e5312a46b8b83febfb0b3f60d8dc0d7d7605.png

Courbes de bonding#

La formule du produit constant n’est qu’un choix parmi plusieurs fonctions possibles. Chaque courbe de bonding implique des compromis differents en matiere de slippage, de risque d’epuisement des reserves et d’efficacite du capital.

Définition 118 (Courbe de bonding)

Une courbe de bonding (bonding curve) est la fonction mathematique qui definit la relation entre les reserves de tokens dans une pool de liquidite et le prix d’echange. Elle determine comment le prix evolue lorsque les reserves changent. Differentes courbes de bonding produisent des comportements de prix radicalement differents, et le choix de la courbe est une decision de conception fondamentale pour un AMM.

Produit constant : \(x \cdot y = k\)#

C’est le modele d’Uniswap v1/v2 et de Raydium sur Solana. La courbe est une hyperbole : le prix varie doucement pour les petites transactions mais augmente rapidement pour les grandes. La pool ne peut jamais etre completement drainee d’un cote, ce qui garantit une liquidite perpetuelle mais au prix d’un slippage croissant.

Somme constante : \(x + y = k\)#

Ce modele fixe un taux de change constant de 1:1 entre les deux tokens. Le slippage est nul pour toute taille de transaction, mais la pool peut etre entierement drainee d’un cote si le prix du marche diverge du taux fixe. Ce modele est impraticable en production pour des actifs dont le prix fluctue.

StableSwap (Curve)#

Le modele StableSwap, concu par Curve Finance, est un hybride entre la somme constante et le produit constant. Pres de l’equilibre (quand les reserves sont proches), il se comporte comme une somme constante (tres peu de slippage). Loin de l’equilibre, il converge vers un produit constant (protection contre l’epuisement). La formule pour deux tokens est :

\[An^n \sum x_i + D = ADn^n + \frac{D^{n+1}}{n^n \prod x_i}\]

ou \(A\) est le parametre d’amplification, \(n\) le nombre de tokens, \(D\) l’invariant, et \(x_i\) les reserves. Un \(A\) eleve rapproche le comportement de la somme constante ; un \(A\) faible le rapproche du produit constant.

Liquidite concentree (Uniswap v3)#

Dans le modele de liquidite concentree, les fournisseurs de liquidite choisissent un intervalle de prix \([p_a, p_b]\) dans lequel ils deploient leur capital. A l’interieur de cet intervalle, la liquidite est plus dense, ce qui reduit le slippage. En dehors de l’intervalle, la position ne genere ni frais ni echanges. Ce modele est plus efficace en capital mais expose les LPs a un impermanent loss plus important.

Remarque 82 (Choisir la bonne courbe)

Le choix de la courbe de bonding depend fondamentalement de la nature des actifs echanges :

  • Actifs indexes (USDC/USDT, stSOL/SOL) : le modele StableSwap est optimal car les prix restent proches de la parite. Le parametre \(A\) eleve minimise le slippage autour de l’equilibre.

  • Paires generales (SOL/USDC, token/SOL) : le produit constant ou la liquidite concentree sont preferes. Le prix fluctue librement et la pool doit fonctionner sur toute la gamme de prix.

  • Liquidite concentree : maximise l’efficacite du capital pour les LPs actifs qui ajustent regulierement leurs positions.

La visualisation suivante compare les quatre courbes sur un meme graphique.

Hide code cell source

import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(10, 7))

# Reserve totale normalisee D = 200 pour toutes les courbes
D = 200

# 1. Produit constant : x * y = k, avec k = (D/2)^2
x_cp = np.linspace(10, 190, 500)
k_cp = (D / 2) ** 2
y_cp = k_cp / x_cp
ax.plot(x_cp, y_cp, linewidth=2.5, label="Produit constant ($xy = k$)", color="#4C72B0")

# 2. Somme constante : x + y = D
x_cs = np.linspace(0, D, 500)
y_cs = D - x_cs
ax.plot(x_cs, y_cs, linewidth=2.5, label="Somme constante ($x + y = k$)",
        color="#55A868", linestyle="--")

# 3. StableSwap (approximation numerique pour A = 50)
def stableswap_y(x_vals, D, A, n=2):
    """Calcule y pour chaque x en resolvant l'invariant StableSwap."""
    y_vals = []
    for x in x_vals:
        # Iteration de Newton pour resoudre l'invariant
        y = D / n  # point de depart
        for _ in range(100):
            ann = A * n**n
            # f(y) = ann*(x + y) + D - ann*D - D^(n+1)/(n^n * x * y)
            f = ann * (x + y) + D - ann * D - D**(n + 1) / (n**n * x * y)
            # f'(y) = ann - D^(n+1)/(n^n * x * y^2) ... on ignore et on simplifie
            df = ann + D**(n + 1) / (n**n * x * y**2)
            y_new = y - f / df
            if abs(y_new - y) < 1e-10:
                y = y_new
                break
            y = y_new
        y_vals.append(max(y, 0))
    return np.array(y_vals)

x_ss = np.linspace(5, 195, 500)
y_ss = stableswap_y(x_ss, D, A=50)
ax.plot(x_ss, y_ss, linewidth=2.5, label="StableSwap (Curve, $A=50$)",
        color="#C44E52", linestyle="-.")

# 4. Liquidite concentree (produit constant tronque entre p_a et p_b)
# On simule une courbe de produit constant virtuelle, active seulement entre x=60 et x=140
x_cl = np.linspace(60, 140, 500)
# Liquidite virtuelle plus dense (k plus grand dans la zone)
k_cl = (D / 2) ** 2 * 2.5
y_cl = k_cl / x_cl
# Ajuster pour que la courbe passe par un point raisonnable
offset_y = y_cl[len(y_cl)//2] - D/2
y_cl = y_cl - offset_y
ax.plot(x_cl, y_cl, linewidth=2.5, label="Liquidite concentree (zone active)",
        color="#8172B3", linestyle=":")

# Point d'equilibre
ax.plot(D/2, D/2, "o", color="#333333", markersize=10, zorder=5)
ax.text(D/2 + 3, D/2 + 5, "Equilibre", fontsize=10, color="#333333")

ax.set_xlabel("Reserve X")
ax.set_ylabel("Reserve Y")
ax.set_title("Comparaison des courbes de bonding")
ax.legend(loc="upper right", fontsize=10)
ax.set_xlim(0, D)
ax.set_ylim(0, D)

plt.show()
_images/1ecf1b642f9db8c505741ecec669494270d0e4dc49105b43bf7eb4fd37b925a4.png

DLMM (Dynamic Liquidity Market Maker)#

Le modele DLMM represente une evolution majeure des AMMs traditionnels. Au lieu de distribuer la liquidite de maniere continue le long d’une courbe, il la repartit dans des bins (compartiments) discrets, chacun correspondant a un prix fixe.

Définition 119 (DLMM)

Un Dynamic Liquidity Market Maker (DLMM) est un AMM a liquidite concentree dans lequel la liquidite est repartie dans des bins (compartiments) discrets. Chaque bin correspond a un prix fixe, et les echanges au sein d’un meme bin s’executent a ce prix sans aucun slippage. Lorsqu’un echange epuise la liquidite d’un bin, il se poursuit dans le bin suivant, de maniere analogue a la progression dans un carnet d’ordres. Ce modele a ete pionnier par Trader Joe (Avalanche) et a ete adapte par Meteora sur Solana.

Fonctionnement des bins#

L’espace des prix est divise en bins discrets, typiquement espaces d’un pas constant en logarithme du prix (par exemple, 1 point de base = 0.01 % entre bins adjacents). Chaque bin contient une quantite de liquidite deposee par les LPs. Lorsqu’un trader effectue un swap :

  1. Le swap commence dans le bin actif (celui qui contient le prix courant).

  2. Si le montant a echanger est inferieur a la liquidite du bin, le swap s’execute entierement a ce prix : zero slippage.

  3. Si le montant depasse la liquidite du bin, le bin est epuise et le swap se poursuit dans le bin suivant, a un prix legerement different.

  4. Ce processus se repete jusqu’a ce que le montant total soit echange.

Ce mecanisme ressemble a un carnet d’ordres discretise, mais avec une liquidite fournie par des LPs passifs plutot que par des market makers actifs.

Exemple 38 (Swap traversant des bins)

Alice fournit de la liquidite dans les bins 100 a 110, correspondant a une fourchette de prix de $1.00 a $1.10 (chaque bin est espace de 1 centime). Chaque bin contient 1 000 unites de liquidite.

Un acheteur arrive avec un ordre de 3 500 unites. Voici l’execution etape par etape :

  1. Bin 105 (prix courant = $1.05) : 1 000 unites echangees a $1.05 exactement. Reste a echanger : 2 500.

  2. Bin 106 (prix = $1.06) : 1 000 unites echangees a $1.06 exactement. Reste : 1 500.

  3. Bin 107 (prix = $1.07) : 1 000 unites echangees a $1.07 exactement. Reste : 500.

  4. Bin 108 (prix = $1.08) : 500 unites echangees a $1.08 exactement. Ordre completement rempli.

Le prix moyen d’execution est \((1000 \times 1.05 + 1000 \times 1.06 + 1000 \times 1.07 + 500 \times 1.08) / 3500 = \$1.063\). Le prix de marche est maintenant a $1.08 (le bin actif a change).

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# --- Parametres ---
n_bins = 15
bin_prices = np.arange(98, 98 + n_bins)  # bins 98 a 112 (en centimes : $0.98 a $1.12)
liquidity = np.array([200, 400, 600, 800, 1200, 1800, 2000, 2200, 1800, 1200, 800, 500, 300, 200, 100])
active_bin = 7  # bin 105 ($1.05)

# --- Graphique 1 : Distribution de liquidite ---
ax = axes[0]
colors = ["#C44E52" if i == active_bin else "#4C72B0" for i in range(n_bins)]
ax.bar(bin_prices / 100, liquidity, width=0.008, color=colors, edgecolor="white", linewidth=0.8)
ax.axvline(x=bin_prices[active_bin] / 100, color="#C44E52", linestyle="--", alpha=0.6, linewidth=1.5)
ax.text(bin_prices[active_bin] / 100, max(liquidity) * 1.05, "Prix\ncourant",
        ha="center", fontsize=9, color="#C44E52", fontweight="bold")
ax.set_xlabel("Prix ($)")
ax.set_ylabel("Liquidite (unites)")
ax.set_title("Distribution de liquidite par bin")

# --- Graphique 2 : Swap traversant les bins ---
ax = axes[1]

# Liquidite avant et apres le swap (bins 105-108 consommes)
liq_before = liquidity.copy()
liq_after = liquidity.copy()
# Le swap consomme bins 7 (105), 8 (106), 9 (107) et partiellement 10 (108)
consumed = [7, 8, 9]
partial = 10
liq_after[7] = 0      # bin 105 : vide
liq_after[8] = 0      # bin 106 : vide
liq_after[9] = 0      # bin 107 : vide
liq_after[10] = liq_after[10] - 500  # bin 108 : partiellement consomme

bar_width = 0.004
x_before = bin_prices / 100 - bar_width / 2
x_after = bin_prices / 100 + bar_width / 2

ax.bar(x_before, liq_before, width=bar_width, color="#4C72B0", alpha=0.5,
       edgecolor="white", linewidth=0.5, label="Avant swap")
ax.bar(x_after, liq_after, width=bar_width, color="#C44E52", alpha=0.7,
       edgecolor="white", linewidth=0.5, label="Apres swap")

# Annoter les bins traverses
for idx in consumed:
    ax.annotate("vide", xy=(bin_prices[idx] / 100, 100),
                fontsize=7, ha="center", color="#C44E52", fontweight="bold")
ax.annotate("partiel", xy=(bin_prices[partial] / 100, liq_after[partial] + 100),
            fontsize=7, ha="center", color="#C44E52", fontweight="bold")

# Nouveau prix actif
new_active = 10
ax.axvline(x=bin_prices[new_active] / 100, color="#E67E22", linestyle="--",
           alpha=0.8, linewidth=1.5)
ax.text(bin_prices[new_active] / 100 + 0.005, max(liquidity) * 1.05, "Nouveau\nprix",
        ha="left", fontsize=9, color="#E67E22", fontweight="bold")

ax.set_xlabel("Prix ($)")
ax.set_ylabel("Liquidite (unites)")
ax.set_title("Swap traversant 3.5 bins")
ax.legend(fontsize=9)

fig.suptitle("DLMM : liquidite distribuee par bins discrets", fontsize=14, fontweight="bold")
plt.subplots_adjust(wspace=0.3)
plt.show()
_images/5f9872924206b25c69d094b15caedbadf2ea3652d2f08e999f22db74c661bfa9.png

Remarque 83 (Avantages du DLMM sur les AMMs classiques)

Le modele DLMM offre plusieurs avantages significatifs par rapport aux AMMs a produit constant :

  1. Efficacite du capital. Les LPs concentrent leur liquidite sur les prix ou les echanges ont lieu, au lieu de la repartir sur toute la courbe. Un dollar de liquidite dans un DLMM genere significativement plus de frais qu’un dollar dans un AMM classique.

  2. Zero slippage intra-bin. Les echanges qui ne traversent pas de frontiere de bin s’executent au prix exact du bin, sans aucun glissement.

  3. Controle granulaire. Les LPs choisissent exactement dans quels bins ils deposent, ce qui permet des strategies de liquidite sur mesure (concentree autour du prix courant, repartie uniformement, asymetrique, etc.).

  4. Transparence. La distribution de liquidite par bin est visible on-chain, ce qui donne aux traders une information precise sur la profondeur du marche.

Remarque 84 (Meteora sur Solana)

Meteora est l’implementation DLMM dominante sur Solana. Le protocole est utilise pour de nombreux cas d’usage : lancement de memecoins (avec des strategies de liquidite asymetriques), paires de stablecoins (bins resserres autour de la parite), et paires volatiles (bins plus espaces). Meteora propose egalement des strategies de liquidite automatisees (auto-compounding) qui reequilibrent les positions des LPs pour maximiser les frais captures. La granularite des bins (typiquement 1 a 100 points de base) est configurable par pool.

Impermanent Loss#

L’impermanent loss est le phenomene le plus important — et le plus incompris — pour les fournisseurs de liquidite. Il represente le cout d’opportunite de deposer des tokens dans une pool plutot que de simplement les conserver.

Définition 120 (Impermanent Loss (IL))

L”impermanent loss (perte impermanente, IL) est la difference de valeur entre :

  1. Detenir des tokens dans une pool de liquidite, et

  2. Simplement les conserver (hold) dans un portefeuille.

Cette perte survient lorsque le ratio de prix entre les deux tokens change par rapport au moment du depot. Pour un AMM a produit constant, si le prix relatif change d’un facteur \(r\) (c’est-a-dire que le prix du token Y en termes de X est multiplie par \(r\)), l’impermanent loss est :

\[\text{IL}(r) = \frac{2\sqrt{r}}{1 + r} - 1\]

L’IL est toujours negative ou nulle : le LP ne gagne jamais par rapport au holding simple. L’IL est nulle si et seulement si \(r = 1\) (le prix revient a sa valeur initiale).

import numpy as np

def impermanent_loss(r: np.ndarray) -> np.ndarray:
    """Calcule l'impermanent loss pour un ratio de prix r.

    IL(r) = 2*sqrt(r) / (1 + r) - 1
    """
    return 2 * np.sqrt(r) / (1 + r) - 1

# Exemples concrets
ratios = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 5.0, 10.0]
print("Ratio de prix (r) | Impermanent Loss")
print("-" * 40)
for r in ratios:
    il = impermanent_loss(np.array([r]))[0]
    print(f"       {r:>5.2f}x       |     {il*100:>6.2f} %")
Ratio de prix (r) | Impermanent Loss
----------------------------------------
        0.50x       |      -5.72 %
        0.75x       |      -1.03 %
        1.00x       |       0.00 %
        1.25x       |      -0.62 %
        1.50x       |      -2.02 %
        2.00x       |      -5.72 %
        3.00x       |     -13.40 %
        5.00x       |     -25.46 %
       10.00x       |     -42.50 %

Visualisation de l’impermanent loss#

Hide code cell source

import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# --- Graphique 1 : Courbe d'IL ---
ax = axes[0]
r = np.linspace(0.1, 10, 500)
il = 2 * np.sqrt(r) / (1 + r) - 1

ax.plot(r, il * 100, color="#C44E52", linewidth=2.5)
ax.fill_between(r, il * 100, alpha=0.1, color="#C44E52")
ax.axhline(y=0, color="grey", linestyle="-", linewidth=0.8)
ax.axvline(x=1, color="grey", linestyle="--", linewidth=0.8, alpha=0.5)

# Points de reference
ref_points = [(0.5, "0.5x"), (2.0, "2x"), (5.0, "5x")]
for rp, label in ref_points:
    il_val = (2 * np.sqrt(rp) / (1 + rp) - 1) * 100
    ax.plot(rp, il_val, "o", color="#4C72B0", markersize=8, zorder=5)
    ax.annotate(f"r={label}\nIL={il_val:.1f}%",
                xy=(rp, il_val), xytext=(10, -20),
                textcoords="offset points", fontsize=9,
                arrowprops=dict(arrowstyle="-", color="#4C72B0", lw=0.8))

ax.set_xlabel("Ratio de prix ($r = P_{final} / P_{initial}$)")
ax.set_ylabel("Impermanent Loss (%)")
ax.set_title("Courbe d'impermanent loss")
ax.set_xlim(0, 10)
ax.set_ylim(-65, 5)

# --- Graphique 2 : Comparaison HOLD vs LP ---
ax = axes[1]

# Scenario : pool SOL/USDC, depot initial 1 SOL ($100) + 100 USDC
# Prix initial SOL = $100
initial_sol_price = 100
initial_sol = 1
initial_usdc = 100
total_initial = initial_sol * initial_sol_price + initial_usdc  # = $200

# Prix final de SOL varie
sol_prices = np.linspace(10, 500, 200)

# Valeur HOLD
value_hold = initial_sol * sol_prices + initial_usdc

# Valeur LP (AMM constant product)
# k = initial_sol * initial_usdc = 100
# A prix p, reserve_sol = sqrt(k/p), reserve_usdc = sqrt(k*p)
k_lp = initial_sol * initial_usdc
value_lp = 2 * np.sqrt(k_lp * sol_prices)

ax.plot(sol_prices, value_hold, linewidth=2.5, color="#4C72B0", label="HOLD (1 SOL + 100 USDC)")
ax.plot(sol_prices, value_lp, linewidth=2.5, color="#C44E52", label="LP (pool SOL/USDC)")
ax.fill_between(sol_prices, value_lp, value_hold, alpha=0.15, color="#C44E52",
                where=value_hold > value_lp, label="Impermanent Loss")

ax.axvline(x=initial_sol_price, color="grey", linestyle="--", linewidth=0.8, alpha=0.5)
ax.text(initial_sol_price + 5, max(value_hold) * 0.9, "Prix\ninitial",
        fontsize=9, color="grey")

ax.set_xlabel("Prix du SOL ($)")
ax.set_ylabel("Valeur du portefeuille ($)")
ax.set_title("HOLD vs LP : pool SOL/USDC\n(depot : 1 SOL + 100 USDC)")
ax.legend(fontsize=9, loc="upper left")

fig.suptitle("Impermanent Loss : theorie et impact concret", fontsize=14, fontweight="bold")
plt.subplots_adjust(wspace=0.3)
plt.show()
_images/ac998ce272da142b6df0263cc043db6035d9b94c2804e09992bacdaa36c11b43.png

Remarque 85 (Pourquoi « impermanente » ?)

La perte est qualifiee d”impermanente parce qu’elle n’est realisee que si le LP retire sa liquidite alors que le ratio de prix a change. Si le prix revient exactement a son niveau initial (\(r = 1\)), l’impermanent loss disparait completement. Tant que le LP reste dans la pool, la perte est une perte latente, non realisee. En pratique cependant, les prix reviennent rarement exactement a leur valeur initiale, et les LPs doivent comparer l’IL aux frais accumules pour determiner si leur position est globalement profitable.

Remarque 86 (Attenuer l’impermanent loss)

Plusieurs strategies permettent de limiter l’impact de l’impermanent loss :

  1. Paires de stablecoins. Les pools USDC/USDT ont un ratio de prix qui reste tres proche de 1, donc un IL quasi nul. Les frais s’accumulent sans etre erodes par l’IL.

  2. Frais eleves. Les pools avec des volumes de trading importants generent suffisamment de frais pour compenser l’IL. Un LP rationnel compare le rendement net (frais \(-\) IL).

  3. Liquidite concentree. Paradoxalement, la liquidite concentree amplifie l’IL mais amplifie aussi les frais. Si l’intervalle de prix est bien choisi, les frais dominent.

  4. Couverture (hedging). Certains LPs sophistiques utilisent des positions en derives (perps, options) pour neutraliser leur exposition directionnelle et isoler les frais.

  5. Programmes d’incitation. De nombreux protocoles distribuent des tokens de gouvernance aux LPs pour compenser l’IL. C’est le modele dit de liquidity mining.

LP Tokens#

Les LP tokens sont le mecanisme comptable qui permet de suivre la part de chaque fournisseur de liquidite dans une pool.

Définition 121 (LP Token)

Un LP token (token de fournisseur de liquidite) est un token emis par le smart contract de la pool lorsqu’un utilisateur depose de la liquidite. Il represente la part proportionnelle du deposant dans les reserves totales de la pool. Lorsque le LP souhaite se retirer, il brule (detruit) ses LP tokens et recoit en echange sa quote-part des reserves courantes, incluant les frais de trading accumules depuis son depot. La quantite de LP tokens emise est calculee proportionnellement a la contribution relative du deposant.

Exemple 39 (Cycle de vie d’un LP)

Considerons une pool SOL/USDC avec les reserves suivantes :

  • Etat initial de la pool : 100 SOL + 10 000 USDC. Total LP tokens en circulation : 1 000.

Depot : Alice depose 10 SOL + 1 000 USDC, soit 10 % des reserves existantes. Elle recoit \(1\,000 \times 10\% = 100\) LP tokens. La pool a maintenant 110 SOL + 11 000 USDC, et 1 100 LP tokens en circulation.

Accumulation des frais : Au fil du temps, les swaps generent des frais. Apres une periode d’activite intense, la pool contient 110 SOL + 11 550 USDC (les frais ont ajoute 550 USDC aux reserves).

Retrait : Alice brule ses 100 LP tokens, soit \(100 / 1\,100 = 9.09\%\) de l’offre totale. Elle recoit :

  • \(110 \times 9.09\% = 10\) SOL

  • \(11\,550 \times 9.09\% = 1\,050\) USDC

Alice a gagne 50 USDC de frais sur son depot initial de 1 000 USDC. Ce rendement de 5 % est le fruit de sa fourniture de liquidite.

Oracles de prix#

Les protocoles DeFi ont besoin de connaitre le prix des actifs pour fonctionner correctement — pour les liquidations de prets, le calcul de collateral, ou le reequilibrage de positions. Les oracles de prix sont les mecanismes qui fournissent cette information on-chain.

Définition 122 (Oracle de prix)

Un oracle de prix (price oracle) est une source de donnees de prix accessible on-chain. L’oracle fait le pont entre le monde reel (ou les donnees off-chain) et les smart contracts qui ont besoin de prix fiables pour fonctionner. La qualite d’un oracle se mesure par sa precision, sa frequence de mise a jour, sa resistance a la manipulation et sa disponibilite. Un oracle defaillant ou manipule peut provoquer des liquidations incorrectes, des arbitrages abusifs ou la perte de fonds.

Pyth Network#

Pyth est l’oracle dominant sur Solana. Il suit un modele pull-based : des editeurs (publishers) — typiquement des market makers, des bourses et des firms de trading — transmettent leurs prix a Pyth, qui les agrege. Les protocoles consommateurs tirent (pull) le prix le plus recent lorsqu’ils en ont besoin. Ce modele est plus efficace que le modele push-based (ou l’oracle met a jour le prix on-chain a intervalles fixes) car il ne consomme des ressources de calcul que lorsque le prix est effectivement utilise. Pyth publie des prix avec des intervalles de confiance, ce qui permet aux protocoles de connaitre la precision de l’information.

Switchboard#

Switchboard est un framework d’oracle plus general qui permet la creation de flux de donnees (data feeds) personnalises. Il supporte non seulement les prix de marche, mais aussi des donnees arbitraires (meteo, resultats sportifs, donnees IoT). Switchboard utilise un reseau de verificateurs TEE (Trusted Execution Environment) pour garantir l’integrite des calculs off-chain.

TWAP (Time-Weighted Average Price)#

Le TWAP est un prix moyen pondere par le temps, calcule a partir des donnees de prix on-chain d’une pool de liquidite. Plutot que d’utiliser le prix instantane (qui peut etre manipule par un flash loan), le TWAP moyenne les prix observes sur une fenetre temporelle (par exemple, 30 minutes ou 1 heure). Cette moyenne rend la manipulation extremement couteuse car l’attaquant devrait maintenir un prix artificiel pendant toute la duree de la fenetre.

Remarque 87 (Manipulation d’oracle : un vecteur d’attaque critique)

La manipulation d’oracle est l’un des vecteurs d’attaque les plus devastateurs en DeFi. Le scenario typique est le suivant :

  1. Un attaquant contracte un flash loan (pret instantane sans collateral).

  2. Il utilise le montant emprunte pour manipuler le prix dans une pool de liquidite (un swap massif deplace le prix temporairement).

  3. Un protocole DeFi qui utilise le prix instantane de cette pool comme oracle lit un prix incorrect.

  4. L’attaquant exploite ce prix fausse (par exemple, pour emprunter plus que la valeur de son collateral, ou pour declencher des liquidations injustifiees).

  5. L’attaquant rembourse le flash loan et conserve le profit.

Pour se proteger, il est essentiel d’utiliser des TWAP (moyennes temporelles), des oracles externes (Pyth, Switchboard) ou de combiner plusieurs sources de prix. Un seul point de defaillance dans le flux de prix est une invitation a l’exploitation.

Comparaison AMM vs DLMM vs CLOB#

Les trois grands modeles d’echange on-chain coexistent sur Solana, chacun avec ses forces et ses limites.

Critere

AMM (produit constant)

DLMM (bins)

CLOB (carnet d’ordres)

Efficacite du capital

Faible (liquidite repartie sur \([0, +\infty[\))

Elevee (liquidite concentree dans les bins actifs)

Maximale (chaque ordre est un prix exact)

Slippage

Croissant avec la taille du swap

Zero intra-bin, croissant entre bins

Zero (execution au prix limite)

Complexite pour les LPs

Minimale (depot et oubli)

Moderee (choix des bins)

Elevee (gestion active des ordres)

Controle du LP

Aucun (liquidite automatique)

Granulaire (choix des bins et quantites)

Total (prix, taille, conditions)

Impermanent loss

Presente

Presente (amplifiee par la concentration)

Absente (pas de concept de pool)

Cas d’usage typique

Paires long-tail, tokens peu liquides

Memecoins, stables, paires actives

Paires majeures, trading professionnel

Exemples sur Solana

Raydium (AMM v4)

Meteora

Phoenix, OpenBook

Remarque 88 (L’avantage de Solana)

Solana est l’une des rares blockchains ou les trois modeles — AMM, DLMM et CLOB — coexistent et prosperent. La raison est architecturale : avec un temps de bloc de ~400 ms et des frais de l’ordre de $0.001, Solana est assez rapide et economique pour supporter des carnets d’ordres on-chain (Phoenix, OpenBook), ce qui est impraticable sur Ethereum L1 ou un seul placement d’ordre coute plusieurs dollars en gas. Neanmoins, les AMMs et DLMMs restent dominants pour les paires moins liquides et pour les utilisateurs qui preferent la simplicite du swap en un clic. La composabilite entre ces differents modeles via le CPI (Cross-Program Invocation) permet aux agregateurs comme Jupiter de router les swaps a travers les trois types de venues pour obtenir le meilleur prix.

Resume#

Le tableau suivant recapitule les concepts cles introduits dans ce chapitre.

Concept

Description

Pool de liquidite

Smart contract detenant des reserves de tokens, permettant des echanges sans carnet d’ordres

Fournisseur de liquidite (LP)

Utilisateur deposant des tokens dans la pool pour percevoir des frais

AMM

Algorithme determinant le prix a partir d’une formule mathematique appliquee aux reserves

Produit constant (\(xy = k\))

Invariant d’Uniswap : le produit des reserves reste constant apres chaque swap

Slippage

Ecart entre prix attendu et prix d’execution, croissant avec la taille du swap

Courbe de bonding

Fonction mathematique definissant la relation prix-reserves (produit constant, somme constante, StableSwap)

StableSwap

Courbe hybride (Curve) optimisee pour les actifs indexes, parametre par \(A\)

DLMM

AMM a bins discrets (Meteora sur Solana) : zero slippage intra-bin, controle granulaire

Impermanent loss

Perte latente du LP par rapport au holding simple, formule \(2\sqrt{r}/(1+r) - 1\)

LP Token

Token representant la part proportionnelle d’un LP dans les reserves de la pool

Oracle de prix

Source de prix on-chain (Pyth, Switchboard, TWAP), critique pour la DeFi

TWAP

Prix moyen pondere par le temps, resistant a la manipulation par flash loan

CLOB

Carnet d’ordres on-chain (Phoenix, OpenBook), viable sur Solana grace a sa performance

Le chapitre suivant explorera les programmes DeFi plus avancees — prets, emprunts, trading de derives — qui s’appuient sur les pools de liquidite et les oracles de prix decrits ici.