SC-23-Cross-Chain-Python - Interoperabilite Cross-Chain

<< Solana & Anchor | Testnet Deploy >>


Objectifs d’apprentissage

  1. Comprendre les enjeux de l’interoperabilite
  2. Utiliser Chainlink CCIP pour les cross-chain messages
  3. Implementer un bridge simple
  4. Comprendre les securite cross-chain

Prerequis

  • SC-3 a SC-10 completes
  • Notions de bridges et messaging cross-chain
  • Compte sur un testnet Ethereum (Sepolia recommande)

Duree estimee : 45 minutes


1. Enjeux Cross-Chain

L’interoperabilite entre blockchains est un defi majeur.

# Concepts cross-chain
print("""
DEFIS CROSS-CHAIN

| Probleme          | Description                         |
|------------------|-------------------------------------|
| Finalite         | Verifier la finalite sur la source  |
| Securite         | Empecher les double-spend           |
| Latence          | Temps de confirmation               |
| Cout             | Gas sur les deux chains             |
| Oracle           | Obtenir l'etat d'une autre chain    |

SOLUTIONS:
- Lock & Mint     : Lock tokens A, mint wrapped A' sur B
- Burn & Mint     : Burn wrapped A', unlock A sur source
- Liquidity Pools : Swap via pools sur chaque chain
- Light Clients   : Verifier les headers distamment

PROTOCOLS POPULAIRES:
- Chainlink CCIP  : Oracle-based messaging
- LayerZero       : Omnichain interoperability
- Axelar          : Cross-chain gateway
- Wormhole        : Guardian network
- Hyperlane       : Permissionless interoperability
""")

DEFIS CROSS-CHAIN

| Probleme          | Description                         |
|------------------|-------------------------------------|
| Finalite         | Verifier la finalite sur la source  |
| Securite         | Empecher les double-spend           |
| Latence          | Temps de confirmation               |
| Cout             | Gas sur les deux chains             |
| Oracle           | Obtenir l'etat d'une autre chain    |

SOLUTIONS:
- Lock & Mint     : Lock tokens A, mint wrapped A' sur B
- Burn & Mint     : Burn wrapped A', unlock A sur source
- Liquidity Pools : Swap via pools sur chaque chain
- Light Clients   : Verifier les headers distamment

PROTOCOLS POPULAIRES:
- Chainlink CCIP  : Oracle-based messaging
- LayerZero       : Omnichain interoperability
- Axelar          : Cross-chain gateway
- Wormhole        : Guardian network
- Hyperlane       : Permissionless interoperability

3. Chain Selectors

# Chain selectors CCIP
print("""
CHAIN SELECTORS CHAINLINK CCIP

| Chain              | Selector              |
|--------------------|-----------------------|
| Ethereum Mainnet   | 5009297550715157      |
| Ethereum Sepolia   | 16015286601757825753  |
| Arbitrum Mainnet   | 4949039107694359620   |
| Arbitrum Sepolia   | 3478487232816046354   |
| Optimism Mainnet   | 7934706634085862462   |
| Optimism Sepolia   | 4547669296748201845   |
| Polygon Mainnet    | 4051577828743386545   |
| Polygon Mumbai     | 12532609583862916517  |
| Avalanche Mainnet  | 6433500567565415111   |
| Avalanche Fuji     | 14767432559492292930  |
| Base Mainnet       | 15971525489660198786  |
| Base Sepolia       | 5795021415249229034   |

ROUTER ADDRESSES:
- Sepolia: 0xD0daae2231A9CB0c823D8aba1E7457f92b587781
- Mainnet: 0x80226fc0Ee2b096224EeAc085Bb9a8cba107A3b9
""")

CHAIN SELECTORS CHAINLINK CCIP

| Chain              | Selector              |
|--------------------|-----------------------|
| Ethereum Mainnet   | 5009297550715157      |
| Ethereum Sepolia   | 16015286601757825753  |
| Arbitrum Mainnet   | 4949039107694359620   |
| Arbitrum Sepolia   | 3478487232816046354   |
| Optimism Mainnet   | 7934706634085862462   |
| Optimism Sepolia   | 4547669296748201845   |
| Polygon Mainnet    | 4051577828743386545   |
| Polygon Mumbai     | 12532609583862916517  |
| Avalanche Mainnet  | 6433500567565415111   |
| Avalanche Fuji     | 14767432559492292930  |
| Base Mainnet       | 15971525489660198786  |
| Base Sepolia       | 5795021415249229034   |

ROUTER ADDRESSES:
- Sepolia: 0xD0daae2231A9CB0c823D8aba1E7457f92b587781
- Mainnet: 0x80226fc0Ee2b096224EeAc085Bb9a8cba107A3b9

Exercice : Simulateur de messagerie CCIP

Le protocole CCIP route les messages entre blockchains via des chain selectors et des routers. Completez la classe CCIPSimulator qui simule le flux complet d’envoi et de reception de messages cross-chain.

Objectifs : 1. Implementer send_message qui enregistre un message avec ses metadonnees (source, destination, données, frais) 2. Implementer receive_messages qui recupere et decode tous les messages pour un destinataire donne 3. Calculer les frais de messagerie en fonction de la taille des données et de la distance entre chains

