Несколько дней назад мы в компании BitClave прочли о недавнем инциденте с мультиподписными кошельками компании Parity Technologies, решили пригляделся к коду их смарт-контракта. Свежий пост в блоге компании Zeppelin Solutions детально описывает произошедший инцидент с технической стороны, поэтому мы хотели бы в нашей статье больше сфокусироваться на принципах проектирования смарт-контрактов.
![Ethereum wallet](https://habrastorage.org/r/w780q1/webt/jb/me/yx/jbmeyx7aep8zmlgatsmyiy_oqs8.jpeg)
Есть несколько широко известных принципов ООП, входящих в аббревиатуру SOLID:
Есть множество приверженцев и противников использования этих принципов, но мы просмотрели множество библиотек на языке Solidity и пришли к выводу, что подход Zeppelin Solutions является наиболее удобным и безопасным. Их библиотека OpenZeppelin.org предоставляет множество небольших смарт-контрактов, которые могут быть скомпилированы для достижения более сложного поведения по паттерну примесей (англ. mixin) через использование множественного наследования в языке Solidity. Вам необходимо произвести декомпозицию обязанностей вашего финального смарт-контракта на множество смарт-контрактов с единственными ответственностями – каждый смарт-контракт должен служить единственной цели. Причем часть необходимых вам контрактов вы скорее всего обнаружите в библиотеках вроде той, что предлагает компания Zeppelin Solutions. Помимо всего прочего вы к тому же сможете протестировать каждый из контрактов по отдельности.
Руководствуясь этими принципами мы разработали смарт-контракты для проведения распродажи токенов: github.com/bitclave/crowdsale. Вы можете заметить в репозитории смарт-контракты
Для того чтобы успешно применять эти принципы на практике необходимо четко понимать как именно работает множественное наследование. На деле компилятор Solidity преобразует множественное наследование в одиночное наследование. Таки образом после компиляции у каждого смарт-контракта будет всего 1 родитель, обращаться к которому можно через ключевое слово
Проблема в том, что смарт-контракт
Так же необходимо учитывать, что любое непосредственное наследование контрактов может быть превано в процессе наследования дочерних классов. Обратите внимание, как в следующем примере компилируется контракт
Возвращаясь к смарт-контракту мультиподписного кошелька Parity Technologies мы ообратили внимание, что он совершенно не декомпозирован. Единственное архитектурное решение замеченное нами: вынос общего кода всех кошельков в единую библиотеку с целью уменьшить стоимость загрузки контракта. Кстати, именно эта особенность и позволила случайному разработчику нарушить работу всех кошельков, сломав единственную библиотеку с общим кодом. Мы поразмышляли на тему множественного владения ресурсом и подготовили своё решение этой задачи в манере библиотек OpenZeppelin. Мы рады представить вам контракт Multiownable.sol, который позволит вам с легкостью добавить функциональность мультиподписи в любые ваши контракты. Его использование так же просто как и использование обычного контракта
Метод
Будем рады любому фидбеку сообщества: github.com/bitclave/Multiownable
P.S. Обновил пример с учетом последних изменений библиотеки – добавил защиту от вложенных вызовов, в случае если получатель будет контрактом, у которого есть метод
![Ethereum wallet](https://habrastorage.org/webt/jb/me/yx/jbmeyx7aep8zmlgatsmyiy_oqs8.jpeg)
Есть несколько широко известных принципов ООП, входящих в аббревиатуру SOLID:
S
ingle responsibility principle (принцип единственной ответственности)O
pen/closed principle (принцип открытости/закрытости)L
iskov substitution principle (принцип подстановки Барбары Лисков)I
nterface segregation principle (принцип разделения интерфейса)D
ependency inversion principle (принцип инверсии зависимостей)
Есть множество приверженцев и противников использования этих принципов, но мы просмотрели множество библиотек на языке 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
.