Aprende a programar contratos inteligentes con Solidity

Solidity - 16 de julio de 2022

Introducción

Un smart contract o contrato inteligente es un programa que vive en un sistema descentralizado como ethereum. El objetivo de este programa es la de actuar como contrato y verificar que las condiciones pactadas entre las partes involucradas se cumplen correctamente sin necesidad de intermediarios. Como los contratos están en un sistema descentralizado, una vez desplegado no se puede ni modificar ni eliminar.

Los smart contract, nos abre una puerta nueva en el desarrollo de software. Sí, estamos hablando de web 3 y de las DApps o aplicaciones descentralizadas que se gestionan con los smart contract.


Te recomiendo que veas los primeros 35 minutos de esta conferencia antes de continuar, la conferencia es de 2018, pero para entender la arquitectura y el potencial que tiene esta tecnología es suficiente.

Si quieres profundizar más, puedes ver esta conferencia un poco más tecnica que explica como funciona una red descentralizada.

Solidity

Solidity es el lenguaje que se utiliza para programar smart contract en la red de ethereum. Es un lenguaje orientado a contratos, está basado en Javascript. A día de hoy, muchas de las redes descentralizadas son compatibles con Solidity, es una muy buena opción para empezar a crear tus primeros smart contract.

Ejemplo de contrato escrito en solidity:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;

// Contrato para tener un registro de las adopciones de mascotas
contract Adoption {
  address[16] public adopters;

  // Registra una adopción
  function adopt(uint petId) public returns (uint) {
    require(petId >= 0 && petId <= 15);

    adopters[petId] = msg.sender;

    return petId;
  }

  // Devuelve los dueños de las mascotas
  function getAdopters() public view returns (address[16] memory) {
    return adopters;
  }

}

Conceptos básicos de Solidity

Estado

El estado del contrato son los datos que se quedan almacenados dentro del contrato y que se pueden consultar en cualquier momento. Son las variables declaradas a nivel del contrato. En el ejemplo anterior la variable adopters se está guardando en el estado del contrato.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;

// Contrato para tener un registro de las adopciones de mascotas
contract Adoption {
  address[16] public adopters;
  
  //statements

}

Funciones

Sintaxis de las funciones:

function function-name(parameter-list) scope returns() {
   //statements
}

Funciones view

Las funciones del tipo view sirven para garantizar que ese método solo devuelve datos y que en ningún momento modifica el estado del contrato.

function getAdopters() public view returns (address[16] memory) {
	return adopters;
}

Funciones payable

Las funciones payable son las que permite enviar criptomonedas al contrato. El número de las criptomonedas enviadas se puede encontrar en la variable global msg.value . Estas funciones se definen con payable.

function pay() public payable {
	value = msg.value;
}

Modificadores

Los modificadores se utilizan para modificar el comportamiento de una función. Un ejemplo muy típico de un modificador es cuando solo queremos que una función solo la pueda ejecutar el creador del contrato.

contract ContractExample {
  address owner;
  uint price;
  
  constructor() public {
    owner = msg.sender;
  }

  modifier onlyOwner {
    require(msg.sender == owner);
    _;
  }

  function changePrice(uint _price) public onlyOwner {
    price = _price;
  }
}

Eventos

El contrato puede emitir eventos y pueden ser capturados desde cualquier sistema.

//Declare an Event
event Deposit(address indexed _from, bytes32 indexed _id, uint _value);

//Emit an event
emit Deposit(msg.sender, _id, msg.value);

Variables globales

Estos son las principales variables globales que tienes disponibles dentro de un contrato, si quieres ver todas las variables globales puedes verlo en la documentación oficial

VariableDescripción
block.coinbaseDirección actual del minero del bloque
block.difficultyDificultad del bloque actual
block.gaslimitLímite de gas del bloque actual
block.numberNúmero de bloque actual
block.timestampTimestamp del bloque
gasleft() returnsGas restante
msg.dataDatos completos de la llamada
msg.senderRemitente del mensaje (dirección que llama actualmente)
msg.sigPrimeros cuatro bytes de los datos de llamada (identificador de función)
msg.valueNúmero de wei enviados con el mensaje
tx.gaspricePrecio del gas de la transacción
tx.originRemitente de la transacción

Si has llegado hasta aquí ya sabes los cuatro conceptos básicos de Solidity con los que ya vas a poder crear tus primeros contratos, te recomiendo que profundices más sobre Solidity, te dejo unos enlaces que servirán de ayuda:

Truffle suite

Truffle suite es un conjunto de herramientas que nos facilita la vida a la hora de crear nuestros contratos inteligentes. Nos ofrecen tres herramientas: truffle, ganache y drizzel.

Truffle

Es el entorno de desarrollo en el que nos facilita la vida a la hora de programar, testear y desplegar nuestros contratos.

Documentación oficial

Ganache

Con Ganache podemos tener nuestra propia red Ethereum en nuestro local fácilmente, es muy útil para desplegar nuestros contratos y para el desarrollo de nuestras DApps.

Documentación oficial

Drizzel

Drizzel es una librería para el frontend, nos ofrece componentes, llamadas a nuestros contratos, etc. Actualmente, solo tiene componentes para React.

Documentación oficial

Nuestro primer smart contract

Para crear nuestro primer smart contract vamos a utilizar Truffle y Ganache.

Requisitos

Requisitos para utilizar truffle:

Inicialización

Instalar truffle en global, ejecutando el comando:

$ npm install -g truffle

Crear la carpeta del proyecto:

$ mkdir fluuo-contracts
$ cd fluuo-contracts

Hay varias maneras para inicializar el proyecto, podemos utilizar truffle unbox para crear un proyecto con un contrato y test de ejemplo:

$ truffle unbox pet-shop

o podemos utilizar truffle init para crear un proyecto vacío:

$ truffle init

Ya tenemos nuestro proyecto creado, vamos a ver un poco la estructura del proyecto.

Estructura del proyecto

  • contracts/: Directorio de los contratos
  • migrations/: Directorio de los scripts para los deploys
  • test/: Directorio de los test
  • truffle-config.js: Fichero de configuración de Truffle

Creando el contrato inteligente

Vamos a crear un sistema de votación sencillo con nuestro smart contract.

Creamos el fichero contacts/SimpleBallot.sol , todos los ficheros en solidity tienen que tener definidos la licencia y la version de solidity en este caso la 0.8.11:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;

contract SimpleBallot {
  
}

Declaramos la estructura Participant:

struct Participant {
  bool voted;
  bool exists;
}

Creamos el estado de nuestro contrato:

// flag para comprobar que ya se ha inicializado el contrato
bool private initialized;

// direccion del creador del contrato
address public owner;

// opciones de la votación
string[] public options;

// contador de votos
mapping(string => uint) public votes;

// participantes que tienen derecho aa voto
mapping(address => Participant) public participants;

En el constructor del contrato guardaremos la dirección que ha ejecutado el contrato por primera vez para poder consultarlo, más adelante.

constructor() {
  owner = msg.sender;
}

Crearemos un método para inicializar el sistema al que le pasáramos las opciones de votación y las direcciones de los votantes. También vamos a añadirle los modificadores para comprobar que el que ejecuta el método es el creador del contrato, que no ha sido inicializado anteriormente y que se le están pasando al menos una opción y un participante.

modifier onlyOwner() {
  require(msg.sender == owner, "Only owner can initialize SimpleBallot");
  _;
}

modifier onlyNotInitialized(string[] calldata _options, address[] calldata _participans) {
  require(!initialized, "SimpleBallot already initialized");
  require(_options.length > 0, "At least one option must be provided");
  require(_participans.length > 0, "At least one participant must be provided");
  _;
}

function initialize(string[] calldata _options, address[] calldata _participans)
  public onlyOwner onlyNotInitialized(_options, _participans) {
  
  options = _options;
  initialized = true;

  for (uint i = 0; i < _participans.length; i++) {
    participants[_participans[i]] = Participant({
      voted: false,
      exists: true
    });
  }
}

Vamos a crear el método vote, que solo podrá ser ejecutado por los participantes y solo podrá votar una vez.

modifier onlyWithVotingRights() {
  require(participants[msg.sender].exists, 'You are not a participant');
  require(!participants[msg.sender].voted, 'You have already voted');
  _;
}

function vote(uint _option) public onlyWithVotingRights {
	participants[msg.sender].voted = true;
  votes[options[_option]]++;
}

Por ultimo, vamos a crear un par de funciones del tipo view , que nos permite obtener las opciones y otra que hace el conteo de los votos.

function getOptions() public view returns (string[] memory) {
  return options;
}

function getVotes() public view returns (uint[] memory) {
  uint[] memory result = new uint[](options.length);
  for (uint i = 0; i < options.length; i++) {
    result[i] = votes[options[i]];
  }
  return result;
}

Y aquí tienes el contrato completo:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;

/// @title Votación simple
contract SimpleBallot {

  struct Participant {
    bool voted;
    bool exists;
  }

  bool private initialized; // flag para comprobar que ya se ha inicializado el contrato
  address public owner; // direccion del creador del contrato
  string[] public options; // opciones de la votación
  mapping(string => uint) public votes; // contador de votos
  mapping(address => Participant) public participants; // participantes que tienen derecho aa voto
  
  
  constructor() {
    owner = msg.sender;
  }

  modifier onlyOwner() {
    require(msg.sender == owner, "Only owner can initialize SimpleBallot");
    _;
  }

  modifier onlyWithVotingRights() {
    require(participants[msg.sender].exists, 'You are not a participant');
    require(!participants[msg.sender].voted, 'You have already voted.');
    _;
  }

  modifier onlyNotInitialized(string[] calldata _options, address[] calldata _participans) {
    require(!initialized, "SimpleBallot already initialized");
    require(_options.length > 0, "At least one option must be provided");
    require(_participans.length > 0, "At least one participant must be provided");
    _;
  }

  // inicializa el contrato, solo puede ser ejecutado por el creador y solo se puede ejecutar una vez
  function initialize(string[] calldata _options, address[] calldata _participans)
    public onlyOwner onlyNotInitialized(_options, _participans) {
    
    options = _options;
    initialized = true;

    for (uint i = 0; i < _participans.length; i++) {
      participants[_participans[i]] = Participant({
        voted: false,
        exists: true
      });
    }
  }

  // vota por una opcion, solo puede ser llamado una vez por participante
  function vote(uint _option) public onlyWithVotingRights {
    participants[msg.sender].voted = true;
    votes[options[_option]]++;
  }

  function getOptions() public view returns (string[] memory) {
    return options;
  }

  function getVotes() public view returns (uint[] memory) {
    uint[] memory result = new uint[](options.length);
    for (uint i = 0; i < options.length; i++) {
      result[i] = votes[options[i]];
    }
    return result;
  }
}

Desplegando el contrato en una red local

Para tener tu red de ethereum en local tienes que instalar Ganache.

Para desplegar nuestro contrato tenemos que crear el fichero migrations/1_initial_-simple-ballot.js.

var SimpleBallot = artifacts.require("./SimpleBallot.sol");

module.exports = function(deployer) {
  deployer.deploy(SimpleBallot);
};

una vez creado el fichero, podemos desplegar nuestro contrato ejecutando el comando truffle migrate

Ahora nos faltaría testear nuestra aplicación, pero será en otro artículo. Si quieres seguir leyendo sobre los smart contracts te dejo este tutorial de truffle.