Indices : - Les frais dependent de la taille des données (en octets) et du chain selector de destination - Chaque message a un messageId unique (hash du contenu) - Un message ne peut etre recu que si la chaîne source est enregistree dans le routeur

import hashlib
import json


class CCIPSimulator:
    """Simulateur du protocole Chainlink CCIP pour messagerie cross-chain."""

    CHAIN_CONFIG = {
        "ethereum_sepolia":  {"selector": 16015286601757825753, "base_fee": 0.001},
        "arbitrum_sepolia":  {"selector": 3478487232816046354,  "base_fee": 0.0005},
        "polygon_mumbai":    {"selector": 12532609583862916517, "base_fee": 0.0003},
        "optimism_sepolia":  {"selector": 4547669296748201845,  "base_fee": 0.0004},
    }

    def __init__(self):
        self.messages = []  # liste des messages envoyes
        self.received = {}  # address -> liste des messages recus

    def send_message(self, source_chain: str, dest_chain: str,
                     sender: str, receiver: str, data: str) -> dict:
        """Envoyer un message cross-chain via CCIP.

        TODO etudiant :
        Etape 1 : verifier que source_chain et dest_chain sont dans CHAIN_CONFIG
        Etape 2 : calculer le messageId (hash des metadonnees)
        Etape 3 : calculer les frais (base_fee * taille_donnees_en_octets)
        Etape 4 : enregistrer le message dans self.messages
        Etape 5 : retourner un dict avec messageId, fees, status

        Args:
            source_chain: nom de la chaine source
            dest_chain: nom de la chaine destination
            sender: adresse de l'expediteur
            receiver: adresse du destinataire
            data: contenu du message

        Returns:
            dict avec messageId, fees, status
        """
        # TODO etudiant : implementez send_message
        pass  # TODO etudiant

    def receive_messages(self, receiver: str, dest_chain: str) -> list:
        """Recuperer les messages recus par un destinataire sur une chaine.

        TODO etudiant :
        Etape 1 : filtrer self.messages pour ceux avec receiver et dest_chain
        Etape 2 : ajouter chaque message dans self.received[receiver]
        Etape 3 : retourner la liste des messages recus

        Args:
            receiver: adresse du destinataire
            dest_chain: chaine sur laquelle on Receptionne

        Returns:
            liste de dict avec messageId, source_chain, sender, data
        """
        # TODO etudiant : implementez receive_messages
        pass  # TODO etudiant


# Test du simulateur
sim = CCIPSimulator()
result = sim.send_message(
    "ethereum_sepolia", "arbitrum_sepolia",
    "0xSender123", "0xReceiver456", "Hello from Ethereum!"
)
if result:
    print(f"Message envoye : {result.get('messageId', 'N/A')}")
    print(f"Frais : {result.get('fees', 0)} ETH")
    msgs = sim.receive_messages("0xReceiver456", "arbitrum_sepolia")
    print(f"Messages recus : {len(msgs) if msgs else 0}")
else:
    print("Message non envoye (chain inconnue)")
print("Exercice a completer : CCIPSimulator")
Message non envoye (chain inconnue)
Exercice a completer : CCIPSimulator

4. Securite Cross-Chain

# Securite cross-chain
print("""
VULNERABILITES CROSS-CHAIN

1. REENTRANCY CROSS-CHAIN
   - Une attaque sur une chain peut affecter l'autre
   - Solution: Verifier la finalite avant execution

2. DOUBLE-SPEND
   - Meme tokens utilises sur plusieurs chains
   - Solution: Lock/Burn atomique

3. ORACLE MANIPULATION
   - Fausse information sur l'etat d'une chain
   - Solution: Multiples oracles, verification

4. BRIDGE EXPLOITS
   - Bugs dans les contrats de bridge
   - Solution: Audits, bug bounties, limits

5. GOVERNANCE ATTACKS
   - Manipulation des votes cross-chain
   - Solution: Snapshot voting, delay

BEST PRACTICES:
- Toujours verifier la finalite (confirmations)
- Rate limits sur les transferts
- Emergency pause mechanism
- Audits reguliers
- Monitoring et alertes
""")

VULNERABILITES CROSS-CHAIN

1. REENTRANCY CROSS-CHAIN
   - Une attaque sur une chain peut affecter l'autre
   - Solution: Verifier la finalite avant execution

2. DOUBLE-SPEND
   - Meme tokens utilises sur plusieurs chains
   - Solution: Lock/Burn atomique

3. ORACLE MANIPULATION
   - Fausse information sur l'etat d'une chain
   - Solution: Multiples oracles, verification

4. BRIDGE EXPLOITS
   - Bugs dans les contrats de bridge
   - Solution: Audits, bug bounties, limits

5. GOVERNANCE ATTACKS
   - Manipulation des votes cross-chain
   - Solution: Snapshot voting, delay

BEST PRACTICES:
- Toujours verifier la finalite (confirmations)
- Rate limits sur les transferts
- Emergency pause mechanism
- Audits reguliers
- Monitoring et alertes

