SC-08-DeFi-Primitives-Python - Primitives DeFi

<< Token Standards | DAO Governance >>


Objectifs d’apprentissage

  1. Comprendre les AMM (Automated Market Makers)
  2. Implementer la formule **x*y=k** (Constant Product)
  3. Créer des liquidity pools simples
  4. Comprendre le price impact et le slippage

Prerequis

  • SC-3 a SC-7 completes
  • Notions de finance decentralisee (AMM, liquidite)
  • Maitrise des interfaces et appels externes Solidity

Duree estimee : 55 minutes


0. Connexion a la blockchain locale

Tous les contrats de ce notebook sont compiles et deployes reellement sur anvil. Lancez anvil dans un terminal avant d’executer les cellules.

# Connection a anvil (blockchain locale Foundry)
# Prerequis: anvil en cours d'execution dans un terminal
from web3 import Web3
import solcx

SOLC_VERSION = "0.8.28"
ANVIL_URL = "http://127.0.0.1:8545"

# Connexion
w3 = Web3(Web3.HTTPProvider(ANVIL_URL))
assert w3.is_connected(), f"Impossible de se connecter a {ANVIL_URL}. Lancez 'anvil' dans un terminal."

# Installer solc si necessaire
installed = [str(v) for v in solcx.get_installed_solc_versions()]
if SOLC_VERSION not in installed:
    solcx.install_solc(SOLC_VERSION)
solcx.set_solc_version(SOLC_VERSION)

deployer = w3.eth.accounts[0]
print(f"Connecte a anvil (chain {w3.eth.chain_id}), deployer: {deployer[:10]}...")


def compile_and_deploy(w3, source_code, deployer, *constructor_args):
    """Compiler et deployer un contrat Solidity.

    Gere les sources avec interfaces + contrats en selectionnant
    automatiquement le contrat qui possede du bytecode.
    """
    compiled = solcx.compile_source(
        source_code, output_values=["abi", "bin"], solc_version=SOLC_VERSION
    )
    # Selectionner le contrat avec du bytecode (ignorer les interfaces)
    contract_id, contract_interface = None, None
    for cid, ci in compiled.items():
        if ci["bin"]:  # Les interfaces n'ont pas de bytecode
            contract_id, contract_interface = cid, ci
    if contract_interface is None:
        contract_id, contract_interface = compiled.popitem()
    Contract = w3.eth.contract(
        abi=contract_interface["abi"], bytecode=contract_interface["bin"]
    )
    tx_hash = Contract.constructor(*constructor_args).transact({"from": deployer})
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    instance = w3.eth.contract(
        address=receipt.contractAddress, abi=contract_interface["abi"]
    )
    print(f"Deploye: {contract_id.split(':')[-1]} a {receipt.contractAddress}")
    return instance, receipt
Connecte a anvil (chain 31337), deployer: 0xf39Fd6e5...

1. Automated Market Maker (AMM)

Les AMM remplacent les carnets d’ordres par des formules mathematiques.

Formule Constant Product : \(x \cdot y = k\)

Ou : - \(x\) = reserve de token A - \(y\) = reserve de token B
- \(k\) = constante (ne change pas lors d’un swap)

# Simulation AMM en Python
def simulate_swap(reserve_a, reserve_b, amount_in, fee_percent=0.3):
    """
    Simule un swap selon la formule x*y=k
    
    Args:
        reserve_a: Reserve de token A
        reserve_b: Reserve de token B
        amount_in: Montant de A a echanger
        fee_percent: Fee en pourcentage
    
    Returns:
        amount_out: Montant de B recu
        new_reserve_a: Nouvelle reserve A
        new_reserve_b: Nouvelle reserve B
        price_impact: Impact sur le prix en %
    """
    # Calcul du fee
    amount_in_with_fee = amount_in * (1 - fee_percent / 100)
    
    # Constante k
    k = reserve_a * reserve_b
    
    # Nouvelles reserves
    new_reserve_a = reserve_a + amount_in_with_fee
    new_reserve_b = k / new_reserve_a
    
    # Montant recu
    amount_out = reserve_b - new_reserve_b
    
    # Prix avant/apres
    price_before = reserve_b / reserve_a
    price_after = new_reserve_b / new_reserve_a
    price_impact = ((price_before - price_after) / price_before) * 100
    
    return amount_out, new_reserve_a, new_reserve_b, price_impact

# Exemple
reserve_a = 1000  # ETH
reserve_b = 2000000  # USDC
amount_in = 10  # ETH a echanger

