javascript

Смарт контракты Ethereum: структурируем токены как акции

  • среда, 10 мая 2017 г. в 03:14:15
https://habrahabr.ru/post/328246/
  • Платежные системы
  • Биллинговые системы
  • JavaScript


В настоящее время идет настоящая волна хайпа криптовалют и череда успешных ICO самых разнообразных проектов, в том числе имеющих весьма сомнительное или не имеющих вообще никакого отношения к децентрализации и другим базовым принципам блокчейн. В ходе ICO на продажу широкой публике выставляются некие виртуальные сущности – токены. Наполнение этих самых токенов какой-либо реальной «ценностью», как правило, уникально для каждого проекта. В рамках данной статьи я хочу рассмотреть структурирование токена как «акции», когда держатель этих токенов претендует на получение дивидендов от проекта, пропорционально имеющемуся у него проценту токенов от общей эмиссии. Это создает целый ряд правовых коллизий и неопределенностей, поэтому на сегодня нет ни одного крупного проекта, построенного по этой логичной и понятной для инвесторов модели, но юридические аспекты мы вынесем за скобки и остановимся лишь на технической реализации.


Чтобы говорить вообще о каком-либо структурировании токена, прежде всего нужно иметь хоть какую-то базовую реализацию токена. Листинг контракта среднестатистического токена без изысков на языке Solidity приведен ниже:
pragma solidity ^0.4.0;
contract Token {
    string public standard = 'Token 0.1';
    string public name;                                  //!< name for display purporses 
    string public symbol;                               //!< symbol for display purporses
    uint8 public decimals;                             //!< amount of decimals for display purporses

    mapping (address => uint256) public balanceOf;      //!< array of all balances
    mapping (address => mapping (address => uint256)) public allowed;

    uint256 totalTokens;

    function Token(
        uint256 initialSupply,
        string tokenName,
        uint8 decimalUnits,
        string tokenSymbol) {
        totalTokens = initialSupply;
        balanceOf[msg.sender] = initialSupply;
        name = tokenName;
        symbol = tokenSymbol;
        decimals = decimalUnits;
    }

    // @brief Send coins
    // @param _to recipient of coins
    // @param _value amount of coins for send
    function transfer(address _to, uint256 _value) {
        if (balanceOf[msg.sender] < _value || _value <= 0) throw;
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
    }

    // @brief Send coins
    // @param _from source of coins
    // @param _to recipient of coins
    // @param _value amount of coins for send
    function transferFrom(address _from, address _to, uint256 _value) {
        if (balanceOf[_from] < _value || _value <= 0) throw;
        if (allowed[_from][msg.sender] < _value) throw;
        balanceOf[_from] -= _value;
        balanceOf[_to] += _value;
        allowed[_from][msg.sender] -= _value;
    }

    // @brief Allow another contract to spend some tokens in your behalf
    // @param _spender another contract address
    // @param _value amount of approved tokens
    function approve(address _spender, uint256 _value) {
        allowed[msg.sender][_spender] = _value;
    }

    // @brief Get allowed amount of tokens
    // @param _owner owner of allowance
    // @param _spender spender contract
    // @return the rest of allowed tokens
    function allowance(address _owner, address _spender) constant returns (uint256 remaining) {
        return allowed[_owner][_spender];
    }
}

Как видно из кода контракта, эмиссия всех токенов осуществляется единовременно в момент загрузки контракта в блокчейн, а все выпущенные токены записываются на баланс адреса, осуществившего эту загрузку. Далее реализуются стандартные функции перемещения токенов между держателями – transfer и transferFrom, а текущий баланс хранится в карте balanceOf.

Не будем думать, каким образом деньги (эфир, ether) попадают на счет данного контракта, это неважно, переводятся ли они напрямую на адрес контракта или попадают туда через какие-то функции, которые могут быть дополнительно реализованы для придания токену прикладной специфичности и функциональности. Важно, что имеет место быть некий ненулевой баланс контракта this.balance, который мы хотим полностью распределить между держателями токенов, пропорционально имеющему у каждого держателя проценту токенов от общей эмиссии.

С точки зрения классического алгоритмического программирования, задача может показаться элементарной и в виде псевдокода выглядит так:
function divideUpReward() {
        for (holder in balanceOf) {
            uint256 reward = this.balance * holder.value / totalTokens;
            holder.key.send(reward);
        }
}

