8 min read

Carreras de canicas en la blockchain: Un vistazo al contrato inteligente

Explora el fascinante mundo de los contratos inteligentes con nuestro ejemplo de CarrerasDeCanicas, donde combinamos la lógica de juego con la potencia de la blockchain y la gestión de roles mediante OpenZeppelin.
Carreras de canicas en la blockchain: Un vistazo al contrato inteligente
blockchain marble races

Las carreras de canicas siempre han sido un juego clásico. Sin embargo, ¿alguna vez imaginaste que podríamos llevarlo a la blockchain? En el vasto y emocionante mundo de la blockchain, las posibilidades son prácticamente infinitas. A medida que continuamos explorando y expandiendo las fronteras de la tecnología web3 es esencial comenzar con ejercicios prácticos que nos ayuden a comprender mejor su potencial.

Hoy vamos a sumergirnos en un ejercicio básico simulado: un contrato inteligente que simula carreras de canicas y permite a los usuarios apostar en sus canicas favoritas. Aunque este ejercicio puede parecer simple, representa la primera piedra en nuestro camino hacia la creación de un portfolio de trabajos de web3. Además, en este artículo, no solo exploraremos este contrato, sino que también introduciremos la librería AccessControl de OpenZeppelin. Esta es una herramienta poderosa que nos permite implementar un sistema de roles para mejorar la gestión y control del juego. A través de ejemplos prácticos, desglosaremos cómo funcionan estos roles y cómo pueden ser utilizados para otorgar o restringir permisos específicos dentro del contrato. En el futuro, planeamos actualizar este contrato con un front-end interactivo, convirtiéndolo en una aplicación completa y funcional. ¡Adentrémonos en el contrato CarrerasDeCanicas!


El contrato base se encuentra en el siguiente enlace: CarreraDeCanicas.sol en GitHub.

Este contrato representa una simulación básica de carreras de canicas en la blockchain. A través de él se pueden explorar las funciones que permiten agregar canicas, crear carreras, apostar en canicas para carreras específicas, finalizar carreras y retirar ganancias.

Es importante mencionar que este contrato es la versión inicial, sin la implementación de roles. A lo largo de nuestro artículo discutiremos cómo agregar un sistema de roles para mejorar la gestión y control del juego. Por lo tanto este contrato es esencialmente la "solución inicial" al puzzle que planteamos. Si están interesados en ver cómo se desarrolla y se mejora con la implementación de roles y otras características avanzadas, les invitamos a seguir leyendo y explorando con nosotros.

Estructuras de Datos (Structs)

Las estructuras de datos, o structs, nos permiten definir tipos de datos personalizados. En nuestro contrato, hemos definido tres estructuras principales:

Canica: Representa una canica individual con un identificador, nombre, color y enlace a IPFS para su imagen.

struct Canica {
    uint256 id;
    string nombre;
    string color;
    string ipfsBaseLink;
}

Apuesta: Representa una apuesta realizada por un usuario, incluyendo la dirección del apostador y la cantidad apostada.

struct Apuesta {
    address payable apostador;
    uint256 cantidad;
}

Carrera: Define una carrera con un identificador, estado (activo o inactivo) y la canica ganadora.

struct Carrera {
    uint256 id;
    bool activo;
    uint256 canicaGanadora;
}

Variables de Estado

Las variables de estado son esenciales para almacenar información en el contrato. Aquí están algunas de las principales variables de estado en nuestro contrato:

Arrays:

  • canicas: Almacena todas las canicas.
Canica[] public canicas;
  • carreras: Almacena todas las carreras.
Carrera[] public carreras;

Mappings:

  • apuestasPorCarreraYCanica: Asocia apuestas con carreras y canicas.
mapping(uint256 => mapping(uint256 => Apuesta[])) public apuestasPorCarreraYCanica;
  • balances: Lleva un registro de los balances de los apostadores.
mapping(address => uint256) public balances;

Variables del Propietario:

  • owner: Dirección del propietario del contrato.
address public owner;
  • comisionOwner: Comisión del propietario expresada en porcentaje.
uint256 public comisionOwner = 5;
  • acumuladoOwner: Acumulado de comisiones del propietario.
uint256 public acumuladoOwner = 0;

