El crowdfunding ha transformado la forma en que proyectos, causas y empresas recaudan fondos, permitiendo a los creadores conectarse directamente con sus seguidores y prescindir de las instituciones financieras tradicionales. La tecnología blockchain mejora este modelo añadiendo transparencia, descentralización e inmutabilidad, lo que garantiza un sistema más justo tanto para los creadores como para los contribuyentes.
Este artículo te guiará en la creación de un Smart Contract de Crowdfunding basado en Solidity, diseñado para blockchains compatibles con EVM como Linea. El contrato permitirá a los usuarios crear campañas, contribuir fondos de forma segura, retirar fondos al alcanzar el objetivo con éxito y solicitar reembolsos si una campaña no llega a su meta. Al aprovechar la automatización de los smart contracts, la plataforma elimina intermediarios y garantiza la rendición de cuentas.
Al explorar la implementación, las características y la funcionalidad del contrato, los desarrolladores obtendrán información valiosa para construir plataformas de crowdfunding descentralizadas. Tanto si eres un entusiasta de blockchain, un desarrollador o un emprendedor, esta guía demostrará cómo los smart contracts pueden revolucionar el crowdfunding haciéndolo más seguro, eficiente y sin necesidad de confianza.
Requisitos previos
Debes tener un conocimiento básico de Solidity.
Debes tener Nodejs y Foundry instalados en tu PC.
Descripción general del contrato
Este contrato permite:
Creación de campañas: Los usuarios pueden iniciar campañas de recaudación de fondos estableciendo un objetivo y una duración.
Contribuciones: Los usuarios pueden aportar fondos a campañas activas.
Retiro de fondos: Los creadores de campañas pueden retirar los fondos si se alcanza el objetivo de recaudación tras la fecha límite de la campaña.
Reembolsos: Los contribuyentes pueden recuperar sus aportaciones si no se alcanza el objetivo dentro del plazo establecido.
Configuración del proyecto
Ejecuta el siguiente comando para iniciar un proyecto Foundry. Utilizaremos el framework Foundry para construir el smart contract.
forge init crowdfunding
Abre la carpeta crowdfunding en VS Code o en tu editor de código favorito, y elimina los archivos scripts/counter.s.sol, src/counter.sol y test/counter.t.sol.
Instalar todas las dependencias
forge install foundry-rs/forge-std --no-commit
Código completo del smart contract de crowdfunding
Crea un archivo crowdfunding.sol en la carpeta src y añade el siguiente código al archivo.
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);
}
}
Características clave del código
Estructura de la campaña El contrato utiliza un struct llamado Campaign para almacenar los detalles de la campaña.
struct Campaign {
address creator;
uint256 goal;
uint256 deadline;
uint256 amountRaised;
bool isWithdrawn;
mapping(address \=\> uint256) contributions;
}
Contador de campañas Cada campaña se identifica de forma única mediante un campaignId incremental, lo que garantiza la escalabilidad y simplifica la gestión de múltiples campañas.
uint256 public campaignCount = 0;
mapping(uint256 => Campaign) public campaigns;
Creación de campañas
La función createCampaign permite a un usuario iniciar una campaña. Recibe dos parámetros: goal y duration. El parámetro goal representa el monto objetivo que el usuario desea recaudar, mientras que duration especifica el tiempo en segundos asignado para alcanzar dicho objetivo.
La función garantiza que tanto goal como duration sean mayores que cero para evitar entradas no válidas. Los detalles de la campaña se almacenan en el struct Campaign y el campaignId se asigna en consecuencia.
El campaignCount se incrementa para asegurar que las campañas recién creadas tengan campaignIds únicos y no se superpongan con campañas anteriores.
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++;
}
Mecanismo de contribución
La función contribute permite a los usuarios financiar campañas activas. Recibe un parámetro, campaignId, que sirve como identificador único de cada campaña y determina a dónde se destinará la contribución.
La función garantiza que los usuarios no contribuyan con un monto de cero. Realiza un seguimiento del amountRaised de la campaña y registra la contribución total realizada por cada usuario.
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);
}
El modificador campaignExists en la función contribute verifica si el campaignId es válido y lanza un error si la campaña no existe.
modifier campaignExists(uint256 campaignId) {
require(campaignId < campaignCount, "Campaign does not exist");
_;
}
El modificador beforeDeadline verifica si ha pasado la fecha límite de la campaña y lanza un error si así ha sido.
modifier beforeDeadline(uint256 campaignId) {
require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
_;
}
Retiro de fondos
Los creadores de campañas pueden retirar fondos mediante la función withdrawFunds, que recibe un parámetro, campaignId, que sirve como identificador único de cada campaña.
Solo el creador de la campaña puede retirar los fondos. Si cualquier otro usuario intenta hacerlo, la función lanzará un error.
La función verifica que se haya alcanzado el objetivo de financiación y que los fondos no hayan sido retirados previamente. Si el objetivo no se ha alcanzado o los fondos ya han sido retirados, la función lanzará un error.
Tras un retiro exitoso, la función actualiza el estado isWithdrawn a true y transfiere los fondos al creador de la campaña.
``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");
_;
}
El modificador afterDeadline garantiza que la fecha límite de la campaña haya pasado y lanza un error si aún no ha ocurrido.
modifier afterDeadline(uint256 campaignId) {
require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
_;
}
Reembolsos para campañas fallidas
Los contribuyentes pueden retirar sus aportaciones si la campaña fracasa. La función recibe un parámetro, campaignId, que identifica de forma única cada campaña.
Verifica que la campaña no haya alcanzado su objetivo de financiación y que el usuario haya realizado una contribución. Si ambas condiciones se cumplen, la función restablece la contribución del usuario a cero y procesa el reembolso.
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);
}
Eventos
El contrato emite eventos para las acciones clave:
CampaignCreated: Registra los detalles de una nueva campaña.
ContributionMade: Registra las contribuciones a una campaña.
FundsWithdrawn: Registra los retiros de fondos exitosos.
RefundIssued: Registra los reembolsos emitidos a los contribuyentes.
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);
Pruebas
Crea un archivo crowdfunding.t.sol en la carpeta test y añade el siguiente código al archivo.
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);
}
}
El conjunto de pruebas garantiza que el contrato Crowdfunding funcione correctamente en todas sus características principales. A continuación se detalla cada prueba:
test_createCampaign
Esta prueba verifica que un usuario pueda crear exitosamente una campaña de crowdfunding. La campaña se crea sin errores, lo que confirma que la función funciona según lo esperado.
test_contributeCampaign
Esta prueba verifica que múltiples usuarios puedan contribuir a una campaña específica. En la prueba, user1 crea una campaña, mientras que user2 y user3 reciben 10 ether cada uno mediante la función deal de Foundry. Tanto user2 como user3 contribuyen 6 ether a la campaña, y el contrato actualiza correctamente el monto total recaudado por la campaña.
test_withdrawCampaign
Esta prueba verifica que el creador de la campaña pueda retirar los fondos una vez que la campaña alcanza su objetivo de financiación y ha pasado la fecha límite. Después de que user2 y user3 contribuyen a la campaña y han transcurrido 11 horas (simuladas con skip), user1 llama a withdrawFunds para retirar los fondos recaudados. Los fondos se transfieren exitosamente y el saldo del contrato refleja el retiro.
test_requestCampaign
Esta prueba verifica que los contribuyentes puedan solicitar reembolsos si la campaña no alcanza su objetivo de financiación. Después de que user2 y user3 contribuyen a la campaña y han transcurrido 11 horas (simuladas con skip), tanto user2 como user3 llaman a requestRefund. Cada usuario recibe exitosamente de vuelta el ether que aportó.
Ejecutar pruebas
forge test
Despliegue y verificación en Linea
Añade las variables de entorno requeridas al archivo .env, asegurándote de que esté ubicado en el nivel raíz del proyecto. Estas variables almacenan las credenciales sensibles necesarias para las interacciones con la blockchain.
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
Añade los siguientes detalles a foundry.toml
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Crea un Makefile y añade los siguientes detalles.
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
Despliegue
make deploy
Verificación
make verify
Conclusión
Esta guía cubrió los fundamentos para construir y desplegar un smart contract de crowdfunding descentralizado en la blockchain de Linea. Exploramos las funcionalidades clave del contrato, como la creación de campañas, las contribuciones, los retiros y los reembolsos, además de la escritura y ejecución de pruebas para validar el comportamiento del contrato. Por último, recorrimos el proceso de despliegue y verificación para poner el contrato en funcionamiento en Linea.
Para más tutoriales prácticos y análisis en profundidad sobre el desarrollo en blockchain, visita el canal de YouTube de MetaMask Developer y mantente al día con nuestras últimas novedades siguiéndonos en Twitter.