WEB3DEV Español

Cover image for Creando un Contrato Inteligente Básico de Subasta Holandesa con Foundry: Una Guía Sencilla para el Desarrollo y Pruebas.
Banana Labs
Banana Labs

Posted on

Creando un Contrato Inteligente Básico de Subasta Holandesa con Foundry: Una Guía Sencilla para el Desarrollo y Pruebas.

portada
Foto de Zoltan Tasi en Unsplash

Antes de entrar en detalles sobre la creación de un contrato inteligente de subasta holandesa usando Foundry, es importante tener en cuenta que este artículo asume que tienes una comprensión básica del desarrollo de contratos inteligentes y las razones detrás del uso de una plataforma blockchain como Foundry. En comparación con otras herramientas populares como HardHat, Foundry ofrece un mejor rendimiento y permite las pruebas en Solidity, convirtiéndose en una excelente opción para desarrolladores que priorizan la eficiencia y seguridad en sus contratos inteligentes.

Con esta base en mente, ahora podemos explorar el proceso, paso a paso, para construir un contrato inteligente básico de subasta holandesa con Foundry.

Vamos directo al grano.

Una subasta holandesa es un tipo de subasta en la que el precio del artículo en venta, comienza alto y luego disminuye hasta que un postor esté dispuesto a pagar el precio actual.

  • En una subasta holandesa básica, el vendedor establece el precio inicial y un precio mínimo que está dispuesto a aceptar.
  • La subasta comienza con el precio más alto y luego disminuye gradualmente a una tasa predeterminada hasta que un postor haga una oferta o se alcance el precio mínimo.
  • El primer postor en hacer una oferta al precio actual gana la subasta y paga ese precio por el artículo. Como primer paso, vamos a instalar Foundry. Hay varias formas de hacerlo, yo lo haré usando foundryup. Otras formas de instalación están listadas en el libro de Foundry.

Introduce los siguientes comandos en el terminal:

> curl -L https://foundry.paradigm.xyz | bash
> foundryup
Enter fullscreen mode Exit fullscreen mode

Deberías ver algo como esto después de ejecutar el comando foundryup.

terminal
Figura 1: Foundry instalado.

Podemos ver que la versión más reciente de Forge está instalada. Lo usaremos para probar nuestros contratos inteligentes más tarde.

El siguiente paso es crear un proyecto. En tu terminal, vaya al directorio necesario y crea un nuevo proyecto DutchAuction usando el siguiente comando.

> forge init DutchAuction
Enter fullscreen mode Exit fullscreen mode

Una vez que este comando se ejecute con éxito, podrás ver una estructura de carpetas como esta.

|
|__lib
|__script
|__src
|__test
Enter fullscreen mode Exit fullscreen mode

La carpeta src contendrá todos tus contratos inteligentes, la carpeta script tendrá los scripts de implementación y la carpeta test albergará las pruebas para los contratos inteligentes.

Escribiendo el contrato inteligente

En tu carpeta /src, crea un nuevo archivo y nómbralo como BasicDutchAuction.sol. Añade el siguiente código en él:

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.19;