Eventos

Los eventos nos permiten registrar acciones significativas que ocurren en el contrato. En nuestro contrato, hemos definido eventos como CanicaAgregada, ApuestaRealizada, CarreraCreada, CarreraFinalizada y GananciasRetiradas para notificar a los usuarios y/o front-ends sobre acciones relevantes.

CanicaAgregada: Se emite cuando se agrega una nueva canica.

event CanicaAgregada(uint256 id, string nombre, string color, string ipfsBaseLink);

ApuestaRealizada: Se emite cuando un usuario realiza una apuesta.

event ApuestaRealizada(address apostador, uint256 canicaId, uint256 carreraId, uint256 cantidad);

CarreraCreada: Se emite cuando se crea una nueva carrera.

event CarreraCreada(uint256 id);

CarreraFinalizada: Se emite cuando una carrera finaliza.

event CarreraFinalizada(uint256 id, uint256 canicaGanadora);

GananciasRetiradas: Se emite cuando un usuario retira sus ganancias.

event GananciasRetiradas(address apostador, uint256 cantidad);

Modificadores

Los modificadores son herramientas poderosas que nos permiten modificar el comportamiento de las funciones:

  • soloOwner: Este modificador garantiza que solo el propietario del contrato pueda ejecutar ciertas funciones.
modifier soloOwner() {
    require(msg.sender == owner, "Solo el dueno puede llamar a esta funcion");
    _;
}

Funciones Principales

1. agregarCanica()

Esta función permite al propietario del contrato agregar una nueva canica al sistema. Se requiere especificar el nombre, color y el enlace base de IPFS para la imagen de la canica.

function agregarCanica(
    string memory _nombre,
    string memory _color,
    string memory _ipfsBaseLink
) public soloOwner {
    canicas.push(Canica(canicas.length, _nombre, _color, _ipfsBaseLink));
    emit CanicaAgregada(canicas.length, _nombre, _color, _ipfsBaseLink);
}

2. crearCarrera()

El propietario puede iniciar una nueva carrera. Una vez creada, los usuarios podrán apostar en las canicas para esta carrera.

function crearCarrera() public soloOwner {
    carreras.push(Carrera(carreras.length, true, 0));
    emit CarreraCreada(carreras.length);
}

3. apostar()

Los usuarios pueden realizar apuestas en una canica específica para una carrera en particular. Deben especificar el ID de la canica, el ID de la carrera y la cantidad que desean apostar.

function apostar(
    uint256 _canicaId,
    uint256 _carreraId,
    uint256 _cantidad
) public payable {
    require(carreras[_carreraId].activo, "La carrera no esta activa");
    require(canicas.length > _canicaId, "Canica no valida");
    require(msg.value == _cantidad, "La cantidad enviada no coincide con la apuesta");

    Apuesta memory nuevaApuesta = Apuesta({
        apostador: payable(msg.sender),
        cantidad: _cantidad
    });
    apuestasPorCarreraYCanica[_carreraId][_canicaId].push(nuevaApuesta);
    emit ApuestaRealizada(msg.sender, _canicaId, _carreraId, _cantidad);
}

4. finalizarCarrera()

El propietario puede finalizar una carrera y determinar la canica ganadora.

function finalizarCarrera(uint256 _carreraId, uint256 _canicaGanadora) public soloOwner {
    require(carreras[_carreraId].activo, "La carrera ya fue finalizada");
    carreras[_carreraId].activo = false;
    carreras[_carreraId].canicaGanadora = _canicaGanadora;

    uint256 totalApostado = 0;
    for (uint256 i = 0; i < canicas.length; i++) {
        totalApostado += totalApostadoPorCanica[_carreraId][i];
    }

    uint256 totalGanado = totalApostadoPorCanica[_carreraId][_canicaGanadora];
    uint256 comisionOwner = (totalApostado * porcentajeComision) / 100;
    acumuladoOwner += comisionOwner;
    totalApostado -= comisionOwner;

    for (uint256 i = 0; i < apuestasPorCarreraYCanica[_carreraId][_canicaGanadora].length; i++) {
        Apuesta memory apuesta = apuestasPorCarreraYCanica[_carreraId][_canicaGanadora][i];
        uint256 ganancia = (apuesta.cantidad * totalApostado) / totalGanado;
        balances[apuesta.apostador] += ganancia;
    }

    emit CarreraFinalizada(_carreraId, _canicaGanadora);
}

