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
Variable | Descripción |
---|---|
block.coinbase | Dirección actual del minero del bloque |
block.difficulty | Dificultad del bloque actual |
block.gaslimit | Límite de gas del bloque actual |
block.number | Número de bloque actual |
block.timestamp | Timestamp del bloque |
gasleft() returns | Gas restante |
msg.data | Datos completos de la llamada |
msg.sender | Remitente del mensaje (dirección que llama actualmente) |
msg.sig | Primeros cuatro bytes de los datos de llamada (identificador de función) |
msg.value | Número de wei enviados con el mensaje |
tx.gasprice | Precio del gas de la transacción |
tx.origin | Remitente 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:
- Documentación oficial Solidity (ingles)
- Libro Solidity para principiantes (ingles)
- Libro Solidity en español
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.
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.
Drizzel
Drizzel es una librería para el frontend, nos ofrece componentes, llamadas a nuestros contratos, etc. Actualmente, solo tiene componentes para React.
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 contratosmigrations/
: Directorio de los scripts para los deploystest/
: Directorio de los testtruffle-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.