Exercice : Analyse de risque d’un bridge

Avant de deployer un bridge en production, il est essentiel d’identifier et de quantifier les risques. Completez la fonction assess_bridge_risk qui analyse les paramètres d’un bridge et retourne un score de risque.

Objectifs : 1. Evaluer le risque lie au nombre de validateurs et au seuil de signature 2. Verifier la presence de protections (rate limit, emergency pause, timelock) 3. Calculer un score de risque composite (0-100)

Indices : - Un seuil M-of-N avec N < 5 est considere comme risque eleve - L’absence de rate limit ou d’emergency pause augmente le score de 20 points chacun - Le score final est plafonne a 100

def assess_bridge_risk(num_validators: int, threshold: int,
                       has_rate_limit: bool, has_emergency_pause: bool,
                       has_timelock: bool, max_daily_transfer: int) -> dict:
    """Analyser le profil de risque d'un bridge cross-chain.

    Args:
        num_validators: Nombre de validateurs (N dans M-of-N)
        threshold: Nombre de signatures requises (M dans M-of-N)
        has_rate_limit: Presence d'une limite quotidienne de transfert
        has_emergency_pause: Presence d'un mecanisme d'arret d'urgence
        has_timelock: Presence d'un delai avant changement de config
        max_daily_transfer: Montant maximum transferable par jour (en USD)

    Returns:
        dict avec score (0-100, 0=sur, 100=dangereux) et liste de recommandations

    TODO etudiant : implementez l'analyse de risque.
    Etape 1 : calculer un score de base selon N et M (plus N est grand, plus c'est sur)
    Etape 2 : ajouter des penalties si les protections manquent
    Etape 3 : ajuster selon le montant quotidien
    Etape 4 : generer des recommandations specifiques
    """
    # TODO etudiant : implementez assess_bridge_risk
    return {"score": 0, "recommendations": []}  # TODO etudiant


# Test avec un bridge hypothetique
result = assess_bridge_risk(
    num_validators=3, threshold=2,
    has_rate_limit=False, has_emergency_pause=True,
    has_timelock=False, max_daily_transfer=1_000_000
)
print(f"Score de risque : {result['score']}/100")
for rec in result.get("recommendations", []):
    print(f"  - {rec}")
print("Exercice a completer : assess_bridge_risk")
Score de risque : 0/100
Exercice a completer : assess_bridge_risk

5. Resume

# Resume
print("""
RESUME CROSS-CHAIN

| Protocol      | Type         | Securite          |
|--------------|--------------|-------------------|
| Chainlink CCIP | Oracle-based | Haute (don-dees) |
| LayerZero     | Ultra-light  | Oracle + Relayer  |
| Axelar        | Gateway      | Proof-of-Stake    |
| Wormhole      | Guardians    | 19/19 signatures  |
| Hyperlane     | Permissionless| Economic security|

QUAND UTILISER:
- CCIP: Pour securite maximale, tokens transfers
- LayerZero: Pour rapidite, omnichain dApps
- Axelar: Pour ecosystem cosmos + EVM
- Wormhole: Pour Solana integration

---

**Notebook suivant** : [SC-24-Testnet-Deploy-Python](SC-24-Testnet-Deploy-Python.ipynb)
""")

RESUME CROSS-CHAIN

| Protocol      | Type         | Securite          |
|--------------|--------------|-------------------|
| Chainlink CCIP | Oracle-based | Haute (don-dees) |
| LayerZero     | Ultra-light  | Oracle + Relayer  |
| Axelar        | Gateway      | Proof-of-Stake    |
| Wormhole      | Guardians    | 19/19 signatures  |
| Hyperlane     | Permissionless| Economic security|

QUAND UTILISER:
- CCIP: Pour securite maximale, tokens transfers
- LayerZero: Pour rapidite, omnichain dApps
- Axelar: Pour ecosystem cosmos + EVM
- Wormhole: Pour Solana integration

---

**Notebook suivant** : [SC-24-Testnet-Deploy-Python](SC-24-Testnet-Deploy-Python.ipynb)

Exemple guide : Bridge Token Cross-Chain — Solution etudiante (Lucas Demuliere & Joanne Jabbour, TP 2026)

Implementation complete d’un bridge Lock & Mint entre deux chaînes simulees, avec protection ECDSA contre les replay attacks et simulation Python du flux cross-chain.

Objectifs couverts

  1. Comprendre le pattern Lock & Mint
  2. Implementer un bridge securise
  3. Gerer les cas d’erreur et reverts
  4. Tester le flux complet

Architecture

Cote source Cote destination
TokenBridge : lock() verrouille, unlock() libere sur preuve ECDSA WrappedToken : mint() sur Lock detecte, burn() pour le retour
Validator unique (ECDSA) Bridge autorise uniquement (onlyBridge)

Securite implementee

  • Nonce unique par opération (protection replay)
  • chainId dans le hash (protection cross-chain replay)
  • Signature ECDSA verifiee via OpenZeppelin ECDSA.recover
  • Double mapping source/destination (un nonce valide d’un cote n’est pas valide de l’autre)
