Índice
- Introducción
- Contratos inteligentes
- Tipos de lenguajes
- Usando el lenguaje Solidity
- Entendiendo el ejemplo Ballot (votaciones)
- Entendiendo como funciona el código de la votación
- Votación paso a paso
- Limitaciones
- Conclusión
Introducción
Este es el tercer tutorial sobre criptomonedas donde vamos a ver cómo empezar con contratos inteligentes y el IDE Remix. Si te apetece, antes de seguir, puedes revisar las publicaciones anteriores a ésta; aquí tienes los enlaces:
Cómo comprar e invertir criptomonedas en Binance: https://adictosaltrabajo.com/2022/01/14/como-comprar-e-invertir-criptomonedas-en-binance/
Entendiendo las criptomonedas: https://adictosaltrabajo.com/2022/01/24/entendiendo-las-criptomonedas/
Usando la wallet MetaMask y la red local Ganache: https://adictosaltrabajo.com/2022/02/09/usando-la-wallet-metamask-y-la-red-local-ganache/
Compilación y despliegue de contratos inteligentes con Truffle y Ganache: https://adictosaltrabajo.com/2022/02/16/compilacion-y-despliegue-de-contratos-inteligentes-con-truffle-y-ganache/
Creando tokens Ethereum: https://adictosaltrabajo.com/2022/03/14/creando-tokens-ethereum/
He de recordaros que estoy compartiendo simplemente lo que voy aprendiendo, así que solo os ofrezco mi proceso de aprendizaje y exploración, por si os ayuda, no la visión de un experto.
Recapitulando un poco las opciones de “intentar” ganar dinero con criptomonedas puedes: comprarlas y venderlas más caras (especular), invertir con ellas (staking o depósitos), crear una criptomoneda (clonar y/o crear una red propia por lo que serías el dueño inicial de las monedas y convencer a gente para que compre), adquirir y vender tokens (de esto ya hablaremos otro día), minar una red existente (contribuir en las pruebas de esfuerzo) o también puedes vender servicios de desarrollo de software (que es a lo que me dedico). Podrías ser programador de contratos inteligentes que auguro que existirá una alta demanda y bien pagada, como de todo lo sofisticado, aunque este mundo cambie mucho en los próximos años.
Sigo insistiendo que antes de invertir en criptomonedas seas muy consciente que puedes perder todo el dinero que metas, así que ojito. Con esto de los contratos pasa lo mismo, si los haces mal puedes perder mucho dinero (o hacérselo perder a otros) aportando poco valor por lo que no parece mala idea tener cerca a gente que realmente sepa.
Contratos inteligentes
Un contrato inteligente “a grandes rasgos” es una porción de código que vincula a dos (o más) partes, y que es almacenado y ejecutado dentro de la red de bloques encadenados. Para que no se inunde la red de bloques de porciones de código se establece que la ejecución tiene un coste llamado gas.
Por tanto, un contrato inteligente tiene un ciclo de vida. Este se puede resumir en: negociación contractual clásica (fuera del código), transcripción a código de las condiciones contractuales (no se os olviden los test), compilación y depuración, optimización (que consuma el menor gas posible), revisión de seguridad (se puede liar una gorda porque el código estará visible para toda la cadena, aunque sea en byte code, en versión compilada), despliegue en la red y su congelación (aceptación por la mayoría de los nodos de la red), ejecución (siendo consciente del coste en gas), actualización del estado de los activos (principalmente la transferencia de criptomonedas o el registro de datos persistentes) y desactivación.
Esto tiene su tela, por lo que os sugiero ser pacientes (me está costando encontrar un sitio donde leerlo ordenadamente). He encontrado un artículo muy interesante que os recomiendo: An Overview on Smart Contracts: Challenges, Advances and Platforms https://www.henrylab.net/wp-content/uploads/2019/12/SmartContractFGCS__arXiv_.pdf
Si queremos programar, lo primero que deberíamos hacer es visitar una de las fuentes sobre los contratos inteligentes en Ethereum: https://ethereum.org/es/developers/docs/smart-contracts/
Tipos de lenguajes
Podemos ver que un contrato inteligente se construye con un lenguaje llamado “Solidity”. Si estás acostumbrado a programar en otros lenguajes veréis que es bastante intuitivo: altamente tipado, orientado a objetos…
Es más, tenemos otros lenguajes alternativos (https://ethereum.org/es/developers/docs/smart-contracts/languages/). Existe otro lenguaje llamado Vyper, más orientado a programadores Python (más complejo y con más control sobre el gas gastado) e incluso un tercero llamado Yul. Obviamente tendríamos que empezar por el entorno más sencillo y descriptivo antes de plantearnos trabajar con lenguajes más complejos.
En la propia documentación podéis encontrar comparativas sobre los lenguajes:
Usando el lenguaje Solidity
Vamos a empezar por el lenguaje aparentemente más sencillo: Solidity.
Parece lógico que, si vamos a programar, lo primero que intentemos conseguir es un entorno de desarrollo con el que nos podamos encontrar cómodos y revisar la documentación del lenguaje https://docs.soliditylang.org/en/latest/.
También he encontrado un libro con la documentación de Solidity en formato pdf: https://buildmedia.readthedocs.org/media/pdf/solidity/develop/solidity.pdf
Vamos a revisar el entorno de desarrollo Remix y ver los ejemplos que vienen por defecto. No hay que instalar nada y basta con visitar https://remix.ethereum.org/. Yo empecé a jugar con el navegador Safari en Mac y no me funcionaba demasiado bien (no me permitía cambiar el nombre de ficheros). He pasado a Chrome y parece que me funciona mejor. Os recomiendo que reviséis que el ordenador y navegador están actualizados a las últimas versiones.
Este es el aspecto de una fuente básica en Solidity:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
}
En el lateral izquierdo vemos las opciones disponibles. La primera es la de revisión de ficheros.
La segunda opción es la de compilación. Podemos pulsar compilar y ver el resultado. Es sorprendentemente lo descriptivo del entorno cuando hay un error.
La siguiente opción es ejecución. Podemos elegir el entorno.
También disponemos de una lista de cuentas y el Ether disponible.
La cuenta que lance el contrato verá disminuido el gas.
Al pulsar deploy veremos cómo aparecen los métodos disponibles. En nuestro ejemplo tan simple veremos que podemos invocar a Store y Retrieve (almacenar y recuperar).
Podemos comprobar cómo disminuye nuestra cantidad de moneda en cada invocación.
También podremos observar que las consultas sin operativa (view o vista) no consumen Ether.
Con esto ya tenemos lo básico, aunque debemos recorrer el sistema de ficheros para entender el resto de los elementos y ejemplos.
Cuando se trabaja medianamente bien, primero se programan los test de un sistema y los test nos demandan las funciones que tenemos que crear (leed un poquito sobre TDD, si no os es familiar).
Entendiendo el ejemplo Ballot (votaciones)
En el ejemplo de Ballot (votaciones) podemos descubrir estos conceptos y algunos más completos. Os recomiendo este tutorial (realmente es una cadena de tutoriales): https://medium.com/coinmonks/voting-on-a-blockchain-how-it-works-3bb41582f403
En otros tutoriales ya lo haremos paso a paso (empezar por el test) pero ahora solo vamos a tratar de entender el código de votaciones.
Primero veamos el código del test:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; import "remix_tests.sol"; // this import is automatically injected by Remix. import "../contracts/3_Ballot.sol"; contract BallotTest { bytes32[] proposalNames; Ballot ballotToTest; function beforeAll () public { proposalNames.push(bytes32("candidate1")); ballotToTest = new Ballot(proposalNames); } function checkWinningProposal () public { ballotToTest.vote(0); Assert.equal(ballotToTest.winningProposal(), uint(0), "proposal at index 0 should be the winning proposal"); Assert.equal(ballotToTest.winnerName(), bytes32("candidate1"), "candidate1 should be the winner name"); } function checkWinninProposalWithReturnValue () public view returns (bool) { return ballotToTest.winningProposal() == 0; } }
Inicialmente se importa el fichero de remix_tests.sol que es el fichero de soporte donde están los métodos para las comprobaciones.
Luego se importa el fichero ../contracts/3_Ballot.sol donde se encuentran nuestras fuentes reales que queremos comprobar (el contrato inteligente en sí). El test es un poquito pobre porque sólo verifica una funcionalidad base.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; /** * @title Ballot * @dev Implements voting process along with vote delegation */ contract Ballot { struct Voter { uint weight; // weight is accumulated by delegation bool voted; // if true, that person already voted address delegate; // person delegated to uint vote; // index of the voted proposal } struct Proposal { // If you can limit the length to a certain number of bytes, // always use one of bytes1 to bytes32 because they are much cheaper bytes32 name; // short name (up to 32 bytes) uint voteCount; // number of accumulated votes } address public chairperson; mapping(address => Voter) public voters; Proposal[] public proposals; /** * @dev Create a new ballot to choose one of 'proposalNames'. * @param proposalNames names of proposals */ constructor(bytes32[] memory proposalNames) { chairperson = msg.sender; voters[chairperson].weight = 1; for (uint i = 0; i < proposalNames.length; i++) { // 'Proposal({...})' creates a temporary // Proposal object and 'proposals.push(...)' // appends it to the end of 'proposals'. proposals.push(Proposal({ name: proposalNames[i], voteCount: 0 })); } } /** * @dev Give 'voter' the right to vote on this ballot. May only be called by 'chairperson'. * @param voter address of voter */ function giveRightToVote(address voter) public { require( msg.sender == chairperson, "Only chairperson can give right to vote." ); require( !voters[voter].voted, "The voter already voted." ); require(voters[voter].weight == 0); voters[voter].weight = 1; } /** * @dev Delegate your vote to the voter 'to'. * @param to address to which vote is delegated */ function delegate(address to) public { Voter storage sender = voters[msg.sender]; require(!sender.voted, "You already voted."); require(to != msg.sender, "Self-delegation is disallowed."); while (voters[to].delegate != address(0)) { to = voters[to].delegate; // We found a loop in the delegation, not allowed. require(to != msg.sender, "Found loop in delegation."); } sender.voted = true; sender.delegate = to; Voter storage delegate_ = voters[to]; if (delegate_.voted) { // If the delegate already voted, // directly add to the number of votes proposals[delegate_.vote].voteCount += sender.weight; } else { // If the delegate did not vote yet, // add to her weight. delegate_.weight += sender.weight; } } /** * @dev Give your vote (including votes delegated to you) to proposal 'proposals[proposal].name'. * @param proposal index of proposal in the proposals array */ function vote(uint proposal) public { Voter storage sender = voters[msg.sender]; require(sender.weight != 0, "Has no right to vote"); require(!sender.voted, "Already voted."); sender.voted = true; sender.vote = proposal; // If 'proposal' is out of the range of the array, // this will throw automatically and revert all // changes. proposals[proposal].voteCount += sender.weight; } /** * @dev Computes the winning proposal taking all previous votes into account. * @return winningProposal_ index of winning proposal in the proposals array */ function winningProposal() public view returns (uint winningProposal_) { uint winningVoteCount = 0; for (uint p = 0; p < proposals.length; p++) { if (proposals[p].voteCount > winningVoteCount) { winningVoteCount = proposals[p].voteCount; winningProposal_ = p; } } } /** * @dev Calls winningProposal() function to get the index of the winner contained in the proposals array and then * @return winnerName_ the name of the winner */ function winnerName() public view returns (bytes32 winnerName_) { winnerName_ = proposals[winningProposal()].name; } }
El test crea un array de candidatos propuestos (bytes32[] proposalNames).
Posteriormente, antes de la ejecución de los test (beforeAll), se crea el objeto Ballot (votación, ballotToTest = new Ballot(proposalNames)) inicializándolo con el elemento “candidato1” (proposalNames.push(bytes32(«candidate1”))).
Al lanzarse el test (se ejecutan todos los métodos que comienzan con check).
Dentro del test se vota al primer candidato (del índice 0, ballotToTest.vote(0)) y se verifican las propuestas y el ganador de la votación.
bytes32[] proposalNames; Ballot ballotToTest; function beforeAll () public { proposalNames.push(bytes32("candidate1")); ballotToTest = new Ballot(proposalNames); } function checkWinningProposal () public { ballotToTest.vote(0); Assert.equal(ballotToTest.winningProposal(), uint(0), "proposal at index 0 should be the winning proposal"); Assert.equal(ballotToTest.winnerName(), bytes32("candidate1"), "candidate1 should be the winner name"); }
Para poder ejecutar la aplicación de votaciones con normalidad desde el entorno, tenemos que hacer una pequeña cosa que no he visto evidente en los manuales, que es hacer un deploy con unos datos iniciales.
Si nos fijamos en el código el constructor de la clase requiere un array de nombres de las opciones a votar:
/** * @dev Create a new ballot to choose one of 'proposalNames'. * @param proposalNames names of proposals */ constructor(bytes32[] memory proposalNames) { chairperson = msg.sender; voters[chairperson].weight = 1; for (uint i = 0; i < proposalNames.length; i++) { // 'Proposal({...})' creates a temporary // Proposal object and 'proposals.push(...)' // appends it to the end of 'proposals'. proposals.push(Proposal({ name: proposalNames[i], voteCount: 0 })); } }
Escribimos los datos de las tres opciones (ya nos preocuparemos en otro tutorial, invocándolo desde Java Script, de que sean datos legibles).
Ver referencia: https://ethereum.stackexchange.com/questions/50310/how-to-pass-the-value-in-bytes32-array.
["0x1234567890123456789100000000000000000000000000000000000000000000", "0x1234567890123456789200000000000000000000000000000000000000000000", "0x1234567890123456789300000000000000000000000000000000000000000000"]
Con esto, ya podemos lanzar el contrato de votaciones y cacharrear.
Si vemos el análisis de código estático es un poco preocupante, lo que significa que tenemos todavía mucho que estudiar.
Entendiendo como funciona el código de la votación
He traducido los comentarios en el código para que sea más comprensible.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; /** * @title Ballot * @dev Implementación de votación con delegación incluida */ contract Ballot { struct Voter { uint weight; // Peso acumulado de su voto contando delegación bool voted; // si true es que ya ha votado address delegate; // la persona a la que delega uint vote; // índice de la proposición votada } struct Proposal { // Si puedes limitar la longitud en numero de bytes, // usar uno de bytes1 aa bytes32 porque es más barato bytes32 name; // nombre corto de la propuesta uint voteCount; // número acumulado de votos } address public chairperson; // la persona responsable mapping(address => Voter) public voters; // las personas con derecho a voto (son Proposal[] public proposals; // array de propuestas /** * @dev Crear una nueva votación a partir del nombre de las propuestas. * @param nombre de las propuestas */ constructor(bytes32[] memory proposalNames) { chairperson = msg.sender; // el jefe es el que envia el contrato voters[chairperson].weight = 1; // el votante con el indice del chairman tiene un peso 1 for (uint i = 0; i < proposalNames.length; i++) { // se van sumando las propuestas con voto 0 inicial proposals.push(Proposal({ name: proposalNames[i], voteCount: 0 })); } } /** * @dev dar al 'voter' el derecho a votar en esta votación. Solo el 'chairperson'. * @param dirección del votante */ function giveRightToVote(address voter) public { require( msg.sender == chairperson, // quien invoca esta funcion solo puede el chairman "Solo el chairman puede ddar el derecho a voto." ); require( !voters[voter].voted, "El votante ya ha votado." ); require(voters[voter].weight == 0); voters[voter].weight = 1; } /** * @dev Delegar el voto al votante 'to'. * @param la dirección a quien se delega el voto */ function delegate(address to) public { Voter storage sender = voters[msg.sender]; // se recupera el objeto votante a partir de la dirección del que envía require(!sender.voted, "Tu ya has votado."); // verificar que no ha votado require(to != msg.sender, "La auto-delegación no está permitida."); // el votante no puede delegarse a si mismo while (voters[to].delegate != address(0)) { // comprobar que no hay bucles en delegación to = voters[to].delegate; // ojito con bucles en contratos que pueden costar caros. require(to != msg.sender, "Encontrado un bucle en la delegación."); } sender.voted = true; // se marca como votado sender.delegate = to; // se asigna delegado Voter storage delegate_ = voters[to]; // se actualiza al que delega if (delegate_.voted) { // si el delegado ya ha votado añadir un voto proposals[delegate_.vote].voteCount += sender.weight; } else { // si el delegado no ha votado todavía, aumentar su peso delegate_.weight += sender.weight; } } /** * @dev dar el voto (incluidos los delegaados) a la propuesta 'proposals[proposal].name'. * @param indice de la propuesta en el array de propuestas */ function vote(uint proposal) public { Voter storage sender = voters[msg.sender]; // recupera el votante require(sender.weight != 0, "No tiene derecho a voto"); // require(!sender.voted, "Ya ha votado."); sender.voted = true; sender.vote = proposal; // Si la 'propuesta' está fuera del rango del aarray, //esto revertirá el cambio. proposals[proposal].voteCount += sender.weight; } /** * @dev Calcular la propuesta ganadora. * @return winningProposal_ Indice de la proposición ganadora */ function winningProposal() public view returns (uint winningProposal_) { uint winningVoteCount = 0; for (uint p = 0; p < proposals.length; p++) { if (proposals[p].voteCount > winningVoteCount) { winningVoteCount = proposals[p].voteCount; winningProposal_ = p; } } } /** * @dev Invoca winningProposal() que recupeera el indice de la propuesta ganadora * @return winnerName_ el nombre deel ganador */ function winnerName() public view returns (bytes32 winnerName_) { winnerName_ = proposals[winningProposal()].name; } }
¿Cómo funciona la votación?
El chairman (el jefe) arranca el contrato proponiendo las opciones a votar.
Se inicializa el contrato y se añade la lista de votantes.
Cada votante vota o delega el voto (solo tenemos que cambiar la cuenta activa que llama a la función).
Superado el tiempo de votación se contabiliza el resultado total (en este ejemplo no hay tiempo máximo).
El contrato inteligente se desactiva pasado ese momento quedando la información invariable en el tiempo (tampoco lo cubre en este ejemplo).
Votación paso a paso
Veamos paso a paso como se hace en las pantallas, que al principio puede parecer un poco confuso:
Necesitamos unas opciones de votación y elegir un Chairman (la cuenta con la que hace el Deploy).
En cuentas pinchamos sobre el primer elemento y copiamos las otras tres primeras cuentas del fichero auxiliar y las pegamos en la opción de Deploy.
Tenemos que recordar que nuestro Chairman termina en C4 para ir siguiendo el ejemplo. Pulsamos el botón de deploy.
Podemos ver en la consola el resultado de la transacción, el gas gastado y otros elementos.
Ahora tenemos que, con la cuenta del Chairman seleccionada, dar el derecho de voto a los votantes que queramos (las otras cuentas que tenemos registradas. Estas son las cuentas que usaremos:
Chairman 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 Primero 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 Segundo 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db Tercero 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB
Vamos cambiando el valor de giveRigthToVote a las otras tres cuentas sucesivamente y haciendo la llamada.
Y repetimos con las tres cuentas.
Ahora cambiamos de cuenta a la que termina en b2 y en delegar (Delegate) elegimos la terminada en db:
Ahora que ya tenemos dados de alta los votantes, y las delegaciones, volvemos a elegir una a una las cuentas y votamos la opción por índice: la 0, 1 o 2.
Si elegimos una cuenta de invocación delegada nos dará error diciendo que no se puede votar.
Esta es las salida que nos encontramos.
Ya solo nos queda convocar a la función winnerName y vemos debajo el nombre de la opción ganadora.
Y esta es la salida.
Podéis leer artículos interesantes sobre las limitaciones de esta votación en: https://medium.com/coinmonks/voting-on-a-blockchain-solidity-contract-codes-explained-c677996d94f2
Limitaciones
Algunas posible limitaciones son: ser un punto simple de fallo, la capacidad de consumir el gas sin terminar con gran número de votantes, consumo infinito de gas, el posible problema de tiempo de espera (concurrencia), etc.
Conclusión
Como podéis comprobar, la construcción de contratos inteligentes no es algo trivial. Solo hemos escarbado un poquito y todavía lo estamos haciendo mal (el ejemplo no es muy completo) y estamos trabajando en un único IDE simulando todo.
En siguientes tutoriales tendremos que ir viendo otros componentes de la arquitectura como serán monederos externos, simuladores de red de cadenas de bloques, optimizados, etc.
Para complementar este tutorial os dejo algunos recursos muy interesantes para continuar la formación:
https://yos.io/2019/11/10/smart-contract-development-best-practices/
https://www.youtube.com/watch?v=JP-dzoDmJFw
https://www.youtube.com/watch?v=coQ5dg8wM2o