SC-07-Token-Standards-Python - Standards de Tokens

<< Précédent : Errors & Events | Retour au sommaire | Suivant : DeFi Primitives >>


Objectifs d’apprentissage

  1. Implementer le standard ERC-20 (tokens fongibles)
  2. Implementer le standard ERC-721 (NFTs)
  3. Decouvrir ERC-1155 (multi-tokens)
  4. Utiliser les contrats OpenZeppelin

Prerequis

  • SC-3 a SC-6 completes
  • Notions de standards ERC (ERC-20, ERC-721)
  • Connaissance de base des wallets Ethereum

Duree estimee : 50 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
except ImportError:
    print("Installation requise : pip install web3")
    raise

try:
    import solcx
except ImportError:
    print("Installation requise : pip install py-solc-x")
    raise

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."""
    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
Connecte a anvil (chain 31337), deployer: 0xf39Fd6e5...

1. Standard ERC-20

ERC-20 est le standard pour les tokens fongibles (interchangeables).

# Interface ERC-20 complete
ERC20_INTERFACE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/// @title ERC-20 Token Standard
interface IERC20 {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function allowance(address owner, address spender) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
}
'''

# Les interfaces ne peuvent pas etre deployees (pas de bytecode)
# Elles definissent uniquement les signatures de fonctions
print("Interface ERC-20 (reference - ne se deploie pas) :")
print("  - totalSupply(), balanceOf(), allowance()")
print("  - transfer(), transferFrom(), approve()")
print("  - events: Transfer, Approval")
print("\nL implementation est dans la cellule suivante.")
Interface ERC-20 (reference - ne se deploie pas) :
  - totalSupply(), balanceOf(), allowance()
  - transfer(), transferFrom(), approve()
  - events: Transfer, Approval

L implementation est dans la cellule suivante.

Interpretation : Architecture de l’interface ERC-20

Résultat obtenu : L’interface IERC20 définit les 6 fonctions et 2 événements que tout token fongible doit implementer.

Catégorie Fonctions Rôle
Consultation totalSupply, balanceOf, allowance Lire l’etat sans modifier la blockchain (view)
Transfert transfer, transferFrom Deplacer des tokens entre adresses
Autorisation approve Deleguer le droit de transferer
Événements Transfer, Approval Journaliser les opérations pour les dApps

Points cles : - Une interface Solidity ne contient que des signatures (pas d’implementation) et ne peut pas etre deployee - Le mot-cle view indique que la fonction ne modifie pas l’etat (lecture gratuite, pas de gas) - Les événements indexed permettent un filtrage efficace dans les logs de la blockchain - Toute adresse Ethereum (EOA ou contrat) peut interagir avec un ERC-20 – la compatibilite est assuree par l’interface commune

Implémentation complète du standard ERC-20 avec toutes les fonctions requises par l’interface.

# Implementation complete ERC-20
ERC20_FULL_SOURCE = ERC20_INTERFACE + '''
contract SimpleERC20 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 * 10 ** uint256(decimals);
        _balances[msg.sender] = _totalSupply;
        emit Transfer(address(0), msg.sender, _totalSupply);
    }

    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, "ERC20: 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, "ERC20: insufficient allowance");
        require(_balances[from] >= amount, "ERC20: insufficient balance");
        _allowances[from][msg.sender] -= amount;
        _balances[from] -= amount;
        _balances[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}
'''

print("--- Implementation ERC-20 : SimpleERC20 ---")
erc20, _ = compile_and_deploy(w3, ERC20_FULL_SOURCE, deployer, 1_000_000)
receiver = w3.eth.accounts[1]
spender = w3.eth.accounts[2]

unit = 10**18
print(f"  totalSupply = {erc20.functions.totalSupply().call() // unit} SIM")
print(f"  balanceOf(deployer) = {erc20.functions.balanceOf(deployer).call() // unit} SIM")

# transfer
erc20.functions.transfer(receiver, 1000 * unit).transact({'from': deployer})
print(f"  Apres transfer(1000 SIM) : receiver = {erc20.functions.balanceOf(receiver).call() // unit} SIM")

# approve + transferFrom
erc20.functions.approve(spender, 500 * unit).transact({'from': deployer})
erc20.functions.transferFrom(deployer, receiver, 200 * unit).transact({'from': spender})
print(f"  Apres approve(500) + transferFrom(200) : receiver = {erc20.functions.balanceOf(receiver).call() // unit} SIM")
print(f"  allowance restante = {erc20.functions.allowance(deployer, spender).call() // unit} SIM")
--- Implementation ERC-20 : SimpleERC20 ---
Deploye: SimpleERC20 a 0x8A791620dd6260079BF849Dc5567aDC3F2FdC318
  totalSupply = 1000000 SIM
  balanceOf(deployer) = 1000000 SIM
  Apres transfer(1000 SIM) : receiver = 1000 SIM
  Apres approve(500) + transferFrom(200) : receiver = 1200 SIM
  allowance restante = 300 SIM

Interpretation : Mécanisme d’allowance ERC-20

Résultat obtenu : Le cycle complet transfer, approve et transferFrom fonctionne comme attendu.

Étape Fonction Solde deployer Solde receiver Allowance
Initial constructeur 1 000 000 0 –
transfer(1000) deployer vers receiver 999 000 1 000 –
approve(500) deployer autorise spender 999 000 1 000 500
transferFrom(200) spender deplace vers receiver 998 800 1 200 300

Points cles : - transfer est direct : l’emetteur signe et envoie - approve + transferFrom separe l’autorisation de l’exécution (pattern delegation). Le spender peut transferer a la place du proprietaire, dans la limite de l’allowance - L’allowance est decrementee après chaque transferFrom, pas reinitialisee - Ce mécanisme est la base des DEX (Uniswap) et des protocoles de lending (Aave)


2. Standard ERC-721 (NFT)

ERC-721 est le standard pour les tokens non-fongibles (uniques).

# Interface ERC-721
ERC721_INTERFACE = '''
interface IERC721 {
    event Transfer(
        address indexed from, 
        address indexed to, 
        uint256 indexed tokenId
    );
    event Approval(
        address indexed owner, 
        address indexed approved, 
        uint256 indexed tokenId
    );
    event ApprovalForAll(
        address indexed owner, 
        address indexed operator, 
        bool approved
    );

    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function transferFrom(address from, address to, uint256 tokenId) external;
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}
'''

print("Interface ERC-721:")
print(ERC721_INTERFACE)
Interface ERC-721:

interface IERC721 {
    event Transfer(
        address indexed from, 
        address indexed to, 
        uint256 indexed tokenId
    );
    event Approval(
        address indexed owner, 
        address indexed approved, 
        uint256 indexed tokenId
    );
    event ApprovalForAll(
        address indexed owner, 
        address indexed operator, 
        bool approved
    );

    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function transferFrom(address from, address to, uint256 tokenId) external;
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

Implémentation simplifiée d’un token ERC-721 (NFT) avec gestion de la propriété et des approbations.

# Implementation simplifiee ERC-721
ERC721_FULL_SOURCE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IERC721 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);
    function transferFrom(address from, address to, uint256 tokenId) external;
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
}

contract SimpleNFT is IERC721 {
    string public name = "Simple NFT";
    string public symbol = "SNFT";

    mapping(uint256 => address) private _owners;
    mapping(address => uint256) private _balances;
    mapping(uint256 => address) private _tokenApprovals;
    mapping(address => mapping(address => bool)) private _operatorApprovals;
    uint256 private _nextTokenId = 1;

    function balanceOf(address owner) public view override returns (uint256) {
        require(owner != address(0), "ERC721: zero address");
        return _balances[owner];
    }

    function ownerOf(uint256 tokenId) public view override returns (address) {
        address owner = _owners[tokenId];
        require(owner != address(0), "ERC721: nonexistent token");
        return owner;
    }

    function approve(address to, uint256 tokenId) public override {
        address owner = ownerOf(tokenId);
        require(msg.sender == owner || isApprovedForAll(owner, msg.sender), "ERC721: not approved");
        _tokenApprovals[tokenId] = to;
        emit Approval(owner, to, tokenId);
    }

    function getApproved(uint256 tokenId) public view override returns (address) {
        require(_owners[tokenId] != address(0), "ERC721: nonexistent token");
        return _tokenApprovals[tokenId];
    }

    function setApprovalForAll(address operator, bool approved) public override {
        _operatorApprovals[msg.sender][operator] = approved;
    }

    function isApprovedForAll(address owner, address operator) public view override returns (bool) {
        return _operatorApprovals[owner][operator];
    }

    function transferFrom(address from, address to, uint256 tokenId) public override {
        address owner = ownerOf(tokenId);
        require(msg.sender == owner || getApproved(tokenId) == msg.sender || isApprovedForAll(owner, msg.sender), "ERC721: not approved");
        require(owner == from, "ERC721: wrong owner");
        require(to != address(0), "ERC721: transfer to zero");
        _tokenApprovals[tokenId] = address(0);
        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;
        emit Transfer(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId) public override {
        transferFrom(from, to, tokenId);
    }

    function mint() public returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _balances[msg.sender] += 1;
        _owners[tokenId] = msg.sender;
        emit Transfer(address(0), msg.sender, tokenId);
        return tokenId;
    }
}
'''

print("--- Implementation ERC-721 : SimpleNFT ---")
nft, _ = compile_and_deploy(w3, ERC721_FULL_SOURCE, deployer)
user1 = w3.eth.accounts[1]
user2 = w3.eth.accounts[2]

# Mint 3 NFTs
nft.functions.mint().transact({'from': deployer})
nft.functions.mint().transact({'from': deployer})
nft.functions.mint().transact({'from': user1})
print(f"  deployer balance = {nft.functions.balanceOf(deployer).call()} NFTs")
print(f"  user1 balance    = {nft.functions.balanceOf(user1).call()} NFTs")
print(f"  ownerOf(1) = {nft.functions.ownerOf(1).call()[:10]}... (deployer)")
print(f"  ownerOf(3) = {nft.functions.ownerOf(3).call()[:10]}... (user1)")

# Transfer tokenId 1 deployer → user2
nft.functions.transferFrom(deployer, user2, 1).transact({'from': deployer})
print(f"  Apres transferFrom(deployer, user2, 1) : ownerOf(1) = {nft.functions.ownerOf(1).call()[:10]}... (user2)")

# Approve user2 to transfer tokenId 2
nft.functions.approve(user2, 2).transact({'from': deployer})
nft.functions.transferFrom(deployer, user2, 2).transact({'from': user2})
print(f"  Apres approve(user2) + transferFrom(2) : ownerOf(2) = {nft.functions.ownerOf(2).call()[:10]}... (user2)")
print(f"  user2 balance = {nft.functions.balanceOf(user2).call()} NFTs")
--- Implementation ERC-721 : SimpleNFT ---
Deploye: SimpleNFT a 0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0
  deployer balance = 2 NFTs
  user1 balance    = 1 NFTs
  ownerOf(1) = 0xf39Fd6e5... (deployer)
  ownerOf(3) = 0x70997970... (user1)
  Apres transferFrom(deployer, user2, 1) : ownerOf(1) = 0x3C44CdDd... (user2)
  Apres approve(user2) + transferFrom(2) : ownerOf(2) = 0x3C44CdDd... (user2)
  user2 balance = 2 NFTs

Interpretation : Mécanismes de propriete et approbation NFT

Résultat obtenu : Le cycle complet mint, transfer et approve fonctionne sur le NFT deploye.

Opération Fonction appelee Etat resultant
Mint (deployer) mint() tokenId 1, 2 appartiennent au deployer
Mint (user1) mint() tokenId 3 appartient a user1
Transfert direct transferFrom(deployer, user2, 1) tokenId 1 passe a user2
Approve + transfert approve(user2, 2) puis transferFrom user2 recupere tokenId 2 pour le deployer

Points cles : - Chaque NFT a un proprietaire unique (ownerOf retourne une seule adresse) - approve() autorise un tiers a transferer un token spécifique - setApprovalForAll() autorise un opérateur a gerer tous les tokens d’un proprietaire - Le require dans transferFrom verifie trois conditions : etre proprietaire, avoir une approbation individuelle, ou avoir une approbation globale


3. Standard ERC-1155 (Multi-Token)

ERC-1155 permet de gerer plusieurs types de tokens dans un seul contrat.

Transition : vers le multi-token

ERC-20 et ERC-721 couvrent respectivement les actifs fongibles et non-fongibles, mais chacun necessite un contrat dedie. Dans un jeu video par exemple, il faudrait un contrat pour l’or (fongible), un pour chaque objet unique (NFT), et un pour les ressources empilables comme les potions. Le standard ERC-1155 resout cette fragmentation en regroupant tous les types de tokens dans un seul contrat.

# Apercu ERC-1155
ERC1155_OVERVIEW = '''
interface IERC1155 {
    event TransferSingle(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 id,
        uint256 value
    );
    event TransferBatch(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256[] ids,
        uint256[] values
    );

    function balanceOf(address account, uint256 id) 
        external view returns (uint256);
    
    function balanceOfBatch(
        address[] calldata accounts,
        uint256[] calldata ids
    ) external view returns (uint256[] memory);
    
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) external;
    
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) external;
}

// Avantages ERC-1155:
// - Un seul contrat pour FT et NFT
// - Transferts batch (gas efficient)
// - Idem pour mint/burn batch
// - Utilise pour les jeux (items, ressources)
'''

print("Apercu ERC-1155 (Multi-Token):")
print(ERC1155_OVERVIEW)
Apercu ERC-1155 (Multi-Token):

interface IERC1155 {
    event TransferSingle(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 id,
        uint256 value
    );
    event TransferBatch(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256[] ids,
        uint256[] values
    );

    function balanceOf(address account, uint256 id) 
        external view returns (uint256);

    function balanceOfBatch(
        address[] calldata accounts,
        uint256[] calldata ids
    ) external view returns (uint256[] memory);

    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) external;

    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) external;
}

// Avantages ERC-1155:
// - Un seul contrat pour FT et NFT
// - Transferts batch (gas efficient)
// - Idem pour mint/burn batch
// - Utilise pour les jeux (items, ressources)

Interpretation : Comparaison des trois standards

Résultat obtenu : ERC-1155 unifie les capacites des standards ERC-20 et ERC-721 dans un seul contrat.

Critere ERC-20 ERC-721 ERC-1155
Type Fongible uniquement Non-fongible uniquement Les deux
Transfert batch Non Non Oui (safeBatchTransferFrom)
Consultation solde balanceOf(addr) balanceOf(addr) balanceOf(addr, id)
Contrats necessaires 1 par token 1 par collection 1 pour tous
Gas (transfert) ~50 000 ~60 000 ~30 000 (batch)
Cas typique Monnaie, governance Art, collectibles Jeux video, DeFi

Note technique : ERC-1155 utilise safeTransferFrom avec un callback onERC1155Received pour empecher les transferts vers des contrats qui ne savent pas gerer les tokens (securite supplementaire par rapport a ERC-721).

7.1. Exercice 1 : Multi-Token (ERC-1155 simplifie)

Implementez un contrat SimpleMultiToken gerant plusieurs types de tokens dans un seul contrat, inspire du standard ERC-1155 vu ci-dessus.

Objectif : Comprendre comment un seul contrat peut gerer a la fois des tokens fongibles et non-fongibles via des identifiants.

Indice 1 (structure de données) : Contrairement a ERC-20 (un seul mapping address => uint256) et ERC-721 (uint256 => address), ERC-1155 utilise un mapping a deux dimensions tokenId => owner => amount. Pour balanceOfBatch, faites un require(ids.length == accounts.length) au debut, puis retournez un tableau dynamique de même longueur.

Indice 2 (événements et conformite) : Emettez un événement TransferSingle(operator, from, to, id, value) après chaque mutation de solde (mint, burn, transfer). Cet événement est ce que les indexeurs (OpenSea, etc.) écoutent pour mettre a jour les inventaires. Pour rester compatible ERC-1155, ne melangez jamais FT et NFT dans le même tokenId.

Indice 3 (securite et pieges) : Dans safeTransferFrom, verifiez from != address(0) et to != address(0) au debut (un mint doit utiliser from = address(0) explicitement). Attention au cas amount == 0 : convention ERC-1155 = accepter sans effet (emettre quand même l’événement). Pour les batch, faites toutes les verifications AVANT d’appliquer les mutations, sinon une transaction partiellement valide peut bloquer le batch entier.

L’implementation complete reste compacte (hors interface).

# Exercice 1 : Multi-Token (ERC-1155 simplifie)
# TODO etudiant : implementer un contrat SimpleMultiToken gerant plusieurs types de tokens
# Indice : utilisez mapping(uint256 => mapping(address => uint256)) pour les soldes
# Etape 1 : implementer mint(address to, uint256 id, uint256 amount)
# Etape 2 : implementer balanceOf(address account, uint256 id)
# Etape 3 : implementer balanceOfBatch(address[], uint256[])
# Etape 4 : implementer safeTransferFrom(address from, address to, uint256 id, uint256 amount)

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

contract SimpleMultiToken {
    // TODO etudiant : mapping des soldes (tokenId => owner => amount)
    // Indice : mapping(uint256 => mapping(address => uint256)) private _balances;

    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);

    function mint(address to, uint256 id, uint256 amount) public {
        // TODO etudiant : incrementer le solde du destinataire pour ce token ID
        // Indice : _balances[id][to] += amount
        // TODO etudiant : emettre l evenement TransferSingle
    }

    function balanceOf(address account, uint256 id) public view returns (uint256) {
        // TODO etudiant : retourner le solde de account pour le token id
        return 0;
    }

    function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
        public view returns (uint256[] memory)
    {
        // TODO etudiant : verifier que accounts.length == ids.length
        // TODO etudiant : retourner un tableau des soldes pour chaque paire (account, id)
        return new uint256[](0);
    }

    function safeTransferFrom(address from, address to, uint256 id, uint256 amount) public {
        // TODO etudiant : verifier que from a suffisamment de tokens
        // TODO etudiant : debiter from et crediter to
        // TODO etudiant : emettre TransferSingle
    }
}
'''

# TODO etudiant : deployer et tester le contrat
# multitoken, receipt = compile_and_deploy(w3, EXERCICE_MULTITOKEN, deployer)
# multitoken.functions.mint(deployer, 1, 100).transact({'from': deployer})
# print(multitoken.functions.balanceOf(deployer, 1).call())
print("Exercice a completer")
Exercice a completer

4. OpenZeppelin

OpenZeppelin fournit des implementations securisees et auditees.

Transition : vers les implementations de production

Implementer un token standard de zero (comme nous venons de le faire) est formateur, mais en production on utilise quasi systematiquement les contrats d’OpenZeppelin. Ces contrats sont audites par la communaute, testes sur des millions de transactions, et integrent des mécanismes de securite avances (reentrancy guards, pausable, access control). Voyons comment deployer des tokens equivalents avec OpenZeppelin.

# --- Compilation et deploiement via Foundry (supporte les imports OpenZeppelin) ---
import sys, os
sys.path.insert(0, os.path.abspath(".."))
from forge_helper import forge_compile_and_deploy

# =====================================================================
# ERC-20 : MyToken avec OpenZeppelin (ERC20 + ERC20Burnable + Ownable)
# =====================================================================
OPENZEPPELIN_ERC20 = """
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, ERC20Burnable, Ownable {
    uint8 private constant _decimals = 6;
    uint256 private constant INITIAL_SUPPLY = 1_000_000 * 10 ** _decimals;

    constructor() ERC20("My Token", "MTK") Ownable(msg.sender) {
        _mint(msg.sender, INITIAL_SUPPLY);
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function decimals() public pure override returns (uint8) {
        return _decimals;
    }
}
"""

token, receipt = forge_compile_and_deploy(w3, OPENZEPPELIN_ERC20, "MyToken", deployer)

# Lecture des metadata et du supply initial
print(f"  name     = {token.functions.name().call()}")
print(f"  symbol   = {token.functions.symbol().call()}")
print(f"  decimals = {token.functions.decimals().call()}")
total = token.functions.totalSupply().call()
print(f"  totalSupply = {total}  ({total // 10**6} MTK)")
print(f"  balanceOf(deployer) = {token.functions.balanceOf(deployer).call()}")

# Mint de 500 MTK supplementaires vers un second compte
recipient = w3.eth.accounts[1]
token.functions.mint(recipient, 500 * 10**6).transact({"from": deployer})
print(f"\nApres mint de 500 MTK vers {recipient[:10]}... :")
print(f"  totalSupply = {token.functions.totalSupply().call() // 10**6} MTK")
print(f"  balanceOf(recipient) = {token.functions.balanceOf(recipient).call() // 10**6} MTK")

# Transfer de 100 MTK du deployer vers le recipient
token.functions.transfer(recipient, 100 * 10**6).transact({"from": deployer})
print(f"\nApres transfer de 100 MTK deployer -> recipient :")
print(f"  balanceOf(deployer)  = {token.functions.balanceOf(deployer).call() // 10**6} MTK")
print(f"  balanceOf(recipient) = {token.functions.balanceOf(recipient).call() // 10**6} MTK")

# =====================================================================
# ERC-721 : MyNFT avec OpenZeppelin (ERC721 + ERC721URIStorage + Ownable)
# =====================================================================
OPENZEPPELIN_NFT = """
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyNFT is ERC721, ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;

    constructor()
        ERC721("My NFT", "MNFT")
        Ownable(msg.sender)
    {}

    function mint(string memory uri) public returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(msg.sender, tokenId);
        _setTokenURI(tokenId, uri);
        return tokenId;
    }

    // Overrides requis par Solidity (linearisation)
    function tokenURI(uint256 tokenId)
        public view override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public view override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