# Exercice a completer : Bridge multi-validateurs (M-of-N)
# Etudiant : etendez TokenBridgeSimulator pour gerer un set de N validators avec seuil M

from typing import Set


class MultiValidatorBridge:
    """Bridge M-of-N : unlock requiert signatures de >= threshold validators.
    
    TODO etudiant : implementez lock(), unlock(), mint(), burn(), rotate_validator().
    """
    
    def __init__(self, validators: Set[str], threshold: int):
        # TODO etudiant : initialiser validators, threshold, etat locked/wrapped/nonces
        self.validators = set(validators)
        self.threshold = threshold
        pass  # TODO etudiant : completer l'initialisation
    
    def lock(self, user: str, amount: int) -> int:
        """Verrouille les tokens de l'utilisateur et emet un nonce.
        
        TODO etudiant : meme logique que TokenBridgeSimulator.lock().
        """
        # Etape 1 : incrementer locked[user]
        # Etape 2 : generer un nonce unique
        # Etape 3 : retourner le nonce
        pass  # TODO etudiant
        return -1  # placeholder, a remplacer
    
    def unlock(self, user: str, amount: int, nonce: int, signing_validators: Set[str]) -> None:
        """Debloque si >= threshold validators distincts ont signe.
        
        TODO etudiant : 
        - Verifier que signing_validators sont tous dans self.validators
        - Verifier que len(signing_validators) >= self.threshold
        - Verifier que nonce pas deja utilise
        - Verifier que locked[user] >= amount
        - Appliquer l'unlock
        """
        # Indice : utiliser set intersection pour verifier l'appartenance
        # valid_signers = signing_validators & self.validators
        # if len(valid_signers) < self.threshold: raise ValueError(...)
        pass  # TODO etudiant
    
    def mint(self, user: str, amount: int, nonce: int) -> None:
        """Mint wrapped token cote destination.
        
        TODO etudiant : protection replay par nonce (comme TokenBridgeSimulator).
        """
        pass  # TODO etudiant
    
    def burn(self, user: str, amount: int) -> int:
        """Burn wrapped token cote destination.
        
        TODO etudiant : verifier balance, decrementer, retourner nonce.
        """
        pass  # TODO etudiant
        return -1  # placeholder
    
    def rotate_validator(self, old_validator: str, new_validator: str,
                          signing_validators: Set[str]) -> None:
        """Remplace old_validator par new_validator avec accord du seuil.
        
        TODO etudiant :
        - Verifier que old_validator in self.validators
        - Verifier que new_validator not in self.validators
        - Verifier que len(signing_validators & self.validators) >= threshold
        - Appliquer la rotation
        """
        pass  # TODO etudiant


# Tests preliminaires (l'estudiant doit les completer)
validators = {f"validator_{i}" for i in range(13)}
bridge = MultiValidatorBridge(validators, threshold=9)
print(f"Bridge multi-validateurs initialise : {len(validators)} validators, seuil 9")
print("Exercice a completer : MultiValidatorBridge M-of-N")
Bridge multi-validateurs initialise : 13 validators, seuil 9
Exercice a completer : MultiValidatorBridge M-of-N

Exercice : Bridge multi-validateurs (M-of-N)

L’exemple précédent securise le bridge avec un seul validator. En pratique, un validator unique est un point de defaillance : s’il est compromis, tous les fonds verrouilles sont en danger. Les bridges industriels (Wormhole, Axelar, LayerZero) utilisent un set de N validators avec un seuil M-of-N (ex: 13 validateurs, seuil 9).

Objectifs

  1. Etendre le simulateur pour gerer plusieurs validators
  2. Implementer la verification de seuil (M signatures requises pour unlock)
  3. Gerer la rotation des validators (join/leave) avec accord du seuil
  4. Tester la robustesse face a un validator compromis

Indices : - Stocker validators: Set[str] et un seuil threshold: int (M validators sur N) - Pour unlock, accepter un Set[str] signing_validators (les adresses des validators ayant signe) et verifier len(signing_validators & self.validators) >= threshold - Le hash signe doit inclure l’adresse du validator pour empecher qu’une signature d’un validator soit reusee pour un autre - rotate_validator(old, new, signing_validators) requiert len(signing_validators & self.validators) >= threshold (changement de configuration = même seuil que unlock)

Cas de test attendus

  1. Unlock valide : 9 validators sur 13 signent -> unlock reussit
  2. Unlock rejete : 8 validators sur 13 signent (sous le seuil) -> revert
  3. Validator compromis : 1 validator signe deux fois le même nonce -> revert (deduplication)
  4. Rotation : remplacement d’un validator avec accord du seuil -> reussit

Analyse : Securite et Limites d’un Bridge Cross-Chain

Reponses proposees par Lucas Demuliere & Joanne Jabbour (TP 2026)

Q1 — Comment empecher les attaques par rejeu (replay attacks) ?

Mécanisme Implementation Garantie
Nonce unique mapping(uint256 => bool) usedNonces Chaque opération ne peut etre executee qu’une fois
ChainId dans le hash keccak256(abi.encodePacked(user, amount, nonce, block.chainid)) Empeche la reutilisation de la signature sur une autre chain
Double mapping Un mapping par chain (source et destination) Un nonce valide sur la source n’est pas valide sur la destination

