Créer un token de staking simple


Le staking est devenu l'un des piliers fondamentaux de l'écosystème blockchain moderne. Loin d'être simplement une méthode pour générer des rendements passifs, le staking représente une évolution fondamentale dans la manière dont les réseaux décentralisés sécurisent leurs opérations et incitent à la participation communautaire.
Dans cet article, nous allons plonger dans les méandres de Solidity pour développer un contrat intelligent de staking simple mais fonctionnel. Nous verrons comment créer un système où les utilisateurs peuvent verrouiller leurs tokens et recevoir des récompenses proportionnelles à leur contribution. Mais avant de nous lancer dans le code, prenons un moment pour comprendre ce qu'est réellement le staking.
Qu'est-ce que le staking ?
Le staking est un processus par lequel les détenteurs de cryptomonnaies immobilisent leurs actifs pour participer à la maintenance du réseau blockchain. En échange de cette immobilisation, ils reçoivent des récompenses. C'est un peu comme si vous déposiez de l'argent sur un compte d'épargne bloqué, mais avec des mécanismes et des implications bien plus complexes.
Dans notre contexte de développement, nous allons créer un système simple où :
Les utilisateurs peuvent déposer des tokens dans notre contrat
Ces tokens sont verrouillés pendant une période déterminée
Les utilisateurs reçoivent des récompenses basées sur la quantité de tokens déposés et la durée du staking
Prérequis
Pour suivre ce tutoriel, vous aurez besoin de :
Une compréhension de base de Solidity et des contrats intelligents
Remix IDE ou un environnement de développement Solidity similaire
Des connaissances de base sur les ERC-20
Une familiarité avec les concepts de DeFi
Structure du contrat
Notre système de staking comprendra deux contrats principaux :
Un token ERC-20 standard que les utilisateurs pourront staker
Un contrat de staking qui gère les dépôts, les retraits et les récompenses
Commençons par développer notre token ERC-20.
Le token ERC-20
Voici un exemple simple d'un token ERC-20 que nous utiliserons pour notre système de staking :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract StakingToken is ERC20, Ownable {
constructor(uint256 initialSupply) ERC20("Staker Token", "STK") Ownable(msg.sender) {
_mint(msg.sender, initialSupply * 10 ** decimals());
}
// Fonction pour frapper de nouveaux tokens (réservée au propriétaire)
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
Ce contrat est assez simple. Il crée un token ERC-20 standard avec :
Un nom : "Staker Token"
Un symbole : "STK"
Une offre initiale définie lors du déploiement
Une fonction de mint pour créer de nouveaux tokens (qui sera utilisée pour les récompenses)
Maintenant, passons à la partie la plus intéressante : le contrat de staking.
Le contrat de staking
Voici notre contrat de staking complet, que nous allons ensuite décomposer et expliquer en détail :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract StakingContract is ReentrancyGuard, Ownable {
using SafeERC20 for IERC20;
// Token à staker
IERC20 public stakingToken;
// Token de récompense (peut être le même)
IERC20 public rewardToken;
// Taux de récompense par bloc (en tokens)
uint256 public rewardRate;
// Dernier bloc où les récompenses ont été calculées
uint256 public lastUpdateBlock;
// Récompense accumulée par token
uint256 public rewardPerTokenStored;
// Suivi des récompenses par utilisateur
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
// Montant total staké
uint256 public totalStaked;
// Suivi du stake par utilisateur
mapping(address => uint256) public balances;
// Période de blocage (en blocs)
uint256 public lockPeriod;
// Suivi du dernier bloc de stake par utilisateur
mapping(address => uint256) public userLastStakeBlock;
// Événements
event Staked(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event RewardPaid(address indexed user, uint256 reward);
constructor(
address _stakingToken,
address _rewardToken,
uint256 _rewardRate,
uint256 _lockPeriod
) Ownable(msg.sender) {
stakingToken = IERC20(_stakingToken);
rewardToken = IERC20(_rewardToken);
rewardRate = _rewardRate;
lockPeriod = _lockPeriod;
lastUpdateBlock = block.number;
}
// Modifier pour mettre à jour les récompenses
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateBlock = block.number;
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
// Calcule la récompense par token
function rewardPerToken() public view returns (uint256) {
if (totalStaked == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
((block.number - lastUpdateBlock) * rewardRate * 1e18) / totalStaked
);
}
// Calcule les gains d'un utilisateur
function earned(address account) public view returns (uint256) {
return (
(balances[account] * (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18
) + rewards[account];
}
// Fonction de stake
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
require(amount > 0, "Impossible de staker 0");
totalStaked += amount;
balances[msg.sender] += amount;
userLastStakeBlock[msg.sender] = block.number;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
// Fonction de retrait
function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) {
require(amount > 0, "Impossible de retirer 0");
require(balances[msg.sender] >= amount, "Solde insuffisant");
require(
block.number >= userLastStakeBlock[msg.sender] + lockPeriod,
"Période de blocage non terminée"
);
totalStaked -= amount;
balances[msg.sender] -= amount;
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
// Récupérer les récompenses
function getReward() external nonReentrant updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardToken.safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
// Fonction d'urgence permettant au propriétaire de récupérer les tokens
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
IERC20(token).safeTransfer(owner(), amount);
}
// Mettre à jour le taux de récompense
function setRewardRate(uint256 _rewardRate) external onlyOwner updateReward(address(0)) {
rewardRate = _rewardRate;
}
// Modifier la période de blocage
function setLockPeriod(uint256 _lockPeriod) external onlyOwner {
lockPeriod = _lockPeriod;
}
}
Analyse détaillée du contrat de staking
Variables d'état importantes
// Token à staker
IERC20 public stakingToken;
// Token de récompense (peut être le même)
IERC20 public rewardToken;
// Taux de récompense par bloc (en tokens)
uint256 public rewardRate;
// Montant total staké
uint256 public totalStaked;
// Période de blocage (en blocs)
uint256 public lockPeriod;
Ces variables définissent les paramètres essentiels de notre système :
stakingToken
: le token que les utilisateurs vont déposerrewardToken
: le token qui sera distribué comme récompense (peut être identique au token de staking)rewardRate
: le taux auquel les récompenses sont distribuées par bloctotalStaked
: le montant total de tokens déposés dans le contratlockPeriod
: la durée pendant laquelle les tokens sont verrouillés
Le mécanisme de récompense
Le calcul des récompenses est l'une des parties les plus complexes d'un système de staking. Notre approche utilise un système de "récompense par token" qui s'accumule avec le temps :
// Calcule la récompense par token
function rewardPerToken() public view returns (uint256) {
if (totalStaked == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
((block.number - lastUpdateBlock) * rewardRate * 1e18) / totalStaked
);
}
// Calcule les gains d'un utilisateur
function earned(address account) public view returns (uint256) {
return (
(balances[account] * (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18
) + rewards[account];
}
Cette approche permet de :
Calculer précisément les récompenses dues à chaque utilisateur
Distribuer les récompenses proportionnellement au montant staké
Éviter d'avoir à mettre à jour les récompenses pour tous les utilisateurs à chaque bloc
Le modificateur updateReward
// Modifier pour mettre à jour les récompenses
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateBlock = block.number;
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
Ce modificateur est crucial. Il met à jour l'état des récompenses avant chaque action importante (stake, withdraw, getReward). Il permet de s'assurer que les récompenses sont correctement calculées et attribuées.
Les fonctions principales
stake
// Fonction de stake
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
require(amount > 0, "Impossible de staker 0");
totalStaked += amount;
balances[msg.sender] += amount;
userLastStakeBlock[msg.sender] = block.number;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
Cette fonction permet aux utilisateurs de déposer leurs tokens. Elle :
Met à jour les récompenses (via le modificateur)
Vérifie que le montant est valide
Met à jour les compteurs internes
Transfère les tokens au contrat
Émet un événement pour traçabilité
withdraw
// Fonction de retrait
function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) {
require(amount > 0, "Impossible de retirer 0");
require(balances[msg.sender] >= amount, "Solde insuffisant");
require(
block.number >= userLastStakeBlock[msg.sender] + lockPeriod,
"Période de blocage non terminée"
);
totalStaked -= amount;
balances[msg.sender] -= amount;
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
Cette fonction permet de récupérer les tokens déposés, mais seulement après la période de blocage. Elle vérifie que :
Le montant est valide
L'utilisateur a suffisamment de tokens déposés
La période de blocage est terminée
getReward
// Récupérer les récompenses
function getReward() external nonReentrant updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardToken.safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
Cette fonction permet aux utilisateurs de réclamer leurs récompenses accumulées, indépendamment de leurs tokens stakés.
Déploiement et interaction
Voici comment vous pourriez déployer et utiliser ces contrats dans un environnement réel :
Déployer d'abord le contrat
StakingToken
avec une offre initiale appropriéeUtiliser l'adresse de ce token pour déployer le contrat
StakingContract
Si vous souhaitez utiliser un token différent pour les récompenses, déployez-le également
Exemple de paramètres de déploiement :
_stakingToken
: l'adresse du token ERC-20 à staker_rewardToken
: peut être la même adresse que_stakingToken
_rewardRate
: par exemple 10 * 10^18 (10 tokens par bloc)_lockPeriod
: par exemple 40320 (environ 1 semaine à 15 secondes par bloc)
Cas d'usage réels
Protocole DeFi
Ce type de contrat est couramment utilisé dans les protocoles DeFi. Par exemple, imaginez un protocole de prêt décentralisé comme Aave ou Compound. Les utilisateurs peuvent staker les tokens de gouvernance du protocole pour :
Participer aux décisions de gouvernance
Recevoir une partie des frais générés par le protocole
Obtenir des avantages comme des taux préférentiels
J'ai personnellement travaillé sur un projet où ce mécanisme était utilisé pour incentiver la fourniture de liquidité. Les utilisateurs qui stakaient leurs tokens LP (Liquidity Provider) recevaient des tokens de gouvernance en récompense, créant ainsi un cercle vertueux.
Start-up en blockchain
Une start-up construisant une application décentralisée pourrait utiliser un système de staking pour :
Fidéliser ses premiers utilisateurs
Distribuer les tokens de manière équitable
Assurer un verrouillage progressif des tokens (vesting)
J'ai vu ce système mis en œuvre chez un client qui développait une marketplace NFT. Les utilisateurs stakant leurs tokens obtenaient des réductions sur les frais de transaction et des accès anticipés aux collections exclusives.
Meilleures pratiques et considérations de sécurité
Protection contre la réentrance
Notre contrat utilise le modificateur nonReentrant
de la bibliothèque OpenZeppelin pour prévenir les attaques par réentrance. C'est crucial car nous manipulons des tokens et des états financiers.
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
// ...
}
Utilisation de SafeERC20
Nous utilisons la bibliothèque SafeERC20
pour interagir avec les tokens ERC-20. Certains tokens non standard peuvent se comporter de manière inattendue, et cette bibliothèque offre une protection supplémentaire.
using SafeERC20 for IERC20;
// ...
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
Fonction d'urgence
Nous avons inclus une fonction d'urgence permettant au propriétaire de récupérer les tokens en cas de problème :
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
IERC20(token).safeTransfer(owner(), amount);
}
Cette fonction doit être utilisée avec précaution, mais peut être cruciale en cas de découverte d'une vulnérabilité.
Test des calculs de récompense
Les calculs de récompense sont complexes et sujets à des erreurs d'arrondi. Il est essentiel de tester minutieusement différents scénarios :
Multiple utilisateurs stakant et retirant à différents moments
Modification du taux de récompense pendant la période de staking
Cas limites comme totalStaked = 0
Audit du contrat
Avant de déployer un tel contrat en production, il est fortement recommandé de faire réaliser un audit par une entreprise spécialisée en sécurité blockchain comme OpenZeppelin, ChainSecurity ou Trail of Bits.
Dans cet article, nous avons exploré la création d'un système de staking simple mais fonctionnel sur Ethereum. Nous avons vu comment :
Mettre en place un token ERC-20 stakable
Créer un contrat de staking avec des récompenses
Gérer les dépôts, retraits et distributions de récompenses
Appliquer les meilleures pratiques de sécurité
Le staking est devenu un mécanisme fondamental dans l'écosystème blockchain, offrant une alternative moins énergivore au Proof of Work tout en incentivant la participation et la fidélité des utilisateurs.
Bien que notre implémentation soit relativement simple, elle constitue une base solide que vous pouvez étendre et personnaliser selon vos besoins spécifiques. Vous pourriez, par exemple, ajouter des fonctionnalités comme :
Des périodes de staking variables avec des récompenses différentes
Un système de boost pour les utilisateurs qui stakent plus longtemps
L'intégration avec un système de gouvernance
Des récompenses dynamiques basées sur la performance du protocole
N'hésitez pas à expérimenter et à adapter ce code à vos propres projets. Et surtout, n'oubliez jamais de prioriser la sécurité lorsque vous travaillez avec des contrats financiers sur la blockchain.
Si vous avez des questions ou des retours, n'hésitez pas à les partager dans les commentaires ci-dessous. Bon développement !
Subscribe to my newsletter
Read articles from Daniel Kambale directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Daniel Kambale
Daniel Kambale
Hello! I’m Daniel, a Web3 developer specializing in Solidity and smart contract development. My journey in the blockchain space is driven by a vision of a fully decentralized world, where technology empowers individuals and transforms industries. As a Web3 ambassador , I’m committed to fostering growth and innovation in this space, helping to shape a future that values transparency and security. Fluent in both English and French, I enjoy connecting with diverse communities and sharing my insights across languages. This is why you’ll find some of my articles in French, while others are in Swahili, as I believe knowledge should be accessible to all. I use my Hashnode blog to document my learning process, explore decentralized solutions, and share practical tutorials on Web3 development. Whether it's diving deep into Solidity, discussing the latest in blockchain, or exploring new tools, I’m passionate about contributing to a decentralized future and connecting with others who share this vision. Let’s build the future together!