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