Тема блокчейна не перестает быть источником не только всяческого хайпа, но и весьма ценных с технологической точки зрения идей. Посему не обошла она стороной и жителей солнечного города. Присматриваются люди, изучают, пытаются переложить свою экспертизу в традиционном инфобезе на блокчейн-системы. Пока что точечно: одна из разработок «Ростелеком-Солар» умеет проверять безопасность софта на базе блокчейна. А попутно возникают некоторые мысли по решению прикладных задач блокчейн-сообщества. Одним из таких лайфхаков – как определить адрес смарт-контракта до деплоя с помощью CREATE2 – сегодня хочу с вами поделиться под катом.
Опкод CREATE2 был добавлен в хард-форке Константинополь 28 февраля этого года. Как указано в EIP, этот опкод был введен в основном для каналов состояний (state channels). Однако, мы использовали его для решения другой проблемы.
На бирже есть пользователи с балансами. Каждому пользователю мы должны предоставить Ethereum-адрес, на который она или он сможет отправлять токены, тем самым пополняя свой аккаунт. Давайте назовем эти адреса «кошельками». Когда токены приходят на кошельки, мы должны отправить их на единый кошелек (hotwallet).
В следующих разделах я анализирую варианты решения этой задачи без CREATE2 и рассказываю, почему мы отказались от них. Если вам интересен только конечный результат, вы можете найти его в разделе «Итоговое решение».
Самое простое решение — генерировать новые ethereum-адреса для новых пользователей. Эти адреса и будут кошельками. Чтобы перевести токены из кошелька в hotwallet, необходимо подписать транзакцию вызовом функции transfer() с приватным ключом кошелька из бэкенда.
Этот подход имеет следующие преимущества:
Тем не менее, мы отказались от этого подхода, поскольку он имеет один существенный недостаток: вам нужно где-то хранить приватные ключи. И дело не только в том, что они могут быть потеряны, но ещё и в том, что вам необходимо тщательно управлять доступом к этим ключам. Если хотя бы один из них скомпрометирован, то токены определенного пользователя не достигнут горячего кошелька.
Развертывание отдельного смарт-контракта для каждого пользователя позволяет не хранить приватные ключи от кошельков на сервере. Биржа вызовет этот умный контракт для передачи токенов в hotwallet.
От этого решения мы тоже отказались, поскольку пользователю нельзя показать адрес его кошелька без развертывания смарт-контракта (это на самом деле возможно, но довольно сложным образом с другими недостатками, которые мы не будем здесь обсуждать). На бирже пользователь может создать столько аккаунтов, сколько ему нужно, и каждому нужен собственный кошелек. Это означает, что нам нужно тратить деньги на деплой контракта, даже не будучи уверенными, что пользователь будет использовать эту учетную запись.
Чтобы устранить проблему предыдущего способа, мы решили использовать опкод CREATE2. CREATE2 позволяет заранее определить адрес, по которому будет развернут смарт-контракт. Адрес рассчитывается по следующей формуле:
keccak256 (0xff ++ address ++ salt ++ keccak256 (init_code)) [12:]
, где:
Таким образом гарантируется, что адрес, который мы предоставляем пользователю, действительно будет содержать желаемый байт-код. Кроме того, этот смарт-контракт может быть развернут, когда нам нужно. Например, когда пользователь решит впервые использовать свой кошелек.
Более того, вы можете рассчитывать адрес смарт-контракта каждый раз вместо того, чтобы хранить его, так как:
Предыдущее решение все еще имеет один недостаток: вам нужно платить за развертывание умного контракта. Тем не менее, вы можете избавиться от этого. Для этого вы можете вызвать функцию transfer(), а затем selfdestruct() в конструкторе кошелька. И тогда газ за развертывание смарт-контракта будет возвращен.
Вопреки распространенному заблуждению, вы можете развернуть смарт-контракт по одному и тому же адресу несколько раз с опкодом CREATE2. Это связано с тем, что CREATE2 проверяет, что nonce целевого адреса равен нулю (ему присваивается значение «1» в начале конструктора). При этом функция selfdestruct() каждый раз сбрасывает nonce адреса. Таким образом, если вы снова вызовете CREATE2 с теми же аргументами, проверка на nonce пройдет.
Обратите внимание, что это решение аналогично варианту с ethereum-адресами, но без необходимости хранить приватные ключи. Стоимость перевода денег с кошелька на hotwallet примерно равна стоимости вызова функции transfer(), поскольку мы не платим за развертывание смарт-контракта.
Изначально подготовлено:
constructor () {
address hotWallet = 0x…;
address token = 0x…;
token.transfer (hotWallet, token.balanceOf (address (this)));
selfdestruct (address (0));
}
Для каждого нового пользователя мы показываем его / ее адрес кошелька путем расчета
keccak256 (0xff ++ address ++ salt ++ keccak256 (init_code)) [12:]
Когда пользователь переводит токены на соответствующий адрес кошелька, наш бэкэнд видит событие Transfer с параметром _to, равным адресу кошелька. На этот момент уже возможно увеличить баланс пользователя на бирже до развертывания кошелька.
Когда на адресе кошелька накапливается достаточное количество токенов, мы можем перевести их все сразу в hotwallet. Для этого бекенд вызывает функцию фабрики смарт-контрактов, которая выполняет следующие действия:
function deployWallet (соль uint256) {
bytes memory walletBytecode =…;
// invoke CREATE2 with wallet bytecode and salt
}
Таким образом, вызывается конструктор смарт-контракта кошелька, который передает все свои токены на адрес hotwallet и затем самоуничтожается.
Полный код можно найти здесь . Обратите внимание, что это не наш продакшн-код, так как мы решили оптимизировать байт-код кошелька и записали его в опкодах.
Автор Павел Кондратенков, специалист в области Ethereum
Опкод CREATE2 был добавлен в хард-форке Константинополь 28 февраля этого года. Как указано в EIP, этот опкод был введен в основном для каналов состояний (state channels). Однако, мы использовали его для решения другой проблемы.
На бирже есть пользователи с балансами. Каждому пользователю мы должны предоставить Ethereum-адрес, на который она или он сможет отправлять токены, тем самым пополняя свой аккаунт. Давайте назовем эти адреса «кошельками». Когда токены приходят на кошельки, мы должны отправить их на единый кошелек (hotwallet).
В следующих разделах я анализирую варианты решения этой задачи без CREATE2 и рассказываю, почему мы отказались от них. Если вам интересен только конечный результат, вы можете найти его в разделе «Итоговое решение».
Ethereum-адреса
Самое простое решение — генерировать новые ethereum-адреса для новых пользователей. Эти адреса и будут кошельками. Чтобы перевести токены из кошелька в hotwallet, необходимо подписать транзакцию вызовом функции transfer() с приватным ключом кошелька из бэкенда.
Этот подход имеет следующие преимущества:
- это просто
- стоимость переноса токенов с кошелька на hotwallet равна цене вызова функции transfer()
Тем не менее, мы отказались от этого подхода, поскольку он имеет один существенный недостаток: вам нужно где-то хранить приватные ключи. И дело не только в том, что они могут быть потеряны, но ещё и в том, что вам необходимо тщательно управлять доступом к этим ключам. Если хотя бы один из них скомпрометирован, то токены определенного пользователя не достигнут горячего кошелька.
Создавать отдельный смарт-контракт для каждого пользователя
Развертывание отдельного смарт-контракта для каждого пользователя позволяет не хранить приватные ключи от кошельков на сервере. Биржа вызовет этот умный контракт для передачи токенов в hotwallet.
От этого решения мы тоже отказались, поскольку пользователю нельзя показать адрес его кошелька без развертывания смарт-контракта (это на самом деле возможно, но довольно сложным образом с другими недостатками, которые мы не будем здесь обсуждать). На бирже пользователь может создать столько аккаунтов, сколько ему нужно, и каждому нужен собственный кошелек. Это означает, что нам нужно тратить деньги на деплой контракта, даже не будучи уверенными, что пользователь будет использовать эту учетную запись.
Опкод CREATE2
Чтобы устранить проблему предыдущего способа, мы решили использовать опкод CREATE2. CREATE2 позволяет заранее определить адрес, по которому будет развернут смарт-контракт. Адрес рассчитывается по следующей формуле:
keccak256 (0xff ++ address ++ salt ++ keccak256 (init_code)) [12:]
, где:
- address — адрес смарт-контракта, который будет вызывать CREATE2
- salt — случайное значение
- init_code — байт-код смарт-контракта для развертывания
Таким образом гарантируется, что адрес, который мы предоставляем пользователю, действительно будет содержать желаемый байт-код. Кроме того, этот смарт-контракт может быть развернут, когда нам нужно. Например, когда пользователь решит впервые использовать свой кошелек.
Более того, вы можете рассчитывать адрес смарт-контракта каждый раз вместо того, чтобы хранить его, так как:
- address в формуле является постоянным, так как это адрес нашей фабрики кошельков
- salt — хеш user_id
- init_code является постоянным, так как мы используем один и тот же кошелек
Больше улучшений
Предыдущее решение все еще имеет один недостаток: вам нужно платить за развертывание умного контракта. Тем не менее, вы можете избавиться от этого. Для этого вы можете вызвать функцию transfer(), а затем selfdestruct() в конструкторе кошелька. И тогда газ за развертывание смарт-контракта будет возвращен.
Вопреки распространенному заблуждению, вы можете развернуть смарт-контракт по одному и тому же адресу несколько раз с опкодом CREATE2. Это связано с тем, что CREATE2 проверяет, что nonce целевого адреса равен нулю (ему присваивается значение «1» в начале конструктора). При этом функция selfdestruct() каждый раз сбрасывает nonce адреса. Таким образом, если вы снова вызовете CREATE2 с теми же аргументами, проверка на nonce пройдет.
Обратите внимание, что это решение аналогично варианту с ethereum-адресами, но без необходимости хранить приватные ключи. Стоимость перевода денег с кошелька на hotwallet примерно равна стоимости вызова функции transfer(), поскольку мы не платим за развертывание смарт-контракта.
Итоговое решение
Изначально подготовлено:
- функция для получения соли по user_id
- умный контракт, который будет вызывать опкод CREATE2 с соответствующей солью (т.е. фабрика кошельков)
- байт-код кошелька, соответствующий контракту со следующим конструктором:
constructor () {
address hotWallet = 0x…;
address token = 0x…;
token.transfer (hotWallet, token.balanceOf (address (this)));
selfdestruct (address (0));
}
Для каждого нового пользователя мы показываем его / ее адрес кошелька путем расчета
keccak256 (0xff ++ address ++ salt ++ keccak256 (init_code)) [12:]
Когда пользователь переводит токены на соответствующий адрес кошелька, наш бэкэнд видит событие Transfer с параметром _to, равным адресу кошелька. На этот момент уже возможно увеличить баланс пользователя на бирже до развертывания кошелька.
Когда на адресе кошелька накапливается достаточное количество токенов, мы можем перевести их все сразу в hotwallet. Для этого бекенд вызывает функцию фабрики смарт-контрактов, которая выполняет следующие действия:
function deployWallet (соль uint256) {
bytes memory walletBytecode =…;
// invoke CREATE2 with wallet bytecode and salt
}
Таким образом, вызывается конструктор смарт-контракта кошелька, который передает все свои токены на адрес hotwallet и затем самоуничтожается.
Полный код можно найти здесь . Обратите внимание, что это не наш продакшн-код, так как мы решили оптимизировать байт-код кошелька и записали его в опкодах.
Автор Павел Кондратенков, специалист в области Ethereum