contract BasicDutchAuction {


   address payable public owner;
   address payable public winner;
   uint256 public auctionEndBlock;
   uint256 public reservePrice;
   uint256 public numBlocksActionOpen;
   uint256 public offerPriceDecrement;
   uint startBlockNumber;
   uint public winningBidAmount;
   bool public auctionEnded;
   uint public initialPrice;
   uint public currentPrice;

   constructor(uint256 _reservePrice, uint256 _numBlocksAuctionOpen, uint256 _offerPriceDecrement) {
       reservePrice = _reservePrice;
       numBlocksActionOpen = _numBlocksAuctionOpen;
       offerPriceDecrement = _offerPriceDecrement;
       owner = payable(msg.sender);
       startBlockNumber = block.number;
       auctionEndBlock = block.number + numBlocksActionOpen;
       initialPrice = _reservePrice + (_offerPriceDecrement * _numBlocksAuctionOpen);
       currentPrice = initialPrice;
       auctionEnded = false;
   }

   function updatePrice() internal {
       if (block.number >= auctionEndBlock) {
           auctionEnded = true;
           return;
       }
       currentPrice = initialPrice - (offerPriceDecrement * (block.number - startBlockNumber));
   }

   function bid() public payable returns(address) {
       require(msg.sender != owner, "Owner cannot bid");
       if(auctionEnded && winner != address(0) && msg.sender != winner) {
           address payable refundCaller = payable(msg.sender);
           refundCaller.transfer(address(this).balance);
       }
       // comprueba si la subasta ha terminado
       require(!auctionEnded, "Auction has ended");
       // comprueba si el número de bloque está dentro del límite de tiempo
       require(block.number < auctionEndBlock, "Auction has ended");
       updatePrice();
       // comprueba si la oferta es mayor que el precio de reserva
       require(msg.value >= currentPrice, "Bid is lower than current price");
       require(winner == address(0), "Auction has already been won");
       // si el monto de la oferta es mayor, cierra la subasta y transfiere los fondos al propietario
       auctionEnded = true;
       winner = payable(msg.sender);
       owner.transfer(msg.value);
       winningBidAmount = msg.value;
       return winner;
   }


}
Enter fullscreen mode Exit fullscreen mode

¿Qué está pasando aquí?

Hemos creado un contrato inteligente llamado BasicDutchAuction que tiene un constructor que utiliza tres parámetros — _reservePrice, _numBlocksAuctionOpen y _offerPriceDecrement.

  • _reservePrice — es el precio mínimo que la subasta aceptará por el producto. El "producto" es una entidad física aquí que asumimos que se transferirá al ganador después que termine la subasta.
  • _numBlocksAuctionOpen — es el número de bloques o lotes de transacciones para los cuales la subasta estará abierta.
  • _offerPriceDecrement — es el valor que se restará del precio actual después de que termine cada bloque.

El constructor también calcula el valor de initialPrice y actualiza todos los demás campos en el contrato inteligente.

updatePrice es una función utilitaria interna utilizada para calcular el precio más reciente del artículo.

bid() es la función de oferta que un postor llamará para participar en la subasta holandesa. Tenemos muchas otras verificaciones antes de declarar un ganador, los no ganadores son inmediatamente reembolsados con sus valores de oferta.

Ahora que tenemos el código del contrato, vamos a verificar si todo está funcionando correctamente. Ejecuta el siguiente comando para compilar y crear el contrato inteligente.

> forge build
Enter fullscreen mode Exit fullscreen mode

terminal
Figura 2: Los contratos se construyen y compilan con éxito.

Probando el contrato de subasta holandesa:

Ahora que hemos escrito el código para el contrato de subasta holandesa, es hora de probarlo. Usaremos Forge para probar nuestro contrato inteligente.

Forge puede ejecutar sus pruebas con el comando forge test. Todas las pruebas están escritas en Solidity.

Forge puede ejecutar sus pruebas con el comando forge test. Todas las pruebas están escritas en Solidity.

Forge buscará las pruebas en cualquier lugar de tu directorio de origen. Cualquier contrato con una función que comience con test se considera una prueba. Normalmente, las pruebas se colocarán en test/ por convención y terminarán con .t.sol.

Vamos a crear un archivo BasicDutchAuction.t.sol en el directorio de pruebas. Añade el siguiente código en el archivo. Observa que la prueba del contrato inteligente de subasta holandesa se realiza por otro contrato inteligente, en nuestro caso, el contrato BasicDutchAuctionTest, esto asegura que las pruebas también estén escritas en Solidity.

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.19;

import "forge-std/Test.sol";

import "../src/BasicDutchAuction.sol";

