- PVSM.RU - https://www.pvsm.ru -
Почти перед каждым программистом, который пишет смарт-контракты Ethereum встают вопросы: «Что делать, если нужно будет расширить функционал контрактов? Как быть, если в контракте найдется баг, который повлечет за собой потерю средств? Что делать, если обнаружится уязвимость в компиляторе solidity (что бывало уже не раз)?» Ведь, контракты, которые мы загружаем в сеть, не могут быть изменены. Поначалу довольно сложно осознать: как это код нельзя обновить? Почему? Но в этом отчасти и сила смарт-контрактов Ethereum — пользователи, возможно, меньше бы стали доверять контрактам, которые можно менять.
Постараемся разобрать несколько подходов, которые все же позволяют менять смарт-контракты.
Эта статья рассчитана на тех, кто обладает хотя бы базовыми навыками программирования на языке solidity и понимает основные принципы работы сети Ethereum.
При этом можно сохранить адреса активных в данный момент контрактов в storage какого-либо из контрактов. Нередко выделяют какой-то один контракт, который отвечает за хранение и изменение ссылок на части всей системы.
Как пример можно привести контракт распродажи токенов, в котором четко не прописаны правила вычисления количества токенов, которые нужно отправить на кошелек с которого пришли Этеры. Вычислением количества может заниматься отдельный контракт, который мы сможем подменять в случае надобности. Не будем долго останавливаться на этом варианте, потому что подобный подход часто используется не только в solidity.
Одним из главных минусов такого подхода является то, что никак нельзя изменить интерфейс какого-то контракта, который является внешним для всей системы. Нельзя добавить или удалить функцию.
В EIP-7 [1] была предложена и реализована инструкция, которая позволяет вызывать код из другого контракта, но контекст вызова остается тем же самым, что и у текущего контракта. То есть, вызываемый контракт будет писать в storage вызывающего контракта, msg.sender и msg.value остаются такими же, как изначально.
В сети можно найти несколько примеров реализации данного механизма. Все они включают использование solidity assembly. Без assembly невозможно добиться возвращения какого-либо значения из delegatecall.
Основная идея всех методов, которые используют delegatecall для проксирования — реализация fallback функции. В ней необходимо прочесть calldata и передать дальше через delegatecall.
Поближе посмотрим на несколько примеров реализации:
Вот реализация fallback функции отсюда:
bytes4 sig;
assembly { sig := calldataload(0) }
var len = _sizes[sig];
var target = _dest;
assembly {
// return _dest.delegatecall(msg.data)
calldatacopy(0x0, 0x0, calldatasize)
delegatecall(sub(gas, 10000), target, 0x0, calldatasize, 0, len)
return(0, len)
}
При этом размер возвращаемого значения (в байтах) хранится в mapping _sizes. Это поле в storage необходимо заполнять при обновлении контракта.
Минусом данного подхода является то, что размер возвращаемого значения жестко привязан к сигнатуре вызываемой функции, то есть вернуть строку произвольного размера или массив байтов не получится.
Кроме того, обращение к storage довольно дорого стоит. А в данном случае у нас будет аж два обращения к storage: когда мы обращаемся к полю _dest и когда мы обращаемся к полю _size.
Код очень похож на предыдущий пример, но всегда используется размер ответа, равный 32 байтам. Это довольно взвешенное решение. Во-первых, большинство типов в solidity умещаются именно в 32 байта, во-вторых, не обращаясь лишний раз к storage, мы экономим довольно приличное количество газа. Позже оценим, сколько примерно газа тратится в разных вариантах реализации
Инструкции позволяют получить размер ответа, который возвращен из call/delegatecall, а также скопировать сам ответ в память. То есть, мы получили возможность реализовать полноценный прокси для любых размеров returndata.
Вот итоговый assembly код:
assembly {
let _target := sload(0)
calldatacopy(0x0, 0x0, calldatasize)
let retval := delegatecall(gas, _target, 0x0, calldatasize, 0x0, 0)
let returnsize := returndatasize
returndatacopy(0x0, 0x0, returnsize)
switch retval case 0 {revert(0, 0)} default {return (0, returnsize)}
}
Рассмотрим вопрос использования газа. Проведенное тестировние показывает, что все 3 приведенных метода увеличивают использование газа на значение от 1000 до 1500. Много это или мало? Это примерно 2% от более-менее средней стоимости транзакции, которая будет изменять storage.
К сожалению, использование данных методик ограничено. Во-первых, чтобы такое обновление контрактов работало, нельзя менять структуру хранения данных в контракте (нельзя переставлять местами поля, удалять поля). В новые версии контракта поля можно добавлять.
Также необходимо очень аккуратно разграничивать доступ к функции, которая меняет адрес активного контракта.
Немаловажным фактом является то, что доверие пользователей к контракту будет меньше, чем к такому же неизменяемому. С другой стороны можно предусмотреть тестовый период времени, в течение которого новую версию контракта можно откатить, а после которого версия контракта будет зафиксирована и меняться больше не сможет.
Несколько контрактов, которые помогут сделать обновление проще и надежнее.
Upgradeable [5] — в этом контракте реализована проверка на то, что поле target (адрес активной версии контракта) хранится в том же самом слоте, что и в текущей версии.
Аналогично можно реализовать проверки и на другие поля storage (пример можно посмотреть в Target.sol [6])
Если вы планируете реализовать Upgradeable контракты, то обязательно посмотрите на тесты [7] для контракта Upgradeable.
Перед деплоем подобных контрактов в сеть, нужно обязательно тестировать все варианты. Иначе после очередного обновления можно остаться без функционирующего контракта и без возможности обновления.
Автор: 0v1se
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/268089
Ссылки в тексте:
[1] EIP-7: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7.md
[2] Upgradeable: https://gist.github.com/Arachnid/4ca9da48d51e23e5cfe0f0e14dd6318f
[3] EVM assembly tricks: http://martin.swende.se/blog/EVM-Assembly-trick.html
[4] Использование новых инструкций: https://github.com/0v1se/contracts-upgradeable/blob/master/contracts/upgradeable/Dispatcher.sol
[5] Upgradeable: https://github.com/0v1se/contracts-upgradeable/blob/master/contracts/upgradeable/Upgradeable.sol
[6] Target.sol: https://github.com/0v1se/contracts-upgradeable/blob/master/test/contracts/Target.sol
[7] тесты: https://github.com/0v1se/contracts-upgradeable/blob/master/test/upgradeable.js
[8] Источник: https://habrahabr.ru/post/342200/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.