Saltar al contenido

Crea e implementa un contrato inteligente de crowdfunding descentralizado en Linea

Aprende a crear e implementar un contrato inteligente de crowdfunding descentralizado en Linea.

9 min de lectura
Crea e implementa un contrato inteligente de crowdfunding descentralizado en Linea

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

Descripción general del contrato

Este contrato permite:

  1. Creación de campañas: Los usuarios pueden iniciar campañas de recaudación de fondos estableciendo un objetivo y una duración.

  2. Contribuciones: Los usuarios pueden aportar fondos a campañas activas.

  3. 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.

  4. 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.

// SPDX-License-Identifier: MIT
  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");
          _;
      }

      // Create a new crowdfunding campaign
      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++;
      }

      // Contribute funds to a campaign
      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);
      }

      // Withdraw funds if the campaign meets its goal
      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);
      }

      // Request a refund if the campaign fails
      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);
      }
  }
Copiar

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;            // Campaign creator  
       uint256 goal;               // Fundraising goal  
       uint256 deadline;           // Deadline to achieve the goal  
       uint256 amountRaised;       // Total contributions raised  
       bool isWithdrawn;           // Tracks if funds were withdrawn  
       mapping(address \=\> uint256) contributions; // Tracks contributions by users  
   }  
Copiar

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;
Copiar

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++;
   }
Copiar

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);
   }
Copiar

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");
           _;
       }
Copiar

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");
           _;
       }
Copiar

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);
Copiar

}

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");
         _;
     }
Copiar

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");
           _;
       }
Copiar

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);
       }
Copiar

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);
Copiar

Pruebas

Crea un archivo crowdfunding.t.sol en la carpeta test y añade el siguiente código al archivo.

// SPDX-License-Identifier: UNLICENSED
  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);
      }

  }
Copiar

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/ // Infura RPC endpoint for connecting to Linea 
PRIVATE_KEY=************************************************* // Wallet private key for signing transactions LINEA_API_KEY=************************* // API key for accessing Linea services via Infura
Copiar

Añade los siguientes detalles a foundry.toml

   [etherscan]
   linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Copiar

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
Copiar

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.

Traducido por IA. Puede contener errores. Por favor, verifique siempre la información.

Califica la traducción

Este artículo fue escrito por:

  • Kingsley Okonkwo

    Kingsley, desarrollador certificado de Ethereum Blockchain, trabaja como escritor técnico en Consensys, con un enfoque especial en herramientas para desarrolladores. Antes de unirse a Consensys, Kingsley trabajó como desarrollador backend freelance en varias plataformas, entre ellas Gigster y Braintrust. Con 5 años de experiencia en desarrollo de software, domina herramientas como JavaScript (Node.js y React), GoLang, SQL y Docker. Kingsley destaca en la creación de tutoriales paso a paso y guías prácticas, aprovechando las últimas tecnologías web3 para ayudar a otros desarrolladores a construir mejores dapps. Actualmente reside en Dubái y en su tiempo libre disfruta jugando al baloncesto y al fútbol.

    Leer todos los artículos