Incluyendo una muestra de código de este poderoso esquema de tokenómica.
Cualquier error que cometo es una inversión en mi futuro. —Rosa Namajunas
Se dice que alguien tiene una participación en una empresa cuando aporta algunos activos a cambio de ejercer cierto nivel de control, influencia o participación en sus actividades.
En el mundo de las criptomonedas esto se entiende como otorgar a los usuarios algún tipo de derecho o recompensa mientras no transfieran algunos tokens en su poder. Un mecanismo de participación generalmente fomenta la tenencia de tokens frente al comercio de tokens, lo que a su vez se espera que aumente la valoración del token.
En TechHQ creemos que el conocimiento existe para ser compartido, y en este artículo vamos a mostrar cómo implementar un mecanismo de staking en solidez. Todo el proyecto, incluido el entorno de desarrollo y las pruebas, está disponible en nuestro github público .
Para construir este mecanismo de participación necesitaremos:
Sigamos con eso.
Se puede crear un token de replanteo como un token ERC20. Voy a necesitar SafeMath y Ownable más adelante, así que vamos a importarlos y usarlos también.
pragma solidity ^ 0.5 .0 ; import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol" ; import "openzeppelin-solidity/contracts/math/SafeMath.sol" ; import "openzeppelin-solidity/contracts/ownership/Ownable.sol" ; /** * @title Staking Token (STK) * @author Alberto Cuesta Canada * @notice Implements a basic ERC20 staking token with incentive distribution. */ contract StakingToken is ERC20, Ownable { using SafeMath for uint256; /** * @notice The constructor for the Staking Token. * @param _owner The address to receive all tokens on construction. * @param _supply The amount of tokens to mint on construction. */ constructor (address _owner, uint256 _supply) public { _mint(_owner, _supply); }
Eso es todo, no se requiere nada más.
En esta implementación vamos a realizar un seguimiento de las partes interesadas para facilitar una distribución sólida de incentivos más adelante. En teoría, sería posible no realizar un seguimiento de ellos como lo haría un token ERC20 normal, pero en la práctica es difícil garantizar que las partes interesadas no jueguen con el sistema de distribución si no se realiza un seguimiento.
Para la implementación, solo usaremos una matriz dinámica de direcciones de partes interesadas.
/** * @notice We usually require to know who are all the stakeholders. */ address[] internal stakeholders;
Los siguientes métodos agregan una parte interesada, eliminan una parte interesada y verifican si una dirección pertenece a una parte interesada. Seguramente son posibles otras implementaciones más eficientes, pero me gusta esta por su legibilidad.
/** * @notice A method to check if an address is a stakeholder. * @param _address The address to verify. * @return bool, uint256 Whether the address is a stakeholder, * and if so its position in the stakeholders array. */ function isStakeholder ( address _address ) public view returns ( bool, uint256 ) { for (uint256 s = 0 ; s < stakeholders.length; s += 1 ){ if (_address == stakeholders[s]) return ( true , s); } return ( false , 0 ); } /** * @notice A method to add a stakeholder. * @param _stakeholder The stakeholder to add. */ function addStakeholder ( address _stakeholder ) public { (bool _isStakeholder, ) = isStakeholder(_stakeholder); if (!_isStakeholder) stakeholders.push(_stakeholder); } /** * @notice A method to remove a stakeholder. * @param _stakeholder The stakeholder to remove. */ function removeStakeholder ( address _stakeholder ) public { (bool _isStakeholder, uint256 s) = isStakeholder(_stakeholder); if (_isStakeholder){ stakeholders[s] = stakeholders[stakeholders.length - 1 ]; stakeholders.pop(); } }
Una participación en su forma más simple deberá registrar el tamaño de la participación y el titular de la participación. Una implementación realmente simple de esto podría ser solo un mapeo desde la dirección de la parte interesada hasta el tamaño de la participación.
/** * @notice The stakes for each stakeholder. */ mapping( address => uint256) internal stakes;
Seguiré los nombres de funciones de ERC20 y crearé equivalentes para obtener los datos del mapeo de apuestas.
/** * @notice A method to retrieve the stake for a stakeholder. * @param _stakeholder The stakeholder to retrieve the stake for. * @return uint256 The amount of wei staked. */ function stakeOf ( address _stakeholder ) public view returns ( uint256 ) { return stakes[_stakeholder]; } /** * @notice A method to the aggregated stakes from all stakeholders. * @return uint256 The aggregated stakes from all stakeholders. */ function totalStakes ( ) public view returns ( uint256 ) { uint256 _totalStakes = 0 ; for (uint256 s = 0 ; s < stakeholders.length; s += 1 ){ _totalStakes = _totalStakes.add(stakes[stakeholders[s]]); } return _totalStakes; }
Ahora vamos a dar a los titulares de STK la capacidad de crear y eliminar apuestas. Quemaremos los tokens a medida que se apuesten para evitar que los usuarios los transfieran hasta que se elimine la apuesta.
Tenga en cuenta que en la creación de apuestas, _burn se revertirá si el usuario intenta apostar más tokens de los que posee, y en la eliminación de apuestas, la actualización del mapeo de apuestas se revertirá si hay un intento de eliminar más tokens que se apostaron.
Finalmente, usamos addStakeholder y removeStakeholder para tener un registro de quién tiene apuestas, para usar más tarde en el sistema de recompensas.
/** * @notice A method for a stakeholder to create a stake. * @param _stake The size of the stake to be created. */ function createStake ( uint256 _stake ) public { _burn(msg.sender, _stake); if (stakes[msg.sender] == 0 ) addStakeholder(msg.sender); stakes[msg.sender] = stakes[msg.sender].add(_stake); } /** * @notice A method for a stakeholder to remove a stake. * @param _stake The size of the stake to be removed. */ function removeStake ( uint256 _stake ) public { stakes[msg.sender] = stakes[msg.sender].sub(_stake); if (stakes[msg.sender] == 0 ) removeStakeholder(msg.sender); _mint(msg.sender, _stake); }
Los mecanismos de recompensa pueden tener muchas implementaciones diferentes y ser bastante complicados de ejecutar. Para este contrato implementaremos una versión muy simple donde los interesados reciben periódicamente una recompensa en tokens STK equivalente al 1% de sus apuestas individuales.
En contratos más sofisticados, la distribución de recompensas se activaría automáticamente cuando se cumplan ciertas condiciones, pero en este caso dejaremos que el propietario la active manualmente. Siguiendo las mejores prácticas, también realizaremos un seguimiento de las recompensas e implementaremos un método para retirarlas.
Como antes, para que el código sea legible, hemos seguido las convenciones de nomenclatura del contrato ERC20.sol, primero la estructura de datos y los métodos de gestión de datos:
/** * @notice The accumulated rewards for each stakeholder. */ mapping( address => uint256) internal rewards; /** * @notice A method to allow a stakeholder to check his rewards. * @param _stakeholder The stakeholder to check rewards for. */ function rewardOf ( address _stakeholder ) public view returns ( uint256 ) { return rewards[_stakeholder]; } /** * @notice A method to the aggregated rewards from all stakeholders. * @return uint256 The aggregated rewards from all stakeholders. */ function totalRewards ( ) public view returns ( uint256 ) { uint256 _totalRewards = 0 ; for (uint256 s = 0 ; s < stakeholders.length; s += 1 ){ _totalRewards = _totalRewards.add(rewards[stakeholders[s]]); } return _totalRewards; }
A continuación se encuentran los métodos para calcular, distribuir y retirar recompensas:
/** * @notice A simple method that calculates the rewards for each stakeholder. * @param _stakeholder The stakeholder to calculate rewards for. */ function calculateReward ( address _stakeholder ) public view returns ( uint256 ) { return stakes[_stakeholder] / 100 ; } /** * @notice A method to distribute rewards to all stakeholders. */ function distributeRewards ( ) public onlyOwner { for (uint256 s = 0 ; s < stakeholders.length; s += 1 ){ address stakeholder = stakeholders[s]; uint256 reward = calculateReward(stakeholder); rewards[stakeholder] = rewards[stakeholder].add(reward); } } /** * @notice A method to allow a stakeholder to withdraw his rewards. */ function withdrawReward ( ) public { uint256 reward = rewards[msg.sender]; rewards[msg.sender] = 0 ; _mint(msg.sender, reward); }
Ningún contrato puede estar completo sin un conjunto completo de pruebas. Tiendo a producir un error por función al menos y, a menudo, las cosas no funcionan como creo que lo hacen. Se podría decir que me equivoco la mayor parte del tiempo, y seguramente no estoy solo en esto.
Además de permitirle producir código que funcione, las pruebas también son bastante útiles para desarrollar un proceso para configurar y usar contratos. Siempre escribo mi documentación de introducción a partir del código que configura el entorno para las pruebas.
Sigue un extracto de cómo se configura y utiliza el entorno de prueba. Acuñaremos 1000 tokens STK y se los daremos a un usuario para que juegue con el sistema. Usamos la trufa para las pruebas, lo que nos da las cuentas para usar.
contract( 'StakingToken' , (accounts) => { let stakingToken; const manyTokens = BigNumber( 10 ).pow( 18 ).multipliedBy( 1000 ); const owner = accounts[ 0 ]; const user = accounts[ 1 ]; before( async () => { stakingToken = await StakingToken.deployed(); }); describe( 'Staking' , () => { beforeEach( async () => { stakingToken = await StakingToken.new( owner, manyTokens.toString( 10 ) ); });
Cuando creo pruebas, siempre escribo las pruebas que hacen que el código se revierta, pero no son muy interesantes de ver. La prueba de createStake muestra lo que se debe hacer para crear una participación y lo que debe cambiar después.
Es importante notar cómo en este contrato de participación tenemos dos estructuras de datos paralelas, una para los saldos STK y otra para las apuestas, y cómo su suma permanece constante a través de la creación y eliminación de apuestas. En este ejemplo, le damos 3 STK wei al usuario, y la suma del saldo más las apuestas para ese usuario siempre será 3.
it( 'createStake creates a stake.' , async () => { await stakingToken.transfer(user, 3 , { from : owner }); await stakingToken.createStake( 1 , { from : user }); assert.equal( await stakingToken.balanceOf(user), 2 ); assert.equal( await stakingToken.stakeOf(user), 1 ); assert.equal( await stakingToken.totalSupply(), manyTokens.minus( 1 ).toString( 10 ), ); assert.equal( await stakingToken.totalStakes(), 1 ); });
Para las recompensas, la prueba a continuación muestra cómo el propietario activa la distribución de tarifas, y el usuario obtiene una recompensa del 1% de su participación.
it( 'rewards are distributed.' , async () => { await stakingToken.transfer(user, 100 , { from : owner }); await stakingToken.createStake( 100 , { from : user }); await stakingToken.distributeRewards({ from : owner }); assert.equal( await stakingToken.rewardOf(user), 1 ); assert.equal( await stakingToken.totalRewards(), 1 ); });
El suministro total de STK aumenta cuando se distribuyen las recompensas, y esta prueba muestra cómo se relacionan entre sí las tres estructuras de datos (saldos, apuestas y recompensas). La cantidad de STK existentes y prometidos siempre será la cantidad acuñada en la creación más la cantidad distribuida en recompensas, que pueden o no ser acuñadas. La cantidad de STK acuñada en la creación será igual a la suma de saldos y participaciones hasta que se realice una distribución.
it( 'rewards can be withdrawn.' , async () => { await stakingToken.transfer(user, 100 , { from : owner }); await stakingToken.createStake( 100 , { from : user }); await stakingToken.distributeRewards({ from : owner }); await stakingToken.withdrawReward({ from : user }); const initialSupply = manyTokens; const existingStakes = 100 ; const mintedAndWithdrawn = 1 ; assert.equal( await stakingToken.balanceOf(user), 1 ); assert.equal( await stakingToken.stakeOf(user), 100 ); assert.equal( await stakingToken.rewardOf(user), 0 ); assert.equal( await stakingToken.totalSupply(), initialSupply .minus(existingStakes) .plus(mintedAndWithdrawn) .toString( 10 ) ); assert.equal( await stakingToken.totalStakes(), 100 ); assert.equal( await stakingToken.totalRewards(), 0 ); });
Un mecanismo de participación y recompensas es una poderosa herramienta de incentivos que solo necesita ser tan complejo como queramos hacerlo. Los métodos proporcionados en el estándar ERC20 y SafeMath nos permiten codificarlo en unas 200 líneas de código disperso.
Siéntase libre de usar el código en nuestro github público para sus propios fines, o contáctenos si desea nuestra ayuda para implementar una versión de producción de este patrón.
Gracias a Vlad Fărcaş por inspirar parte de este código, a Sergio Pereira y Tiago Martins por sus reseñas para publicación y especialmente a Bernardo Vieira por enseñarme cómo hacer aplicaciones blockchain en el mundo real.