5. retirarGanancias()

Los usuarios que apostaron por la canica ganadora pueden retirar sus ganancias.

function retirarGanancias() public {
    uint256 cantidad = balances[msg.sender];
    require(cantidad > 0, "No tienes ganancias para retirar");
    balances[msg.sender] = 0;
    payable(msg.sender).transfer(cantidad);
    emit GananciasRetiradas(msg.sender, cantidad);
}

6. retirarComision()

El propietario del contrato puede retirar las comisiones acumuladas.

function retirarComision() public soloOwner {
    uint256 cantidad = acumuladoOwner;
    require(cantidad > 0, "No hay comisiones para retirar");
    acumuladoOwner = 0;
    payable(msg.sender).transfer(cantidad);
}

Funciones Auxiliares

1. obtenerLinkImagenCanica()

Devuelve el enlace completo de la imagen de una canica en IPFS.

function obtenerLinkImagenCanica(uint256 _canicaId) public view returns (string memory) {
    require(_canicaId < canicas.length, "Canica no valida");
    return string(abi.encodePacked(canicas[_canicaId].ipfsBaseLink, uint2str(_canicaId), ".png"));
}

2. uint2str()

Convierte un número entero sin signo (uint256) a su representación en cadena de caracteres (string).

function uint2str(uint256 _i) internal pure returns (string memory) {
    if (_i == 0) {
        return "0";
    }
    uint256 j = _i;
    uint256 length;
    while (j != 0) {
        length++;
        j /= 10;
    }
    bytes memory bstr = new bytes(length);
    uint256 k = length;
    while (_i != 0) {
        bstr[--k] = bytes1(uint8(48 + (_i % 10)));
        _i /= 10;
    }
    return string(bstr);
}

Profundizando en el Contrato

Nuestro contrato "CarrerasDeCanicas" es una muestra destacada de la ingeniería de contratos inteligentes. Hemos definido estructuras (structs) para representar canicas, apuestas y carreras, y hemos empleado mapeos (mappings) complejos para relacionar apuestas con carreras y canicas. Estos mapeos facilitan el rastreo de todas las apuestas realizadas en una carrera específica para una canica determinada, simplificando el cálculo de las ganancias y la distribución de recompensas.

Estas funciones, tanto principales como auxiliares, trabajan en conjunto para proporcionar la lógica y funcionalidad integral del contrato. Permiten a los usuarios interactuar con el contrato, realizar apuestas, y al propietario gestionar las carreras y las canicas. Además, el contrato utiliza eventos para notificar acciones relevantes, como la creación de una nueva canica o la finalización de una carrera. También se ha incorporado una función, "uint2str", que convierte un número entero sin signo en su representación de cadena, siendo crucial para construir el enlace completo a la imagen de una canica en IPFS.

Conclusión

La blockchain y los contratos inteligentes brindan una potencia y flexibilidad asombrosas para simular y mejorar sistemas del mundo real. Con la adición de un sistema de roles y la combinación de técnicas avanzadas con herramientas proporcionadas por OpenZeppelin, estamos preparados para construir aplicaciones descentralizadas seguras, confiables e interactivas. Aunque nuestro contrato es una simulación básica, demuestra el poder y la flexibilidad de los contratos inteligentes y sienta las bases para futuras expansiones y mejoras, incluida la posible integración con un front-end interactivo.


Introducción a los Roles

En cualquier sistema es esencial tener diferentes niveles de acceso para diferentes usuarios. En el mundo de los contratos inteligentes esto es especialmente crítico para garantizar la seguridad y la integridad del contrato. En nuestro contrato CarrerasDeCanicas, inicialmente, el propietario tenía control total. Sin embargo, en una situación real, podríamos querer que ciertas funciones, como iniciar o finalizar una carrera, sean manejadas por un rol específico, digamos, un "COMISARIO".

// Importando la librería AccessControl de OpenZeppelin
import "@openzeppelin/contracts/access/AccessControl.sol";