Sans le chainid dans le hash signe, une signature valide sur Sepolia pourrait etre rejouee sur Mainnet et drainer les fonds.


Q2 — Quelle est la latence minimale pour une transaction cross-chain ?

Confirmation source  (12 blocs Ethereum ~ 2 min)
     + Detection oracle/relayer            (~30 sec)
     + Transmission du message             (~variable)
     + Confirmation destination            (~2-12 sec selon L2)
     -----------------------------------------------
     TOTAL minimal : ~3-5 minutes  (Chainlink CCIP Sepolia)
                     ~20-30 min    (Mainnet haute securite)
  • Les L2 (Arbitrum, Optimism) recoivent plus vite, mais la finalite sur L1 reste lente
  • Chainlink CCIP attend plusieurs confirmations avant de relayer pour eviter les reorgs
  • Les bridges “optimistic” (Hop, Across) sont plus rapides mais acceptent un risque de reorg

Q3 — Comment gerer les echecs sur la chaîne destination ?

Scénario Problème Solution
Contrat destination en pause Le mint echoue silencieusement Mécanisme de retry + monitoring oracle
Gas insuffisant La transaction destination est droppee Fournir un surplus de gas + retry automatique CCIP
Contrat destination inexistant Tokens bloques indefiniment sur source Whitelist des destinations autorisees avant envoi
Reorg sur source Lock non finalisee mais Mint effectue Attendre N confirmations avant de relayer (CCIP : 7 blocs)

Pattern recommande : circuit breaker avec emergency pause + timelock de 48h sur les changements de config du validator.

# Simulation Python du flux cross-chain (Lock -> Mint -> Burn -> Unlock)
# Solution proposee par Lucas Demuliere & Joanne Jabbour (TP 2026)

class TokenBridgeSimulator:
    """Simule le pattern Lock & Mint entre deux chains"""

    def __init__(self):
        self.locked = {}
        self.used_nonces_source = set()
        self.nonce = 0
        self.wrapped_balances = {}
        self.used_nonces_dest = set()
        self.total_supply = 0

    def lock(self, user: str, amount: int) -> int:
        self.locked[user] = self.locked.get(user, 0) + amount
        n = self.nonce
        self.nonce += 1
        print(f"[SOURCE] Lock   : {user} verrouille {amount} ETH  (nonce={n})")
        return n

    def mint(self, user: str, amount: int, nonce: int):
        if nonce in self.used_nonces_dest:
            raise ValueError(f"Replay detecte ! nonce {nonce} deja utilise sur destination")
        self.used_nonces_dest.add(nonce)
        self.wrapped_balances[user] = self.wrapped_balances.get(user, 0) + amount
        self.total_supply += amount
        print(f"[DEST]   Mint   : {user} recoit {amount} WETHb   (nonce={nonce})")

    def burn(self, user: str, amount: int) -> int:
        if self.wrapped_balances.get(user, 0) < amount:
            raise ValueError("Balance insuffisante")
        self.wrapped_balances[user] -= amount
        self.total_supply -= amount
        n = self.nonce
        self.nonce += 1
        print(f"[DEST]   Burn   : {user} retourne {amount} WETHb  (nonce={n})")
        return n

    def unlock(self, user: str, amount: int, nonce: int, has_valid_signature: bool = True):
        if not has_valid_signature:
            raise ValueError("Rejete : Signature du validateur invalide. Preuve de Burn requise.")

        if nonce in self.used_nonces_source:
            raise ValueError(f"Replay detecte ! nonce {nonce} deja utilise sur source")
        if self.locked.get(user, 0) < amount:
            raise ValueError("Balance verrouilee insuffisante")

        self.used_nonces_source.add(nonce)
        self.locked[user] -= amount
        print(f"[SOURCE] Unlock : {user} recoit {amount} ETH orig (nonce={nonce})")

    def state(self):
        print(f"  locked={self.locked}  wrapped={self.wrapped_balances}  supply={self.total_supply}")


print("=" * 55)
print("TEST 1 : Round-trip complet (Lock -> Mint -> Burn -> Unlock)")
print("=" * 55)
b = TokenBridgeSimulator()
n1 = b.lock("Alice", 100)
b.mint("Alice", 100, n1)
b.state()
n2 = b.burn("Alice", 100)
b.unlock("Alice", 100, n2)
b.state()

print("\n" + "=" * 55)
print("TEST 2 : Replay attack (reutilisation du meme nonce)")
print("=" * 55)
b2 = TokenBridgeSimulator()
n = b2.lock("Bob", 50)
b2.mint("Bob", 50, n)
print("Tentative de second mint avec le meme nonce...")
try:
    b2.mint("Bob", 50, n)  # Doit echouer
    print("ERREUR : Replay non detecte !")
except ValueError as e:
    print(f"Bloque : {e}")