contract BasicDutchAuctionTest is Test {
   BasicDutchAuction auction;

   function setUp() public {
       auction = new BasicDutchAuction(1000, 100, 10);
   }

   function testInitialPrice() public {
       assertEq(auction.initialPrice(), 2000, "Initial price should be 2000");
   }

   function testCurrentPrice() public {
       assertEq(auction.currentPrice(), 2000, "Current price should be 2000");
   }


   function testBid() public {
       // Asegura que un propietario (owner) no pueda hacer ofertas.
       // dirección del propietario (owner) = auction.owner();
       uint256 initialPrice = auction.initialPrice();
       uint256 currentPrice = auction.currentPrice();
       uint256 balanceBefore = address(this).balance;
       uint256 bidAmount = currentPrice + 1;
       try auction.bid{value: bidAmount}() {
           vm.expectRevert(bytes("Owner cannot bid"));
       } catch Error(string memory reason) {
           console.log('reason: ', reason);
       }
       address payable someRandomUser = payable(vm.addr(1));
       vm.deal(someRandomUser, 1 ether);
       vm.startPrank(someRandomUser);

       assertEq(address(this).balance, balanceBefore, "Contract balance should not change after owner tries to bid");

       // Asegura que una oferta inferior al precio de reserva sea rechazada
       balanceBefore = address(this).balance;
       bidAmount = initialPrice - 1;

       assertEq(address(this).balance, balanceBefore, "Contract balance should not change after bid lower than reserve price is rejected");

       // Garantiza que se acepte una oferta superior al precio de reserva
       balanceBefore = address(this).balance;
       bidAmount = initialPrice + 1;
       address winningBidder = auction.bid{value: bidAmount}();
       assertEq(winningBidder, someRandomUser, "The winning bidder should be returned by the bid function");
       assertEq(auction.winner(), someRandomUser, "The winner should be the bidder who made the highest bid");
       assertEq(auction.winningBidAmount(), bidAmount, "The winning bid amount should be the highest bid made");

       uint256 blocksToWait = 5;
       uint256 blocksElapsed = 0;
       while (blocksElapsed < blocksToWait) {
           blocksElapsed++;
       }
       vm.stopPrank();
   }

}
Enter fullscreen mode Exit fullscreen mode

El contrato BasicDutchAuctionTest extiende el contrato de prueba de la biblioteca estándar de Forge que, implementa varias funciones que usaremos para nuestros propósitos de prueba.

La función setUp() se ejecuta antes de cada caso de prueba, similar a la función beforeEach() en el marco de pruebas Mocha.

La mayoría de las veces, simplemente probar las salidas de tus contratos inteligentes no es suficiente. Para manipular el estado de la blockchain, así como probar reversiones y eventos específicos, Foundry viene con un conjunto de cheatcodes (códigos de trampa). Los cheatcodes te permiten cambiar el número de bloque, tu identidad y mucho más.

Puedes acceder a los cheatcodes fácilmente a través de la instancia vm disponible en el contrato Test de la biblioteca estándar de Forge.

Usamos el cheatcode "prank" en nuestra prueba para simular las ofertas provenientes de una cuenta no propietaria. También usamos el cheatcode "deal" para agregar algunos Ethers a la cuenta de pruebas.

Ahora podemos ejecutar las pruebas usando el siguiente comando.

> forge test
Enter fullscreen mode Exit fullscreen mode

terminal
Figura 3: Probando el contrato inteligente

Para modificar el nivel de detalle proporcionado por las pruebas, podemos utilizar el indicador -v. Dependiendo de tus preferencias, puedes incluir hasta cinco v para aumentar el detalle de la salida.

> forge test -vvvv
Enter fullscreen mode Exit fullscreen mode

El comando anterior proporciona la siguiente salida:

salida
Figura 4: Añade más complejidad a tus pruebas.

Podemos generar el informe de cobertura usando el siguiente comando:

> forge coverage
Enter fullscreen mode Exit fullscreen mode

¡Buenas noticias! Hemos completado la tarea de crear y verificar la funcionalidad de nuestro contrato inteligente de subasta holandesa en Foundry. Si deseas desarrollar aún más este proyecto, considera agregar casos de prueba adicionales para cubrir todos los aspectos del contrato inteligente, implementando la capacidad de subastar NFTs y permitiendo la participación con un token personalizado diferente del ETH.

Para el código utilizado en este artículo, consulta el repositorio Git asociado.

Además, puedes explorar la Dutch Auction Application (Aplicación de subasta holandesa) y descubrir varias mejoras que pueden ser implementadas en este repositorio de Hardhat.

Este artículo es una traducción realizada por @bananlabs. Puedes encontrar el artículo original aquí

Discussion (0)