К сожалению, данный псевдокод нереализуем на языке Solidity, т.к. структура данных mapping не является итерируемой и отсутствует какая-либо возможность пройтись по всем ее элементам. На форумах Ethereum эта задача неоднократно обсуждалась, и основной аргумент, почему сделано так, заключается в том, что это банально дорого. Тут самое время вспомнить, что смартконтракт выполняется на распределенной виртуальной машине EVM, т.е. выполняется на каждой полной ноде, а поскольку мы расходуем чужие вычислительные ресурсы, то за это придется платить, причем чем больше мы делаем операций, тем больше комиссия, которую потребуется заплатить тому, кто будет вызывать эти операции. В нашем случае платить будет тот, кто будет вызывать divideUpReward().

Если продолжить упорствовать и пытаться реализовать данный псевдокод на Solidity, то можно изобрести собственный «велосипед» и сделать итерируемый аналог mapping. Пример такой реализации имеется в открытом доступе (https://github.com/chriseth/solidity-examples/blob/master/iterable_mapping.sol), но он не решает проблему высокой стоимости выполнения функции divideUpReward(). Более того, мы еще и берем на себя все расходы по оплате транзакций отправки эфиров holder.key.send(reward) всем держателях токенов.

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

А почему бы не сделать вот так?
mapping (address => uint256) balanceOfOld;
uint totalReward;
uint lastDivideRewardTime;

function divideUpReward() public {
        // prevent call if less than 30 days passed from previous one
        if (lastDivideRewardTime + 30 days > now) throw;
        // make copy of mapping
        balanceOfOld = balanceOf;
        // reward is contract’s balance
        totalReward = this.balance;
        lastDivideRewardTime = now;
}

// this can be called by token’s holders
function withdrawReward() public returns(uint) {
        uint256 tokens = balanceOfOld[msg.sender];
        if (tokens > 0) {
                uint amount = totalReward * tokens / totalTokens;
                if (!msg.sender.send(amount)) return 0;
                balanceOfOld[msg.sender] = 0;
                return amount;
        }
        return 0;
}

Этот код выглядит уже значительно лучше, т.к. не содержит циклов! Мы, как владелец контракта, или любой другой пользователь Ethereum, вызываем публичную функцию divideUpReward(), которая фиксирует дивиденды на момент вызова, создавая копию контейнера с текущим распределением токенов и запоминая текущий баланс контракта, предоставляя его весь на распределение между держателями токенов. При этом мы запоминаем время последнего вызова divideUpReward() и предотвращаем повторный вызов в течение 30 дней, давая тем самым возможность держателям токенов вывести свои дивиденды через публичную функцию reward(). Если какой-то держатель в течение обозначенного времени не вывел свои дивиденды, то они возвращаются в общую корзину и в течение следующего периода будут доступны к распределению между всеми держателями – как говорится, кто не успел, тот опоздал.

Функцию withdrawReward() вызывают уже непосредственно держатели токенов, а следовательно именно они оплачивают все комиссии, связанные с отправкой средств на их адреса. Можно было бы порадоваться найденному решению, если бы оно не содержало одну конструкцию, недопустимую с точки зрения языка Solidity, а именно “balanceOfOld = balanceOf” – создание копии mapping не предусмотрено в Solidity. Но даже если предположить, что оно было бы, то логично ожидать, что стоимость такого копирования была бы в пределе крайне дорогой, т.к. все равно предполагала бы наличие пусть скрытого, но цикла по всем элементам карты.

Попробуем избавиться от операции явного копирования, введением дополнительного контейнера, который будет динамически заполняться в зависимости от действий конкретного держателя токенов, а следовательно, производиться за его счет.
    uint totalReward;
    uint lastDivideRewardTime;

    struct TokenHolder {
        uint256 balance;
        uint       balanceUpdateTime;
        uint       rewardWithdrawTime;
    }
    mapping(address => TokenHolder) holders;

    function reward() constant public returns(uint) {
        if (holders[msg.sender].rewardWithdrawTime >= lastDivideRewardTime) {
            return 0;
        }
        uint256 balance;
        if (holders[msg.sender].balanceUpdateTime <= lastDivideRewardTime) {
            balance = balanceOf[msg.sender];
        } else {
            balance = holders[msg.sender].balance;
        }
        return totalReward * balance / totalTokens;
    }

    function withdrawReward() public returns(uint) {
        uint value = reward();
        if (value == 0) {
            return 0;
        }
        if (!msg.sender.send(value)) {
            return 0;
        }
        if (balanceOf[msg.sender] == 0) {
            // garbage collector
            delete holders[msg.sender];
        } else {
            holders[msg.sender].rewardWithdrawTime = now;
        }
        return value;
    }

    // Divide up reward and make it accessible for withdraw
    function divideUpReward() public {
        if (lastDivideRewardTime + 30 days > now) throw;
        lastDivideRewardTime = now;
        totalReward = this.balance;
        restReward = this.balance;
    }

    function beforeBalanceChanges(address _who) public {
        if (holders[_who].balanceUpdateTime <= lastDivideRewardTime) {
            holders[_who].balanceUpdateTime = now;
            holders[_who].balance = balanceOf[_who];
        }
    }

Следует обратить внимание на функцию beforeBalanceChanged(address _who), которая как раз и заменяет нам копирование карты mapping. Вызов этой функции следует добавить в исходные функции transfer и transferFrom нашего контракта прямо перед модификацией баланса для конкретного адреса. Функция проверит, что осуществляется движение токенов после фиксации периода вывода дивидендов и осуществит сохранения баланса конкретного держателя токенов для периода распределения вознаграждения, т.е. мы делаем копию balanceOf поэлементно, только если баланс конкретного держателя меняется.

Если соединить все сказанное воедино, то получится следующий текст смарт контракта, осуществляющего эмиссию токенов, их структурирование как акций с последующим начислением дивидендов:
pragma solidity ^0.4.0;
contract Token {
    string public standard = 'Token 0.1';
    string public name;                                  //!< name for display purporses 
    string public symbol;                               //!< symbol for display purporses
    uint8 public decimals;                             //!< amount of decimals for display purporses

    mapping (address => uint256) public balanceOf;      //!< array of all balances
    mapping (address => mapping (address => uint256)) public allowed;

    uint256 totalTokens;

    uint totalReward;
    uint lastDivideRewardTime;

    struct TokenHolder {
        uint256 balance;
        uint       balanceUpdateTime;
        uint       rewardWithdrawTime;
    }
    mapping(address => TokenHolder) holders;

    function Token(
        uint256 initialSupply,
        string tokenName,
        uint8 decimalUnits,
        string tokenSymbol) {
        totalTokens = initialSupply;
        balanceOf[msg.sender] = initialSupply;
        name = tokenName;
        symbol = tokenSymbol;
        decimals = decimalUnits;
    }

    // @brief Send coins
    // @param _to recipient of coins
    // @param _value amount of coins for send
    function transfer(address _to, uint256 _value) {
        if (balanceOf[msg.sender] < _value || _value <= 0) throw;
        beforeBalanceChanges(msg.sender);
        beforeBalanceChanges(_to);
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
    }

    // @brief Send coins
    // @param _from source of coins
    // @param _to recipient of coins
    // @param _value amount of coins for send
    function transferFrom(address _from, address _to, uint256 _value) {
        if (balanceOf[_from] < _value || _value <= 0) throw;
        if (allowed[_from][msg.sender] < _value) throw;
        beforeBalanceChanges(_from);
        beforeBalanceChanges(_to);
        balanceOf[_from] -= _value;
        balanceOf[_to] += _value;
        allowed[_from][msg.sender] -= _value;
    }

    // @brief Allow another contract to spend some tokens in your behalf
    // @param _spender another contract address
    // @param _value amount of approved tokens
    function approve(address _spender, uint256 _value) {
        allowed[msg.sender][_spender] = _value;
    }

    // @brief Get allowed amount of tokens
    // @param _owner owner of allowance
    // @param _spender spender contract
    // @return the rest of allowed tokens
    function allowance(address _owner, address _spender) constant returns (uint256 remaining) {
        return allowed[_owner][_spender];
    }

    function reward() constant public returns(uint) {
        if (holders[msg.sender].rewardWithdrawTime >= lastDivideRewardTime) {
            return 0;
        }
        uint256 balance;
        if (holders[msg.sender].balanceUpdateTime <= lastDivideRewardTime) {
            balance = balanceOf[msg.sender];
        } else {
            balance = holders[msg.sender].balance;
        }
        return totalReward * balance / totalTokens;
    }

    function withdrawReward() public returns(uint) {
        uint value = reward();
        if (value == 0) {
            return 0;
        }
        if (!msg.sender.send(value)) {
            return 0;
        }
        if (balanceOf[msg.sender] == 0) {
            // garbage collector
            delete holders[msg.sender];
        } else {
            holders[msg.sender].rewardWithdrawTime = now;
        }
        return value;
    }

    // Divide up reward and make it accesible for withdraw
    function divideUpReward() public {
        if (lastDivideRewardTime + 30 days > now) throw;
        lastDivideRewardTime = now;
        totalReward = this.balance;
        restReward = this.balance;
    }

    function beforeBalanceChanges(address _who) public {
        if (holders[_who].balanceUpdateTime <= lastDivideRewardTime) {
            holders[_who].balanceUpdateTime = now;
            holders[_who].balance = balanceOf[_who];
        }
    }
}

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

Полезные ссылки по теме разработки смарт контрактов для Ethereum: