SC-05-Inheritance-Python - Heritage et Interfaces

<< Functions & State | Suivant : Errors & Events >>


Objectifs d’apprentissage

  1. Comprendre l’heritage simple et multiple
  2. Utiliser les interfaces pour l’abstraction
  3. Maitriser abstract et override

Prerequis

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 terminal
try:
    from web3 import Web3
    import solcx
except ImportError as 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"

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

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

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


def compile_and_deploy(w3, source_code, deployer, *constructor_args):
    """Compiler et deployer un contrat Solidity (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, receipt


def 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, receipt
    raise ValueError(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 simple
INHERITANCE_BASIC = """
// SPDX-License-Identifier: MIT
pragma 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 constructeurs
CONSTRUCTOR_ORDER = """
// SPDX-License-Identifier: MIT
pragma 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 multiple
MULTIPLE_INHERITANCE = """
// SPDX-License-Identifier: MIT
pragma 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 bloque
token.functions.pause().transact({"from": deployer})
try:
    token.functions.transfer(receiver, 100).transact({"from": deployer})
    print("  ERREUR : transfer aurait du revert!")
except Exception:
    print(f"  transfer reverte quand paused=True (attendu)")

# Mint via onlyOwner
token.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.

# Interfaces
INTERFACE_EXAMPLE = """
// SPDX-License-Identifier: MIT
pragma 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 SimpleToken is IERC20 {
    string public constant name = "Simple Token";
    string public constant symbol = "SIM";
    uint8 public constant decimals = 18;

    uint256 private _totalSupply;
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    constructor(uint256 initialSupply) {
        _totalSupply = initialSupply;
        _balances[msg.sender] = initialSupply;
        emit Transfer(address(0), msg.sender, initialSupply);
    }

    function totalSupply() external view override returns (uint256) { return _totalSupply; }
    function balanceOf(address account) external view override returns (uint256) { return _balances[account]; }
    function allowance(address owner, address spender) external view override returns (uint256) { return _allowances[owner][spender]; }

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

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

    function transferFrom(address from, address to, uint256 amount) external override returns (bool) {
        require(_allowances[from][msg.sender] >= amount, "Insufficient allowance");
        require(_balances[from] >= amount, "Insufficient balance");
        _allowances[from][msg.sender] -= amount;
        _balances[from] -= amount;
        _balances[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}
"""

print("--- Interface IERC20 implementee par SimpleToken ---")
erc20, _ = deploy_named(w3, INTERFACE_EXAMPLE, "SimpleToken", deployer, 1_000_000)
receiver = w3.eth.accounts[1]
spender = w3.eth.accounts[2]
print(f"  totalSupply() = {erc20.functions.totalSupply().call()}")
print(f"  balanceOf(deployer) = {erc20.functions.balanceOf(deployer).call()}")

# Transfer
erc20.functions.transfer(receiver, 1000).transact({"from": deployer})
print(
    f"  Apres transfer(1000) : receiver balance = {erc20.functions.balanceOf(receiver).call()}"
)

# Approve + transferFrom
erc20.functions.approve(spender, 500).transact({"from": deployer})
print(
    f"  allowance(deployer, spender) = {erc20.functions.allowance(deployer, spender).call()}"
)
erc20.functions.transferFrom(deployer, receiver, 200).transact({"from": spender})
print(
    f"  Apres transferFrom(200) : receiver balance = {erc20.functions.balanceOf(receiver).call()}"
)
--- Interface IERC20 implementee par SimpleToken ---
Deploye: SimpleToken a 0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE
  totalSupply() = 1000000
  balanceOf(deployer) = 1000000
  Apres transfer(1000) : receiver balance = 1000
  allowance(deployer, spender) = 500
  Apres transferFrom(200) : receiver balance = 1200

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: MIT
pragma 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.

# Contrats abstraits
ABSTRACT_EXAMPLE = """
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

abstract contract PriceFeed {
    function getPrice() public view virtual returns (uint256);

    function getPriceWithDecimals() public view virtual returns (uint256) {
        return getPrice() * 10 ** 18;
    }
}

contract ChainlinkPriceFeed is PriceFeed {
    uint256 private price;

    constructor(uint256 _price) {
        price = _price;
    }

    function getPrice() public view override returns (uint256) {
        return price;
    }
}

contract UniswapPriceFeed is PriceFeed {
    uint256 private price;

    constructor(uint256 _price) {
        price = _price;
    }

    function getPrice() public view override returns (uint256) {
        return price;
    }

    function getPriceWithDecimals() public view override returns (uint256) {
        return getPrice() * 10 ** 6;
    }
}
"""

print("--- Contrats abstraits : implementations differentes ---")
chainlink, _ = deploy_named(w3, ABSTRACT_EXAMPLE, "ChainlinkPriceFeed", deployer, 100)
uniswap, _ = deploy_named(w3, ABSTRACT_EXAMPLE, "UniswapPriceFeed", deployer, 200)

print(f"  Chainlink getPrice() = {chainlink.functions.getPrice().call()}")
print(
    f"  Chainlink getPriceWithDecimals() = {chainlink.functions.getPriceWithDecimals().call()} (x10^18)"
)
print(f"  Uniswap  getPrice() = {uniswap.functions.getPrice().call()}")
print(
    f"  Uniswap  getPriceWithDecimals() = {uniswap.functions.getPriceWithDecimals().call()} (x10^6, override)"
)
--- Contrats abstraits : implementations differentes ---
Deploye: ChainlinkPriceFeed a 0xc6e7DF5E7b4f2A278906862b61205850344D4e7d
Deploye: UniswapPriceFeed a 0x59b670e9fA9D0A427751Af201D676719a970857b
  Chainlink getPrice() = 100
  Chainlink getPriceWithDecimals() = 100000000000000000000 (x10^18)
  Uniswap  getPrice() = 200
  Uniswap  getPriceWithDecimals() = 200000000 (x10^6, override)

Interpretation : contrats abstraits vs interfaces

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 linearisation
SUPER_EXAMPLE = """
// SPDX-License-Identifier: MIT
pragma 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.

Exemple guide : Contrat Vault — Solution étudiante (NicoLP04, TP 2026)

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: MIT
pragma 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 anvil
ownable, 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

Exemple guide : Interface IERC721 simplifiée — Solution étudiante (NicoLP04, TP 2026)

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: MIT
pragma 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 anvil
simplenft, 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 ecoule

EXERCICE_TIMELOCK_VAULT = """
// SPDX-License-Identifier: MIT
pragma 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 token

EXERCICE_METADATA_NFT = """
// SPDX-License-Identifier: MIT
pragma 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")
Exercice a completer : MetadataNFT avec tokenURI

7. Resume

Concept Description
is Heritage d’un contrat parent
virtual Fonction pouvant etre overidee
override Redefinition d’une fonction parente
interface Contrat 100% abstrait
abstract Contrat partiellement implemente
super Appel a la fonction parente

Notebook suivant : SC-06-Errors-Events-Python

Resume et perspectives

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.


<< Functions & State | Suivant : Errors & Events >>

Retour au sommet