Что такое стакинг в блокчейне?


Введение в понятие ставки

Считается, что кто-то имеет долю в предприятии, когда он предоставляет некоторые активы в обмен на осуществление определенного уровня контроля, влияния или участия в его деятельности.

В мире криптовалют это понимается как предоставление пользователям неких прав или вознаграждения до тех пор, пока они не передадут некоторые токены, находящиеся в их распоряжении. Механизм стейкинга обычно поощряет хранение токенов против торговли токенами, что, в свою очередь, должно способствовать росту стоимости токенов.

Весь проект, включая среду разработки и тесты, доступен на моем github.

Для создания этого ставочного механизма нам понадобятся:

  1. Ставочный токен.
  2. Структуры данных для отслеживания ставок, заинтересованных сторон и вознаграждений.
  3. Методы для создания и удаления ставок.
  4. Система вознаграждений.

Ставочный токен

Ставочный токен может быть создан как токен ERC20. В дальнейшем мне понадобятся SafeMath и Ownable, поэтому давайте импортируем и используем и их.

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 Smile
* @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);
   }
Вход в полноэкранный режим Выход из полноэкранного режима

Заинтересованные стороны
В этой реализации мы собираемся отслеживать заинтересованные стороны, чтобы в дальнейшем облегчить надежное распределение стимулов. Теоретически можно было бы не отслеживать их, как это делает обычный токен ERC20, но на практике трудно гарантировать, что заинтересованные стороны не будут играть в систему распределения, если их не отслеживать.

Для реализации мы просто будем использовать динамический массив адресов заинтересованных сторон.

 /**
     * @notice We usually require to know who are all the stakeholders.
     */
    address[] internal stakeholders;
Вход в полноэкранный режим Выход из полноэкранного режима

Следующие методы добавляют заинтересованную сторону, удаляют заинтересованную сторону и проверяют, принадлежит ли адрес заинтересованной стороне. Конечно, возможны и другие, более эффективные реализации, но мне нравится эта для удобства чтения.

 /**
    * @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();
       }
   }
Войти в полноэкранный режим Выйти из полноэкранного режима

Ставки

В самом простом виде ставка должна записывать размер ставки и владельца ставки. Действительно простой реализацией этого может быть простое отображение адреса держателя ставки на размер ставки.

   /**
    * @notice The stakes for each stakeholder.
    */
   mapping(address => uint256) internal stakes;
Вход в полноэкранный режим Выход из полноэкранного режима

Я собираюсь следовать названиям функций из ERC20 и создать эквиваленты для получения данных из отображения ставок.

   /**
    * @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;
   }
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы собираемся дать держателям STK возможность создавать и удалять колья. Мы будем сжигать токены при создании кола, чтобы пользователи не могли переводить их до тех пор, пока кол не будет удален.

Обратите внимание, что при создании кола _burn будет возвращаться, если пользователь попытается передать больше токенов, чем ему принадлежит, а при удалении кола обновление отображения колов будет возвращаться, если будет попытка передать больше токенов, чем было передано.

Наконец, мы используем addStakeholder и removeStakeholder, чтобы иметь запись о том, кто владеет ставками, которая впоследствии будет использоваться в системе вознаграждений.

 /**
    * @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);
   }
Вход в полноэкранный режим Выход из полноэкранного режима

Вознаграждения

Механизмы вознаграждений могут иметь множество различных реализаций и быть довольно тяжелыми в работе. Для данного контракта мы реализуем очень простую версию, в которой заинтересованные стороны периодически получают вознаграждение в токенах STK, эквивалентное 1% от их индивидуальных ставок.

В более сложных контрактах распределение вознаграждения будет происходить автоматически при выполнении определенных условий, но в данном случае мы позволим владельцу запускать его вручную. Следуя лучшей практике, мы также будем отслеживать вознаграждения и реализуем метод их вывода.

Как и прежде, чтобы сделать код читабельным, мы придерживались соглашений об именовании из контракта ERC20.sol, сначала структуры данных и методов управления данными:

 /**
    * @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;
   }
Вход в полноэкранный режим Выход из полноэкранного режима

Далее следуют методы расчета, распределения и вывода вознаграждений:

 /**
    * @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);
   }
Войти в полноэкранный режим Выход из полноэкранного режима

Тестирование

Ни один контракт не может быть полноценным без полного набора тестов. Как правило, на каждую функцию приходится не менее одной ошибки, и часто все работает не так, как я думаю. Можно сказать, что я ошибаюсь большую часть времени, и, конечно, я не одинок в этом.

Помимо того, что тесты позволяют создавать работающий код, они также весьма полезны для разработки процесса создания и использования контрактов. Я всегда пишу свою документацию по началу работы на основе кода, который создает среду для тестов.

Далее следует выдержка из того, как настраивается и используется тестовая среда. Мы чеканим 1000 токенов STK и даем их пользователю, чтобы он поиграл с системой. Для тестирования мы используем truffle, который дает нам учетные записи для использования.

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)
           );
       });
Вход в полноэкранный режим Выход из полноэкранного режима

При создании тестов я всегда пишу тесты, которые заставляют код реверсироваться, но они не очень интересны для просмотра. Тест для createStake показывает, что нужно сделать, чтобы создать кол, и что должно измениться после этого.

Важно заметить, что в этом контракте на ставку у нас есть две параллельные структуры данных, одна для баланса STK, другая для ставок, и что их сумма остается постоянной при создании и удалении ставок. В этом примере мы даем пользователю 3 STK wei, и сумма баланса плюс ставки для этого пользователя всегда будет равна 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);
       });

Вход в полноэкранный режим Выход из полноэкранного режима

Что касается вознаграждений, то в приведенном ниже тесте показано, как владелец запускает распределение вознаграждений, при этом пользователь получает вознаграждение в размере 1% от его ставки.

    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);
       });
Войти в полноэкранный режим Выйти из полноэкранного режима

Общее предложение STK увеличивается при распределении вознаграждений, и этот тест показывает, как три структуры данных (балансы, ставки и вознаграждения) соотносятся друг с другом. Количество существующих и обещанных STK всегда будет равно количеству, отчеканенному при создании, плюс количество, распределенное в наградах, которые могут быть отчеканены, а могут и не быть. Количество STK, отчеканенных при создании, будет равно сумме балансов и ставок до тех пор, пока не будет произведено распределение.

  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);
       });
Войти в полноэкранный режим Выход из полноэкранного режима

Заключение

Механизм ставок и вознаграждений – это мощный инструмент стимулирования, который должен быть настолько сложным, насколько мы хотим его сделать. Методы, предоставляемые стандартом ERC20 и SafeMath, позволяют нам написать его примерно в 200 строках редкого кода.

Спасибо, что прочитали мой пост.

Оцените статью
Procodings.ru
Добавить комментарий