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-chainprint("""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 distammentPROTOCOLS 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
L’interoperabilite entre blockchains presente des defis uniques que les contrats single-chain n’ont pas.
Problème
Description
Impact sur les bridges
Finalite
Une transaction peut etre revertue même après etre incluse dans un bloc
Il faut attendre suffisamment de confirmations avant de considerer une transaction comme finale
Securite
Une attaque sur une chain peut affecter l’autre (reentrancy cross-chain)
Les bridges doivent etre extremement securises, car une breach peut drainer des fonds sur plusieurs chains
Latence
Le temps de confirmation varie entre les chains (secondes a minutes)
L’UX cross-chain est plus lent que les transactions single-chain
Cout
Gas sur les deux chains + frais de bridge
Les transferts cross-chain sont beaucoup plus chers que les transferts locaux
Oracle
Il faut un moyen de verifier l’etat d’une autre chain
Dépendance a des tiers (oracles, validators, guardians)
Points cles : 1. Lock & Mint : Lock tokens A sur Ethereum, mint wrapped A’ sur Polygon (le plus courant) 2. Burn & Mint : Burn wrapped A’ sur Polygon, unlock A sur Ethereum (reverse opération) 3. Liquidity Pools : Utiliser des pools de liquidite sur chaque chaîne (ex: Hop Protocol, Stargate) 4. Light Clients : Verifier les headers de la blockchain source (ex: Optimism, Arbitrum use optimistic rollups)
Note technique : Aucun bridge n’est parfait. Chaque protocol fait des trade-offs entre securite, decentralisation, rapidite et cout. Le choix depend du cas d’usage (transferts de grande valeur = securite maximale).
Ce contrat montre comment recevoir des messages cross-chain via Chainlink CCIP.
Composant
Rôle
Details
CCIPReceiver
Contract de base Anchor
Implemente la logique de reception CCIP
_ccipReceive()
Callback obligatoire
Appelé automatiquement quand un message arrive
messages[]
Stockage des messages
Historique de tous les messages recus
MessageReceived event
Notification
Permet aux clients d’ecouter les arrivées
Points cles : 1. Le contrat herite de CCIPReceiver(router) qui gere la logique de verification du router CCIP 2. _ccipReceive() est internal override : Anchor appelle cette fonction automatiquement 3. message.sourceChainSelector : identifie la chaîne d’ou vient le message (ex: Sepolia, Arbitrum) 4. abi.decode(message.sender, (address)) : l’envoyeur est encode en bytes, il faut le decoder 5. message.data : contenu du message (peut etre n’importe quoi : string, struct, etc.)
Workflow de reception : 1. Un contrat Sender sur une autre chaîne appelle ccipSend() avec ce contrat comme destinataire 2. Les oracles Chainlink relayent le message vers cette chaîne 3. Le router CCIP sur cette chaîne appelle _ccipReceive() de ce contrat 4. Le contrat stocke le message et emet un événement MessageReceived
Cas d’usage : - Bridge de tokens : recevoir un message “Lock” et mint des tokens representatifs - Messagerie : recevoir des données d’une dApp sur une autre chaîne - Oracle multi-chain : agreger des données de plusieurs chains dans un seul contrat
Note technique : _ccipReceive() doit etre marque internal et override. C’est Anchor qui appelle cette fonction, pas un utilisateur. Ne pas la marquer public.
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 hashlibimport jsonclass 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 envoyesself.received = {} # address -> liste des messages recusdef 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_messagepass# TODO etudiantdef 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_messagespass# TODO etudiant# Test du simulateursim = 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 else0}")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-chainprint("""VULNERABILITES CROSS-CHAIN1. REENTRANCY CROSS-CHAIN - Une attaque sur une chain peut affecter l'autre - Solution: Verifier la finalite avant execution2. DOUBLE-SPEND - Meme tokens utilises sur plusieurs chains - Solution: Lock/Burn atomique3. ORACLE MANIPULATION - Fausse information sur l'etat d'une chain - Solution: Multiples oracles, verification4. BRIDGE EXPLOITS - Bugs dans les contrats de bridge - Solution: Audits, bug bounties, limits5. GOVERNANCE ATTACKS - Manipulation des votes cross-chain - Solution: Snapshot voting, delayBEST 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 recommandationsTODO 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_riskreturn {"score": 0, "recommendations": []} # TODO etudiant# Test avec un bridge hypothetiqueresult = 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
Interpretation : Sender CCIP Chainlink
Ce contrat montre comment envoyer des messages et des tokens vers d’autres blockchains via Chainlink CCIP.
Fonction
Opération
Contenu du message
sendMessage
Message simple (text, données)
data uniquement, pas de tokens
sendMessageWithToken
Message + transfert de tokens ERC-20
data + tokenAmounts[] (1 token dans l’exemple)
Points cles : 1. destinationChainSelector : identifiant unique de la chaîne cible (ex: Sepolia = 16015286601757825753) 2. Client.EVM2AnyMessage : structure CCIP qui contient toutes les infos necessaires au routage 3. router.getFee() : calcule les frais CCIP en fonction de la destination et de la taille du message 4. router.ccipSend{value: fees}() : envoie le message CCIP (paye les frais avec ETH via {value: fees})
Workflow d’envoi avec tokens : 1. L’utilisateur appelle sendMessageWithToken() avec les tokens transferes (transferFrom) 2. Le contrat approuve le router CCIP pour depenser les tokens (approve) 3. Le router CCIP lock les tokens et créé un message avec tokenAmounts[] 4. Les oracles Chainlink relayent le message et debloquent les tokens sur la destination
Securite : - ConfirmedOwner : seul le proprietaire peut changer la configuration du router - require(msg.value >= fees) : verifie que l’envoyeur a paye assez de frais - Les tokens sont transferes au contrat AVANT l’envoi (pas de reentrancy possible)
Note technique : CCIP supporte l’envoi de multiples tokens dans un seul message (tokenAmounts[]). L’exemple n’en montre qu’un, mais on pourrait envoyer 3+ tokens en un appel.
Les chain selectors sont des identifiants uniques que CCIP utilise pour router les messages entre les blockchains.
Chaîne
Selector
Usage
Ethereum Sepolia
16015286601757825753
Testnet Ethereum (développement)
Ethereum Mainnet
5009297550715157
Production Ethereum
Arbitrum Sepolia
3478487232816046354
Testnet L2
Polygon Mumbai
12532609583862916517
Testnet Polygon
Base Sepolia
5795021415249229034
Testnet Base (Coinbase)
Points cles : 1. Les selectors sont des nombres uint64 uniques generes par Chainlink pour chaque chaîne supportee 2. Le router est le point d’entree unique pour envoyer des messages CCIP sur une chaîne donnee 3. L’adresse du router Sepolia (0xD0daae...) est utilisee pour construire les contrats Sender/Receiver 4. Le router Mainnet (0x80226fc...) est différent : toujours utiliser le bon router pour la bonne chaîne
Workflow CCIP : 1. Sur la source : sender.sendMessage(destinationSelector, receiver, data) → paye des frais CCIP 2. Les oracles Chainlink detectent le message et le relayent vers la destination 3. Sur la destination : receiver._ccipReceive(message) est appele automatiquement 4. Les frais sont payes en feeToken (souvent LINK ou native token de la chaîne)
Gas economise : CCIP gere la complexite du routing cross-chain. Sans CCIP, il faudrait implementer un reseau d’oracles et de relayers soi-même.
Note technique : Les selectors CCIP ne changent pas. Une fois deploye, un contrat CCIP fonctionnera tant que la chaîne est supportee. Verifier la documentation Chainlink pour la liste a jour des selectors.
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
Comprendre le pattern Lock & Mint
Implementer un bridge securise
Gerer les cas d’erreur et reverts
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 Mfrom typing import Setclass 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/noncesself.validators =set(validators)self.threshold = thresholdpass# TODO etudiant : completer l'initialisationdef 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 noncepass# TODO etudiantreturn-1# placeholder, a remplacerdef 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 etudiantdef mint(self, user: str, amount: int, nonce: int) ->None:"""Mint wrapped token cote destination.TODO etudiant : protection replay par nonce (comme TokenBridgeSimulator). """pass# TODO etudiantdef burn(self, user: str, amount: int) ->int:"""Burn wrapped token cote destination.TODO etudiant : verifier balance, decrementer, retourner nonce. """pass# TODO etudiantreturn-1# placeholderdef 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 inrange(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")
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
Etendre le simulateur pour gerer plusieurs validators
Implementer la verification de seuil (M signatures requises pour unlock)
Gerer la rotation des validators (join/leave) avec accord du seuil
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)
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 =0self.wrapped_balances = {}self.used_nonces_dest =set()self.total_supply =0def lock(self, user: str, amount: int) ->int:self.locked[user] =self.locked.get(user, 0) + amount n =self.nonceself.nonce +=1print(f"[SOURCE] Lock : {user} verrouille {amount} ETH (nonce={n})")return ndef mint(self, user: str, amount: int, nonce: int):if nonce inself.used_nonces_dest:raiseValueError(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) + amountself.total_supply += amountprint(f"[DEST] Mint : {user} recoit {amount} WETHb (nonce={nonce})")def burn(self, user: str, amount: int) ->int:ifself.wrapped_balances.get(user, 0) < amount:raiseValueError("Balance insuffisante")self.wrapped_balances[user] -= amountself.total_supply -= amount n =self.nonceself.nonce +=1print(f"[DEST] Burn : {user} retourne {amount} WETHb (nonce={n})")return ndef unlock(self, user: str, amount: int, nonce: int, has_valid_signature: bool=True):ifnot has_valid_signature:raiseValueError("Rejete : Signature du validateur invalide. Preuve de Burn requise.")if nonce inself.used_nonces_source:raiseValueError(f"Replay detecte ! nonce {nonce} deja utilise sur source")ifself.locked.get(user, 0) < amount:raiseValueError("Balance verrouilee insuffisante")self.used_nonces_source.add(nonce)self.locked[user] -= amountprint(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 echouerprint("ERREUR : Replay non detecte !")exceptValueErroras 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 !")exceptValueErroras 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: MITpragma 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: MITpragma 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.
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.