print("\n" + "=" * 55)
print("TEST 3 : Faux unlock (signature invalide)")
print("=" * 55)
b3 = TokenBridgeSimulator()
n = b3.lock("Charlie", 75)
print("Tentative d'unlock sans signature valide...")
try:
    b3.unlock("Charlie", 75, n, has_valid_signature=False)
    print("ERREUR : Faux unlock accepte !")
except ValueError as e:
    print(f"Bloque : {e}")

print("\n" + "=" * 55)
print("TEST 4 : Invariants (total_supply == somme wrapped_balances)")
print("=" * 55)
b4 = TokenBridgeSimulator()
b4.lock("Dave", 200)
b4.mint("Dave", 200, 0)
b4.lock("Eve", 50)
b4.mint("Eve", 50, 1)
somme = sum(b4.wrapped_balances.values())
assert b4.total_supply == somme, f"Invariant violé : {b4.total_supply} != {somme}"
print(f"OK : total_supply ({b4.total_supply}) == somme wrapped ({somme})")
print("Tous les tests du bridge passent avec succes.")
=======================================================
TEST 1 : Round-trip complet (Lock -> Mint -> Burn -> Unlock)
=======================================================
[SOURCE] Lock   : Alice verrouille 100 ETH  (nonce=0)
[DEST]   Mint   : Alice recoit 100 WETHb   (nonce=0)
  locked={'Alice': 100}  wrapped={'Alice': 100}  supply=100
[DEST]   Burn   : Alice retourne 100 WETHb  (nonce=1)
[SOURCE] Unlock : Alice recoit 100 ETH orig (nonce=1)
  locked={'Alice': 0}  wrapped={'Alice': 0}  supply=0

=======================================================
TEST 2 : Replay attack (reutilisation du meme nonce)
=======================================================
[SOURCE] Lock   : Bob verrouille 50 ETH  (nonce=0)
[DEST]   Mint   : Bob recoit 50 WETHb   (nonce=0)
Tentative de second mint avec le meme nonce...
Bloque : Replay detecte ! nonce 0 deja utilise sur destination

=======================================================
TEST 3 : Faux unlock (signature invalide)
=======================================================
[SOURCE] Lock   : Charlie verrouille 75 ETH  (nonce=0)
Tentative d'unlock sans signature valide...
Bloque : Rejete : Signature du validateur invalide. Preuve de Burn requise.

=======================================================
TEST 4 : Invariants (total_supply == somme wrapped_balances)
=======================================================
[SOURCE] Lock   : Dave verrouille 200 ETH  (nonce=0)
[DEST]   Mint   : Dave recoit 200 WETHb   (nonce=0)
[SOURCE] Lock   : Eve verrouille 50 ETH  (nonce=1)
[DEST]   Mint   : Eve recoit 50 WETHb   (nonce=1)
OK : total_supply (250) == somme wrapped (250)
Tous les tests du bridge passent avec succes.

Implementation Solidity du Bridge Lock & Mint

La simulation Python a valide la logique du pattern Lock & Mint (protection replay, verification de signature, invariants de supply). La cellule suivante presente l’implementation Solidity complete avec TokenBridge (chaîne source : lock/unlock avec verification ECDSA) et WrappedToken (chaîne destination : mint/burn avec protection onlyBridge). Les contrats utilisent block.chainid dans le hash signe pour empecher le cross-chain replay.

# Solution : Bridge Token Cross-Chain (pattern Lock & Mint)
# Solution proposee par Lucas Demuliere & Joanne Jabbour (TP 2026)

TOKEN_BRIDGE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

