Универсальный cмарт-контракт мультиподписи в Ethereum

Автор оригинала: Anton Bukov
  • Перевод
Несколько дней назад мы в компании BitClave прочли о недавнем инциденте с мультиподписными кошельками компании Parity Technologies, решили пригляделся к коду их смарт-контракта. Свежий пост в блоге компании Zeppelin Solutions детально описывает произошедший инцидент с технической стороны, поэтому мы хотели бы в нашей статье больше сфокусироваться на принципах проектирования смарт-контрактов.

Ethereum wallet

Есть несколько широко известных принципов ООП, входящих в аббревиатуру SOLID:


Есть множество приверженцев и противников использования этих принципов, но мы просмотрели множество библиотек на языке Solidity и пришли к выводу, что подход Zeppelin Solutions является наиболее удобным и безопасным. Их библиотека OpenZeppelin.org предоставляет множество небольших смарт-контрактов, которые могут быть скомпилированы для достижения более сложного поведения по паттерну примесей (англ. mixin) через использование множественного наследования в языке Solidity. Вам необходимо произвести декомпозицию обязанностей вашего финального смарт-контракта на множество смарт-контрактов с единственными ответственностями – каждый смарт-контракт должен служить единственной цели. Причем часть необходимых вам контрактов вы скорее всего обнаружите в библиотеках вроде той, что предлагает компания Zeppelin Solutions. Помимо всего прочего вы к тому же сможете протестировать каждый из контрактов по отдельности.

Руководствуясь этими принципами мы разработали смарт-контракты для проведения распродажи токенов: github.com/bitclave/crowdsale. Вы можете заметить в репозитории смарт-контракты BonusCrowdsale и TokensCappedCrowdsale, которые были разработаны таким образом чтобы обрабатывать такие аспекты нашей распродажи, как обработку бонусов участников в зависимости от времени и суммы инвестиций, а также контролировать суммарное число продаваемых токенов. На наш код мы получили довольно хвалебный отзыв аудитора безопасности смарт-контрактов:

«Великолепная работа по повторному использованию существующих контрактов библиотек OpenZeppelin! Дополнительные контракты выглядят очень продуманно спроектированными и выглядят хорошим расширением этого фреймвока» («Great work reusing the existing OpenZeppelin libraries! The additional contracts are very thoughtfully designed, and are a good extension of the framework») — Zeppelin Solutions. С полным заключением можно ознакомиться по ссылке.

Для того чтобы успешно применять эти принципы на практике необходимо четко понимать как именно работает множественное наследование. На деле компилятор Solidity преобразует множественное наследование в одиночное наследование. Таки образом после компиляции у каждого смарт-контракта будет всего 1 родитель, обращаться к которому можно через ключевое слово super. Возможно следующий пример дополнительно поможет понять как именно работает линеаризация множественного наследования C3:

contract A { }
contract B { }
contract C is A, B { } // C(A,B) = ABC
contract D is C, A { } // D(C(A,B),A) = D(ABC,A) = ABCAD !!! Error !!!
contract E is A, C { } // E(A,C(A,B)) = E(A,ABC) = ABCE

Проблема в том, что смарт-контракт A не может переопределять C, потому что C переопределяет B, который в свою очередь переопределяет A:

TypeError: Linearization of inheritance graph impossible
contract D is C, A { }
^ — — — — — — — — — — ^
Compiliation failed. See above.

Так же необходимо учитывать, что любое непосредственное наследование контрактов может быть превано в процессе наследования дочерних классов. Обратите внимание, как в следующем примере компилируется контракт W, и его родительский контракт Z становится наследником контракта Y(который является наследником X), вместо непосредственного наследования от X:

contract X {}
contract Y is X {}    // Y(X) = XY
contract Z is X {}    // Z(X) = XZ
contract W is Y, Z {} // W(Y(X),Z(X)) = W(XY, XZ) = XYZW

