Introduction
En 2026, Solidity 0.8.26 domine le développement de smart contracts sur Ethereum et ses layer-2 comme Base ou Optimism, grâce à ses vérifications intégrées contre les overflows et ses optimisations gas post-Dencun upgrade. Un token ERC-20 reste le pilier de la DeFi : stablecoins, governance tokens, LP rewards en dépendent. Ce tutoriel intermédiaire vous guide pour implémenter un ERC-20 from scratch avec Hardhat, sans OpenZeppelin initialement, pour maîtriser les mécanismes internes comme les mappings pour balances, events pour indexation, et modifiers pour accès contrôle.
Pourquoi from scratch ? Comprendre les pièges (reentrancy résiduelle, front-running sur approve) avant les libs. Nous construirons un token avec mint contrôlé, transfer/approve sécurisés, tests unitaires, et déploiement sur Sepolia testnet. À la fin : un contrat live vérifiable sur Etherscan, prêt pour audits. Durée : 30 min setup + dev. Compétences acquises : gas profiling, NatSpec docs, script deploy. Parfait pour transition web2 → web3.
Prérequis
- Node.js 20+ et npm
- Compte MetaMask avec ETH Sepolia (faucet Alchemy)
- VS Code avec extensions Solidity + Hardhat
- Bases blockchain : tx fees, blocks, ABI
- Connaissances JS pour scripts deploy/tests
Initialiser le projet Hardhat
mkdir mon-token-erc20 && cd mon-token-erc20
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox@latest
npm install @openzeppelin/contracts
npx hardhat init
# Sélectionnez : Create a JavaScript project
# Yes pour .gitignore, install deps
npx hardhat compile
# Vérifiez : Compiled successfullyCe script crée un projet Hardhat prêt pour Solidity, avec toolbox pour compile, test (Mocha/Chai), deploy et verify. L'installation d'OpenZeppelin est optionnelle ici mais utile pour comparer. Évitez les prompts en automatisant ; post-init, folders contracts/, scripts/, test/ sont générés. Erreur courante : Node <20 cause des échecs npm.
Comprendre la structure Hardhat
Hardhat simule une blockchain locale (fork mainnet possible). contracts/ : vos .sol ; scripts/ : deploy JS ; test/ : tests unitaires ; hardhat.config.js : networks, solidity compiler version. Utilisez npx hardhat console pour interagir live. Pour 2026, activez viaIR pour gas optim (dans config). Prochaine étape : configurer pour Sepolia.
Configurer hardhat.config.js pour testnets
require('@nomicfoundation/hardhat-toolbox');
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: {
version: '0.8.26',
settings: {
optimizer: {
enabled: true,
runs: 200
},
viaIR: true
}
},
networks: {
hardhat: {},
sepolia: {
url: 'https://sepolia.infura.io/v3/VOTRE_INFURA_KEY',
accounts: ['0xVOTRE_PRIVATE_KEY_SANS_0x']
}
},
etherscan: {
apiKey: {
sepolia: 'VOTRE_ETHERSCAN_API_KEY'
}
}
};Cette config cible Solidity 0.8.26 avec optimizer IR pour ~10-20% gas savings en 2026. Remplacez clés Infura/Etherscan (gratuits) et private key MetaMask (export sans 0x). viaIR active nouvel IR pipeline pour bugs fixes. Testez avec npx hardhat node pour localhost chain.
Créer un contrat Token basique
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract MonToken {
string public name = 'Mon Token';
string public symbol = 'MTK';
uint8 public decimals = 18;
uint256 public totalSupply;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * 10 ** decimals;
}
function balanceOf(address _owner) public view returns (uint256 balance) {
return 0; // À implémenter
}
}Contrat skeleton ERC-20 : metadata, totalSupply minté au deploy, event Transfer standard. Constructor scale supply par decimals (18 comme ETH). view pour gas-free reads. Compilez : npx hardhat compile. Piège : Oublier SPDX cause warnings Remix.
Ajouter les balances et transfer
Mappings stockent balances efficacement (O(1) access, slots storage optimisés). Event Transfer indexé pour TheGraph/Subgraph queries. Prochain : implémenter transfer sécurisé sans reentrancy (checks-effects-interactions).
Implémenter balanceOf et transfer
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract MonToken {
string public name = 'Mon Token';
string public symbol = 'MTK';
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * 10 ** decimals;
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value, 'Solde insuffisant');
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
}Mapping balanceOf tracke soldes ; mint initial à deployer. Transfer suit CEI pattern : require avant modify, emit après. SafeMath implicite en 0.8+. Test local : npx hardhat console --network localhost. Piège : msg.sender pas indexé pour privacy.
Ajouter approve et transferFrom
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract MonToken {
string public name = 'Mon Token';
string public symbol = 'MTK';
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(uint256 _initialSupply) {
totalSupply = _initialSupply * 10 ** decimals;
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value, 'Solde insuffisant');
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
function approve(address _spender, uint256 _value) public returns (bool success) {
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(balanceOf[_from] >= _value, 'Solde insuffisant');
require(allowance[_from][msg.sender] >= _value, 'Allowance insuffisant');
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
}Nested mapping pour allowances ; approve set limite, transferFrom spend. Events pour DEX comme Uniswap V2. CEI protège contre front-run (mais use permit ERC-2612 prod). Full ERC-20 compliant maintenant. Gas : ~50k deploy.
Script de déploiement
Scripts JS utilisent ethers.js v6 (2026 standard). Lancez npx hardhat run scripts/deploy.js --network sepolia post-config clés. Vérifiez : npx hardhat verify --network sepolia ADRESSE_CONTRAT 1000000.
Script deploy.js
const hre = require('hardhat');
async function main() {
const initialSupply = '1000000';
const MonToken = await hre.ethers.getContractFactory('MonToken');
const monToken = await MonToken.deploy(initialSupply);
await monToken.waitForDeployment();
console.log('MonToken déployé à :', await monToken.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Script deploy 1M tokens (scaled 18 decimals). waitForDeployment() pour tx confirm. Log adresse pour verify/add MetaMask. Sur Sepolia : ~0.01 ETH, 2min blocktime. Ajoutez try/catch prod.
Test unitaire complet
const { expect } = require('chai');
const { ethers } = require('hardhat');
describe('MonToken', function () {
it('Devrait mint initial supply au owner', async function () {
const MonToken = await ethers.getContractFactory('MonToken');
const monToken = await MonToken.deploy('1000');
await monToken.waitForDeployment();
const owner = await ethers.getSigners()[0];
expect(await monToken.balanceOf(owner.address)).to.equal(1000n * 10n ** 18n);
});
it('Devrait transfer tokens', async function () {
const [owner, addr1] = await ethers.getSigners();
const MonToken = await ethers.getContractFactory('MonToken');
const monToken = await MonToken.deploy('1000');
await monToken.waitForDeployment();
await monToken.connect(owner).transfer(addr1.address, 100n * 10n ** 18n);
expect(await monToken.balanceOf(addr1.address)).to.equal(100n * 10n ** 18n);
});
});Tests Chai : check mint, transfer. npx hardhat test run. BigInt pour uint256. Couvre 80% edge cases ; ajoutez approveFrom. Green tests = prêt deploy.
Bonnes pratiques
- NatSpec : Ajoutez /// @notice, @dev pour docs auto-générées.
- OpenZeppelin : Heritez ERC20 en prod pour pausability, upgrades.
- Gas optim : Pack storage slots (name+symbol adjacents), immutable vars.
- Sécurité : Use modifiers onlyOwner, Slither audits.
- Vérif : Etherscan + Sourcify post-deploy.
Erreurs courantes à éviter
- Approve race : Set approve(0) puis nouveau montant anti-front-run.
- Decimals mismatch : Toujours *10**decimals en JS deploy.
- Storage collision : Mappings nested gas-heavy ; profilez avec
npx hardhat run --gas. - No events : Block explorers/TheGraph cassés sans Transfer/Approval.
Pour aller plus loin
Étudiez OpenZeppelin Contracts pour ERC20Permit, extensions. Migrez vers Foundry pour faster tests. Formations avancées : Learni Group Blockchain. Lisez Solidity Blog pour 0.8.27 previews, explorez Vyper alternative.