/// @title TokenBridge - Chaine source (Lock & Unlock)
contract TokenBridge {
    using ECDSA for bytes32;

    mapping(address => uint256) public locked;      // user => montant verrouille
    mapping(uint256 => bool)    public usedNonces;  // protection replay
    address public immutable validator;
    uint256 public nonce;

    event Lock(address indexed user, uint256 amount, uint256 nonce);
    event Unlock(address indexed user, uint256 amount, uint256 nonce);

    constructor(address _validator) {
        validator = _validator;
    }

    /// Verrouille des ETH natifs et emet Lock pour que le relayer detecte
    function lock() external payable {
        require(msg.value > 0, "Amount must be > 0");
        locked[msg.sender] += msg.value;
        emit Lock(msg.sender, msg.value, nonce++);
    }

    /// Deverrouille les tokens - appele par le validator apres Burn sur dest
    function unlock(
        address user,
        uint256 amount,
        uint256 _nonce,
        bytes calldata signature
    ) external {
        require(!usedNonces[_nonce],          "Nonce already used");
        require(locked[user] >= amount,       "Insufficient locked balance");
        require(
            validateSignature(user, amount, _nonce, signature),
            "Invalid validator signature"
        );

        usedNonces[_nonce] = true;
        locked[user] -= amount;

        (bool ok,) = payable(user).call{value: amount}("");
        require(ok, "Transfer failed");

        emit Unlock(user, amount, _nonce);
    }

    /// Verifie la signature ECDSA du validator autorise
    /// Hash = keccak256(user, amount, nonce, chainId) - empeche le replay cross-chain
    function validateSignature(
        address user,
        uint256 amount,
        uint256 _nonce,
        bytes memory signature
    ) public view returns (bool) {
        bytes32 hash    = keccak256(abi.encodePacked(user, amount, _nonce, block.chainid));
        bytes32 ethHash = MessageHashUtils.toEthSignedMessageHash(hash);
        return ECDSA.recover(ethHash, signature) == validator;
    }

    receive() external payable {}
}
'''

WRAPPED_TOKEN = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/// @title WrappedToken - Chaine destination (Mint & Burn)
contract WrappedToken {
    string  public constant name     = "Wrapped ETH Bridge";
    string  public constant symbol   = "WETHb";
    uint8   public constant decimals = 18;

    mapping(address => uint256) public balanceOf;
    mapping(uint256 => bool)    public mintedNonces; // protection replay
    uint256 public totalSupply;
    address public immutable bridge;

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Mint(address indexed to, uint256 amount, uint256 nonce);
    event Burn(address indexed from, uint256 amount);

    modifier onlyBridge() {
        require(msg.sender == bridge, "Only bridge");
        _;
    }

    constructor(address _bridge) {
        bridge = _bridge;
    }

    /// Mint appele par le bridge quand evenement Lock detecte sur la source
    function mint(address user, uint256 amount, uint256 _nonce) external onlyBridge {
        require(!mintedNonces[_nonce], "Nonce already minted");
        mintedNonces[_nonce] = true;
        balanceOf[user]  += amount;
        totalSupply      += amount;
        emit Mint(user, amount, _nonce);
        emit Transfer(address(0), user, amount);
    }

    /// Burn appele par l'utilisateur pour initier le retour vers la source
    function burn(uint256 amount) external {
        require(balanceOf[msg.sender] >= amount, "Insufficient balance");
        balanceOf[msg.sender] -= amount;
        totalSupply           -= amount;
        emit Burn(msg.sender, amount);
        emit Transfer(msg.sender, address(0), amount);
    }
}
'''

print("=== TokenBridge (chaine source) ===")
print(TOKEN_BRIDGE)
print("=== WrappedToken (chaine destination) ===")
print(WRAPPED_TOKEN)
=== TokenBridge (chaine source) ===

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

/// @title TokenBridge - Chaine source (Lock & Unlock)
contract TokenBridge {
    using ECDSA for bytes32;

    mapping(address => uint256) public locked;      // user => montant verrouille
    mapping(uint256 => bool)    public usedNonces;  // protection replay
    address public immutable validator;
    uint256 public nonce;

    event Lock(address indexed user, uint256 amount, uint256 nonce);
    event Unlock(address indexed user, uint256 amount, uint256 nonce);

    constructor(address _validator) {
        validator = _validator;
    }

    /// Verrouille des ETH natifs et emet Lock pour que le relayer detecte
    function lock() external payable {
        require(msg.value > 0, "Amount must be > 0");
        locked[msg.sender] += msg.value;
        emit Lock(msg.sender, msg.value, nonce++);
    }

    /// Deverrouille les tokens - appele par le validator apres Burn sur dest
    function unlock(
        address user,
        uint256 amount,
        uint256 _nonce,
        bytes calldata signature
    ) external {
        require(!usedNonces[_nonce],          "Nonce already used");
        require(locked[user] >= amount,       "Insufficient locked balance");
        require(
            validateSignature(user, amount, _nonce, signature),
            "Invalid validator signature"
        );

        usedNonces[_nonce] = true;
        locked[user] -= amount;

        (bool ok,) = payable(user).call{value: amount}("");
        require(ok, "Transfer failed");

        emit Unlock(user, amount, _nonce);
    }

    /// Verifie la signature ECDSA du validator autorise
    /// Hash = keccak256(user, amount, nonce, chainId) - empeche le replay cross-chain
    function validateSignature(
        address user,
        uint256 amount,
        uint256 _nonce,
        bytes memory signature
    ) public view returns (bool) {
        bytes32 hash    = keccak256(abi.encodePacked(user, amount, _nonce, block.chainid));
        bytes32 ethHash = MessageHashUtils.toEthSignedMessageHash(hash);
        return ECDSA.recover(ethHash, signature) == validator;
    }

    receive() external payable {}
}

=== WrappedToken (chaine destination) ===

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/// @title WrappedToken - Chaine destination (Mint & Burn)
contract WrappedToken {
    string  public constant name     = "Wrapped ETH Bridge";
    string  public constant symbol   = "WETHb";
    uint8   public constant decimals = 18;

    mapping(address => uint256) public balanceOf;
    mapping(uint256 => bool)    public mintedNonces; // protection replay
    uint256 public totalSupply;
    address public immutable bridge;

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Mint(address indexed to, uint256 amount, uint256 nonce);
    event Burn(address indexed from, uint256 amount);

    modifier onlyBridge() {
        require(msg.sender == bridge, "Only bridge");
        _;
    }

    constructor(address _bridge) {
        bridge = _bridge;
    }

    /// Mint appele par le bridge quand evenement Lock detecte sur la source
    function mint(address user, uint256 amount, uint256 _nonce) external onlyBridge {
        require(!mintedNonces[_nonce], "Nonce already minted");
        mintedNonces[_nonce] = true;
        balanceOf[user]  += amount;
        totalSupply      += amount;
        emit Mint(user, amount, _nonce);
        emit Transfer(address(0), user, amount);
    }

    /// Burn appele par l'utilisateur pour initier le retour vers la source
    function burn(uint256 amount) external {
        require(balanceOf[msg.sender] >= amount, "Insufficient balance");
        balanceOf[msg.sender] -= amount;
        totalSupply           -= amount;
        emit Burn(msg.sender, amount);
        emit Transfer(msg.sender, address(0), amount);
    }
}