amount_out, new_a, new_b, impact = simulate_swap(reserve_a, reserve_b, amount_in)

print(f"Reserves initiales: {reserve_a} ETH / {reserve_b} USDC")
print(f"Swap: {amount_in} ETH -> {amount_out:.2f} USDC")
print(f"Nouvelles reserves: {new_a:.2f} ETH / {new_b:.2f} USDC")
print(f"Prix: {reserve_b/reserve_a:.2f} USDC/ETH -> {new_b/new_a:.2f} USDC/ETH")
print(f"Price impact: {impact:.2f}%")
Reserves initiales: 1000 ETH / 2000000 USDC
Swap: 10 ETH -> 19743.16 USDC
Nouvelles reserves: 1009.97 ETH / 1980256.84 USDC
Prix: 2000.00 USDC/ETH -> 1960.71 USDC/ETH
Price impact: 1.96%

Exercice 1 : Calcul de la quantite de LP tokens

Lorsqu’un fournisseur de liquidite ajoute des fonds dans un pool AMM existant, il recoit des LP tokens proportionnels a sa contribution. Implementez le calcul du montant de LP tokens mintes.

Indice : Dans un pool existant avec des reserves et un total de LP tokens, le montant de LP tokens mintes est le minimum des deux ratios : - lp0 = amount0 * totalLiquidity / reserve0 - lp1 = amount1 * totalLiquidity / reserve1 - liquidityMinted = min(lp0, lp1)

# Exercice 1 : Calcul de la quantite de LP tokens
def calculate_lp_tokens(amount0, amount1, reserve0, reserve1, total_liquidity):
    """
    Calcule le nombre de LP tokens a minter pour un fournisseur de liquidite.

    Args:
        amount0: Montant de token0 depose
        amount1: Montant de token1 depose
        reserve0: Reserve actuelle de token0 dans le pool
        reserve1: Reserve actuelle de token1 dans le pool
        total_liquidity: Total des LP tokens en circulation

    Returns:
        float: Nombre de LP tokens a minter.
               Si total_liquidity == 0, retourne la moyenne geometrique sqrt(amount0 * amount1).
    """
    # TODO etudiant : gerer le cas initial (total_liquidity == 0)
    # TODO etudiant : calculer lp0 et lp1 proportionnellement aux reserves
    # TODO etudiant : retourner le minimum des deux
    return 0


# Test apres implementation
# Premier fournisseur (pool vide)
lp_initial = calculate_lp_tokens(100, 200_000, 0, 0, 0)
print(f"LP tokens (pool vide) : {lp_initial:.2f}")

# Deuxieme fournisseur (pool existant)
lp_second = calculate_lp_tokens(50, 100_000, 100, 200_000, lp_initial)
print(f"LP tokens (pool existant, reserves 100/200k) : {lp_second:.2f}")
LP tokens (pool vide) : 0.00
LP tokens (pool existant, reserves 100/200k) : 0.00

Interpretation : Simulation Python d’un AMM Constant Product

Résultat obtenu : La simulation calcule le résultat d’un swap de 10 ETH contre des USDC dans un pool avec 1000 ETH et 2 000 000 USDC. Le swap produit 19 743.16 USDC avec un price impact de 1.96%.

Analyse pas a pas :

Étape Valeur Explication
k (constante) 2 000 000 000 reserve_a * reserve_b = 1000 * 2 000 000
Amount avec fee 9.97 ETH 10 * (1 - 0.003)
Nouvelle reserve A 1009.97 ETH 1000 + 9.97
Nouvelle reserve B 1 980 256.84 USDC k / nouvelle_reserve_a
Montant recu 19 743.16 USDC 2 000 000 - 1 980 256.84
Impact prix 1.96% Glissement de 2000 a 1960.71 USDC/ETH

Points cles : - Le fee de 0.3% est deduit du montant d’entree AVANT le calcul (10 ETH -> 9.97 ETH effectifs) ; dans cette simulation le fee (0.03 ETH) est ecarte, NON reinjecte dans les reserves — la constante k reste donc exactement invariante (2 000 000 000) - Le price impact de 1.96% pour un swap de 1% des reserves est typique d’un AMM a produit constant - Le prix effectif recu (1974.32 USDC/ETH) est inferieur au prix spot (2000 USDC/ETH) — cette différence est le slippage - Note : un vrai Uniswap-V2 reinjecte la fee dans le pool (k croit legerement a chaque swap, au benefice des fournisseurs de liquidite) ; cette simulation simplifiee ne modelise pas cette reinjecton, d’ou l’invariance exacte de k ci-dessus