// Nuestro contrato ahora hereda de AccessControl
contract CarrerasDeCanicas is AccessControl {
    // Definimos el rol de COMISARIO
    bytes32 public constant COMISARIO_ROLE = keccak256("COMISARIO_ROLE");
    ...
}

Para implementar este sistema de roles, utilizaremos la librería AccessControl.sol de OpenZeppelin. OpenZeppelin es una de las bibliotecas más confiables y ampliamente utilizadas en el desarrollo de contratos inteligentes, ya que proporciona implementaciones seguras de funciones comunes, evitando errores comunes y vulnerabilidades.

Implementando el Rol de COMISARIO

Con AccessControl, podemos definir y gestionar roles fácilmente. Por ejemplo, para definir un nuevo rol llamado COMISARIO, simplemente creamos una constante con un valor hash único. En este ejemplo siguiente también hacemos que en la llamada al constructor adquiera también rol de DEFAULT_ADMIN_ROLE

    constructor() {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(COMISARIO_ROLE, msg.sender);
        _setupRole(TALLER_ROLE, msg.sender);
    }

El propietario del contrato, o cualquier otro rol con el permiso de admin adecuado, puede otorgar o revocar este rol a cualquier dirección:

function otorgarRolComisario(address cuenta) public {
    require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "No tiene permiso para otorgar el rol");
    grantRole(COMISARIO_ROLE, cuenta);
}

function revocarRolComisario(address cuenta) public {
    require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "No tiene permiso para revocar el rol");
    revokeRole(COMISARIO_ROLE, cuenta);
}

Una vez que una dirección tiene el rol de COMISARIO, puede iniciar o finalizar carreras. Sin embargo, no puede, por ejemplo, agregar nuevas canicas o cambiar la comisión. Esto se logra utilizando el modificador onlyRole en las funciones correspondientes:

    function crearCarrera() public {
        require(hasRole(COMISARIO_ROLE, msg.sender), "No tienes permiso para crear carreras");
        carreras.push(Carrera(carreras.length, true, 0));
    }
    function finalizarCarrera(uint256 _carreraId, uint256 _canicaGanadora) public {
        require(hasRole(COMISARIO_ROLE, msg.sender), "No tienes permiso para finalizar carreras");

        [... aquí continuaría el código igual que en el anterior ejemplo ...]
    }

Beneficios de Usar un Sistema de Roles

Utilizar un sistema de roles en contratos inteligentes ofrece varios beneficios:

  1. Segregación de Responsabilidades: Asegura que solo las partes autorizadas puedan realizar acciones específicas, lo que reduce el riesgo de acciones malintencionadas o errores.
  2. Flexibilidad: Permite adaptar el contrato a diferentes escenarios y necesidades, otorgando o revocando roles según sea necesario.
  3. Seguridad Mejorada: Al limitar el acceso a funciones críticas, se reduce la superficie de ataque del contrato.
  4. Gestión Clara: Facilita la gestión y administración del contrato al tener roles claramente definidos con responsabilidades específicas.

Al combinar la potencia de la blockchain con sistemas de roles, podemos crear aplicaciones descentralizadas más robustas, seguras y flexibles.



Enlaces y Bibliografía

OpenZeppelin - AccessControl: Una librería para gestionar roles y permisos en contratos inteligentes.

OpenZeppelin - Documentación general: Una fuente completa de información sobre las herramientas y librerías ofrecidas por OpenZeppelin.

Ethereum Solidity: Documentación oficial del lenguaje de programación utilizado para escribir contratos inteligentes en Ethereum.

Introducción a los Contratos Inteligentes: Una guía introductoria sobre qué son los contratos inteligentes y cómo funcionan.

Gestión de permisos en DApps: Un artículo que explora la importancia de la gestión de permisos en aplicaciones descentralizadas.

IPFS - Sistema de Archivos Interplanetario: Una explicación y guía sobre cómo usar IPFS para almacenar y recuperar datos en la blockchain.


Estos enlaces proporcionan una combinación de documentación oficial, guías y artículos que pueden ayudar a los lectores a profundizar en los temas tratados y expandir su conocimiento sobre contratos inteligentes, gestión de roles y desarrollo en la blockchain.