Interpretation : Securite Cross-Chain

Les systèmes cross-chain introduisent des vulnerabilites spécifiques qui n’existent pas dans les contrats single-chain.

Vulnerabilite Description Impact Mitigation
Reentrancy cross-chain Une attaque sur une chain affecte l’autre Drain de fonds Verifier la finalite avant exécution
Double-spend Même tokens utilises sur plusieurs chains Inflation Lock/Burn atomique avec preuves cryptographiques
Oracle manipulation Fausse information sur l’etat d’une chain Decisions basees sur données fausses Multiples oracles, verification croisee
Bridge exploits Bugs dans les contrats de bridge Pertes massives (centaines de millions) Audits, formals verification, limits
Governance attacks Manipulation des votes cross-chain Prise de contrôle illegitime Snapshot voting, time locks

Points cles : 1. La finalite est critique : attendre suffisamment de confirmations avant d’executer une action cross-chain 2. Les rate limits (limites quotidiennes) reduisent l’impact d’une exploitation (ex: max 1M$ par jour) 3. L’emergency pause permet de geler un bridge si une attaque est detectee 4. Le monitoring en temps reel est essentiel pour detecter les activites suspectes

Exemples d’exploits reels : - Ronin Bridge (2022) : 625M$ voles - attaque sur les validators - Wormhole (2022) : 320M$ voles - bug de signature forgeable - Harmony Bridge (2022) : 100M$ voles - compromission des cles privees

Note technique : La règle d’or des bridges : “Trust, but verify”. Faire confiance au bridge ne suffit pas, il faut verifier la finalite, la signature, et l’etat de la chaîne source.


<< Solana & Anchor | Testnet Deploy >>

Indice : Exercice Bridge Token Cross-Chain

Pour implementer un bridge securise, rappelez-vous les proprietes critiques d’un système cross-chain :

Pattern Lock & Mint : 1. Source chain : L’utilisateur appelle lock(amount) → les tokens sont transfères dans le contrat bridge (verrouilles) 2. Detection : Un oracle/relayer detecte l’événement Lock et prouve la transaction sur la destination 3. Destination chain : Le bridge appelle mint(user, amount) sur le WrappedToken → creation de tokens representatifs

Securite critique - Protection contre replay attacks : - Le nonce doit etre unique et incrementer a chaque opération (timestamp ou compteur) - La signature doit inclure : user, amount, nonce, chainId (pour eviter la reutilisation sur une autre chaîne) - validateSignature doit verifier que le signataire est le validator autorise (pas d’utilisateur arbitraire)

Indice implementation : - Utiliser ECDSA.recover(hash, signature) pour verifier le signataire - Le hash a signer doit etre : keccak256(abi.encodePacked(user, amount, nonce, chainId)) - Stocker les nonces utilises dans un mapping(uint256 => bool) pour empecher la reutilisation - unlock ne doit etre appelable que par le validator (pas par l’utilisateur directement)

Cas d’erreur a gerer : - Le bridge destination est en pause (utilise whenNotPaused) - La signature est invalide (revert) - Le nonce a déjà ete utilise (revert) - Le montant depasse la limite quotidienne (rate limiting)

Resume et perspectives

Ce notebook a explore les defis et les solutions de l’interoperabilite entre blockchains. Nous avons etudie les problemes fondamentaux du cross-chain (finalite, securite, latence, cout, oracle), implemente des contrats Chainlink CCIP pour l’envoi et la reception de messages cross-chain avec transfert de tokens, examine les chain selectors et l’architecture de routage de Chainlink, et analyse les vulnerabilites spécifiques aux systèmes cross-chain (reentrancy cross-chain, double-spend, manipulation d’oracle, exploits de bridge, attaques de gouvernance).

L’enjeu central de l’interoperabilite est le compromis entre securite et performance : les protocols bases sur des oracles (CCIP) offrent une securite elevee au prix d’une latence plus importante, tandis que les solutions ultra-legers (LayerZero) privilegient la rapidite avec un modèle de confiance différent. Les exploits historiques majeurs (Ronin Bridge 625M$, Wormhole 320M$, Harmony 100M$) rappellent que les bridges concentrent un risque systemique considerable et qu’une verification rigoureuse de la finalite, des rate limits et des mécanismes de pause d’urgence sont indispensables.

Le notebook suivant, SC-24-Testnet-Deploy-Python, aborde le deploiement concret de contrats sur des reseaux de test publics (Sepolia, XRP Testnet) avant le passage en production.

Retour au sommet