И пример кода этой мощной схемы токеномики
«Любые ошибки, которые я совершаю, — это инвестиции в мое будущее». — Роуз Намаюнас
Введение в стейкинг
Когда говорят, что у кого-то есть доля в предприятии, это значит, что они внесли некоторое количество активов в обмен на осуществление определенного уровня контроля, влияния или участия в его деятельности.
В мире криптовалют это понимается как предоставление пользователям какого-то права или вознаграждения до тех пор, пока они не осуществят передачу токенов, которыми владеют. Механизм стейкинга обычно поощряет удержание токенов, а не торговлю ими, что, в свою очередь, должно приводить к росту стоимости токенов.
В TechHQ мы считаем, что знаниями нужно делиться, и в этой статье мы покажем, как реализовать механизм стейкинга в Solidity. Весь проект, включая среду разработки и тесты, доступен на нашем общедоступном github.
Для построения этого механизма нам понадобится:
- Токен для ставки (или долевой токен).
- Структуры данных для отслеживания ставок (долей), владельцев долей и вознаграждений.
- Методы для создания и удаления ставок.
- Система вознаграждений.
Теперь обо всем по порядку.
Долевой токен
Долевой токен может быть создан как токен 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 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);
}
Вот и все, больше ничего не нужно.
Владельцы долей
В этой схеме мы будем отслеживать владельцев долей, чтобы впоследствии упростить функциональное распределение стимулов. Теоретически можно было бы и не отслеживать их, как в случае обычного токена 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 всегда будет суммой, отчеканенной при создании плюс сумма, распределенная в вознаграждениях, которые могут быть отчеканены или нет. Сумма СТК, отчеканенная при создании, будет равна сумме остатков и ставок до тех пор, пока распределение не будет выполнено.
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 строках разреженного кода.