Le financement participatif a transformé la façon dont les projets, les causes et les entreprises lèvent des fonds, en permettant aux créateurs de se connecter directement avec leurs soutiens et de contourner les institutions financières traditionnelles. La technologie blockchain améliore ce modèle en y ajoutant transparence, décentralisation et immuabilité, garantissant ainsi un système plus équitable pour les créateurs comme pour les contributeurs.
Cet article vous guidera dans la création d'un Smart Contract de financement participatif basé sur Solidity, conçu pour les blockchains compatibles EVM comme Linea. Le contrat permettra aux utilisateurs de créer des campagnes, de contribuer des fonds en toute sécurité, de retirer des fonds en cas d'atteinte de l'objectif, et de demander des remboursements si une campagne n'atteint pas son but. En tirant parti de l'automatisation des smart contracts, la plateforme élimine les intermédiaires et garantit la responsabilisation.
En explorant l'implémentation, les fonctionnalités et le fonctionnement du contrat, les développeurs acquerront des connaissances précieuses pour créer des plateformes de financement participatif décentralisées. Que vous soyez passionné de blockchain, développeur ou entrepreneur, ce guide vous montrera comment les smart contracts peuvent révolutionner le financement participatif en le rendant plus sécurisé, plus efficace et trustless.
Prérequis
Vous devez avoir une compréhension de base de Solidity.
Vous devez avoir Nodejs et Foundry installés sur votre PC.
Présentation du contrat
Ce contrat permet :
Création de campagne : Les utilisateurs peuvent lancer des campagnes de collecte de fonds en définissant un objectif et une durée.
Contributions : Les utilisateurs peuvent contribuer des fonds à des campagnes actives.
Retrait de fonds : Les créateurs de campagnes peuvent retirer les fonds si l'objectif de collecte est atteint après la date limite de la campagne.
Remboursements : Les contributeurs peuvent récupérer leurs contributions si l'objectif n'est pas atteint dans les délais impartis.
Configuration du projet
Exécutez la commande ci-dessous pour initialiser un projet Foundry. Nous utiliserons le framework Foundry pour créer le smart contract.
forge init crowdfunding
Ouvrez le dossier crowdfunding dans VS Code ou votre éditeur de code favori, et supprimez les fichiers scripts/counter.s.sol, src/counter.sol et test/counter.t.sol.
Installer toutes les dépendances
forge install foundry-rs/forge-std --no-commit
Code complet du smart contract de financement participatif
Créez un fichier crowdfunding.sol dans le dossier src et ajoutez-y le code ci-dessous.
pragma solidity 0.8.18;
contract Crowdfunding {
struct Campaign {
address creator;
uint256 goal;
uint256 deadline;
uint256 amountRaised;
bool isWithdrawn;
mapping(address => uint256) contributions;
}
uint256 public campaignCount = 0;
mapping(uint256 => Campaign) public campaigns;
event CampaignCreated(uint256 campaignId, address creator, uint256 goal, uint256 deadline);
event ContributionMade(uint256 campaignId, address contributor, uint256 amount);
event FundsWithdrawn(uint256 campaignId, uint256 amount);
event RefundIssued(uint256 campaignId, address contributor, uint256 amount);
modifier campaignExists(uint256 campaignId) {
require(campaignId < campaignCount, "Campaign does not exist");
_;
}
modifier onlyCreator(uint256 campaignId) {
require(msg.sender == campaigns[campaignId].creator, "Only the campaign creator can perform this action");
_;
}
modifier beforeDeadline(uint256 campaignId) {
require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
_;
}
modifier afterDeadline(uint256 campaignId) {
require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
_;
}
function createCampaign(uint256 goal, uint256 duration) external {
require(goal > 0, "Goal must be greater than zero");
require(duration > 0, "Duration must be greater than zero");
Campaign storage newCampaign = campaigns[campaignCount];
newCampaign.creator = msg.sender;
newCampaign.goal = goal;
newCampaign.deadline = block.timestamp + duration;
emit CampaignCreated(campaignCount, msg.sender, goal, newCampaign.deadline);
campaignCount++;
}
function contribute(uint256 campaignId) external payable campaignExists(campaignId) beforeDeadline(campaignId) {
require(msg.value > 0, "Contribution must be greater than zero");
Campaign storage campaign = campaigns[campaignId];
campaign.amountRaised += msg.value;
campaign.contributions[msg.sender] += msg.value;
emit ContributionMade(campaignId, msg.sender, msg.value);
}
function withdrawFunds(uint256 campaignId) external campaignExists(campaignId) onlyCreator(campaignId) afterDeadline(campaignId) {
Campaign storage campaign = campaigns[campaignId];
require(campaign.amountRaised >= campaign.goal, "Funding goal not reached");
require(!campaign.isWithdrawn, "Funds already withdrawn");
campaign.isWithdrawn = true;
(bool sent, ) = campaign.creator.call{value: campaign.amountRaised}("");
require(sent, "Failed to send Ether");
emit FundsWithdrawn(campaignId, campaign.amountRaised);
}
function requestRefund(uint256 campaignId) external campaignExists(campaignId) afterDeadline(campaignId) {
Campaign storage campaign = campaigns[campaignId];
require(campaign.amountRaised < campaign.goal, "Campaign succeeded, refunds not allowed");
uint256 contribution = campaign.contributions[msg.sender];
require(contribution > 0, "No contributions found for this campaign");
campaign.contributions[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: contribution}("");
require(sent, "Failed to send Ether");
emit RefundIssued(campaignId, msg.sender, contribution);
}
}
Fonctionnalités clés du code
Structure de la campagne Le contrat utilise un struct nommé Campaign pour stocker les détails de la campagne.
struct Campaign {
address creator;
uint256 goal;
uint256 deadline;
uint256 amountRaised;
bool isWithdrawn;
mapping(address \=\> uint256) contributions;
}
Compteur de campagnes Chaque campagne est identifiée de manière unique par un campaignId incrémental, garantissant la scalabilité et simplifiant la gestion de plusieurs campagnes.
uint256 public campaignCount = 0;
mapping(uint256 => Campaign) public campaigns;
Création de campagne
La fonction createCampaign permet à un utilisateur de lancer une campagne. Elle prend deux paramètres : goal et duration. Le paramètre goal représente le montant cible que l'utilisateur souhaite collecter, tandis que duration spécifie le temps en secondes alloué pour atteindre cet objectif.
La fonction vérifie que goal et duration sont tous deux supérieurs à zéro afin d'éviter les entrées invalides. Les détails de la campagne sont ensuite stockés dans le struct Campaign, et le campaignId est attribué en conséquence.
Le campaignCount est incrémenté pour garantir que les campagnes nouvellement créées disposent de campaignIds uniques et ne chevauchent pas les campagnes précédentes.
function createCampaign(uint256 goal, uint256 duration) external {
require(goal > 0, "Goal must be greater than zero");
require(duration > 0, "Duration must be greater than zero");
Campaign storage newCampaign = campaigns[campaignCount];
newCampaign.creator = msg.sender;
newCampaign.goal = goal;
newCampaign.deadline = block.timestamp + duration;
emit CampaignCreated(campaignCount, msg.sender, goal, newCampaign.deadline);
campaignCount++;
}
Mécanisme de contribution
La fonction contribute permet aux utilisateurs de financer des campagnes actives. Elle prend un paramètre, campaignId, qui sert d'identifiant unique pour chaque campagne et détermine où la contribution sera allouée.
La fonction vérifie que les utilisateurs ne contribuent pas un montant nul. Elle suit le amountRaised pour la campagne et enregistre la contribution totale effectuée par chaque utilisateur.
function contribute(uint256 campaignId) external payable campaignExists(campaignId) beforeDeadline(campaignId) {
require(msg.value > 0, "Contribution must be greater than zero");
Campaign storage campaign = campaigns[campaignId];
campaign.amountRaised += msg.value;
campaign.contributions[msg.sender] += msg.value;
emit ContributionMade(campaignId, msg.sender, msg.value);
}
Le modificateur campaignExists dans la fonction contribute vérifie si le campaignId est valide et génère une erreur si la campagne n'existe pas.
modifier campaignExists(uint256 campaignId) {
require(campaignId < campaignCount, "Campaign does not exist");
_;
}
Le modificateur beforeDeadline vérifie si la date limite de la campagne est dépassée et génère une erreur si c'est le cas.
modifier beforeDeadline(uint256 campaignId) {
require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
_;
}
Retrait des fonds
Les créateurs de campagnes peuvent retirer des fonds à l'aide de la fonction withdrawFunds, qui prend un paramètre, campaignId, servant d'identifiant unique pour chaque campagne.
Seul le créateur de la campagne peut retirer les fonds. Si un autre utilisateur tente de le faire, la fonction génère une erreur.
La fonction vérifie que l'objectif de financement a été atteint et que les fonds n'ont pas déjà été retirés. Si l'objectif n'est pas atteint ou si les fonds ont déjà été retirés, la fonction génère une erreur.
Lors d'un retrait réussi, la fonction met à jour le statut isWithdrawn à true et transfère les fonds au créateur de la campagne.
``Solidity function withdrawFunds(uint256 campaignId) external campaignExists(campaignId) onlyCreator(campaignId) afterDeadline(campaignId) { Campaign storage campaign = campaigns[campaignId]; require(campaign.amountRaised >= campaign.goal, "Funding goal not reached"); require(!campaign.isWithdrawn, "Funds already withdrawn");
campaign.isWithdrawn = true;
(bool sent, ) = campaign.creator.call{value: campaign.amountRaised}("");
require(sent, "Failed to send Ether");
emit FundsWithdrawn(campaignId, campaign.amountRaised);
}
The `onlyCreator` modifier verifies whether the caller is the campaign creator and throws an error if they are not.
``Solidity
modifier onlyCreator(uint256 campaignId) {
require(msg.sender == campaigns[campaignId].creator, "Only the campaign creator can perform this action");
_;
}
Le modificateur afterDeadline vérifie que la date limite de la campagne est dépassée et génère une erreur si ce n'est pas le cas.
modifier afterDeadline(uint256 campaignId) {
require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
_;
}
Remboursements pour les campagnes échouées
Les contributeurs peuvent retirer leurs contributions si la campagne échoue. La fonction prend un paramètre, campaignId, qui identifie de manière unique chaque campagne.
Elle vérifie que la campagne n'a pas atteint son objectif de financement et que l'utilisateur a effectué une contribution. Si les deux conditions sont remplies, la fonction remet la contribution de l'utilisateur à zéro et traite le remboursement.
function requestRefund(uint256 campaignId) external campaignExists(campaignId) afterDeadline(campaignId) {
Campaign storage campaign = campaigns[campaignId];
require(campaign.amountRaised < campaign.goal, "Campaign succeeded, refunds not allowed");
uint256 contribution = campaign.contributions[msg.sender];
require(contribution > 0, "No contributions found for this campaign");
campaign.contributions[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: contribution}("");
require(sent, "Failed to send Ether");
emit RefundIssued(campaignId, msg.sender, contribution);
}
Événements
Le contrat émet des événements pour les actions clés :
CampaignCreated : Enregistre les détails d'une nouvelle campagne.
ContributionMade : Enregistre les contributions à une campagne.
FundsWithdrawn : Enregistre les retraits de fonds réussis.
RefundIssued : Enregistre les remboursements émis aux contributeurs.
event CampaignCreated(uint256 campaignId, address creator, uint256 goal, uint256 deadline);
event ContributionMade(uint256 campaignId, address contributor, uint256 amount);
event FundsWithdrawn(uint256 campaignId, uint256 amount);
event RefundIssued(uint256 campaignId, address contributor, uint256 amount);
Tests
Créez un fichier crowdfunding.t.sol dans le dossier test et ajoutez-y le code ci-dessous.
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {Crowdfunding} from "../src/Crowdfunding.sol";
contract CounterTest is Test {
Crowdfunding public crowd;
address public user1 = makeAddr("user1");
address public user2 = makeAddr("user2");
address public user3 = makeAddr("user3");
function setUp() public {
crowd = new Crowdfunding();
}
function test_createCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
}
function test_contributeCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
deal(user2, 10 ether);
deal(user3, 10 ether);
vm.startPrank(user2);
crowd.contribute{value: 6 ether}(0);
vm.startPrank(user3);
crowd.contribute{value: 6 ether}(0);
}
function test_withdrawCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
deal(user2, 10 ether);
deal(user3, 10 ether);
vm.startPrank(user2);
crowd.contribute{value: 6 ether}(0);
vm.startPrank(user3);
crowd.contribute{value: 6 ether}(0);
skip(11 hours);
console.log("b4", user1.balance);
vm.startPrank(user1);
crowd.withdrawFunds(0);
console.log("after",user1.balance);
}
function test_requestCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
deal(user2, 10 ether);
deal(user3, 10 ether);
vm.startPrank(user2);
crowd.contribute{value: 2 ether}(0);
vm.startPrank(user3);
crowd.contribute{value: 2 ether}(0);
skip(11 hours);
vm.startPrank(user2);
crowd.requestRefund(0);
vm.startPrank(user3);
crowd.requestRefund(0);
console.log("after",user2.balance);
console.log("after",user3.balance);
}
}
La suite de tests garantit que le contrat Crowdfunding fonctionne correctement pour l'ensemble de ses fonctionnalités principales. Voici un aperçu des tests :
test_createCampaign
Ce test vérifie qu'un utilisateur peut créer avec succès une campagne de financement participatif. La campagne est créée sans erreur, confirmant que la fonction fonctionne comme prévu.
test_contributeCampaign
Ce test vérifie que plusieurs utilisateurs peuvent contribuer à une campagne spécifique. Dans le test, user1 crée une campagne, tandis que user2 et user3 reçoivent chacun 10 ether via la fonction deal de Foundry. user2 et user3 contribuent chacun 6 ether à la campagne, et le contrat met correctement à jour le montant total collecté par la campagne.
test_withdrawCampaign
Ce test vérifie que le créateur de la campagne peut retirer les fonds une fois que la campagne a atteint son objectif de financement et que la date limite est dépassée. Après que user2 et user3 ont contribué à la campagne et que 11 heures se sont écoulées (simulées avec skip), user1 appelle withdrawFunds pour retirer les fonds collectés. Les fonds sont transférés avec succès et le solde du contrat reflète le retrait.
test_requestCampaign
Ce test vérifie que les contributeurs peuvent demander des remboursements si la campagne n'atteint pas son objectif de financement. Après que user2 et user3 ont contribué à la campagne et que 11 heures se sont écoulées (simulées avec skip), user2 et user3 appellent requestRefund. Chaque utilisateur récupère avec succès les ether qu'il a contribués.
Exécuter les tests
forge test
Déploiement et vérification sur Linea
Ajoutez les variables d'environnement requises dans le fichier .env, en veillant à le placer à la racine du projet. Ces variables stockent les identifiants sensibles nécessaires aux interactions avec la blockchain.
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
Ajoutez les informations ci-dessous dans foundry.toml
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Créez un Makefile et ajoutez-y les informations ci-dessous.
deploy:
forge create src/Crowdfunding.sol:Crowdfunding --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)
verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Crowdfunding.sol:Crowdfunding
Déploiement
make deploy
Vérification
make verify
Conclusion
Ce guide a couvert les fondamentaux de la création et du déploiement d'un smart contract de financement participatif décentralisé sur la blockchain Linea. Nous avons exploré les fonctionnalités clés du contrat, telles que la création de campagnes, les contributions, les retraits et les remboursements, ainsi que la rédaction et l'exécution de tests pour valider le comportement du contrat. Enfin, nous avons parcouru le processus de déploiement et de vérification pour mettre le contrat en ligne sur Linea.
Pour d'autres tutoriels pratiques et des analyses approfondies du développement blockchain, consultez la chaîne YouTube MetaMask Developer, et restez informé de nos dernières actualités en nous suivant sur Twitter.