Возвращаясь к смарт-контракту мультиподписного кошелька Parity Technologies мы ообратили внимание, что он совершенно не декомпозирован. Единственное архитектурное решение замеченное нами: вынос общего кода всех кошельков в единую библиотеку с целью уменьшить стоимость загрузки контракта. Кстати, именно эта особенность и позволила случайному разработчику нарушить работу всех кошельков, сломав единственную библиотеку с общим кодом. Мы поразмышляли на тему множественного владения ресурсом и подготовили своё решение этой задачи в манере библиотек OpenZeppelin. Мы рады представить вам контракт Multiownable.sol, который позволит вам с легкостью добавить функциональность мультиподписи в любые ваши контракты. Его использование так же просто как и использование обычного контракта Ownable — нужно лишь отнаследоваться от него и добавить модификаторы onlyAnyOwner и onlyManyOwners к необходимым функциям:

contract SimplestMultiWallet is Multiownable {

    bool avoidReentrancy = false;

    function () payable { }

    function transferTo(address to, uint256 amount) onlyManyOwners {
        require(!avoidReentrancy);
        avoidReentrancy = true;
        to.transfer(amount);
        avoidReentrancy = false;
    }

}

Метод transferTo смарт-контракта будет вызван только после того как все владельцы кошелька вызовут его с одинаковыми аргументами. Вы не поверите, но это полный код простейшего мультиподписного кошелька для валюты Эфир. Дополнительно можно реализовать поддержку любых токенов совместимых со спецификацией ERC20, вот такой метод:

function transferTokens(address token, address to, uint256 amount) onlyManyOwners {
    require(!avoidReentrancy);
    avoidReentrancy = true;
    ERC20(token).transfer(to, amount);
    avoidReentrancy = false;
}

Будем рады любому фидбеку сообщества: github.com/bitclave/Multiownable

P.S. Обновил пример с учетом последних изменений библиотеки – добавил защиту от вложенных вызовов, в случае если получатель будет контрактом, у которого есть метод transfer.

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

    +2
    Это событие с Parity ещё раз подтверждает, что тьюринг полный язык контрактов — зло. Все кому не лень кинулись на Solidity как в свoё время на JavaScript, независимо от кривизны рук.
    Bitcoin script не полнотьюринговый, язык смарт контрактов BitShares не полнотьюринговый. Каждйы для своей области ограничены и детерминистичны. Язык smart-контрактов должен быть детерминистичным, иначе это катастрофа. В этом году из за кривых контрактов уже 6 проблем на всю сеть было. Solidity ЯП на блокчейне, с соответствующими тормозами. Имея тюринг-полный ЯП, всегда можно найти возможность создать неявную проблему в смарт-контракте.
      –1

      Но полнота по Тьюрингу слишком привлекательна для проектов. Не всё же кошельки делать и краудсейлы :)

        0

        Тьюринг полный язык контрактов не зло. Зло — отсутствие инструментов и методик позволяющих проверить получилась ли система детерменизированной.

        +1
        Чисто и лаконично написано.
        Но в функции transferOwnership есть цикл, число итераций которого может оказаться порядка тысячи и более:
        for (i = 0; i < allPendingOperations.length; i++) {
        что приведет к достижению лимита газа блока и невозможности поменять владельцев контракта.
        Мультиподпись ethereum wallet также этим страдает (функция clearPending).
        В своей мультиподписи на базе МП ethereum wallet мы ввели принудительный сброс pending-операций в потенциально опасной ситуации, но я считаю это временным решением — планируем переработать алгоритм сброса в итерационный, который адаптивно учитывает msg.gas.
          0

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

            0

            Что скажете про такой способ ухода от циклов по операциям? https://github.com/bitclave/Multiownable/pull/2


            Вынесение всех переменных в структуру не помогло. При пересоздании структуры внутренние маппинги не очищаются! Зато помогли вложенные маппинги, первым ключом которых является фактически номер версии, которая растет при каждой передачи прав владения.

              +1
              Т.е. по сути введены поколения структур. Да, хорошая идея. Вложенные маппинги работают за счет того, что локация в storage определяется условно говоря как хэш(локация мапинга, uid, ключ) — новый uid — все обнулено.
                0

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

                  0
                  С другой стороны непонятно насколько плохая идея не очищать маппинг, а только засорять его.

                  А с какой целью очищать? Уменьшить размер блокчейна не получится, разве что размер текущего состояния.
                    –1

                    Мне кажется ноды вычищают у себя удаленные данные, смысла то нет их хранить дальше.

                      0
                      Ну тут как в VCS. Нода должна (по апи в т.ч.) уметь отвечать на вопросы о прошлом.

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

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