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 Web3exceptImportError:print("Installation requise : pip install web3")raisetry:import solcxexceptImportError:print("Installation requise : pip install py-solc-x")raiseSOLC_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.""" 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 completeERC20_INTERFACE ='''// SPDX-License-Identifier: MITpragma solidity ^0.8.28;/// @title ERC-20 Token Standardinterface 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 fonctionsprint("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.
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-721ERC721_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-721ERC721_FULL_SOURCE ='''// SPDX-License-Identifier: MITpragma 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 NFTsnft.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 → user2nft.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 2nft.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")
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-1155ERC1155_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).
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: MITpragma 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, ossys.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: MITpragma 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 initialprint(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 compterecipient = 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 recipienttoken.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: MITpragma 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 NFTstx1 = 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 plafondEXERCICE_CAPPED ='''// SPDX-License-Identifier: MITpragma 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 enumerationEXERCICE_ENUMERABLE ='''// SPDX-License-Identifier: MITpragma 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 anvilenumerablenft, receipt = compile_and_deploy(w3, EXERCICE_ENUMERABLE, deployer)
Deploye: EnumerableNFT a 0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44
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.