"""

print("\n" + "=" * 60)
nft, receipt_nft = forge_compile_and_deploy(w3, OPENZEPPELIN_NFT, "MyNFT", deployer)

# Mint de 2 NFTs
tx1 = nft.functions.mint("ipfs://QmExemple1/metadata.json").transact({"from": deployer})
w3.eth.wait_for_transaction_receipt(tx1)
tx2 = nft.functions.mint("ipfs://QmExemple2/metadata.json").transact({"from": deployer})
w3.eth.wait_for_transaction_receipt(tx2)

print(f"  name   = {nft.functions.name().call()}")
print(f"  symbol = {nft.functions.symbol().call()}")
print(f"  ownerOf(0) = {nft.functions.ownerOf(0).call()[:10]}...")
print(f"  ownerOf(1) = {nft.functions.ownerOf(1).call()[:10]}...")
print(f"  tokenURI(0) = {nft.functions.tokenURI(0).call()}")
print(f"  tokenURI(1) = {nft.functions.tokenURI(1).call()}")
print(f"  balanceOf(deployer) = {nft.functions.balanceOf(deployer).call()} NFTs")
Deploye: MyToken a 0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE
  name     = My Token
  symbol   = MTK
  decimals = 6
  totalSupply = 1000000000000  (1000000 MTK)
  balanceOf(deployer) = 1000000000000

Apres mint de 500 MTK vers 0x70997970... :
  totalSupply = 1000500 MTK
  balanceOf(recipient) = 500 MTK

Apres transfer de 100 MTK deployer -> recipient :
  balanceOf(deployer)  = 999900 MTK
  balanceOf(recipient) = 600 MTK

============================================================
Deploye: MyNFT a 0xc6e7DF5E7b4f2A278906862b61205850344D4e7d
  name   = My NFT
  symbol = MNFT
  ownerOf(0) = 0xf39Fd6e5...
  ownerOf(1) = 0xf39Fd6e5...
  tokenURI(0) = ipfs://QmExemple1/metadata.json
  tokenURI(1) = ipfs://QmExemple2/metadata.json
  balanceOf(deployer) = 2 NFTs

Interpretation : Avantages d’OpenZeppelin

Résultat obtenu : Les deux contrats OpenZeppelin (MyToken et MyNFT) se deploient et fonctionnent correctement avec des fonctionnalites avancees.

Fonctionnalite Implementation manuelle OpenZeppelin
Mint contrôle Non implemente onlyOwner + _mint()
Burn Non implemente ERC20Burnable herite
URI metadata Non implemente ERC721URIStorage + IPFS
Decimals Fixe a 18 Personnalisable (ici 6)
Securite A verifier manuellement Audite et teste

Points cles : - OpenZeppelin ajoute des garde-fous (onlyOwner) sans code supplementaire - L’heritage multiple (is ERC20, ERC20Burnable, Ownable) permet de composer des fonctionnalites - Les overrides Solidity (override(ERC721, ERC721URIStorage)) resolvent les conflits d’heritage diamond


5. Exemples guidés

7.2. Exercice 2 : Token avec plafond (ERC-20 capped)

Créez un contrat CappedToken qui herite de IERC20 et impose un supply maximum fixe a la construction (cap).

Objectif : Comprendre comment limiter la quantite totale de tokens qui peuvent etre crees via mint, tout en preservant l’interface ERC-20 standard.

Indice 1 (variable immutable) : Le cap doit etre declare uint256 public immutable cap; car il est fixe a la construction et ne change jamais. Dans le constructor, prenez _cap en argument, verifiez _cap > 0 avec require, puis assignez cap = _cap. Un immutable est plus gas-efficient qu’un storage variable pour les valeurs constantes après deploy.

Indice 2 (logique de mint) : Le coeur du mécanisme est require(_totalSupply + amount <= cap, "CappedToken: cap exceeded"); AU DEBUT de mint(). Ensuite, _totalSupply += amount; _balances[to] += amount; emit Transfer(address(0), to, amount);. La verification overflow est importante : _totalSupply + amount peut overflow si amount est proche de 2^256, mais en pratique avec un cap raisonnable, ce n’est pas un problème. Pour une securite maximale, utilisez OpenZeppelin Math.max ou unchecked block.

Indice 3 (fonctions a reutiliser) : Les fonctions transfer, approve, transferFrom, allowance, balanceOf, totalSupply sont identiques a SimpleERC20 vu precedemment (section 1). Vous pouvez copier-coller ces implementations telles quelles – le cap n’affecte que mint. Pensez a emettre event Transfer(address(0), to, amount) dans mint (convention ERC-20 pour signaler une creation) et event Transfer(from, address(0), amount) si vous implementez un burn optionnel.

Une fois deploye, testez : capped.functions.mint(addr1, cap).transact(...) doit reussir, mais capped.functions.mint(addr2, 1).transact(...) après doit echouer avec “cap exceeded”.

# Exercice 2 : Token avec plafond
EXERCICE_CAPPED = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract CappedToken is IERC20 {
    string public constant name = "Capped Token";
    string public constant symbol = "CAP";
    uint8 public constant decimals = 18;
    
    uint256 public immutable cap;        // Maximum supply
    uint256 private _totalSupply;
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    constructor(uint256 _cap) {
        // TODO: Verifier que _cap est positif
        // TODO: Stocker le cap
    }

    function mint(address to, uint256 amount) public {
        // TODO: Verifier que _totalSupply + amount ne depasse pas le cap
        // TODO: Incrementer _totalSupply
        // TODO: Crediter le solde du destinataire
        // TODO: Emettre l evenement Transfer (from address(0))
    }

    // TODO: Implementer les fonctions restantes de l interface ERC-20
    // (totalSupply, balanceOf, transfer, approve, transferFrom, allowance)
}
'''

# Deployer (ajuster les arguments du constructeur si necessaire)
# cappedtoken, receipt = compile_and_deploy(w3, EXERCICE_CAPPED, deployer)
# print(f"Deploye a: {cappedtoken.address}")
print("Exercice a completer")
Exercice a completer

7.3. Exercice 3 : NFT avec enumeration (ERC-721 enumerable)

Créez un contrat EnumerableNFT qui permet d’enumerer tous les tokens existants et les tokens d’un proprietaire donne, en plus des opérations ERC-721 de base.

Objectif : Comprendre comment maintenir des index secondaires (listes inversees) en parallele du mapping _owners, pour permettre des fonctions comme tokenByIndex(i) ou tokenOfOwnerByIndex(owner, i) qui sont impossibles avec uniquement mapping(uint256 => address).

Indice 1 (les 3 structures paralleles) : Pour enumerer, vous devez maintenir trois structures en parallele : _ownedTokens[owner] (liste des tokenIds possedes), _ownedTokensIndex[tokenId] (position inverse dans la liste du proprietaire), _allTokens (liste globale), et _allTokensIndex[tokenId] (position inverse dans la liste globale). Chaque structure consomme du storage mais permet un acces en O(1) aux fonctions d’enumeration. Le pattern “swap-and-pop” est preferable a delete array[i] pour eviter de laisser des trous.

**Indice 2 (fonction _mint atomique)** : Dans _mint(to, tokenId), faites TOUTES les opérations dans l’ordre suivant : (1) _owners[tokenId] = to; (2) _ownedTokensIndex[tokenId] = _ownedTokens[to].length; (3) _ownedTokens[to].push(tokenId); (4) _allTokensIndex[tokenId] = _allTokens.length; (5) _allTokens.push(tokenId); Si vous oubliez une étape, les fonctions d’enumeration retourneront des valeurs incorrectes ou revert. L’ordre des push AVANT l’assignation de l’index est crucial pour la coherence.

Indice 3 (transfert et burn optionnels) : Pour implementer _transfer(from, to, tokenId) (et _burn(tokenId)) avec enumeration, vous devez mettre a jour les 4 structures : retirer le token de la liste de from (swap avec le dernier + pop + maj de l’index inverse), ajouter a la liste de to (push + maj de l’index), et gerer _allTokens si c’est un burn. Le swap-and-pop preserve l’ordre des autres éléments. Cette complexite supplementaire est la raison pour laquelle ERC-721 standard n’inclut PAS l’enumeration par defaut (optionnel via extension ERC721Enumerable d’OpenZeppelin).

Test : deployez, mint 5 NFTs avec _mint(deployer, i) pour i in [1..5], puis verifiez totalSupply() == 5, tokenByIndex(0) == 1, tokenOfOwnerByIndex(deployer, 0) == 1.

# Exercice 3 : NFT avec enumeration
EXERCICE_ENUMERABLE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract EnumerableNFT {
    mapping(uint256 => address) private _owners;
    mapping(address => uint256[]) private _ownedTokens;
    mapping(uint256 => uint256) private _ownedTokensIndex;
    uint256[] private _allTokens;
    mapping(uint256 => uint256) private _allTokensIndex;

    function totalSupply() public view returns (uint256) {
        // TODO: Retourner le nombre total de tokens
    }

    function tokenByIndex(uint256 index) public view returns (uint256) {
        // TODO: Verifier que l index est valide
        // TODO: Retourner le tokenId a cet index global
    }

    function tokenOfOwnerByIndex(address owner, uint256 index) 
        public view returns (uint256) 
    {
        // TODO: Verifier que l index est valide pour ce proprietaire
        // TODO: Retourner le tokenId a cet index pour ce proprietaire
    }

    function _mint(address to, uint256 tokenId) internal {
        // TODO: Assigner le proprietaire dans _owners
        // TODO: Ajouter le token a la liste du proprietaire (_ownedTokens)
        // TODO: Enregistrer l index du token (_ownedTokensIndex)
        // TODO: Ajouter le token a la liste globale (_allTokens)
        // TODO: Enregistrer l index global (_allTokensIndex)
    }
}
'''

# Compilation et deploiement reel sur anvil
enumerablenft, receipt = compile_and_deploy(w3, EXERCICE_ENUMERABLE, deployer)
Deploye: EnumerableNFT a 0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44

6. Resume

Standard Type Cas d’usage
ERC-20 Fongible Cryptomonnaies, governance tokens
ERC-721 Non-fongible NFTs, assets uniques
ERC-1155 Multi Jeux, items, FT + NFT

Fonctions cles ERC-20

  • transfer() : Envoyer des tokens
  • approve() : Autoriser un spender
  • transferFrom() : Transfer via allowance

Fonctions cles ERC-721

  • ownerOf() : Proprietaire d’un NFT
  • safeTransferFrom() : Transfer securise
  • approve() : Autoriser un transfert

Notebook suivant : SC-08-DeFi-Primitives-Python

Resume et perspectives

Ce notebook a couvert les trois standards majeurs de tokens Ethereum, de l’implementation manuelle a l’utilisation de bibliotheques auditees. Le standard ERC-20 a ete implemente de zero avec le mécanisme d’allowance (approve + transferFrom), fondement des DEX et protocoles DeFi. Le standard ERC-721 a demontre la gestion de propriete unique avec les approbations individuelles (approve) et globales (setApprovalForAll). Le standard ERC-1155 a ete presente comme unification des deux approches, avec ses transferts batch et son efficacite en gas.

L’utilisation des contrats OpenZeppelin (ERC20 + ERC20Burnable + Ownable, ERC721 + ERC721URIStorage) a illustre les bonnes pratiques de production : heritage multiple pour composer des fonctionnalites, securite auditee par la communaute, et personnalisation via des overrides Solidity. La différence entre implementation pedagogique (SimpleERC20, SimpleNFT) et implementation de production (MyToken, MyNFT) reside dans les garde-fous integres (access control, burn, URI storage).

Le notebook suivant, SC-08-DeFi-Primitives-Python, utilise ces standards comme base pour construire des primitives de finance decentralisee : Automated Market Makers avec la formule x*y=k, et protocoles de lending avec collateralisation et health factor.


<< Précédent : Errors & Events | Retour au sommaire | Suivant : DeFi Primitives >>

Retour au sommet