Implémentation Solidity d’un AMM basé sur la formule de produit constant, correspondant à la simulation Python précédente.

# AMM Solidity implementation
AMM_CONTRACT = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}

contract SimpleAMM {
    IERC20 public immutable token0;  // Ex: WETH
    IERC20 public immutable token1;  // Ex: USDC

    uint256 public reserve0;
    uint256 public reserve1;
    uint256 public totalLiquidity;

    mapping(address => uint256) public liquidity;

    uint256 private constant FEE_NUMERATOR = 3;
    uint256 private constant FEE_DENOMINATOR = 1000;  // 0.3%

    event Swap(
        address indexed sender,
        uint256 amount0In,
        uint256 amount1In,
        uint256 amount0Out,
        uint256 amount1Out
    );
    event LiquidityAdded(
        address indexed provider,
        uint256 amount0,
        uint256 amount1,
        uint256 liquidityMinted
    );
    event LiquidityRemoved(
        address indexed provider,
        uint256 amount0,
        uint256 amount1,
        uint256 liquidityBurned
    );

    constructor(address _token0, address _token1) {
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

    // Ajouter de la liquidite
    function addLiquidity(
        uint256 amount0Desired,
        uint256 amount1Desired,
        uint256 amount0Min,
        uint256 amount1Min
    ) external returns (uint256 liquidityMinted) {
        // Transfer les tokens
        token0.transferFrom(msg.sender, address(this), amount0Desired);
        token1.transferFrom(msg.sender, address(this), amount1Desired);

        // Calcul de la liquidite a mint
        if (totalLiquidity == 0) {
            // Premier fournisseur: geometric mean
            liquidityMinted = sqrt(amount0Desired * amount1Desired);
        } else {
            // Proportionnel aux reserves existantes
            uint256 liquidity0 = (amount0Desired * totalLiquidity) / reserve0;
            uint256 liquidity1 = (amount1Desired * totalLiquidity) / reserve1;
            liquidityMinted = liquidity0 < liquidity1 ? liquidity0 : liquidity1;
        }

        require(liquidityMinted > 0, "Insufficient liquidity minted");
        require(amount0Desired >= amount0Min, "Slippage: amount0");
        require(amount1Desired >= amount1Min, "Slippage: amount1");

        liquidity[msg.sender] += liquidityMinted;
        totalLiquidity += liquidityMinted;
        reserve0 += amount0Desired;
        reserve1 += amount1Desired;

        emit LiquidityAdded(msg.sender, amount0Desired, amount1Desired, liquidityMinted);
    }

    // Retirer de la liquidite
    function removeLiquidity(uint256 liquidityToRemove)
        external
        returns (uint256 amount0, uint256 amount1)
    {
        require(liquidity[msg.sender] >= liquidityToRemove, "Insufficient liquidity");

        amount0 = (liquidityToRemove * reserve0) / totalLiquidity;
        amount1 = (liquidityToRemove * reserve1) / totalLiquidity;

        liquidity[msg.sender] -= liquidityToRemove;
        totalLiquidity -= liquidityToRemove;
        reserve0 -= amount0;
        reserve1 -= amount1;

        token0.transfer(msg.sender, amount0);
        token1.transfer(msg.sender, amount1);

        emit LiquidityRemoved(msg.sender, amount0, amount1, liquidityToRemove);
    }

    // Swap token0 -> token1
    function swap0For1(uint256 amount0In, uint256 amount1OutMin)
        external
        returns (uint256 amount1Out)
    {
        token0.transferFrom(msg.sender, address(this), amount0In);

        // Fee deduite
        uint256 amount0InWithFee = (amount0In * (FEE_DENOMINATOR - FEE_NUMERATOR)) / FEE_DENOMINATOR;

        // Constant product formula: (reserve0 + amountIn) * (reserve1 - amountOut) = k
        // amountOut = reserve1 - (reserve0 * reserve1) / (reserve0 + amountInWithFee)
        amount1Out = (amount0InWithFee * reserve1) / (reserve0 + amount0InWithFee);

        require(amount1Out >= amount1OutMin, "Slippage: amount1Out too low");
        require(amount1Out <= reserve1, "Insufficient reserve1");

        reserve0 += amount0In;
        reserve1 -= amount1Out;

        token1.transfer(msg.sender, amount1Out);

        emit Swap(msg.sender, amount0In, 0, 0, amount1Out);
    }

    // Swap token1 -> token0
    function swap1For0(uint256 amount1In, uint256 amount0OutMin)
        external
        returns (uint256 amount0Out)
    {
        token1.transferFrom(msg.sender, address(this), amount1In);

        uint256 amount1InWithFee = (amount1In * (FEE_DENOMINATOR - FEE_NUMERATOR)) / FEE_DENOMINATOR;
        amount0Out = (amount1InWithFee * reserve0) / (reserve1 + amount1InWithFee);

        require(amount0Out >= amount0OutMin, "Slippage: amount0Out too low");
        require(amount0Out <= reserve0, "Insufficient reserve0");

        reserve1 += amount1In;
        reserve0 -= amount0Out;

        token0.transfer(msg.sender, amount0Out);

        emit Swap(msg.sender, 0, amount1In, amount0Out, 0);
    }

    // Prix actuel
    function getPrice0() external view returns (uint256) {
        require(reserve0 > 0, "No liquidity");
        return (reserve1 * 1e18) / reserve0;
    }

    // Square root function (Babylonian method)
    function sqrt(uint256 y) internal pure returns (uint256 z) {
        if (y > 3) {
            z = y;
            uint256 x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }
}
'''

# Token ERC-20 minimal pour tester l'AMM
ERC20_HELPER = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract TestToken {
    string public name;
    string public symbol;
    uint8 public constant decimals = 18;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
        name = _name;
        symbol = _symbol;
        totalSupply = _initialSupply;
        balanceOf[msg.sender] = _initialSupply;
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        require(balanceOf[msg.sender] >= amount, "Insufficient");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        require(allowance[from][msg.sender] >= amount, "Not approved");
        require(balanceOf[from] >= amount, "Insufficient");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}
'''

# Deployer deux tokens pour l'AMM
token0, _ = compile_and_deploy(w3, ERC20_HELPER, deployer, "Wrapped ETH", "WETH", 1_000_000 * 10**18)
token1, _ = compile_and_deploy(w3, ERC20_HELPER, deployer, "USD Coin", "USDC", 10_000_000 * 10**18)

# Deployer l'AMM
amm, _ = compile_and_deploy(w3, AMM_CONTRACT, deployer, token0.address, token1.address)

# --- Demonstration: ajouter de la liquidite ---
amount0 = 100 * 10**18      # 100 WETH
amount1 = 200_000 * 10**18  # 200,000 USDC (prix initial: 2000 USDC/WETH)

# Approuver l'AMM pour les transferts
token0.functions.approve(amm.address, amount0).transact({"from": deployer})
token1.functions.approve(amm.address, amount1).transact({"from": deployer})

# Ajouter la liquidite
tx = amm.functions.addLiquidity(amount0, amount1, 0, 0).transact({"from": deployer})
w3.eth.wait_for_transaction_receipt(tx)

print(f"\nLiquidite ajoutee:")
print(f"  Reserve WETH : {amm.functions.reserve0().call() / 10**18:.0f}")
print(f"  Reserve USDC : {amm.functions.reserve1().call() / 10**18:.0f}")
print(f"  Prix WETH    : {amm.functions.getPrice0().call() / 10**18:.2f} USDC")

# --- Demonstration: swap 5 WETH -> USDC ---
swap_amount = 5 * 10**18
token0.functions.approve(amm.address, swap_amount).transact({"from": deployer})

usdc_before = token1.functions.balanceOf(deployer).call()
tx = amm.functions.swap0For1(swap_amount, 0).transact({"from": deployer})
w3.eth.wait_for_transaction_receipt(tx)
usdc_after = token1.functions.balanceOf(deployer).call()
usdc_received = (usdc_after - usdc_before) / 10**18

print(f"\nSwap 5 WETH -> USDC:")
print(f"  USDC recu       : {usdc_received:.2f}")
print(f"  Prix d'execution: {usdc_received / 5:.2f} USDC/WETH")
print(f"  Nouveau prix    : {amm.functions.getPrice0().call() / 10**18:.2f} USDC/WETH")
Deploye: TestToken a 0x5FbDB2315678afecb367f032d93F642f64180aa3
Deploye: TestToken a 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Deploye: SimpleAMM a 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0

Liquidite ajoutee:
  Reserve WETH : 100
  Reserve USDC : 200000
  Prix WETH    : 2000.00 USDC

Swap 5 WETH -> USDC:
  USDC recu       : 9496.59
  Prix d'execution: 1899.32 USDC/WETH
  Nouveau prix    : 1814.32 USDC/WETH

Interpretation : AMM Solidity — swap et variation de prix

Résultat obtenu : Le contrat SimpleAMM deploye avec 100 WETH et 200 000 USDC (prix initial 2000 USDC/WETH). Un swap de 5 WETH produit 9496.59 USDC, soit un prix d’exécution de 1899.32 — inferieur au prix spot de 2000.

Analyse du swap :

Metrique Valeur Formule
Prix spot initial 2000.00 USDC/WETH reserve1/reserve0
Montant recu 9496.59 USDC (amountIn * reserve1) / (reserve0 + amountIn)
Prix d’exécution 1899.32 USDC/WETH montant_recu / montant_envoye
Nouveau prix spot 1814.32 USDC/WETH nouvelles_reserves
Slippage ~5.0% (2000 - 1899) / 2000

Points cles : - La fee de 0.3% (3/1000) est deduite du montant d’entree avant le calcul — le montant effectif qui entre dans la formule x*y=k est amountIn * 997/1000 - Le prix d’exécution (1899) est toujours inferieur au prix spot initial (2000) — c’est le cout du slippage - Le nouveau prix spot (1814) est encore plus bas : le swap a deplace l’equilibre du pool - La protection amountOutMin dans swap0For1 evite les pertes excessives si un autre trade arrive dans le même bloc (front-running)


2. Price Impact et Slippage

  • Price Impact : Changement de prix cause par votre trade
  • Slippage : Différence entre le prix attendu et obtenu
# Visualisation du price impact
def calculate_price_impact_curve(reserve_a, reserve_b, max_swap_percent=50):
    """Calcule le price impact pour differentes tailles de swap"""
    import math
    
    k = reserve_a * reserve_b
    initial_price = reserve_b / reserve_a
    
    results = []
    for swap_percent in range(1, max_swap_percent + 1):
        amount_in = reserve_a * swap_percent / 100
        amount_out, new_a, new_b, impact = simulate_swap(reserve_a, reserve_b, amount_in)
        new_price = new_b / new_a
        
        results.append({
            'swap_percent': swap_percent,
            'amount_in': amount_in,
            'amount_out': amount_out,
            'price_impact': impact,
            'execution_price': amount_out / amount_in,
            'spot_price': new_price
        })
    
    return results

# Affichage
results = calculate_price_impact_curve(1000, 2000000)
print("Swap% | Amount In | Amount Out | Price Impact | Exec Price")
print("-" * 65)
for r in results[::10]:  # Every 10%
    print(f"{r['swap_percent']:5}% | {r['amount_in']:9.1f} | {r['amount_out']:10.2f} | {r['price_impact']:11.2f}% | {r['execution_price']:.2f}")
Swap% | Amount In | Amount Out | Price Impact | Exec Price
-----------------------------------------------------------------
    1% |      10.0 |   19743.16 |        1.96% | 1974.32
   11% |     110.0 |  197662.37 |       18.79% | 1796.93
   21% |     210.0 |  346246.39 |       31.63% | 1648.79
   31% |     310.0 |  472197.82 |       41.65% | 1523.22
   41% |     410.0 |  580321.84 |       49.61% | 1415.42

Exercice 2 : Fractionner un swap — un gain ou une illusion ?

Un trader souhaite echanger un grand montant de token A contre du token B. Pour minimiser le price impact, il envisage de repartir le swap en n_splits sous-swaps egaux dans le meme pool (meme paire de reserves).

Ecrivez une fonction qui calcule le montant total de token B recu pour un swap de total_amount reparti en n_splits sous-swaps egaux, comparez avec un swap unique, et expliquez ce que vous constatez a la lumiere de la formule x*y=k.

Indice : Chaque sous-swap modifie les reserves. Le montant recu par le 2e sous-swap sera calcule avec les reserves mises a jour apres le 1er. - Appelez simulate_swap en boucle en mettant a jour les reserves a chaque iteration - Comparez le total avec un seul swap de total_amount - Attention a la precision numerique : un ecart de l’ordre de 1e-10 USDC n’est pas un gain, c’est l’erreur machine

# Exercice 2 : Split optimal d'un swap
def compare_split_vs_single(reserve_a, reserve_b, total_amount, n_splits=5):
    """
    Compare un swap unique vs N sous-swaps egaux.

    Args:
        reserve_a: Reserve initiale de token A
        reserve_b: Reserve initiale de token B
        total_amount: Montant total de A a echanger
        n_splits: Nombre de sous-swaps

    Returns:
        dict avec:
        - single_out: Montant recu en un seul swap
        - split_total: Montant total recu en N sous-swaps
        - difference_pct: Difference en pourcentage
    """
    # Etape 1 : Calculer le swap unique avec simulate_swap
    # Etape 2 : Pour chaque sous-swap, calculer le montant et mettre a jour les reserves
    # Etape 3 : Comparer les deux resultats
    # TODO etudiant : implementer la logique
    return {"single_out": 0, "split_total": 0, "difference_pct": 0}


# Test apres implementation
result = compare_split_vs_single(1000, 2_000_000, 100, n_splits=5)
print(f"Swap unique  : {result['single_out']:.2f} USDC")
print(f"Split ({5}x)   : {result['split_total']:.2f} USDC")
print(f"Difference    : {result['difference_pct']:.2f}%")
Swap unique  : 0.00 USDC
Split (5x)   : 0.00 USDC
Difference    : 0.00%

Interpretation : Courbe de price impact

Résultat obtenu : Le tableau montre la relation non-lineaire entre la taille du swap et le price impact. Un swap de 1% des reserves cause 1.96% d’impact, mais un swap de 41% cause 49.61% d’impact.

Impact vs taille du swap :

Swap (% reserves) Price Impact Exécution Price Observation
1% (10 ETH) 1.96% 1974.32 USDC Impact marginal
11% (110 ETH) 18.79% 1796.93 USDC Impact significatif
21% (210 ETH) 31.63% 1648.79 USDC Glissement majeur
41% (410 ETH) 49.61% 1415.42 USDC Presque moitie du prix

Points cles : - La relation est concave : le price impact croit, mais de moins en moins vite — chaque ETH supplementaire deplace le prix moins que le precedent (l’impact relatif vaut 1 - (1 + rq/x)^-2, concave en q). La lecon reste : plus le swap est gros, plus l’impact est fort, ce qui decoule directement de la formule x*y=k - C’est pourquoi les traders Whales provoquent des “slippages” importants sur les pools peu liquides - Le paramètre amountOutMin dans les swaps sert a proteger contre ce phenomene : la transaction revert si le price impact depasse la tolerance - Les agregateurs (1inch, ParaSwap) routent les gros ordres a travers plusieurs pools pour minimiser l’impact total


3. Lending Simplifie

Principes du lending DeFi : - Deposer des collateraux - Emprunter contre collateral - Taux d’intérêt dynamiques

# Lending Pool simplifie
LENDING_CONTRACT = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}

contract SimpleLending {
    IERC20 public immutable asset;

    // Utilisation: montant emprunte / montant depose
    uint256 public constant MAX_LTV = 75;  // 75%
    uint256 public constant LIQUIDATION_THRESHOLD = 85;  // 85%
    uint256 public constant LIQUIDATION_BONUS = 10;  // 10%

    // Taux d'interet (simplifie)
    uint256 public interestRate = 5;  // 5% APY

    struct UserAccount {
        uint256 collateral;      // Montant depose
        uint256 borrowed;        // Montant emprunte
        uint256 lastUpdate;      // Derniere mise a jour
    }

    mapping(address => UserAccount) public accounts;

    event Deposit(address indexed user, uint256 amount);
    event Borrow(address indexed user, uint256 amount);
    event Repay(address indexed user, uint256 amount);
    event Liquidation(
        address indexed liquidator,
        address indexed borrower,
        uint256 collateralSeized,
        uint256 debtRepaid
    );

    constructor(address _asset) {
        asset = IERC20(_asset);
    }

    // Deposer du collateral
    function deposit(uint256 amount) external {
        asset.transferFrom(msg.sender, address(this), amount);
        _accrueInterest(msg.sender);
        accounts[msg.sender].collateral += amount;
        emit Deposit(msg.sender, amount);
    }

    // Emprunter
    function borrow(uint256 amount) external {
        _accrueInterest(msg.sender);
        UserAccount storage user = accounts[msg.sender];

        uint256 maxBorrow = (user.collateral * MAX_LTV) / 100;
        require(user.borrowed + amount <= maxBorrow, "Insufficient collateral");

        user.borrowed += amount;
        asset.transfer(msg.sender, amount);
        emit Borrow(msg.sender, amount);
    }

    // Rembourser
    function repay(uint256 amount) external {
        _accrueInterest(msg.sender);
        UserAccount storage user = accounts[msg.sender];

        require(amount <= user.borrowed, "Amount exceeds debt");

        asset.transferFrom(msg.sender, address(this), amount);
        user.borrowed -= amount;
        emit Repay(msg.sender, amount);
    }

    // Retirer du collateral
    function withdraw(uint256 amount) external {
        _accrueInterest(msg.sender);
        UserAccount storage user = accounts[msg.sender];

        require(amount <= user.collateral, "Insufficient collateral");

        // Verifier LTV apres retrait
        uint256 remainingCollateral = user.collateral - amount;
        uint256 maxBorrow = (remainingCollateral * MAX_LTV) / 100;
        require(user.borrowed <= maxBorrow, "Would breach LTV");

        user.collateral -= amount;
        asset.transfer(msg.sender, amount);
    }

    // Liquidation
    function liquidate(address borrower) external {
        _accrueInterest(borrower);
        UserAccount storage user = accounts[borrower];

        // Verifier si sous-collateralise
        uint256 collateralValue = user.collateral;
        uint256 maxAllowedDebt = (collateralValue * LIQUIDATION_THRESHOLD) / 100;

        require(user.borrowed > maxAllowedDebt, "Position healthy");

        // Calculer montant a liquider (50% de la dette max)
        uint256 debtToRepay = user.borrowed / 2;
        uint256 collateralToSeize = (debtToRepay * (100 + LIQUIDATION_BONUS)) / 100;

        // Verifier qu'on ne prend pas plus que disponible
        if (collateralToSeize > user.collateral) {
            collateralToSeize = user.collateral;
        }

        // Transferts
        asset.transferFrom(msg.sender, address(this), debtToRepay);
        user.borrowed -= debtToRepay;
        user.collateral -= collateralToSeize;
        asset.transfer(msg.sender, collateralToSeize);

        emit Liquidation(msg.sender, borrower, collateralToSeize, debtToRepay);
    }

    // Calcul interets
    function _accrueInterest(address user) internal {
        UserAccount storage account = accounts[user];
        if (account.borrowed == 0 || account.lastUpdate == 0) {
            account.lastUpdate = block.timestamp;
            return;
        }

        uint256 timeElapsed = block.timestamp - account.lastUpdate;
        uint256 interest = (account.borrowed * interestRate * timeElapsed) / (100 * 365 days);

        account.borrowed += interest;
        account.lastUpdate = block.timestamp;
    }

    // Getters
    function getHealthFactor(address user) external view returns (uint256) {
        UserAccount memory account = accounts[user];
        if (account.borrowed == 0) return type(uint256).max;
        return (account.collateral * LIQUIDATION_THRESHOLD * 1e18) / (account.borrowed * 100);
    }
}
'''

# Deployer un token pour le lending pool
lending_token, _ = compile_and_deploy(w3, ERC20_HELPER, deployer, "DAI Stablecoin", "DAI", 1_000_000 * 10**18)

# Deployer le lending pool
lending, _ = compile_and_deploy(w3, LENDING_CONTRACT, deployer, lending_token.address)

# --- Demonstration: deposer, emprunter, verifier health factor ---

# Alimenter le pool (le lending pool a besoin de liquidite pour les emprunts)
fund_amount = 500_000 * 10**18
lending_token.functions.approve(lending.address, fund_amount).transact({"from": deployer})
# On utilise deposit pour fournir de la liquidite
lending.functions.deposit(fund_amount).transact({"from": deployer})

# Un second utilisateur depose du collateral et emprunte
user = w3.eth.accounts[1]
user_deposit = 10_000 * 10**18
borrow_amount = 5_000 * 10**18

# Envoyer des tokens au user
lending_token.functions.transfer(user, user_deposit).transact({"from": deployer})
lending_token.functions.approve(lending.address, user_deposit).transact({"from": user})

# Deposer du collateral
lending.functions.deposit(user_deposit).transact({"from": user})

# Emprunter (max LTV = 75%, donc 7500 max sur 10000 de collateral)
lending.functions.borrow(borrow_amount).transact({"from": user})

# Verifier le health factor
hf = lending.functions.getHealthFactor(user).call()
account = lending.functions.accounts(user).call()

print(f"\nLending Pool:")
print(f"  Collateral depose : {account[0] / 10**18:,.0f} DAI")
print(f"  Montant emprunte  : {account[1] / 10**18:,.0f} DAI")
print(f"  Max LTV           : {lending.functions.MAX_LTV().call()}%")
print(f"  Health Factor     : {hf / 10**18:.4f}")
print(f"  Status            : {'Safe' if hf > 10**18 else 'Liquidable'}")
Deploye: TestToken a 0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6
Deploye: SimpleLending a 0x8A791620dd6260079BF849Dc5567aDC3F2FdC318

Lending Pool:
  Collateral depose : 10,000 DAI
  Montant emprunte  : 5,000 DAI
  Max LTV           : 75%
  Health Factor     : 1.7000
  Status            : Safe

Exercice 3 : Calculer le seuil de liquidation

Un utilisateur a depose 15 000 DAI en collateral et a emprunte un montant variable. Ecrivez une fonction qui calcule le montant maximal qu’un utilisateur peut emprunter sans risquer la liquidation, puis determine si une position est liquidable.

Indice : Le health factor depend du ratio collateral * liquidationThreshold / borrowed. Un HF < 1.0 declenche la liquidation. - MAX_LTV = 75% : ratio d’emprunt maximal autorise - LIQUIDATION_THRESHOLD = 85% : seuil declenchant la liquidation

# Exercice 3 : Calculer le seuil de liquidation
def max_borrow_and_liquidation_status(collateral, borrowed, max_ltv=0.75, liquidation_threshold=0.85):
    """
    Calcule le montant max empruntable et le statut de liquidation.

    Args:
        collateral: Montant depose en collateral
        borrowed: Montant actuellement emprunte
        max_ltv: Ratio d'emprunt maximal autorise (0.75 = 75%)
        liquidation_threshold: Seuil de liquidation (0.85 = 85%)

    Returns:
        dict avec:
        - max_borrow: Montant maximal empruntable
        - health_factor: Facteur de sante actuel
        - is_liquidable: bool, True si la position peut etre liquidee
    """
    # TODO etudiant : calculer max_borrow = collateral * max_ltv
    # TODO etudiant : calculer health_factor = (collateral * liquidation_threshold) / borrowed
    # TODO etudiant : determiner is_liquidable (health_factor < 1.0)
    return {"max_borrow": 0, "health_factor": 0, "is_liquidable": False}


# Test apres implementation
result = max_borrow_and_liquidation_status(collateral=15_000, borrowed=10_000)
print(f"Max borrow: {result['max_borrow']}, HF: {result['health_factor']}, Liquidable: {result['is_liquidable']}")
Max borrow: 0, HF: 0, Liquidable: False

Interpretation : Lending Pool — collateral, emprunt et health factor

Résultat obtenu : Le contrat SimpleLending permet a un utilisateur de deposer 10 000 DAI en collateral, d’emprunter 5 000 DAI (50% du collateral, sous la limite LTV 75%), et affiche un health factor de 1.70 (position safe).

Metriques de securite du lending :

Metrique Valeur Seuil critique Signification
LTV (Loan-to-Value) 50% 75% max Ratio emprunt/collateral
Liquidation Threshold 85% - Seuil declenchant la liquidation
Health Factor 1.70 < 1.0 = liquidable Marge de securite

Calcul du health factor :

HF = (Collateral * LiquidationThreshold) / Borrowed
   = (10 000 * 0.85) / 5 000 = 1.70

Points cles : - Le health factor represente la “marge avant liquidation” — un HF de 1.70 signifie que le collateral pourrait baisser de ~41% avant liquidation - Le lending pool est prealablement alimente en liquidite par le deployer (500 000 DAI) pour permettre les emprunts - Les intérêts sont accumules (_accrueInterest) a chaque interaction mais restent negligeables dans ce demo (blocs mines rapidement) - En production (Aave, Compound), les taux varient dynamiquement selon l’utilisation du pool


4. Resume

Concept Formule Description
Constant Product \(x \cdot y = k\) Base des AMM
Swap Output \(\frac{\Delta x \cdot y}{x + \Delta x}\) Montant recu
Price Impact \(\frac{P_{avant} - P_{après}}{P_{avant}}\) Impact du trade
Health Factor \(\frac{Collateral \cdot LT}{Dette}\) Securite lending

Points cles

  1. Les AMM remplacent les order books par des formules mathematiques
  2. Le price impact augmente avec la taille du swap
  3. Toujours définir un slippage tolerance
  4. Le health factor < 1 declenche la liquidation

Notebook suivant : SC-09-DAO-Governance-Python


<< Token Standards | DAO Governance >>

Retour au sommet