# Tutoriel ERC-20 — Créez Votre Premier Token en Solidity
Créer son propre token ERC-20 est une compétence fondamentale pour tout développeur Solidity. Que vous construisiez un token de gouvernance, un système de récompenses, ou que vous appreniez simplement les bases, comprendre ERC-20 est essentiel.
Dans ce tutoriel, nous allons construire un token ERC-20 from scratch, le tester rigoureusement, et le déployer sur un testnet. À la fin, vous aurez un contrat de token production-ready.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Connaissances de base en Solidity (variables, fonctions, mappings)
- Un wallet (MetaMask recommandé)
- Du testnet ETH (Sepolia recommandé)
- Foundry installé (
curl -L https://foundry.paradigm.xyz | bash)
Qu'est-ce qu'ERC-20 ?
ERC-20 est le standard de token le plus largement adopté sur Ethereum. Il définit une interface commune que tous les tokens doivent implémenter, assurant la compatibilité avec les wallets, exchanges et dApps.
Le standard spécifie 6 fonctions principales et 2 événements :
Fonctions :
totalSupply(): Supply totale du token
balanceOf(address account): Obtenir le solde d'une adresse
transfer(address to, uint256 amount): Transférer des tokens
approve(address spender, uint256 amount): Approuver une dépense
allowance(address owner, address spender): Vérifier l'allowance
transferFrom(address from, address to, uint256 amount): Transférer au nom de quelqu'un
Événements :
Transfer(address indexed from, address indexed to, uint256 value)
Approval(address indexed owner, address indexed spender, uint256 value)
Construire ERC-20 From Scratch
Créons un token ERC-20 complet. Voici l'implémentation :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract MyToken {
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
totalSupply = _initialSupply * 10**decimals;
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address to, uint256 amount) public returns (bool) {
require(to != address(0), "Transfer to zero address");
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
require(spender != address(0), "Approve to zero address");
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(from != address(0), "Transfer from zero address");
require(to != address(0), "Transfer to zero address");
require(balanceOf[from] >= amount, "Insufficient balance");
require(allowance[from][msg.sender] >= amount, "Allowance exceeded");
balanceOf[from] -= amount;
balanceOf[to] += amount;
allowance[from][msg.sender] -= amount;
emit Transfer(from, to, amount);
return true;
}
}
Détails d'Implémentation
Decimals
Les tokens utilisent 18 décimales par convention (comme ETH). Cela signifie :
- 1 token = 1 × 10¹⁸ unités de base
- 0.5 token = 500000000000000000 unités
Transfer vs TransferFrom
transfer: vous transférez vos propres tokens
transferFrom: vous transférez les tokens de quelqu'un d'autre (après approbation)
Le pattern approve/transferFrom est crucial pour les DEX et protocoles DeFi.
Utiliser OpenZeppelin
En production, utilisez les implémentations battle-tested d'OpenZeppelin :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") Ownable(msg.sender) {
_mint(msg.sender, initialSupply * 10**decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
Tests avec Foundry
// test/MyToken.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
address alice = address(0x1);
address bob = address(0x2);
function setUp() public {
token = new MyToken("MyToken", "MTK", 1000000);
}
function testInitialSupply() public {
assertEq(token.totalSupply(), 1000000 * 10**18);
assertEq(token.balanceOf(address(this)), 1000000 * 10**18);
}
function testTransfer() public {
token.transfer(alice, 100 * 10**18);
assertEq(token.balanceOf(alice), 100 * 10**18);
}
function testApproveAndTransferFrom() public {
token.transfer(alice, 1000 * 10**18);
vm.prank(alice);
token.approve(bob, 500 * 10**18);
vm.prank(bob);
token.transferFrom(alice, bob, 300 * 10**18);
assertEq(token.balanceOf(bob), 300 * 10**18);
assertEq(token.allowance(alice, bob), 200 * 10**18);
}
function testFuzz_transfer(address to, uint256 amount) public {
vm.assume(to != address(0));
vm.assume(amount <= token.balanceOf(address(this)));
token.transfer(to, amount);
assertEq(token.balanceOf(to), amount);
}
}
Lancez les tests :
forge test -vv
Déploiement
1. Script de Déploiement
// script/Deploy.s.sol
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import "../src/MyToken.sol";
contract DeployScript is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
MyToken token = new MyToken("MyToken", "MTK", 1000000);
console.log("Token deployed at:", address(token));
vm.stopBroadcast();
}
}
2. Déployer sur Sepolia
forge script script/Deploy.s.sol \
--rpc-url https://sepolia.infura.io/v3/YOUR_KEY \
--broadcast \
--verify
3. Vérifier sur Etherscan
forge verify-contract \
--chain-id 11155111 \
--compiler-version v0.8.20 \
CONTRACT_ADDRESS \
src/MyToken.sol:MyToken
Fonctionnalités Avancées
Minting/Burning
function mint(address to, uint256 amount) public onlyOwner {
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
function burn(uint256 amount) public {
require(balanceOf[msg.sender] >= amount);
totalSupply -= amount;
balanceOf[msg.sender] -= amount;
emit Transfer(msg.sender, address(0), amount);
}
Snapshots (pour governance)
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
contract MyToken is ERC20, ERC20Snapshot, Ownable {
function snapshot() public onlyOwner returns (uint256) {
return _snapshot();
}
}
Pausable (circuit breaker)
import "@openzeppelin/contracts/security/Pausable.sol";
contract MyToken is ERC20, Pausable, Ownable {
function pause() public onlyOwner {
_pause();
}
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal whenNotPaused override
{
super._beforeTokenTransfer(from, to, amount);
}
}
Sécurité : Checklist
Avant de déployer en production :
- [ ] Tests unitaires (coverage > 95%)
- [ ] Fuzz testing sur les fonctions critiques
- [ ] Analyse avec Slither (
slither .)
- [ ] Audit professionnel (si high value)
- [ ] Test sur testnet pendant 1+ semaine
- [ ] Vérification du contrat sur Etherscan
- [ ] Renonciation au ownership si token immutable
- [ ] Documentation claire
Cas d'Usage Réels
Prochaines Étapes
Maintenant que vous maîtrisez ERC-20 :
Conclusion
Vous savez maintenant créer un token ERC-20 professionnel. Les tokens sont la base de nombreux protocoles DeFi, NFT marketplaces, DAOs et applications blockchain.
Pratiquez sur Solingo avec nos 100+ exercices ERC-20, du token basique au système de gouvernance avancé.
---
Ressources complémentaires :