Bonne maitrise des fonctions et modificateurs Solidity
Python 3.10+ avec pip install web3 py-solc-x
Duree estimee : 35 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 terminaltry:from web3 import Web3import solcxexceptImportErroras e:print(f"Installation requise : pip install web3 py-solc-x")print(f"Erreur : {e}")SOLC_VERSION ="0.8.28"ANVIL_URL ="http://127.0.0.1:8545"# Connexionw3 = 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 necessaireinstalled = [str(v) for v in solcx.get_installed_solc_versions()]if SOLC_VERSION notin 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 (source a un seul contrat).""" compiled = solcx.compile_source( source_code, output_values=["abi", "bin"], solc_version=SOLC_VERSION ) 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, receiptdef deploy_named(w3, source_code, contract_name, deployer, *constructor_args):"""Compiler une source multi-contrats et deployer un contrat specifique par nom.""" compiled = solcx.compile_source( source_code, output_values=["abi", "bin"], solc_version=SOLC_VERSION )for contract_id, contract_interface in compiled.items():if contract_id.split(":")[-1] == contract_name: 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_name} a {receipt.contractAddress}")return instance, receiptraiseValueError(f"Contrat '{contract_name}' introuvable dans la source compilee")
Connecte a anvil (chain 31337), deployer: 0xf39Fd6e5...
1. Heritage simple
Solidity supporte l’heritage avec le mot-cle is.
# Heritage simpleINHERITANCE_BASIC ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract Animal { string public name; constructor(string memory _name) { name = _name; } function speak() public pure virtual returns (string memory) { return "..."; }}contract Dog is Animal { constructor(string memory _name) Animal(_name) {} function speak() public pure override returns (string memory) { return "Woof!"; }}contract Cat is Animal { constructor(string memory _name) Animal(_name) {} function speak() public pure override returns (string memory) { return "Meow!"; }}"""print("--- Heritage simple avec virtual/override ---")dog, _ = deploy_named(w3, INHERITANCE_BASIC, "Dog", deployer, "Rex")cat, _ = deploy_named(w3, INHERITANCE_BASIC, "Cat", deployer, "Whiskers")print(f" dog.name = '{dog.functions.name().call()}', speak() = '{dog.functions.speak().call()}'")print(f" cat.name = '{cat.functions.name().call()}', speak() = '{cat.functions.speak().call()}'")
--- Heritage simple avec virtual/override ---
Deploye: Dog a 0x8A791620dd6260079BF849Dc5567aDC3F2FdC318
Deploye: Cat a 0x610178dA211FEF7D417bC0e6FeD39F05609AD788
dog.name = 'Rex', speak() = 'Woof!'
cat.name = 'Whiskers', speak() = 'Meow!'
Interpretation : heritage simple avec virtual/override
Résultat obtenu : Dog et Cat heritent de Animal et redéfinissent la méthode speak(). Chaque sous-contrat a son propre comportement tout en partageant la propriete name.
Contrat
Parent
name()
speak()
Mécanisme
Animal
-
“Rex” ou “Whiskers”
“…”
virtual : autorise le override
Dog
Animal
herite
“Woof!”
override : redefinition
Cat
Animal
herite
“Meow!”
override : redefinition
Points cles : - Le mot-cle virtual sur Animal.speak() autorise les contrats enfants a la redefinir. Sans virtual, toute tentative de override provoque une erreur de compilation - Le mot-cle override sur Dog.speak() indique explicitement que cette fonction redefinit une fonction parente. Cette double declaration est une protection contre les surcharges accidentelles - Le constructeur Dog(string memory _name) Animal(_name) transmet le paramètre au constructeur parent via la syntaxe d’appel de constructeur de base
1.1 Ordre des constructeurs
# Ordre des constructeursCONSTRUCTOR_ORDER ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract A { string public nameA; constructor(string memory _name) { nameA = _name; }}contract B is A { string public nameB; constructor(string memory _nameA, string memory _nameB) A(_nameA) { nameB = _nameB; }}"""print("--- Ordre des constructeurs : parent initialise avant enfant ---")b, _ = deploy_named(w3, CONSTRUCTOR_ORDER, "B", deployer, "Parent", "Child")print(f" nameA (depuis A) = '{b.functions.nameA().call()}'")print(f" nameB (depuis B) = '{b.functions.nameB().call()}'")
--- Ordre des constructeurs : parent initialise avant enfant ---
Deploye: B a 0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e
nameA (depuis A) = 'Parent'
nameB (depuis B) = 'Child'
Interpretation : ordre des constructeurs
Résultat obtenu : Le contrat B herite de A et appelle le constructeur parent via A(_nameA). Les deux valeurs sont correctement initialisees.
Attribut
Source
Valeur
nameA
Constructeur de A
“Parent”
nameB
Constructeur de B
“Child”
Points cles : - L’appel A(_nameA) dans le constructeur de B est obligatoire car A a un constructeur avec paramètres. Sans cet appel, la compilation echouerait - L’ordre d’exécution est toujours parent avant enfant : le constructeur de A s’execute en premier, puis celui de B - Quand aucun constructeur parent n’est specifie, Solidity appelle le constructeur par defaut (sans arguments) du parent
2. Heritage multiple
Solidity permet l’heritage multiple avec resolution de linearisation (C3).
# Heritage multipleMULTIPLE_INHERITANCE ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract Ownable { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; }}contract Pausable { bool public paused = false; modifier whenNotPaused() { require(!paused, "Paused"); _; } function pause() public { paused = true; } function unpause() public { paused = false; }}contract Token is Ownable, Pausable { string public name; uint256 public totalSupply; mapping(address => uint256) public balances; constructor(string memory _name, uint256 _supply) { name = _name; totalSupply = _supply; balances[msg.sender] = _supply; } function transfer(address to, uint256 amount) public whenNotPaused { require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; balances[to] += amount; } function mint(address to, uint256 amount) public onlyOwner { totalSupply += amount; balances[to] += amount; }}"""print("--- Heritage multiple : Token herite de Ownable et Pausable ---")token, _ = deploy_named( w3, MULTIPLE_INHERITANCE, "Token", deployer, "MyToken", 1_000_000)receiver = w3.eth.accounts[1]print(f" name = '{token.functions.name().call()}', totalSupply = {token.functions.totalSupply().call()}")print(f" deployer balance = {token.functions.balances(deployer).call()}")# Transfer fonctionne (pas en pause)token.functions.transfer(receiver, 500).transact({"from": deployer})print(f" Apres transfer(500) : receiver balance = {token.functions.balances(receiver).call()}")# Pause puis transfer bloquetoken.functions.pause().transact({"from": deployer})try: token.functions.transfer(receiver, 100).transact({"from": deployer})print(" ERREUR : transfer aurait du revert!")exceptException:print(f" transfer reverte quand paused=True (attendu)")# Mint via onlyOwnertoken.functions.unpause().transact({"from": deployer})token.functions.mint(receiver, 1000).transact({"from": deployer})print(f" Apres mint(1000) : receiver balance = {token.functions.balances(receiver).call()}")
--- Heritage multiple : Token herite de Ownable et Pausable ---
Deploye: Token a 0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0
name = 'MyToken', totalSupply = 1000000
deployer balance = 1000000
Apres transfer(500) : receiver balance = 500
transfer reverte quand paused=True (attendu)
Apres mint(1000) : receiver balance = 1500
Interpretation : heritage multiple et modificateurs combins
Résultat obtenu : Le contrat Token herite de Ownable et Pausable simultanement, combinant les modificateurs onlyOwner et whenNotPaused.
Action
Résultat
Modificateur active
transfer(500)
OK
whenNotPaused (pausing = false)
pause()
paused = true
aucun
transfer(100) après pause
Revert
whenNotPaused bloque
mint(1000) après unpause
OK
onlyOwner (deployer = owner)
Points cles : - L’heritage multiple permet de composer des comportements : Ownable gere la propriete, Pausable gere l’arret d’urgence, Token gere la logique metier - L’ordre d’heritage is Ownable, Pausable suit la linearisation C3 (vue en section 5) - Les modificateurs sont herites comme les fonctions : Token peut utiliser onlyOwner et whenNotPaused sans les redefinir - Ce pattern (Ownable + Pausable) est omnipresent dans les contrats DeFi en production (OpenZeppelin l’implemente dans sa librairie standard)
3. Interfaces
Les interfaces definissent des signatures sans implementation.
Interpretation : interface IERC20 et standardisation
Résultat obtenu : SimpleToken implemente l’interface IERC20 avec succes : les opérations transfer, approve et transferFrom fonctionnent comme attendu.
Opération
Résultat
Explication
totalSupply()
1 000 000
Offre initiale allouee au deployeur
transfer(1000)
receiver = 1000
Debit du deployeur, credit du receveur
approve(500)
allowance = 500
Le spender peut depenser jusqu’a 500
transferFrom(200)
receiver = 1200
Debit du deployeur via le spender
Points cles : - L’interface IERC20 (EIP-20) est le standard le plus deploye sur Ethereum : tout token (USDT, USDC, UNI) l’implemente - Le pattern approve/transferFrom est essentiel pour les DeFi : un contrat (Uniswap, Aave) ne peut pas transferer vos tokens sans votre approbation prealable - L’interface ne contient aucune implementation : elle définit seulement les signatures de fonctions et les événements. C’est un contrat 100% abstrait - En Solidity, les fonctions d’une interface sont implicitement virtual et n’ont pas de corps
Exercice : Interface IERC20 avec decimales personnalisees
L’interface IERC20 ne définit pas de fonction decimals(), mais en pratique chaque token ERC-20 precise son nombre de decimales. Créez un contrat StableCoin qui implemente l’interface IERC20 avec 6 decimales (comme USDC/USDT) au lieu de 18.
Indices : - Inspirez-vous du contrat SimpleToken de la section 3 - Ajoutez une fonction decimals() public pure returns (uint8) qui retourne 6 - Dans le constructeur, multipliez le supply initial par 10 ** 6 au lieu de 10 ** 18 - Les fonctions transfer, approve, transferFrom sont identiques a SimpleToken
# Exercice : Interface IERC20 avec decimales personnalisees (StableCoin)EXERCISE_STABLECOIN ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value);}contract StableCoin is IERC20 { // TODO etudiant : definir les variables d'etat (name, symbol, decimals, _totalSupply, _balances, _allowances) constructor(uint256 initialSupply) { // TODO etudiant : initialiser le supply avec 6 decimales (comme USDC) pass } function totalSupply() external view override returns (uint256) { // TODO etudiant : retourner le supply total return 0; } function balanceOf(address account) external view override returns (uint256) { // TODO etudiant : retourner le solde de l'adresse return 0; } function transfer(address to, uint256 amount) external override returns (bool) { // TODO etudiant : transferer les tokens // Indice : verifier le solde, debiter l'emetteur, crediter le destinataire, emettre Transfer return false; } function approve(address spender, uint256 amount) external override returns (bool) { // TODO etudiant : autoriser le spender return false; } function transferFrom(address from, address to, uint256 amount) external override returns (bool) { // TODO etudiant : transfer via allowance // Indice : verifier allowance ET balance, decrementer les deux return false; }}"""print("Exercice a completer : StableCoin avec 6 decimales")
Exercice a completer : StableCoin avec 6 decimales
4. Contrats abstraits
Les contrats abstraits peuvent avoir des fonctions implementees ET non implementees.
Résultat obtenu : ChainlinkPriceFeed et UniswapPriceFeed implementent toutes deux le contrat abstrait PriceFeed, mais avec des comportements différents pour getPriceWithDecimals().
Implementation
getPrice()
Decimales
getPriceWithDecimals()
ChainlinkPriceFeed
100
18 (standard ERC)
100 * 10^18 = 1.0e20
UniswapPriceFeed
200
6 (standard USDC)
200 * 10^6 = 2.0e8
Points cles : - Le contrat abstrait PriceFeed fournit une implementation par defaut de getPriceWithDecimals() (x10^18). Chainlink l’utilise telle quelle - Uniswap surcharge (override) cette méthode pour utiliser 6 decimales, ce qui correspond au standard USDC/USDT - Un abstract contract est plus flexible qu’une interface : il peut contenir du code implemente ET des fonctions sans corps. Une interface est 100% abstraite - En production, les oracles Chainlink utilisent effectivement 18 decimales et les tokensUniswap 6, rendant ce pattern très realiste
5. Super et linearisation
Le mot-cle super appelle la fonction du contrat parent.
# Super et linearisationSUPER_EXAMPLE ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract A { function foo() public pure virtual returns (string memory) { return "A"; }}contract B is A { function foo() public pure virtual override returns (string memory) { return string.concat(super.foo(), " B"); }}contract C is A { function foo() public pure virtual override returns (string memory) { return string.concat(super.foo(), " C"); }}// Linearisation C3 : D -> C -> B -> A// super dans D appelle C (linearisation right-to-left)contract D is B, C { function foo() public pure override(B, C) returns (string memory) { return string.concat(super.foo(), " D"); }}"""print("--- Linearisation C3 et super ---")d_contract, _ = deploy_named(w3, SUPER_EXAMPLE, "D", deployer)result = d_contract.functions.foo().call()print(f" D.foo() = '{result}'")print(f" Ordre de resolution : A -> B -> C -> D => '{result}'")
--- Linearisation C3 et super ---
Deploye: D a 0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1
D.foo() = 'A B C D'
Ordre de resolution : A -> B -> C -> D => 'A B C D'
Interpretation : linearisation C3 et le problème du diamant
Résultat obtenu : D.foo() retourne 'A B C D', ce qui revele l’ordre de resolution : A -> B -> C -> D.
Étape
Appel
Résultat partiel
1
D.foo() appelle super.foo()
transmet a C (MRO : D -> C -> B -> A)
2
C.foo() appelle super.foo()
transmet a B
3
B.foo() appelle super.foo()
transmet a A
4
A.foo() retourne “A”
remonte la chaîne
5
B concatene ” B”
“A B”
6
C concatene ” C”
“A B C”
7
D concatene ” D”
“A B C D”
Points cles : - La linearisation C3 garantit un ordre déterministe et monotone : si X apparait avant Y dans la liste des parents, X restera avant Y dans la linearisation finale - Le contrat D is B, C liste B en premier, mais la linearisation C3 traite les bases de droite a gauche : le MRO est D, C, B, A. Ainsi super dans D appelle C (pas B) - Ce mécanisme resout le problème du diamant (diamond problem) : quand deux parents heritent d’un même ancetre, la linearisation evite les appels doubles
6. Exemples guidés et Exercices
Les exemples guidés ci-dessous montrent des contrats résolus (solutions étudiantes). Chaque exemple est suivi d’un exercice à compléter.
Transition : de la théorie a la pratique
Les sections précédentes ont couvert les mécanismes fondamentaux de l’heritage Solidity : heritage simple, heritage multiple, interfaces, contrats abstraits et linearisation C3. Les exercices suivants vous demandent de combiner ces concepts pour créer des contrats reels : un Vault securise par heritage et un NFT base sur une interface IERC721.
Contrat Vault qui hérite de Ownable avec fonctions de dépôt, retrait et retrait total réservé au propriétaire.
# Exemple guide : Contrat Vault (solution NicoLP04, TP 2026)EXERCICE_VAULT ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract Ownable { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; }}contract Vault is Ownable { mapping(address => uint256) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Not enough balance"); balances[msg.sender] -= amount; payable(msg.sender).transfer(amount); } function withdrawAll() public onlyOwner { payable(owner).transfer(address(this).balance); }}"""# Compilation et deploiement reel sur anvilownable, receipt = compile_and_deploy(w3, EXERCICE_VAULT, deployer)
Deploye: Vault a 0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44
Analyse : Contrat Vault
Solution validée : Le contrat Vault hérite de Ownable et implémente trois fonctions : - deposit() : crédite le solde de l’appelant via msg.value - withdraw(uint256) : vérifie le solde suffisant, débite et transfère - withdrawAll() : réservé au owner via le modificateur onlyOwner hérité
Fonction
Modificateur
Mécanisme
deposit()
public payable
balances[msg.sender] += msg.value
withdraw(uint256)
public
require + débit + transfer
withdrawAll()
public onlyOwner
Hérité de Ownable
Points clés : - L’héritage Vault is Ownable donne accès au modificateur onlyOwner sans le redéfinir - Le pattern mapping(address => uint256) est standard pour suivre les soldes individuels - payable(msg.sender).transfer(amount) envoie de l’ETH à l’appelant
Contrat SimpleNFT implémentant une interface NFT simplifiée avec mint, ownerOf et transferFrom.
# Exemple guide : Interface IERC721 simplifiee (solution NicoLP04, TP 2026)EXERCICE_NFT ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;interface IERC721 { function ownerOf(uint256 tokenId) external view returns (address); function transferFrom(address from, address to, uint256 tokenId) external; event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);}contract SimpleNFT is IERC721 { mapping(uint256 => address) private _owners; uint256 private _nextTokenId = 1; function mint() public returns (uint256) { uint256 tokenId = _nextTokenId; _nextTokenId++; _owners[tokenId] = msg.sender; emit Transfer(address(0), msg.sender, tokenId); return tokenId; } function ownerOf(uint256 tokenId) public view override returns (address) { require(_owners[tokenId] != address(0), "Token does not exist"); return _owners[tokenId]; } function transferFrom(address from, address to, uint256 tokenId) public override { require(_owners[tokenId] == from, "Not the owner"); _owners[tokenId] = to; emit Transfer(from, to, tokenId); }}"""# Compilation et deploiement reel sur anvilsimplenft, receipt = compile_and_deploy(w3, EXERCICE_NFT, deployer)
Deploye: SimpleNFT a 0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f
Analyse : Interface IERC721 simplifiée
Solution validée : Le contrat SimpleNFT implémente l’interface IERC721 avec trois fonctions :
Fonction
Mécanisme
Détail
mint()
Auto-incrément _nextTokenId
Assigne à msg.sender, émet Transfer(address(0), ...)
ownerOf(tokenId)
Lecture _owners[tokenId]
require que le token existe
transferFrom(from, to, tokenId)
Vérification + transfert
require(from == _owners[tokenId]), mise à jour + événement
Points clés : - L’interface IERC721 (simplifiée) définit les signatures sans implémentation — SimpleNFT doit fournir le code - _nextTokenId commence à 1 (pas 0) car address(0) dans _owners signifie “token inexistant” - Le pattern event Transfer(address(0), ...) pour le mint est la convention ERC-721 standard
Exercice 3 : Vault avec délai de retrait (TimelockVault)
Créez un contrat TimelockVault qui hérite de Vault et ajoute un délai obligatoire entre la demande de retrait et l’exécution.
# Exercice 3 : Vault avec delai de retrait (TimelockVault)# TODO etudiant : implementer un contrat TimelockVault qui herite de Vault# Indice : inspirez-vous du Vault (Exemple guide 1) pour la structure de base# Etape 1 : ajouter une constante LOCK_DURATION (ex: 1 jour = 86400 secondes)# Etape 2 : ajouter un mapping pendingWithdrawals (address => uint256 timestamp)# Etape 3 : implementer requestWithdraw(uint256) qui enregistre le timestamp# Etape 4 : implementer executeWithdraw(uint256) qui verifie le delai ecouleEXERCICE_TIMELOCK_VAULT ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract Ownable { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; }}contract Vault is Ownable { mapping(address => uint256) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Not enough balance"); balances[msg.sender] -= amount; payable(msg.sender).transfer(amount); } function withdrawAll() public onlyOwner { payable(owner).transfer(address(this).balance); }}contract TimelockVault is Vault { // TODO etudiant : definir LOCK_DURATION et le mapping pendingWithdrawals uint256 public constant LOCK_DURATION = 86400; // 1 jour // TODO etudiant : mapping pour stocker le timestamp de demande par utilisateur function requestWithdraw(uint256 amount) public { // TODO etudiant : verifier le solde et enregistrer le timestamp pass } function executeWithdraw(uint256 amount) public { // TODO etudiant : verifier que le delai est ecoule, puis retirer // Indice : utiliser block.timestamp et LOCK_DURATION // Indice : appeler withdraw() du parent si le delai est passe pass }}"""print("Exercice a completer : TimelockVault avec delai de retrait")
Exercice a completer : TimelockVault avec delai de retrait
Exercice 4 : NFT avec métadonnées URI
Créez un contrat MetadataNFT qui hérite de SimpleNFT et ajoute une URI de métadonnées pour chaque token.
# Exercice 4 : NFT avec metadonnees URI# TODO etudiant : implementer un contrat MetadataNFT qui herite de SimpleNFT# Etape 1 : ajouter un mapping tokenURI (uint256 => string)# Etape 2 : override mint() pour accepter un parametre _tokenURI et le stocker# Etape 3 : implementer tokenURI(uint256) qui retourne l'URI du tokenEXERCICE_METADATA_NFT ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;// Interface IERC721 simplifiee (cf. Exemple guide SimpleNFT)interface IERC721Metadata { function ownerOf(uint256 tokenId) external view returns (address); function transferFrom(address from, address to, uint256 tokenId) external; function tokenURI(uint256 tokenId) external view returns (string memory); event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);}contract MetadataNFT is IERC721Metadata { // TODO etudiant : definir les variables d'etat // _owners, _nextTokenId, _tokenURIs pass function mint(string memory _uri) public returns (uint256) { // TODO etudiant : generer un tokenId, assigner le owner et l'URI return 0; } function ownerOf(uint256 tokenId) public view override returns (address) { // TODO etudiant : retourner le proprietaire du token return address(0); } function transferFrom(address from, address to, uint256 tokenId) public override { // TODO etudiant : verifier le proprietaire et transferer pass } function tokenURI(uint256 tokenId) public view override returns (string memory) { // TODO etudiant : retourner l'URI du token (revert si inexistant) return ""; }}"""print("Exercice a completer : MetadataNFT avec tokenURI")
Ce notebook a explore les mécanismes d’heritage et d’abstraction en Solidity, depuis l’heritage simple avec virtual/override jusqu’a l’heritage multiple regie par la linearisation C3. L’implementation de l’interface IERC20 a illustre le rôle central des interfaces dans la standardisation Ethereum : un token comme USDT ou USDC n’est qu’un contrat qui implemente cette interface, permettant a n’importe quel protocol DeFi d’interagir avec lui sans connaitre son implementation. Les contrats abstraits, quant a eux, offrent un compromis entre interface pure et implementation complete, avec des méthodes par defaut pouvant etre surchargees.
L’heritage multiple et la linearisation C3 meritent une attention particuliere car ils determinent l’ordre d’appel de super dans les chaînes d’heritage complexes. Ce mécanisme resout le problème du diamant de maniere déterministe, mais peut surprendre si l’on ignore les règles de resolution. Le pattern Ownable + Pausable combine par heritage est omnipresent dans les contrats DeFi en production et constitue un modèle architectural fondamental.
Avec la maitrise des types (SC-3), des fonctions et de l’etat (SC-4), puis de l’heritage et des interfaces, le notebook suivant SC-06-Errors-Events-Python complete le socle fondamental en abordant la gestion des erreurs (require, revert, assert, custom errors) et les events pour le logging on-chain.