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

    В настоящее время идет настоящая волна хайпа криптовалют и череда успешных 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. Пример такой реализации имеется в открытом доступе (тут), но он не решает проблему высокой стоимости выполнения функции 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;
        uint restReward;
    
        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;
        uint restReward;
    
        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:

    Средняя зарплата в IT

    113 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 5 091 анкеты, за 2-ое пол. 2020 года Узнать свою зарплату
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 9

      +1

      Синтакс солидити конечно хромает :(.


      Был класс стал контракт, при этом в контракте-классе функции, а не методы.Больше всего удивляет throw.
      Из-за магии в бекграунде немного непонятно, что вирт машина делает, откуда берет все эти данные и куда их сохраняет и как сдает...


      Видимо у меня это все пролетает мимо в голове.

        0
        Очень рекомендую ресурс learnxinyminutes.com/docs/solidity, где очень концентрированно «в кучу» приводится вся информация о Solitidy. Для более упорядоченных знаний имеет смысл обратиться к оригинальной документации https://solidity.readthedocs.io/en/latest/.
        Чтобы писать смарт контракты не обязательно понимать, как оно хранится, важнее понимать, что и сколько стОит:)
          +1

          Есть вещь, на которую я хотел бы обратить внимание. Не знаю, где вы искали описание стандарта на токен, самой главной ссылки у вас нет: ERC20. Отсюда видно, что balanceOf — необязательно mapping, по стандарту это function balanceOf(address _owner) constant returns (uint256 balance). Вы можете объявить это просто как mapping (address => uint256) только потому, что компилятор Solidity создаёт для этой карты геттер с указанной в стандарте сигнатурой (пруф из документации).


          Это всё к чему: вместо лишнего уровня обёрток можно было просто объявить приватный mapping (address => TokenHolder), написать balanceOf и всё остальное, что требуется по ERC20, поверх этого маппинга и очень существенно упростить исходник.

            0
            Вы совершенно правы, но существенного упрощения исходников я не вижу. Но, возможно, будет чуть понятнее код.
              +1

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

                0
                Это существенный аргумент!
            +1
            Компилятор сначала заявил, что restReward не объявлен
            следом, после того, как запустил сам контракт — mist сказал, что «It seems this transaction will fail.If you submit it, it may consune all the gas you provide», то есть сожрёт весь газ. В чём может быть трабла?

            image
              0
              restReward поправил. Касательно ошибки загрузки можно попробовать сделать конструктор payable (https://ethereum.stackexchange.com/questions/11726/mist-testnet-error-it-seems-this-transaction-will-fail)
              0
              Может я ошибаюсь, но в withdrawReward() естть уязвимость с fallback функцией.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое