Solidity hints. Часть 13
Понемногу движемся дальше и сегодня на очереди у нас:
19. Internal function calls do not create an EVM message call. They are called using simple jump statements. Same for functions of inherited contracts.
что в переводе:
Internal функции не создают EVM вызов, а используют внутренние опкоды jump. Тоже самое актуально и для наследуемых контрактов.
Тема может быть немного сложной для новичков в Solidity, так как связана с работой EVM и его низкоуровневых инструкций - опкодов. Но постараюсь объяснить простыми словами.
У нас есть 4 области видимости для функций: public, external, internal и private. Первые две - открытые для доступа других контрактов, вторые - закрыты.
Внешние функции являются частью интерфейса контракта, что означает, что их можно вызывать из других контрактов и через транзакции.
Доступ к внутренним функциям возможен только внутри текущего контракта или контрактов, вытекающих из него. Они не могут быть доступны извне. Поскольку они не открыты для внешнего доступа через ABI контракта, они могут принимать параметры внутренних типов, таких как mapping или storage references.
Внутренние функции вызываются через инструкции опкоды JUMP / JUMPI, просто перепрыгивая в другую точку текущего кода. Это имеет смысл, потому что внутренние функции не меняют контекст (то есть остаются внутри одного и того же контракта при вызове).
Другими словами, вы можете вызвать внутреннюю функцию только в том случае, если вы выполняете код внутри самого контракта и этот вызов внутренней функции не требует (да и не позволяет) напрямую обращаться к ней через EVM, а только через Jump.
Думаю, можно подытожить: если мы можем вызывать функцию извне, то это EVM вызов, если же она предназначена только для внутреннего использования - то это работа опкодов.
Следующий пункт:
20. If you have a public state variable of array type, then you can only retrieve single elements of the array via the generated getter function.
что в переводе и полной версии из документации звучит как:
Если у вас есть публичная переменная состояния типа массив, то вы можете получить только отдельные элементы массива через сгенерированную функцию getter. Этот механизм существует для того, чтобы избежать высоких газовых затрат при возврате целого массива. Вы можете использовать аргументы, чтобы указать, какой отдельный элемент возвращать, например myArray(0). Если вы хотите вернуть весь массив за один вызов, то вам нужно написать функцию, например:
Также немного запутанный пункт и документация для начинающих разработчиков.
Смотрите, когда мы создаем переменную с областью видимости public, EVM также создает для нее специальный геттер, по которому можно прочитать значение этой переменной. Проще всего это будет понять, попытавшись создать в Ремиксе контракт с двумя переменными public и internal.
Для первой переменной у вас появится кнопка в левой панели Ремикса, для того чтобы посмотреть ее значение, но для internal - ее не будет.
Так вот, если мы создадим массив с этой областью видимости, то обращаться через геттер к значениям хранящимся в нем, мы сможем только по специальному индексу. И у нас не получится вернуть весь массив сразу.
А чтобы посмотреть значения всего массива, нам нужно будет написать отдельную функцию для этого, например той, что была показана выше.
Хмм, вот сейчас перечитал пост и понял, что для новичков он вообще ничего не скажет, а для продвинутых - это и так само собой разумеющееся... Но надеюсь, хоть кому-то это будет полезно.
#array #getter #jump #external
Понемногу движемся дальше и сегодня на очереди у нас:
19. Internal function calls do not create an EVM message call. They are called using simple jump statements. Same for functions of inherited contracts.
что в переводе:
Internal функции не создают EVM вызов, а используют внутренние опкоды jump. Тоже самое актуально и для наследуемых контрактов.
Тема может быть немного сложной для новичков в Solidity, так как связана с работой EVM и его низкоуровневых инструкций - опкодов. Но постараюсь объяснить простыми словами.
У нас есть 4 области видимости для функций: public, external, internal и private. Первые две - открытые для доступа других контрактов, вторые - закрыты.
Внешние функции являются частью интерфейса контракта, что означает, что их можно вызывать из других контрактов и через транзакции.
Доступ к внутренним функциям возможен только внутри текущего контракта или контрактов, вытекающих из него. Они не могут быть доступны извне. Поскольку они не открыты для внешнего доступа через ABI контракта, они могут принимать параметры внутренних типов, таких как mapping или storage references.
Внутренние функции вызываются через инструкции опкоды JUMP / JUMPI, просто перепрыгивая в другую точку текущего кода. Это имеет смысл, потому что внутренние функции не меняют контекст (то есть остаются внутри одного и того же контракта при вызове).
Другими словами, вы можете вызвать внутреннюю функцию только в том случае, если вы выполняете код внутри самого контракта и этот вызов внутренней функции не требует (да и не позволяет) напрямую обращаться к ней через EVM, а только через Jump.
Думаю, можно подытожить: если мы можем вызывать функцию извне, то это EVM вызов, если же она предназначена только для внутреннего использования - то это работа опкодов.
Следующий пункт:
20. If you have a public state variable of array type, then you can only retrieve single elements of the array via the generated getter function.
что в переводе и полной версии из документации звучит как:
Если у вас есть публичная переменная состояния типа массив, то вы можете получить только отдельные элементы массива через сгенерированную функцию getter. Этот механизм существует для того, чтобы избежать высоких газовых затрат при возврате целого массива. Вы можете использовать аргументы, чтобы указать, какой отдельный элемент возвращать, например myArray(0). Если вы хотите вернуть весь массив за один вызов, то вам нужно написать функцию, например:
contract arrayExample {
uint[] public myArray;
function myArray(uint i) public view returns (uint) {
return myArray[i];
}
function getArray() public view returns (uint[] memory) {
return myArray;
}
}Также немного запутанный пункт и документация для начинающих разработчиков.
Смотрите, когда мы создаем переменную с областью видимости public, EVM также создает для нее специальный геттер, по которому можно прочитать значение этой переменной. Проще всего это будет понять, попытавшись создать в Ремиксе контракт с двумя переменными public и internal.
Для первой переменной у вас появится кнопка в левой панели Ремикса, для того чтобы посмотреть ее значение, но для internal - ее не будет.
Так вот, если мы создадим массив с этой областью видимости, то обращаться через геттер к значениям хранящимся в нем, мы сможем только по специальному индексу. И у нас не получится вернуть весь массив сразу.
А чтобы посмотреть значения всего массива, нам нужно будет написать отдельную функцию для этого, например той, что была показана выше.
Хмм, вот сейчас перечитал пост и понял, что для новичков он вообще ничего не скажет, а для продвинутых - это и так само собой разумеющееся... Но надеюсь, хоть кому-то это будет полезно.
#array #getter #jump #external
👍2💯2🌭1
Solidity hints. Часть 14
На этих выходных жара настолько разошлась, что город ввел веерные отключения света по всем районам, и несколько вышек, которые отвечают за интернет в моем ЖК просто "полетели"... Выходные я провел на мобильном интернете, который едва тянул, чтобы отправить сообщение в мессенджеры. Никогда бы не подумал, что буду скучать контенту, в котором "и так ничего нет"...
А мы продолжаем изучать нюансы Solidity и сегодня на очереди следующие пункты из репо Chinmaya:
21. Without a payable keyword in function declaration, it will auto reject all ether sent to it. It will revert.
что в переводе:
Без ключевого модификатора payable в функции, она не сможет принимать Эфир и будет откатывать транзакцию.
Вообще, удивительно, что при таком легком правиле, которое учится на самых первых уроках по Solidity, даже профессиональные протоколы могут упускать это из виду.
Вы можете сами зайти на сайт с подбором багов из контестов - Solodit - и ввести в поиск missing payable. На данный момент там 67 багов по этой теме...
Чаще всего разработчики забывают установить этот модификатор во внешних вызовах между контрактами. Например, из одного контракта мы отправляем Эфир в другой, там в функции делаем расчеты с msg.value, но забываем payable.
Тут единственное, что можно порекомендовать проверять наличие payable в функциях, где есть обработка msg.value, а также отслеживать движение Эфира и токенов между контрактами. Так мы сможете обнаружить и другие возможные баги.
22. Modifiers can also be defined in libraries but their use is limited to functions of the same library
что в переводе:
Модификаторы также можно создавать и в библиотеках, но они могут быть использованы только для функций в этих библиотеках.
Вообще библиотеки такая странная штука в Solidity, что она порой вызывает недоумение и у опытных разработчиков.
Библиотеки не могут хранить Эфир, не имеют своего storage, а то как они работают с вашим контрактом при наличии internal / external функций и говорить не стоит...
А тут еще и модификаторы подоспели.
В целом, тут указывается на то, что если в библиотеке был установлен модификатор, который используется в нескольких функциях, то использовать его в функциях протокола нельзя. Если я правильно помню, компилятор даже не даст собрать контракт с этой ошибкой, поэтому пофиксить ее можно будет еще на этапе разработки.
#modifier #payable #library
На этих выходных жара настолько разошлась, что город ввел веерные отключения света по всем районам, и несколько вышек, которые отвечают за интернет в моем ЖК просто "полетели"... Выходные я провел на мобильном интернете, который едва тянул, чтобы отправить сообщение в мессенджеры. Никогда бы не подумал, что буду скучать контенту, в котором "и так ничего нет"...
А мы продолжаем изучать нюансы Solidity и сегодня на очереди следующие пункты из репо Chinmaya:
21. Without a payable keyword in function declaration, it will auto reject all ether sent to it. It will revert.
что в переводе:
Без ключевого модификатора payable в функции, она не сможет принимать Эфир и будет откатывать транзакцию.
Вообще, удивительно, что при таком легком правиле, которое учится на самых первых уроках по Solidity, даже профессиональные протоколы могут упускать это из виду.
Вы можете сами зайти на сайт с подбором багов из контестов - Solodit - и ввести в поиск missing payable. На данный момент там 67 багов по этой теме...
Чаще всего разработчики забывают установить этот модификатор во внешних вызовах между контрактами. Например, из одного контракта мы отправляем Эфир в другой, там в функции делаем расчеты с msg.value, но забываем payable.
Тут единственное, что можно порекомендовать проверять наличие payable в функциях, где есть обработка msg.value, а также отслеживать движение Эфира и токенов между контрактами. Так мы сможете обнаружить и другие возможные баги.
22. Modifiers can also be defined in libraries but their use is limited to functions of the same library
что в переводе:
Модификаторы также можно создавать и в библиотеках, но они могут быть использованы только для функций в этих библиотеках.
Вообще библиотеки такая странная штука в Solidity, что она порой вызывает недоумение и у опытных разработчиков.
Библиотеки не могут хранить Эфир, не имеют своего storage, а то как они работают с вашим контрактом при наличии internal / external функций и говорить не стоит...
А тут еще и модификаторы подоспели.
В целом, тут указывается на то, что если в библиотеке был установлен модификатор, который используется в нескольких функциях, то использовать его в функциях протокола нельзя. Если я правильно помню, компилятор даже не даст собрать контракт с этой ошибкой, поэтому пофиксить ее можно будет еще на этапе разработки.
#modifier #payable #library
👍3
Solidity hints. Часть 15
Мы уже разобрали более 20 пунктов из репо и я подумываю сделать недельный перерыв от них. В том плане, что хочется изучить или рассмотреть что-то новое, и вот смотрю я на Account Abstraction. Может разберем его на следующей неделе?
Было несколько годных статей на Alchemy по этой теме, да и Патрик Коллинс выпустил 4 часовое видео. Для первого касания хватит, а там посмотрим чего нам не будет хватать для понимания темы.
Ну, а пока что, еще пара пунктов:
23. Multiple modifiers are applied to a function by specifying them in a whitespace-separated list and are evaluated in the order presented. Modifier Order Matters
что в переводе:
Несколько модификаторов применяются к функции путем указания их в списке, разделенном пробелами, и оцениваются в указанном порядке. Порядок модификаторов имеет значение.
Тут, в целом, и так все понятно. Когда мы проходили наследования контрактов, нам указывали на то, что очень важен порядок наследования. Например, когда мы создаем контракт токена, в котором разрешены функции permit, то последовательность наследований у нас будет такая:
и если мы нарушим этот порядок и поставим ERC20Permit перед основным контрактом ERC20, то сам компилятор будет выдавать ошибку.
С модификаторами чуточку сложнее. В большинстве случаев компилятор на покажет вам предупреждений или ошибок по вопросу их порядка в функции. Однако сама логика работы функции может быть нарушена.
Если вам интересно разобраться с работой модификаторов, то вот прекрасный пример для головоломки:
Как думаете, как значения получатся после выполнения функции? И ответ на этот вопрос моно будет найти в следующем пункте:
24. The _ symbol can appear in the modifier multiple times. Each occurrence is replaced with the function body. Symbols introduced in the modifier are not visible in the function
что в переводе:
Символ _ может встречаться в модификаторе несколько раз. Каждое появление заменяется телом функции. Символы, введенные в модификатор, не видны в функции.
Думаю, тут будет лучше самим перенести код в Ремикс и попробовать "поиграться" с функцией, так будет намного понятнее, как происходит работа внутри и на что влияет порядок.
#modifier
Мы уже разобрали более 20 пунктов из репо и я подумываю сделать недельный перерыв от них. В том плане, что хочется изучить или рассмотреть что-то новое, и вот смотрю я на Account Abstraction. Может разберем его на следующей неделе?
Было несколько годных статей на Alchemy по этой теме, да и Патрик Коллинс выпустил 4 часовое видео. Для первого касания хватит, а там посмотрим чего нам не будет хватать для понимания темы.
Ну, а пока что, еще пара пунктов:
23. Multiple modifiers are applied to a function by specifying them in a whitespace-separated list and are evaluated in the order presented. Modifier Order Matters
что в переводе:
Несколько модификаторов применяются к функции путем указания их в списке, разделенном пробелами, и оцениваются в указанном порядке. Порядок модификаторов имеет значение.
Тут, в целом, и так все понятно. Когда мы проходили наследования контрактов, нам указывали на то, что очень важен порядок наследования. Например, когда мы создаем контракт токена, в котором разрешены функции permit, то последовательность наследований у нас будет такая:
contract MyToken is ERC20, ERC20Permit, Ownable {
}
и если мы нарушим этот порядок и поставим ERC20Permit перед основным контрактом ERC20, то сам компилятор будет выдавать ошибку.
С модификаторами чуточку сложнее. В большинстве случаев компилятор на покажет вам предупреждений или ошибок по вопросу их порядка в функции. Однако сама логика работы функции может быть нарушена.
Если вам интересно разобраться с работой модификаторов, то вот прекрасный пример для головоломки:
pragma solidity ^0.4.18;
contract modifierTest {
uint public modState1;
uint public modState2;
uint public modState3;
modifier modA() {
modState1 = modState1 + 1;
_;
}
modifier modB() {
modState2 = modState2 + 1;
_;
modState2 = modState2 + 1;
_;
}
function func() public modA modB {
modState3 = modState3 + 1;
}
}
Как думаете, как значения получатся после выполнения функции? И ответ на этот вопрос моно будет найти в следующем пункте:
24. The _ symbol can appear in the modifier multiple times. Each occurrence is replaced with the function body. Symbols introduced in the modifier are not visible in the function
что в переводе:
Символ _ может встречаться в модификаторе несколько раз. Каждое появление заменяется телом функции. Символы, введенные в модификатор, не видны в функции.
Думаю, тут будет лучше самим перенести код в Ремикс и попробовать "поиграться" с функцией, так будет намного понятнее, как происходит работа внутри и на что влияет порядок.
#modifier
🔥3👍2
Solidity hints. Часть 16
Последний пост перед небольшим перерывом с циклом Solidity Hints, так как на следующей неделе мы поговорим про Account Abstractions.
Итак:
25. For values of immutable variables, 32 bytes are reserved in the code, even if they would fit in fewer bytes
26. The compiler does not reserve a storage slot for constant and immutable variables, and every occurrence is replaced by the respective value directly in the code.
что в переводе:
25. Для значений immutable переменных всегда резервируется 32 байта памяти, не смотря на то, что сами значения могут по итогу заниматься меньше места.
26. Компилятор не резервирует слот для хранения постоянных и неизменяемых переменных, и каждое их вхождение заменяется соответствующим значением непосредственно в коде.
Я решил объединить эти пункты, так как разбор описывает оба этих случая.
Если вы хоть раз интересовались работой памяти в Solidity, то наверняка слышали рекомендацию по упаковке в правильный порядок переменных состояния контракта, чтобы они занимали меньше места. Например,
uint256 - занимает весь слот памяти в 32 байта;
две переменных uint128 можно расположить рядом, чтобы они занимали только один слот, вместо двух.
address занимает 20 байтов, а bool - всего 1.
Зная размерность наших переменных мы может сгруппировать их таким образом, чтобы они компактно размещались в памяти.
При этом два вида переменных, constant и immutable - размещаются не в памяти контракта, а в его байткоде.
Значение переменной константы, при деплое контракта, просто копируется во все места в самом контракте, где к ней обращаются. Для значений immutable переменной происходит тоже самое, только для нее резервируется 32 байта, даже если значение занимает гораздо меньше места. Именно поэтому константы иногда могут быть дешевле по газу, чем immutable переменные.
#immutable #constant #storage #bytecode
Последний пост перед небольшим перерывом с циклом Solidity Hints, так как на следующей неделе мы поговорим про Account Abstractions.
Итак:
25. For values of immutable variables, 32 bytes are reserved in the code, even if they would fit in fewer bytes
26. The compiler does not reserve a storage slot for constant and immutable variables, and every occurrence is replaced by the respective value directly in the code.
что в переводе:
25. Для значений immutable переменных всегда резервируется 32 байта памяти, не смотря на то, что сами значения могут по итогу заниматься меньше места.
26. Компилятор не резервирует слот для хранения постоянных и неизменяемых переменных, и каждое их вхождение заменяется соответствующим значением непосредственно в коде.
Я решил объединить эти пункты, так как разбор описывает оба этих случая.
Если вы хоть раз интересовались работой памяти в Solidity, то наверняка слышали рекомендацию по упаковке в правильный порядок переменных состояния контракта, чтобы они занимали меньше места. Например,
uint256 - занимает весь слот памяти в 32 байта;
две переменных uint128 можно расположить рядом, чтобы они занимали только один слот, вместо двух.
address занимает 20 байтов, а bool - всего 1.
Зная размерность наших переменных мы может сгруппировать их таким образом, чтобы они компактно размещались в памяти.
При этом два вида переменных, constant и immutable - размещаются не в памяти контракта, а в его байткоде.
Значение переменной константы, при деплое контракта, просто копируется во все места в самом контракте, где к ней обращаются. Для значений immutable переменной происходит тоже самое, только для нее резервируется 32 байта, даже если значение занимает гораздо меньше места. Именно поэтому константы иногда могут быть дешевле по газу, чем immutable переменные.
#immutable #constant #storage #bytecode
👍4
Account abstraction (ERC-4337). Часть 1
С этого дня начнем разбирать AA и для начала я решил прочитать серию статей с Alchemy, перевод чего и буду делать тут. Если у вас есть еще какие интересные материалы по этой теме, делитесь в комментариях.
Мне понравилась эта статья тем, что автор разбирает шаги, которые проходило сообщество прежде чем сформировать идею АА, а также множество примеров кода. Далее пойдет уже перевод первой части статьи.
Для начала давайте придумаем, как защитить наши самые ценные активы. Мы хотим иметь возможность подписывать большинство транзакций одним закрытым ключом (как в обычном аккаунте кошельке), и наш бесценный Courage NFT можно будет передать, только если мы подпишем его вторым ключом, который мы закроем в банковском хранилище, охраняемом трехголовым псом.
И вот какой будет наша первая задача.
Каждый счет Ethereum - это либо смарт-контракт, либо счет, принадлежащий внешнему владельцу (EOA), причем последний управляется off-chain с помощью закрытого ключа. Должен ли счет, на котором хранятся эти активы, быть смарт-контрактом или EOA?
На самом деле, держателем активов должен быть смарт-контракт. Если бы это был EOA, то активы всегда можно было бы передать с помощью транзакций, подписанных закрытым ключом EOA, что не обеспечит нужную нам безопасность.
Поэтому, в отличие от большинства людей, наше присутствие onchain будет представлено смарт-контрактом, а не EOA, который мы будем называть кошельком смарт-контракта или просто «кошельком».
Нам нужен способ отдавать команды этому смарт-контракту, чтобы он выполнял нужные нам действия. В частности, нам нужно иметь возможность приказать смарт-контракту выполнить любой вид call()/transfer(), который я мог бы отправить из EOA.
P.S. Каждому пользователю, желающему защитить свои активы таким образом, потребуется собственный смарт-контракт. Не может быть одного большого контракта, в котором хранятся активы нескольких человек, потому что остальная экосистема предполагает, что один адрес представляет одно лицо, и не сможет различать отдельных пользователей.
Например, если кто-то захочет отправить NFT кому-то в контракте с объединенным кошельком, API передачи NFT позволит отправителю указать только адрес объединенного кошелька, но не отдельного пользователя в нем.
Пользовательские операции
Я разверну смарт-контракт кошелька, который будет хранить мои активы и который имеет один метод, в котором я передаю ему информацию о том, какой вызов я хочу, чтобы он сделал.
Назовем данные, представляющие действие, которое я хочу, чтобы кошелек выполнил, пользовательской операцией или user op.
Итак, контракт кошелька выглядит следующим образом:
Что входит в пользовательскую операцию?
Во-первых, нам нужны все параметры, которые мы обычно передаем в eth_sendTransaction:
Кроме того, нам нужно предоставить что-то для авторизации запроса - то есть фрагмент данных, на который кошелек будет смотреть, чтобы решить, хочет ли он выполнить операцию или нет.
Для нашего кошелька, защищающего NFT, для большинства пользовательских операций мы передадим подпись остальной части операции, подписанную нашим главным ключом.
Но если пользовательская операция заключается в передаче нашего сверхценного NFT Carbonated Courage, то кошельку потребуется передать подписи остальной части операции, подписанные каждым из двух наших ключей.
Мы также добавим nonce для предотвращения атак повторного воспроизведения, когда кто-то может повторно отправить предыдущую пользовательскую операцию, чтобы запустить ее снова:
Это то, что нам нужно! Пока мой Carbonated Courage NFT находится под действием этого контракта, он не может быть передан без двух подписей.
С этого дня начнем разбирать AA и для начала я решил прочитать серию статей с Alchemy, перевод чего и буду делать тут. Если у вас есть еще какие интересные материалы по этой теме, делитесь в комментариях.
Мне понравилась эта статья тем, что автор разбирает шаги, которые проходило сообщество прежде чем сформировать идею АА, а также множество примеров кода. Далее пойдет уже перевод первой части статьи.
Для начала давайте придумаем, как защитить наши самые ценные активы. Мы хотим иметь возможность подписывать большинство транзакций одним закрытым ключом (как в обычном аккаунте кошельке), и наш бесценный Courage NFT можно будет передать, только если мы подпишем его вторым ключом, который мы закроем в банковском хранилище, охраняемом трехголовым псом.
И вот какой будет наша первая задача.
Каждый счет Ethereum - это либо смарт-контракт, либо счет, принадлежащий внешнему владельцу (EOA), причем последний управляется off-chain с помощью закрытого ключа. Должен ли счет, на котором хранятся эти активы, быть смарт-контрактом или EOA?
На самом деле, держателем активов должен быть смарт-контракт. Если бы это был EOA, то активы всегда можно было бы передать с помощью транзакций, подписанных закрытым ключом EOA, что не обеспечит нужную нам безопасность.
Поэтому, в отличие от большинства людей, наше присутствие onchain будет представлено смарт-контрактом, а не EOA, который мы будем называть кошельком смарт-контракта или просто «кошельком».
Нам нужен способ отдавать команды этому смарт-контракту, чтобы он выполнял нужные нам действия. В частности, нам нужно иметь возможность приказать смарт-контракту выполнить любой вид call()/transfer(), который я мог бы отправить из EOA.
P.S. Каждому пользователю, желающему защитить свои активы таким образом, потребуется собственный смарт-контракт. Не может быть одного большого контракта, в котором хранятся активы нескольких человек, потому что остальная экосистема предполагает, что один адрес представляет одно лицо, и не сможет различать отдельных пользователей.
Например, если кто-то захочет отправить NFT кому-то в контракте с объединенным кошельком, API передачи NFT позволит отправителю указать только адрес объединенного кошелька, но не отдельного пользователя в нем.
Пользовательские операции
Я разверну смарт-контракт кошелька, который будет хранить мои активы и который имеет один метод, в котором я передаю ему информацию о том, какой вызов я хочу, чтобы он сделал.
Назовем данные, представляющие действие, которое я хочу, чтобы кошелек выполнил, пользовательской операцией или user op.
Итак, контракт кошелька выглядит следующим образом:
contract Wallet {
function executeOp(UserOperation op);
}Что входит в пользовательскую операцию?
Во-первых, нам нужны все параметры, которые мы обычно передаем в eth_sendTransaction:
struct UserOperation {
address to;
bytes data;
uint256 value; // Amount of wei sent
uint256 gas;
// ...
}Кроме того, нам нужно предоставить что-то для авторизации запроса - то есть фрагмент данных, на который кошелек будет смотреть, чтобы решить, хочет ли он выполнить операцию или нет.
Для нашего кошелька, защищающего NFT, для большинства пользовательских операций мы передадим подпись остальной части операции, подписанную нашим главным ключом.
Но если пользовательская операция заключается в передаче нашего сверхценного NFT Carbonated Courage, то кошельку потребуется передать подписи остальной части операции, подписанные каждым из двух наших ключей.
Мы также добавим nonce для предотвращения атак повторного воспроизведения, когда кто-то может повторно отправить предыдущую пользовательскую операцию, чтобы запустить ее снова:
struct UserOperation {
// ...
bytes signature;
uint256 nonce;
}Это то, что нам нужно! Пока мой Carbonated Courage NFT находится под действием этого контракта, он не может быть передан без двух подписей.
❤2
P.S. Хотя кошелек может интерпретировать поля подписи и nonce по своему усмотрению, я ожидаю, что почти все кошельки будут использовать поле подписи для получения некой подписи поверх всех остальных полей, чтобы предотвратить подделку или фальсификацию операции неавторизованными лицами. Аналогично, я ожидаю, что почти любой кошелек будет отклонять операции с уже виденным им nonce.
Далее поговорим о том, что может вызывать наш кошелек - смарт контракт.
#accountabstraction
Далее поговорим о том, что может вызывать наш кошелек - смарт контракт.
#accountabstraction
👍4
Account abstraction (ERC-4337). Часть 2
Кто вызывает кошелек смарт-контракта?
Один из вопросов, на который здесь нет ответа, - это то, как вызывается executeOp(op) (смотрите пост выше). Поскольку она ничего не сделает без подписей моих закрытых ключей, мы можем позволить любому попытаться вызвать ее, и никаких рисков для безопасности не будет. Но нам нужно, чтобы кто-то действительно сделал этот вызов, чтобы операция произошла.
В Ethereum все транзакции должны исходить от EOA, а вызывающий EOA должен оплачивать газ своими собственными ETH.
Я могу завести отдельный счет EOA, единственной целью которого будет вызов контракта моего кошелька. Хотя этот EOA не будет иметь такой же защиты от двух подписей, как контракт кошелька, на нем должно храниться только ETH, чтобы оплатить бензин для работы моего кошелька, в то время как более надежный контракт кошелька может хранить все мое ценное имущество.
Таким образом, мы фактически получили большую часть функциональности а account abstraction с помощью всего одного довольно простого контракта!
#accountabstraction
Кто вызывает кошелек смарт-контракта?
Один из вопросов, на который здесь нет ответа, - это то, как вызывается executeOp(op) (смотрите пост выше). Поскольку она ничего не сделает без подписей моих закрытых ключей, мы можем позволить любому попытаться вызвать ее, и никаких рисков для безопасности не будет. Но нам нужно, чтобы кто-то действительно сделал этот вызов, чтобы операция произошла.
В Ethereum все транзакции должны исходить от EOA, а вызывающий EOA должен оплачивать газ своими собственными ETH.
Я могу завести отдельный счет EOA, единственной целью которого будет вызов контракта моего кошелька. Хотя этот EOA не будет иметь такой же защиты от двух подписей, как контракт кошелька, на нем должно храниться только ETH, чтобы оплатить бензин для работы моего кошелька, в то время как более надежный контракт кошелька может хранить все мое ценное имущество.
Таким образом, мы фактически получили большую часть функциональности а account abstraction с помощью всего одного довольно простого контракта!
#accountabstraction
Account abstraction (ERC-4337). Часть 3
Цель: отсутствие отдельного EOA
Недостатком вышеописанного решения является требование завести отдельный аккаунт EOA для вызова кошелька. Что, если я не хочу этого делать? На данный момент я по-прежнему готов платить за свой газ с помощью ETH. Я просто не хочу иметь два отдельных счета.
Мы говорили, что метод executeOp контракта кошелька может быть вызван кем угодно, так что мы можем просто попросить кого-то другого с EOA вызвать его для нас. Я буду называть этого EOA и человека, управляющего им, «исполнителем».
Поскольку исполнитель - это тот, кто платит за газ, не многие захотят делать это бесплатно. Поэтому новый план заключается в том, что в контракте кошелька будет храниться некоторое количество ETH, и в рамках вызова исполнителя кошелек будет переводить некоторое количество ETH исполнителю, чтобы компенсировать ему израсходованный газ.
P.S. «Исполнитель» - это не термин ERC-4337, но это хорошее описание того, что делает этот участник. Позже мы заменим его на реальный термин, используемый в ERC-4337, «bundler», но пока нет смысла делать это, поскольку в данный момент мы не занимаемся никакими связками. В других протоколах этот участник может также называться «ретранслятором».
#accountabstraction
Цель: отсутствие отдельного EOA
Недостатком вышеописанного решения является требование завести отдельный аккаунт EOA для вызова кошелька. Что, если я не хочу этого делать? На данный момент я по-прежнему готов платить за свой газ с помощью ETH. Я просто не хочу иметь два отдельных счета.
Мы говорили, что метод executeOp контракта кошелька может быть вызван кем угодно, так что мы можем просто попросить кого-то другого с EOA вызвать его для нас. Я буду называть этого EOA и человека, управляющего им, «исполнителем».
Поскольку исполнитель - это тот, кто платит за газ, не многие захотят делать это бесплатно. Поэтому новый план заключается в том, что в контракте кошелька будет храниться некоторое количество ETH, и в рамках вызова исполнителя кошелек будет переводить некоторое количество ETH исполнителю, чтобы компенсировать ему израсходованный газ.
P.S. «Исполнитель» - это не термин ERC-4337, но это хорошее описание того, что делает этот участник. Позже мы заменим его на реальный термин, используемый в ERC-4337, «bundler», но пока нет смысла делать это, поскольку в данный момент мы не занимаемся никакими связками. В других протоколах этот участник может также называться «ретранслятором».
#accountabstraction
👍3
Account abstraction (ERC-4337). Часть 4
Первая попытка: Кошелек возвращает деньги исполнителю в конце
Давайте попробуем упростить задачу. Мы сказали, что интерфейс кошелька:
Мы попробуем изменить поведение executeOp таким образом, чтобы в самом конце он смотрел, сколько газа он использовал, и отправлял соответствующее количество ETH исполнителю, который оплатил транзакцию.
Первое знакомство с симуляцией
Данная реализация отлично сработает, если мой кошелек заслуживает доверия у исполнителя. Но второй должен быть уверен, что кошелек действительно выплатит возмещение. Если исполнитель вызовет executeOp, а кошелек не вернет деньги за использованный газ, то исполнитель окажется обманут.
Чтобы попытаться избежать этого сценария, исполнитель может попробовать смоделировать операцию executeOp локально, вероятно, с помощью debug_traceCall, и посмотреть, действительно ли ему компенсируют его газ. Только после этого он отправит настоящую транзакцию.
Проблема здесь в том, что моделирование не позволяет идеально предсказать будущее. Вполне возможно, что кошелек платит за газ во время симуляции и не сделает этого, когда транзакция действительно будет добавляться в блок.
Недобросовестный кошелек может сделать это намеренно, чтобы его операции выполнялись бесплатно, а исполнителю приходил огромный счет за газ.
Симуляция может отличаться от реального выполнения по нескольким причинам:
1. Операция может считываться из хранилища (storage), а хранилище может меняться между временем симуляции и временем реального исполнения функции.
2. Операция может использовать такие опкоды, как TIMESTAMP, BLOCKHASH, BASEFEE и т. д. Эти опкоды считывают информацию из среды и непредсказуемо меняются от блока к блоку.
Исполнитель мог бы попытаться ограничить то, что разрешено делать при исполнении функции, например, отклонить любую операцию, использующую любой из опкодов «окружения». Но это было бы слишком жестким ограничением.
Помните, что мы хотим, чтобы кошелек мог делать все, что может делать EOA, поэтому запрет этих опкодов заблокирует слишком много законных применений. Например, это не позволит кошельку взаимодействовать с Uniswap, который широко использует TIMESTAMP.
Поскольку executeOp кошелька может содержать произвольный код, и мы не можем разумно ограничить его, чтобы не дать ему обмануть симуляцию, эта проблема неразрешима с текущим интерфейсом. executeOp - это просто слишком большой «черный ящик».
#accountabstraction
Первая попытка: Кошелек возвращает деньги исполнителю в конце
Давайте попробуем упростить задачу. Мы сказали, что интерфейс кошелька:
contract Wallet {
function executeOp(UserOperation op);
}Мы попробуем изменить поведение executeOp таким образом, чтобы в самом конце он смотрел, сколько газа он использовал, и отправлял соответствующее количество ETH исполнителю, который оплатил транзакцию.
Первое знакомство с симуляцией
Данная реализация отлично сработает, если мой кошелек заслуживает доверия у исполнителя. Но второй должен быть уверен, что кошелек действительно выплатит возмещение. Если исполнитель вызовет executeOp, а кошелек не вернет деньги за использованный газ, то исполнитель окажется обманут.
Чтобы попытаться избежать этого сценария, исполнитель может попробовать смоделировать операцию executeOp локально, вероятно, с помощью debug_traceCall, и посмотреть, действительно ли ему компенсируют его газ. Только после этого он отправит настоящую транзакцию.
Проблема здесь в том, что моделирование не позволяет идеально предсказать будущее. Вполне возможно, что кошелек платит за газ во время симуляции и не сделает этого, когда транзакция действительно будет добавляться в блок.
Недобросовестный кошелек может сделать это намеренно, чтобы его операции выполнялись бесплатно, а исполнителю приходил огромный счет за газ.
Симуляция может отличаться от реального выполнения по нескольким причинам:
1. Операция может считываться из хранилища (storage), а хранилище может меняться между временем симуляции и временем реального исполнения функции.
2. Операция может использовать такие опкоды, как TIMESTAMP, BLOCKHASH, BASEFEE и т. д. Эти опкоды считывают информацию из среды и непредсказуемо меняются от блока к блоку.
Исполнитель мог бы попытаться ограничить то, что разрешено делать при исполнении функции, например, отклонить любую операцию, использующую любой из опкодов «окружения». Но это было бы слишком жестким ограничением.
Помните, что мы хотим, чтобы кошелек мог делать все, что может делать EOA, поэтому запрет этих опкодов заблокирует слишком много законных применений. Например, это не позволит кошельку взаимодействовать с Uniswap, который широко использует TIMESTAMP.
Поскольку executeOp кошелька может содержать произвольный код, и мы не можем разумно ограничить его, чтобы не дать ему обмануть симуляцию, эта проблема неразрешима с текущим интерфейсом. executeOp - это просто слишком большой «черный ящик».
#accountabstraction
🔥2
Account abstraction (ERC-4337). Часть 5
Попытка 2: Внедрение точки входа
Проблема здесь в том, что мы просим исполнителя запустить код из контракта, которому, возможно, нет доверия. Исполнитель хочет выполнять эти "недоверенные" операции в контексте, который предоставляет определенные гарантии. Чувствуете тонкую грань?
В этом и заключается вся цель смарт-контрактов, поэтому мы введем новый доверенный (т. е. прошедший аудит и проверку исходного кода) контракт, называемый точкой входа, и дадим ему метод, который исполнитель будет вызывать вместо него:
handleOp будет делать следующее:
1. Проверять, достаточно ли у кошелька средств для оплаты максимального количества газа, которое он может использовать (на основе поля gas в пользовательской операции). Если нет, то отказ.
2. Вызов метода executeOp кошелька (с соответствующим газом), отслеживая, сколько газа он фактически использует.
3. Отправка части ETH из кошелька исполнителю, как оплата за использованный газ.
Чтобы этот третий пункт работал, нам на самом деле нужно, чтобы точка входа хранила ETH для оплаты газа, а не сам кошелек, потому что, как мы видели в предыдущем разделе, мы не можем быть уверены, что сможем получить ETH из кошелька. Таким образом, точка входа также должна иметь метод для кошелька (или кого-то от имени кошелька), чтобы положить ETH в точку входа для оплаты газа, и у нас будет другой метод, чтобы кошелек мог забрать свой ETH обратно, когда захочет:
При такой реализации исполнитель получает компенсацию за газ, несмотря ни на что.
Этот способ отлично работает для исполнителя, но создает некоторые проблемы для кошелька...
#accountabstraction
Попытка 2: Внедрение точки входа
Проблема здесь в том, что мы просим исполнителя запустить код из контракта, которому, возможно, нет доверия. Исполнитель хочет выполнять эти "недоверенные" операции в контексте, который предоставляет определенные гарантии. Чувствуете тонкую грань?
В этом и заключается вся цель смарт-контрактов, поэтому мы введем новый доверенный (т. е. прошедший аудит и проверку исходного кода) контракт, называемый точкой входа, и дадим ему метод, который исполнитель будет вызывать вместо него:
contract EntryPoint {
function handleOp(UserOperation op);
handleOp будет делать следующее:
1. Проверять, достаточно ли у кошелька средств для оплаты максимального количества газа, которое он может использовать (на основе поля gas в пользовательской операции). Если нет, то отказ.
2. Вызов метода executeOp кошелька (с соответствующим газом), отслеживая, сколько газа он фактически использует.
3. Отправка части ETH из кошелька исполнителю, как оплата за использованный газ.
Чтобы этот третий пункт работал, нам на самом деле нужно, чтобы точка входа хранила ETH для оплаты газа, а не сам кошелек, потому что, как мы видели в предыдущем разделе, мы не можем быть уверены, что сможем получить ETH из кошелька. Таким образом, точка входа также должна иметь метод для кошелька (или кого-то от имени кошелька), чтобы положить ETH в точку входа для оплаты газа, и у нас будет другой метод, чтобы кошелек мог забрать свой ETH обратно, когда захочет:
contract EntryPoint {
// ...
function deposit(address wallet) payable;
function withdrawTo(address payable destination);
}При такой реализации исполнитель получает компенсацию за газ, несмотря ни на что.
Этот способ отлично работает для исполнителя, но создает некоторые проблемы для кошелька...
#accountabstraction
Account abstraction (ERC-4337). Часть 6
Разделение проверки и исполнения функции
Ранее мы определили интерфейс кошелька как:
Этот метод на самом деле делает две вещи: он проверяет, что пользовательская операция авторизована, а затем он фактически выполняет вызов, указанный ею. Это не имеет значения, когда владелец кошелька оплачивает газ со своего счета, но теперь, когда мы просим исполнителя сделать это, оно стало важным.
В нашей текущей реализации кошелек возвращает плату за бензин исполнителю, несмотря ни на что. Но на самом деле мы не хотим, чтобы кошелек платил, если проверка не прошла.
Если проверка не проходит, это означает, что кто-то, не имеющий полномочий над кошельком, попросил кошелек сделать что-то.
В этом случае executeOp кошелька корректно заблокирует операцию, но в соответствии с текущей реализацией кошельку все равно придется заплатить за газ.
Это проблема, потому что кто-то, не имеющий отношения к кошельку, может запросить у него кучу операций и израсходовать все деньги кошелька на газ.
С другой стороны, если проверка прошла успешно, но операция после этого не удалась, то кошельку следует заплатить за использованный газ. Это означает, что владелец кошелька санкционировал действие, которое оказалось неудачным, например отправку отмененной транзакции из EOA, и поскольку он санкционировал это действие, он должен нести ответственность за это.
Текущий интерфейс кошелька с одним методом не дает возможности отличить сбои проверки от сбоев выполнения, поэтому нам нужно разделить его на две части.
Дополним функции в нашем кошельке:
Новая реализация handleOp будет выглядеть так:
1. Вызов validateOp. Если это не удастся, то исполнение прекратится.
2. Отложить ETH из депозита кошелька, чтобы оплатить максимальное количество газа, которое он может использовать (на основе переменной газа в ОП). Если у кошелька недостаточно средств, отказать в исполнении.
3. Вызвать executeOp и отследить, сколько газа он использует. Независимо от того, удался этот вызов или нет, возместите исполнителю стоимость газа из средств, которые мы отложили, и верните оставшиеся средства на депозит кошелька.
Теперь для кошелька все должно работать как надо! С него не будет взиматься плата за газ, за исключением операций, которые он санкционировал.
Однако... дела у исполнителя снова идут не так как надо...
P.S. Мы должны быть уверены, что неавторизованный пользователь не сможет напрямую вызвать executeOp на кошельке, заставив его выполнить действие без проверки. Кошелек может предотвратить это, обеспечив, чтобы executeOp мог быть вызван только точкой входа.
#accountabstraction
Разделение проверки и исполнения функции
Ранее мы определили интерфейс кошелька как:
contract Wallet {
function executeOp(UserOperation op);
}Этот метод на самом деле делает две вещи: он проверяет, что пользовательская операция авторизована, а затем он фактически выполняет вызов, указанный ею. Это не имеет значения, когда владелец кошелька оплачивает газ со своего счета, но теперь, когда мы просим исполнителя сделать это, оно стало важным.
В нашей текущей реализации кошелек возвращает плату за бензин исполнителю, несмотря ни на что. Но на самом деле мы не хотим, чтобы кошелек платил, если проверка не прошла.
Если проверка не проходит, это означает, что кто-то, не имеющий полномочий над кошельком, попросил кошелек сделать что-то.
В этом случае executeOp кошелька корректно заблокирует операцию, но в соответствии с текущей реализацией кошельку все равно придется заплатить за газ.
Это проблема, потому что кто-то, не имеющий отношения к кошельку, может запросить у него кучу операций и израсходовать все деньги кошелька на газ.
С другой стороны, если проверка прошла успешно, но операция после этого не удалась, то кошельку следует заплатить за использованный газ. Это означает, что владелец кошелька санкционировал действие, которое оказалось неудачным, например отправку отмененной транзакции из EOA, и поскольку он санкционировал это действие, он должен нести ответственность за это.
Текущий интерфейс кошелька с одним методом не дает возможности отличить сбои проверки от сбоев выполнения, поэтому нам нужно разделить его на две части.
Дополним функции в нашем кошельке:
contract Wallet {
function validateOp(UserOperation op);
function executeOp(UserOperation op);
}Новая реализация handleOp будет выглядеть так:
1. Вызов validateOp. Если это не удастся, то исполнение прекратится.
2. Отложить ETH из депозита кошелька, чтобы оплатить максимальное количество газа, которое он может использовать (на основе переменной газа в ОП). Если у кошелька недостаточно средств, отказать в исполнении.
3. Вызвать executeOp и отследить, сколько газа он использует. Независимо от того, удался этот вызов или нет, возместите исполнителю стоимость газа из средств, которые мы отложили, и верните оставшиеся средства на депозит кошелька.
Теперь для кошелька все должно работать как надо! С него не будет взиматься плата за газ, за исключением операций, которые он санкционировал.
Однако... дела у исполнителя снова идут не так как надо...
P.S. Мы должны быть уверены, что неавторизованный пользователь не сможет напрямую вызвать executeOp на кошельке, заставив его выполнить действие без проверки. Кошелек может предотвратить это, обеспечив, чтобы executeOp мог быть вызван только точкой входа.
#accountabstraction
Account abstraction (ERC-4337). Часть 7
Повторное моделирование
Теперь, когда неавторизованные пользователи отправляют операции для кошелька, операция проваливается в validateOp, и контракту не нужно платить за это. Однако исполнитель по-прежнему платит за работу validateOp и не получает за это компенсации.
Недобросовестные кошельки больше не могут выполнять свои операции бесплатно, но злоумышленники по-прежнему могут заставлять исполнителя тратить деньги за неудачные операции.
В предыдущем разделе, исполнитель сначала пытался смоделировать операцию локально, чтобы проверить, пройдет ли она, и только потом отправлял транзакцию для вызова handleOp на цепочке.
Тогда мы столкнулись с проблемой, когда исполнитель не мог контролировать выполнение операции: в том смысле, что она могла быть успешной в симуляции, но провалиться в реальности.
Но в этот раз будет по-другому.
Исполнителю не нужно симулировать все выполнение, которое теперь состоит из validateOp и executeOp. Ему нужно симулировать только первую часть, validateOp, чтобы знать, получит он деньги или нет. И в отличие от executeOp, который должен иметь возможность выполнять произвольные действия, чтобы кошелек мог свободно взаимодействовать с блокчейном, на validateOp мы можем наложить более строгие ограничения.
Если быть точным, исполнитель отклонит пользовательскую операцию, не передавая ее в цепочку, если validateOp не удовлетворяет следующим ограничениям:
1. Он никогда не использует опкоды из определенного банлиста, в который входят такие коды, как TIMESTAMP, BLOCKHASH и т. д.
2. Единственное хранилище, к которому он обращается, - это связанное хранилище кошелька, определяемое как любое из следующих:
- Собственное хранилище кошелька.
- Хранилище другого контракта в слоте, соответствующем кошельку в связке(адрес => значение).
- Хранилище другого контракта в слоте, равном адресу кошелька (это необычная схема хранения, которая не встречается в Solidity).
Цель этих правил - свести к минимуму случаи, когда validateOp успешно работает в симуляции, но терпит неудачу при реальном выполнении.
Запрещенные опкоды не требуют пояснений, но ограничения на хранение данных могут показаться несколько странными.
Идея заключается в том, что любой доступ к хранилищу представляет опасность ложной симуляции, потому что слот хранилища может измениться между симуляцией и выполнением, но если мы ограничим хранилище только теми местами, которые связаны с данным кошельком, то для фальсификации симуляции злоумышленнику потребуется обновить хранилище, относящееся именно к этому кошельку. Хочется надеяться, что стоимость обновления этого хранилища будет достаточной, чтобы отпугнуть злоумышленника.
С такой симуляцией и кошелек, и исполнитель находятся в безопасности.
P.S. Это ограничение хранилища имеет еще одно преимущество: мы знаем, что вызовы validateOp для операций с разными кошельками вряд ли будут мешать друг другу, поскольку хранилище, к которому они могут обращаться, ограничено. Это будет важно, когда мы будем говорить о пакетировании.
#accountabstraction
Повторное моделирование
Теперь, когда неавторизованные пользователи отправляют операции для кошелька, операция проваливается в validateOp, и контракту не нужно платить за это. Однако исполнитель по-прежнему платит за работу validateOp и не получает за это компенсации.
Недобросовестные кошельки больше не могут выполнять свои операции бесплатно, но злоумышленники по-прежнему могут заставлять исполнителя тратить деньги за неудачные операции.
В предыдущем разделе, исполнитель сначала пытался смоделировать операцию локально, чтобы проверить, пройдет ли она, и только потом отправлял транзакцию для вызова handleOp на цепочке.
Тогда мы столкнулись с проблемой, когда исполнитель не мог контролировать выполнение операции: в том смысле, что она могла быть успешной в симуляции, но провалиться в реальности.
Но в этот раз будет по-другому.
Исполнителю не нужно симулировать все выполнение, которое теперь состоит из validateOp и executeOp. Ему нужно симулировать только первую часть, validateOp, чтобы знать, получит он деньги или нет. И в отличие от executeOp, который должен иметь возможность выполнять произвольные действия, чтобы кошелек мог свободно взаимодействовать с блокчейном, на validateOp мы можем наложить более строгие ограничения.
Если быть точным, исполнитель отклонит пользовательскую операцию, не передавая ее в цепочку, если validateOp не удовлетворяет следующим ограничениям:
1. Он никогда не использует опкоды из определенного банлиста, в который входят такие коды, как TIMESTAMP, BLOCKHASH и т. д.
2. Единственное хранилище, к которому он обращается, - это связанное хранилище кошелька, определяемое как любое из следующих:
- Собственное хранилище кошелька.
- Хранилище другого контракта в слоте, соответствующем кошельку в связке(адрес => значение).
- Хранилище другого контракта в слоте, равном адресу кошелька (это необычная схема хранения, которая не встречается в Solidity).
Цель этих правил - свести к минимуму случаи, когда validateOp успешно работает в симуляции, но терпит неудачу при реальном выполнении.
Запрещенные опкоды не требуют пояснений, но ограничения на хранение данных могут показаться несколько странными.
Идея заключается в том, что любой доступ к хранилищу представляет опасность ложной симуляции, потому что слот хранилища может измениться между симуляцией и выполнением, но если мы ограничим хранилище только теми местами, которые связаны с данным кошельком, то для фальсификации симуляции злоумышленнику потребуется обновить хранилище, относящееся именно к этому кошельку. Хочется надеяться, что стоимость обновления этого хранилища будет достаточной, чтобы отпугнуть злоумышленника.
С такой симуляцией и кошелек, и исполнитель находятся в безопасности.
P.S. Это ограничение хранилища имеет еще одно преимущество: мы знаем, что вызовы validateOp для операций с разными кошельками вряд ли будут мешать друг другу, поскольку хранилище, к которому они могут обращаться, ограничено. Это будет важно, когда мы будем говорить о пакетировании.
#accountabstraction
Account abstraction (ERC-4337). Часть 8
Оплата газа непосредственно из кошелька
В настоящее время кошелек предоставляет ETH для оплаты газа, сначала пополняя его в точке входа и только потом отправляя пользовательскую операцию. Но обычный EOA платит за газ из своих собственных запасов ETH. Не должны ли наши кошельки делать то же самое?
Мы можем сделать это теперь, когда разделили проверку и выполнение, потому что точка входа может потребовать, чтобы кошелек отправил ETH в точку входа как часть шага проверки, иначе операция будет отклонена.
Мы обновим метод validateOp кошелька, чтобы точка входа могла запросить у него средства и отклонить операцию, если validateOp не выплатит запрашиваемую сумму точке входа:
Поскольку во время проверки мы не знаем точного количества газа, которое будет использовано во время выполнения, точка входа запрашивает максимальную сумму, которую может использовать выполнение, основываясь на газовом поле операции. Затем, в конце выполнения, мы хотим вернуть неиспользованные деньги за газ в кошелек.
Но здесь мы сталкиваемся с проблемой.
При написании смарт-контракта отправлять ETH произвольному контракту нежелательно, так как это вызывает произвольный код на этом контракте, который может дать сбой, использовать непредсказуемое количество газа или даже попытаться провести атаку реентранси против нас. Поэтому мы не будем напрямую отправлять дополнительные деньги за газ обратно на кошелек.
Вместо этого мы оставим их у себя и позволим кошельку получить их, сделав вызов для снятия денег позже. Это и есть схема pull-payment.
Так что на самом деле мы сделаем так, что лишние деньги на газ будут попадать туда же, куда попадают ETH, отправленные с депозитом, а кошелек сможет вывести их позже с помощью withdrawTo.
Оказывается, система пополнения/снятия средств нам все-таки была нужна (или, по крайней мере, ее часть).
Это означает, что платеж за газ в кошельке может фактически поступать из двух разных мест: из ETH, хранящихся в точке входа, и ETH, которые хранятся в самом кошельке.
Точка входа сначала попытается оплатить газ с помощью депонированных ETH, а затем, если депонированных недостаточно, попросит оставшуюся часть при вызове валидатора кошелька.
#accountabstraction
Оплата газа непосредственно из кошелька
В настоящее время кошелек предоставляет ETH для оплаты газа, сначала пополняя его в точке входа и только потом отправляя пользовательскую операцию. Но обычный EOA платит за газ из своих собственных запасов ETH. Не должны ли наши кошельки делать то же самое?
Мы можем сделать это теперь, когда разделили проверку и выполнение, потому что точка входа может потребовать, чтобы кошелек отправил ETH в точку входа как часть шага проверки, иначе операция будет отклонена.
Мы обновим метод validateOp кошелька, чтобы точка входа могла запросить у него средства и отклонить операцию, если validateOp не выплатит запрашиваемую сумму точке входа:
contract Wallet {
function validateOp(UserOperation op, uint256 requiredPayment);
function executeOp(UserOperation op);
}Поскольку во время проверки мы не знаем точного количества газа, которое будет использовано во время выполнения, точка входа запрашивает максимальную сумму, которую может использовать выполнение, основываясь на газовом поле операции. Затем, в конце выполнения, мы хотим вернуть неиспользованные деньги за газ в кошелек.
Но здесь мы сталкиваемся с проблемой.
При написании смарт-контракта отправлять ETH произвольному контракту нежелательно, так как это вызывает произвольный код на этом контракте, который может дать сбой, использовать непредсказуемое количество газа или даже попытаться провести атаку реентранси против нас. Поэтому мы не будем напрямую отправлять дополнительные деньги за газ обратно на кошелек.
Вместо этого мы оставим их у себя и позволим кошельку получить их, сделав вызов для снятия денег позже. Это и есть схема pull-payment.
Так что на самом деле мы сделаем так, что лишние деньги на газ будут попадать туда же, куда попадают ETH, отправленные с депозитом, а кошелек сможет вывести их позже с помощью withdrawTo.
Оказывается, система пополнения/снятия средств нам все-таки была нужна (или, по крайней мере, ее часть).
Это означает, что платеж за газ в кошельке может фактически поступать из двух разных мест: из ETH, хранящихся в точке входа, и ETH, которые хранятся в самом кошельке.
Точка входа сначала попытается оплатить газ с помощью депонированных ETH, а затем, если депонированных недостаточно, попросит оставшуюся часть при вызове валидатора кошелька.
#accountabstraction
❤1
Account abstraction (ERC-4337). Часть 9
Стимулы для исполнителей
В настоящее время быть исполнителем - неблагодарное занятие. Им приходится проводить много симуляций, они не получают прибыли и иногда вынуждены платить за бензин, когда их симуляции фальсифицируются.
Чтобы вознаградить исполнителей, мы позволим владельцам кошельков отправлять чаевые вместе с пользовательскими операциями, которые пойдут исполнителю.
Мы добавим поле в пользовательские операции, чтобы выразить это:
Как и аналогичное поле в обычных транзакциях, maxPriorityFeePerGas представляет собой плату, которую отправитель готов заплатить за то, чтобы его операция была приоритетной.
Исполнитель, отправляя свою транзакцию для вызова handleOp, может выбрать меньшее значение maxPriorityFeePerGas и сэкономить на разнице.
Точка входа как синглтон (singleton)
Мы говорили о том, что точка входа должна быть доверенным контрактом и что она делает. Вы можете заметить, что ничто в точке входа не является специфичным для кошелька или исполнителя. Таким образом, точка входа может быть синглтоном для всей экосистемы. Все кошельки и все исполнители будут взаимодействовать с одним и тем же контрактом точки входа.
Это означает, что нам нужно изменить пользовательские операции так, чтобы они также указывали, для какого кошелька они предназначены, чтобы при передаче операции в handleOp точки входа точка входа знала, какой кошелек запрашивать для проверки и выполнения.
Без отдельного EOA
Нашей целью было создать внутрицепочечный кошелек, который платит за свой газ без необходимости управления отдельным EOA, и теперь мы достигли этой цели!
У нас есть кошелек с интерфейсом:
У нас также есть точка входа с интерфейсом для всего блокчейна:
Когда владелец кошелька хочет выполнить какое-либо действие, он создает пользовательскую операцию и, вне цепи, просит исполнителя обработать ее для него.
Исполнитель симулирует метод validateOp кошелька на этой пользовательской операции, чтобы решить, принять ее или нет.
Если он принимает, исполнитель отправляет транзакцию в точку входа для вызова handleOp.
Затем точка входа обрабатывает проверку и выполнение операции на цепи, после чего возвращает ETH исполнителю из средств, внесенных в кошелек.
Это было долго, но мы справились!
#accountabstraction
Стимулы для исполнителей
В настоящее время быть исполнителем - неблагодарное занятие. Им приходится проводить много симуляций, они не получают прибыли и иногда вынуждены платить за бензин, когда их симуляции фальсифицируются.
Чтобы вознаградить исполнителей, мы позволим владельцам кошельков отправлять чаевые вместе с пользовательскими операциями, которые пойдут исполнителю.
Мы добавим поле в пользовательские операции, чтобы выразить это:
struct UserOperation {
// ...
uint256 maxPriorityFeePerGas;
}Как и аналогичное поле в обычных транзакциях, maxPriorityFeePerGas представляет собой плату, которую отправитель готов заплатить за то, чтобы его операция была приоритетной.
Исполнитель, отправляя свою транзакцию для вызова handleOp, может выбрать меньшее значение maxPriorityFeePerGas и сэкономить на разнице.
Точка входа как синглтон (singleton)
Мы говорили о том, что точка входа должна быть доверенным контрактом и что она делает. Вы можете заметить, что ничто в точке входа не является специфичным для кошелька или исполнителя. Таким образом, точка входа может быть синглтоном для всей экосистемы. Все кошельки и все исполнители будут взаимодействовать с одним и тем же контрактом точки входа.
Это означает, что нам нужно изменить пользовательские операции так, чтобы они также указывали, для какого кошелька они предназначены, чтобы при передаче операции в handleOp точки входа точка входа знала, какой кошелек запрашивать для проверки и выполнения.
struct UserOperation {
// ...
address sender;
}Без отдельного EOA
Нашей целью было создать внутрицепочечный кошелек, который платит за свой газ без необходимости управления отдельным EOA, и теперь мы достигли этой цели!
У нас есть кошелек с интерфейсом:
contract Wallet {
function validateOp(UserOperation op, uint256 requiredPayment);
function executeOp(UserOperation op);
}У нас также есть точка входа с интерфейсом для всего блокчейна:
contract EntryPoint {
function handleOp(UserOperation op);
function deposit(address wallet) payable;
function withdrawTo(address destination);
}Когда владелец кошелька хочет выполнить какое-либо действие, он создает пользовательскую операцию и, вне цепи, просит исполнителя обработать ее для него.
Исполнитель симулирует метод validateOp кошелька на этой пользовательской операции, чтобы решить, принять ее или нет.
Если он принимает, исполнитель отправляет транзакцию в точку входа для вызова handleOp.
Затем точка входа обрабатывает проверку и выполнение операции на цепи, после чего возвращает ETH исполнителю из средств, внесенных в кошелек.
Это было долго, но мы справились!
#accountabstraction
Account abstraction (ERC-4337). Часть 10
Сегодня, наконец, мы закончим читать первую статью из 4 про Account Abstraction и перейдем к следующим. Напомню, что в планах разобрать каждую, а потом изучить видео от Патрика, разобрав примеры.
Итак, продолжим.
Пакетирование (Bundling)
Прежде чем мы перейдем к следующей важной функции, давайте уделим время удивительно простой оптимизации.
До этого мы реализовывали простую логику: исполнитель отправляет одну транзакцию для выполнения одной пользовательской операции. Но теперь, когда у нас есть контракт точки входа, не привязанный к одному кошельку, мы можем сэкономить немного газа, собрав кучу пользовательских операций от разных людей и выполнив их все в одной транзакции!
Такое объединение пользовательских операций позволит сэкономить газ за счет того, что вы не будете постоянно платить фиксированную плату в 21 000 газа за отправку транзакции, а также снизит плату за доступ к холодным хранилищам (доступ к одному и тому же хранилищу несколько раз в одной транзакции становится дешевле после первого раза).
Для этого потребуется совсем немного изменений.
Мы заменим:
На:
И, по сути, это все!
Новый метод handleOps делает примерно то же самое, что вы ожидали:
1. Для каждой операции вызываем validateOp на кошельке-отправителе. Все операции, которые не прошли проверку, мы отбрасываем.
2. Для каждой операции вызываем executeOp на кошельке отправителя, отслеживая, сколько газа мы используем, затем переводим ETH исполнителю, чтобы оплатить этот газ.
Здесь стоит отметить, что мы сначала выполняем все проверки и только потом выполняем все операции, а не проверяем и выполняем каждую операцию, прежде чем переходить к следующей.
Это важно для сохранения симуляций.
Если бы во время handleOps мы выполняли одну операцию перед валидацией следующей, то выполнение первой операции могло бы испортить хранилище, от которого зависит валидация второй операции, и привести к ее отказу, даже если бы вторая операция прошла валидацию, когда мы моделировали ее.
Аналогичным образом мы хотим избежать ситуаций, когда проверка одной операции нарушает проверку последующей операции в связке.
Пока пакет не включает несколько операций для одного и того же кошелька, мы фактически получаем это бесплатно благодаря ограничениям на хранение, о которых говорилось выше: если валидации двух операций не касаются одного и того же хранилища, они не могут мешать друг другу. Чтобы воспользоваться этим преимуществом, исполнители будут следить за тем, чтобы в пакете было не более одной операции для любого кошелька.
Для исполнителей это новый источник дохода!
У исполнителя появилась возможность получить максимальную извлекаемую ценность (MEV), организуя пользовательские операции в связке (и, возможно, вставляя свои собственные) таким образом, чтобы это приносило прибыль.
Теперь, когда у нас есть пакетирование, мы можем перестать называть этих участников "исполнителями" и начать называть их настоящим именем - пакетировщики.
Я буду называть их бандлерами до конца этой серии из 4 частей, чтобы соответствовать терминологии ERC-4337, но на самом деле я считаю, что "исполнитель" - это хороший способ думать о них, потому что это подчеркивает, что их работа заключается в том, чтобы быть тем, кто фактически начинает исполнение на цепи, отправляя транзакцию из EOA.
#accountabstraction
Сегодня, наконец, мы закончим читать первую статью из 4 про Account Abstraction и перейдем к следующим. Напомню, что в планах разобрать каждую, а потом изучить видео от Патрика, разобрав примеры.
Итак, продолжим.
Пакетирование (Bundling)
Прежде чем мы перейдем к следующей важной функции, давайте уделим время удивительно простой оптимизации.
До этого мы реализовывали простую логику: исполнитель отправляет одну транзакцию для выполнения одной пользовательской операции. Но теперь, когда у нас есть контракт точки входа, не привязанный к одному кошельку, мы можем сэкономить немного газа, собрав кучу пользовательских операций от разных людей и выполнив их все в одной транзакции!
Такое объединение пользовательских операций позволит сэкономить газ за счет того, что вы не будете постоянно платить фиксированную плату в 21 000 газа за отправку транзакции, а также снизит плату за доступ к холодным хранилищам (доступ к одному и тому же хранилищу несколько раз в одной транзакции становится дешевле после первого раза).
Для этого потребуется совсем немного изменений.
Мы заменим:
contract EntryPoint {
function handleOp(UserOperation op);
}На:
contract EntryPoint {
function handleOps(UserOperation[] ops);
}И, по сути, это все!
Новый метод handleOps делает примерно то же самое, что вы ожидали:
1. Для каждой операции вызываем validateOp на кошельке-отправителе. Все операции, которые не прошли проверку, мы отбрасываем.
2. Для каждой операции вызываем executeOp на кошельке отправителя, отслеживая, сколько газа мы используем, затем переводим ETH исполнителю, чтобы оплатить этот газ.
Здесь стоит отметить, что мы сначала выполняем все проверки и только потом выполняем все операции, а не проверяем и выполняем каждую операцию, прежде чем переходить к следующей.
Это важно для сохранения симуляций.
Если бы во время handleOps мы выполняли одну операцию перед валидацией следующей, то выполнение первой операции могло бы испортить хранилище, от которого зависит валидация второй операции, и привести к ее отказу, даже если бы вторая операция прошла валидацию, когда мы моделировали ее.
Аналогичным образом мы хотим избежать ситуаций, когда проверка одной операции нарушает проверку последующей операции в связке.
Пока пакет не включает несколько операций для одного и того же кошелька, мы фактически получаем это бесплатно благодаря ограничениям на хранение, о которых говорилось выше: если валидации двух операций не касаются одного и того же хранилища, они не могут мешать друг другу. Чтобы воспользоваться этим преимуществом, исполнители будут следить за тем, чтобы в пакете было не более одной операции для любого кошелька.
Для исполнителей это новый источник дохода!
У исполнителя появилась возможность получить максимальную извлекаемую ценность (MEV), организуя пользовательские операции в связке (и, возможно, вставляя свои собственные) таким образом, чтобы это приносило прибыль.
Теперь, когда у нас есть пакетирование, мы можем перестать называть этих участников "исполнителями" и начать называть их настоящим именем - пакетировщики.
Я буду называть их бандлерами до конца этой серии из 4 частей, чтобы соответствовать терминологии ERC-4337, но на самом деле я считаю, что "исполнитель" - это хороший способ думать о них, потому что это подчеркивает, что их работа заключается в том, чтобы быть тем, кто фактически начинает исполнение на цепи, отправляя транзакцию из EOA.
#accountabstraction
Account abstraction (ERC-4337). Часть 11
Бандлеры как участники сети
По итогу, у нас есть система, в которой владельцы кошельков отправляют пользовательские операции бандлерам в надежде на то, что эти операции будут включены в бандл. Это очень похоже на схему обычных транзакций, когда владельцы счетов отправляют транзакции блокчейнам в надежде, что те включат их в блок, поэтому мы можем воспользоваться некоторыми преимуществами той же сетевой архитектуры.
Точно так же, как узлы хранят обычные транзакции в mempool и транслируют их другим узлам, бандлеры могут хранить подтвержденные пользовательские операции в mempool и транслировать их другим бандлерам. Бандлеры могут проверять пользовательские операции перед тем, как передать их другим бандлерам, избавляя друг друга от необходимости проверять каждую операцию.
Бандлер может извлечь выгоду, будучи также создателем блоков, поскольку если он может выбирать блок, в который будет включен его бандл, он может уменьшить или даже исключить вероятность сбоя операций во время выполнения, после успеха в моделировании. Кроме того, создатели и упаковщики блоков могут получить аналогичную выгоду, зная, как извлекать MEV.
Со временем мы можем ожидать, что создатели пакетов и блоков объединятся в одну роль.
Далее мы поговорим о спонсорских сделках с использованием Paymasters.
#accountabstraction
Бандлеры как участники сети
По итогу, у нас есть система, в которой владельцы кошельков отправляют пользовательские операции бандлерам в надежде на то, что эти операции будут включены в бандл. Это очень похоже на схему обычных транзакций, когда владельцы счетов отправляют транзакции блокчейнам в надежде, что те включат их в блок, поэтому мы можем воспользоваться некоторыми преимуществами той же сетевой архитектуры.
Точно так же, как узлы хранят обычные транзакции в mempool и транслируют их другим узлам, бандлеры могут хранить подтвержденные пользовательские операции в mempool и транслировать их другим бандлерам. Бандлеры могут проверять пользовательские операции перед тем, как передать их другим бандлерам, избавляя друг друга от необходимости проверять каждую операцию.
Бандлер может извлечь выгоду, будучи также создателем блоков, поскольку если он может выбирать блок, в который будет включен его бандл, он может уменьшить или даже исключить вероятность сбоя операций во время выполнения, после успеха в моделировании. Кроме того, создатели и упаковщики блоков могут получить аналогичную выгоду, зная, как извлекать MEV.
Со временем мы можем ожидать, что создатели пакетов и блоков объединятся в одну роль.
Далее мы поговорим о спонсорских сделках с использованием Paymasters.
#accountabstraction
❤1
Account abstraction (ERC-4337). Часть 12
В предыдущих постах мы разбирали базовые понятия Account Abstraction, полностью скопировали функциональность EOA и улучшили его, позволив пользователям выбирать собственную логику проверки операции. Но пока что кошельку все еще нужно оплачивать газ, а это значит, что владельцу кошелька нужно найти способ получить немного Эфира, прежде чем он сможет выполнять какие-либо действия.
А что если бы, газ мог заплатить не владелец кошелька, а кто-то другой? И мы переходим ко второй статье от Alchemy: Account Abstraction Part 2: Sponsoring Transactions Using Paymasters.
Есть несколько веских причин, чтобы разрешить другому пользователю оплачивать газ:
1. Если владелец кошелька - новичок в блокчейне, то необходимость приобретения ETH перед выполнением действий на цепочке станет огромным камнем преткновения;
2. Dapp может быть готов оплачивать бензин для своих методов, чтобы плата за газ не отпугивала потенциальных пользователей;
3. Спонсор может позволить кошельку оплачивать газ в каком-либо токене, отличном от ETH, например, в USDC;
4. Для обеспечения конфиденциальности пользователь может захотеть вывести активы из миксера на свежий адрес и списать плату за газ на счет, не связанный с ним;
Представляем paymasters
Допустим, я создаю dapp, который хочет платить за газ других людей. Предположительно, я не хочу платить за газ всех и каждого, поэтому мне нужно внедрить в цепочку кстомную логику, которая может посмотреть на операции пользователя и решить, хочет ли она оплатить эти операции или нет.
Чтобы внедрить кастомную логику в цепочку, нужно развернуть контракт, который мы назовем paymaster.
У него будет один метод, который смотрит на пользовательскую операцию и решает, хочет ли он заплатить за эту операцию или нет:
Затем, когда кошелек отправляет операцию, ему нужно будет указать, какой paymaster (если таковой имеется) будет оплачивать его газ.
Мы добавим новое поле в UserOperation, чтобы обозначить это.
Мы также добавим в пользовательскую операцию поле, которое кошелек сможет использовать для передачи произвольных данных в paymaster, чтобы помочь ему убедить paymaster оплатить свои расходы.
Например, это может быть что-то, что было подписано владельцем кошелька вне цепи.
Далее мы изменим handleOps, чтобы использовать новые paymasters.
Теперь ее поведение будет таким для каждой операции:
1. Вызываем validateOp на кошельке, указанном отправителем операции;
2. Если у операции есть адрес paymaster, то вызовается validatePaymasterOp для этого paymaster;
3. Все операции, которые не прошли проверку, отбрасываются;
4. Для каждой операции вызываем executeOp на кошельке отправителя операции, отслеживая, сколько газа мы использовали, затем переводим ETH исполнителю, чтобы оплатить этот газ. Если у операции есть поле paymaster, то этот ETH поступает от paymaster. В противном случае он поступает из кошелька, как и раньше.
Как и кошельки, paymaster'ы депонируют свои ETH через метод депозита, прежде чем они могут быть использованы для оплаты операций.
На самом деле это довольно просто, верно?
Мы просто попросим бандлер обновить свои симуляции и...
#accountabstraction
В предыдущих постах мы разбирали базовые понятия Account Abstraction, полностью скопировали функциональность EOA и улучшили его, позволив пользователям выбирать собственную логику проверки операции. Но пока что кошельку все еще нужно оплачивать газ, а это значит, что владельцу кошелька нужно найти способ получить немного Эфира, прежде чем он сможет выполнять какие-либо действия.
А что если бы, газ мог заплатить не владелец кошелька, а кто-то другой? И мы переходим ко второй статье от Alchemy: Account Abstraction Part 2: Sponsoring Transactions Using Paymasters.
Есть несколько веских причин, чтобы разрешить другому пользователю оплачивать газ:
1. Если владелец кошелька - новичок в блокчейне, то необходимость приобретения ETH перед выполнением действий на цепочке станет огромным камнем преткновения;
2. Dapp может быть готов оплачивать бензин для своих методов, чтобы плата за газ не отпугивала потенциальных пользователей;
3. Спонсор может позволить кошельку оплачивать газ в каком-либо токене, отличном от ETH, например, в USDC;
4. Для обеспечения конфиденциальности пользователь может захотеть вывести активы из миксера на свежий адрес и списать плату за газ на счет, не связанный с ним;
Представляем paymasters
Допустим, я создаю dapp, который хочет платить за газ других людей. Предположительно, я не хочу платить за газ всех и каждого, поэтому мне нужно внедрить в цепочку кстомную логику, которая может посмотреть на операции пользователя и решить, хочет ли она оплатить эти операции или нет.
Чтобы внедрить кастомную логику в цепочку, нужно развернуть контракт, который мы назовем paymaster.
У него будет один метод, который смотрит на пользовательскую операцию и решает, хочет ли он заплатить за эту операцию или нет:
contract Paymaster {
function validatePaymasterOp(UserOperation op);
}Затем, когда кошелек отправляет операцию, ему нужно будет указать, какой paymaster (если таковой имеется) будет оплачивать его газ.
Мы добавим новое поле в UserOperation, чтобы обозначить это.
Мы также добавим в пользовательскую операцию поле, которое кошелек сможет использовать для передачи произвольных данных в paymaster, чтобы помочь ему убедить paymaster оплатить свои расходы.
Например, это может быть что-то, что было подписано владельцем кошелька вне цепи.
struct UserOperation {
// ...
address paymaster;
bytes paymasterData;
}Далее мы изменим handleOps, чтобы использовать новые paymasters.
Теперь ее поведение будет таким для каждой операции:
1. Вызываем validateOp на кошельке, указанном отправителем операции;
2. Если у операции есть адрес paymaster, то вызовается validatePaymasterOp для этого paymaster;
3. Все операции, которые не прошли проверку, отбрасываются;
4. Для каждой операции вызываем executeOp на кошельке отправителя операции, отслеживая, сколько газа мы использовали, затем переводим ETH исполнителю, чтобы оплатить этот газ. Если у операции есть поле paymaster, то этот ETH поступает от paymaster. В противном случае он поступает из кошелька, как и раньше.
Как и кошельки, paymaster'ы депонируют свои ETH через метод депозита, прежде чем они могут быть использованы для оплаты операций.
На самом деле это довольно просто, верно?
Мы просто попросим бандлер обновить свои симуляции и...
#accountabstraction
👍1
Account abstraction (ERC-4337). Часть 13
Paymaster staking
В предыдущих постах мы описывали ситуацию, где кошелек возвращал деньги за газ. Бандлер использовал симуляцию, чтобы попытаться избежать выполнения операций, которые не прошли проверку, так как это означало, что кошелек не заплатит, и в итоге он окажется "на крючке" с потраченными деньгами.
Здесь возникает та же проблема:
Бандлер хочет избежать отправки операций, которые не прошли проверку paymaster, потому что paymaster не заплатит, и бандлер снова окажется в минусе.
Сначала кажется, что мы можем наложить на validatePaymasterOp те же ограничения, что и на validateOp (т. е. он может обращаться только к хранилищу кошелька и своему собственному связанному хранилищу и не может использовать запрещенные опкоды), и тогда бандлер может просто симулировать validatePaymasterOp для пользовательской операции в то же время, когда он симулирует validateOp кошелька.
Но здесь есть одна загвоздка.
Из-за ограничения на хранение, согласно которому валидатор кошелька может обращаться только к связанному с ним хранилищу, мы знали, что валидации нескольких операций в пачке не могут мешать друг другу, если они происходят из разных кошельков, поскольку они обращаются к очень небольшому общему хранилищу.
Но хранилище paymaster'а является общим для всех операций в пакете, которые используют этот paymaster.
Это означает, что действия одного validatePaymasterOp потенциально могут привести к сбою проверки для множества других операций в пакете, использующих этот же paymaster.
Вредоносный paymaster может использовать это для DoS-атаки на систему.
Чтобы предотвратить это, мы ввели систему репутации.
Мы заставим бандлер отслеживать, как часто paymaster не проходил валидацию в последнее время, и наказывать тех, которые имели множество проблем, путём запрета операций.
Такая система может не сработать, в случае если вредоносный paymaster просто создаст множество своих копий (атака Sybil). Именно поэтому мы потребуем, чтобы paymaster ставили на кон свой Эфир. Таким образом, наличие нескольких аккаунтов не принесет ему выгоды.
Давайте добавим новые методы для работы со стейкингом:
Как только сделан депозит Эфира, он не может быть снят до тех пор, пока не пройдет некоторая задержка (delay) после вызова unlockStake.
Эти новые методы отличаются от ранее рассмотренных функций deposit и withdrawTo, которые используются кошельками и paymaster'ами для пополнения ETH, и могут быть использованы для оплаты газа или немедленно сняты в любой момент.
Существует исключений из правил:
Если paymaster обращается только к связанному с кошельком хранилищу, а не к своему собственному, то ему не нужно делать ставку (stake ETH), потому что в этом случае, хранилища, к которым обращаются несколько операторов в связке, не будут пересекаться друг с другом.
Кроме того, каждый бандлер отслеживает репутацию локально, поэтому код бандлера может реализовать свою собственную логику репутации, если он считает, что может сделать работу лучше и не создаст проблем для других бандлеров.
P.S. В отличие от многих других схем ставок (stake ETH), здесь ставки никогда не снижаются. Они существуют просто как способ заставить потенциального злоумышленника заблокировать очень большую сумму капитала для проведения масштабной атаки.
#accountabstraction
Paymaster staking
В предыдущих постах мы описывали ситуацию, где кошелек возвращал деньги за газ. Бандлер использовал симуляцию, чтобы попытаться избежать выполнения операций, которые не прошли проверку, так как это означало, что кошелек не заплатит, и в итоге он окажется "на крючке" с потраченными деньгами.
Здесь возникает та же проблема:
Бандлер хочет избежать отправки операций, которые не прошли проверку paymaster, потому что paymaster не заплатит, и бандлер снова окажется в минусе.
Сначала кажется, что мы можем наложить на validatePaymasterOp те же ограничения, что и на validateOp (т. е. он может обращаться только к хранилищу кошелька и своему собственному связанному хранилищу и не может использовать запрещенные опкоды), и тогда бандлер может просто симулировать validatePaymasterOp для пользовательской операции в то же время, когда он симулирует validateOp кошелька.
Но здесь есть одна загвоздка.
Из-за ограничения на хранение, согласно которому валидатор кошелька может обращаться только к связанному с ним хранилищу, мы знали, что валидации нескольких операций в пачке не могут мешать друг другу, если они происходят из разных кошельков, поскольку они обращаются к очень небольшому общему хранилищу.
Но хранилище paymaster'а является общим для всех операций в пакете, которые используют этот paymaster.
Это означает, что действия одного validatePaymasterOp потенциально могут привести к сбою проверки для множества других операций в пакете, использующих этот же paymaster.
Вредоносный paymaster может использовать это для DoS-атаки на систему.
Чтобы предотвратить это, мы ввели систему репутации.
Мы заставим бандлер отслеживать, как часто paymaster не проходил валидацию в последнее время, и наказывать тех, которые имели множество проблем, путём запрета операций.
Такая система может не сработать, в случае если вредоносный paymaster просто создаст множество своих копий (атака Sybil). Именно поэтому мы потребуем, чтобы paymaster ставили на кон свой Эфир. Таким образом, наличие нескольких аккаунтов не принесет ему выгоды.
Давайте добавим новые методы для работы со стейкингом:
contract EntryPoint {
// ...
function addStake() payable;
function unlockStake();
function withdrawStake(address payable destination);
}Как только сделан депозит Эфира, он не может быть снят до тех пор, пока не пройдет некоторая задержка (delay) после вызова unlockStake.
Эти новые методы отличаются от ранее рассмотренных функций deposit и withdrawTo, которые используются кошельками и paymaster'ами для пополнения ETH, и могут быть использованы для оплаты газа или немедленно сняты в любой момент.
Существует исключений из правил:
Если paymaster обращается только к связанному с кошельком хранилищу, а не к своему собственному, то ему не нужно делать ставку (stake ETH), потому что в этом случае, хранилища, к которым обращаются несколько операторов в связке, не будут пересекаться друг с другом.
Кроме того, каждый бандлер отслеживает репутацию локально, поэтому код бандлера может реализовать свою собственную логику репутации, если он считает, что может сделать работу лучше и не создаст проблем для других бандлеров.
P.S. В отличие от многих других схем ставок (stake ETH), здесь ставки никогда не снижаются. Они существуют просто как способ заставить потенциального злоумышленника заблокировать очень большую сумму капитала для проведения масштабной атаки.
#accountabstraction
❤1
Закрытый канал для разбора отчетов
Сегодня сделаю перерыв от Account Abstraction и напомню, что с февраля этого года у нас есть небольшой закрытый канал, где мы разбираем аудитоhские отчеты (найденные баги) и понемногу повышаем свою насмотренность в коде и поиске потенциальных проблем.
На канале делаются посты в формате:
Протокол Panoptic
[M-03] CREATE2 address collision during pool deployment allows for complete draining of the pool
Ссылка на репорт: https://code4rena.com/reports/2024-04-panoptic#m-03-create2-address-collision-during-pool-deployment-allows-for-complete-draining-of-the-pool
Описание: уязвимость, о которой я раньше и не знал. Есть некоторые нюансы в описании бага, точнее в вероятностях хеширования, поэтому если вы сможете разобраться в них, буду очень признателен, если напишите отдельный пост разбор.
Для начала посмотрите на код:
Деплой нового пула происходит с помощью cloneDeterministic(), в которой пользователь указывает соль. Проблема именно в том, что этот аргумент может контролироваться, а не рассчитывается самостоятельно в функции.
Грубо говоря, cloneDeterministic() использует create2 опкод для расчета адреса нового пула. Также может сделать и хакер.
Он рассчитывает адрес пула (вот тут и нужно понимание хеширования, о котором я писал выше) и делает вот что.
1. Делает деплой контракта, раздает в различные токены апруву на перевод самому хакеру, и с помощью selfdestruct() уничтожает этот контракт.
2. Не смотря на обновление Денкун, selfdestruct все еще можно выполнить, если он происходит в одной транзакции с созданием контракта.
Таким образом, когда будет создан реальный пул и пользователи наполнят его токенами, хакер вызывает перевод с пула и опустошает его!
Такая вот необычная атака!
Как рекомендация от аудитора, было добавить в соль другие параметры от функции, типа block.timestamp или block.number, чтобы сделать соль более случайной.
Такие разборы могут быть интересны тем, кто хочет повысить свои знания в безопасности смарт контрактов или начать принимать участие в конкурсных аудитах.
Это закрытый канал для небольшого количества участников по ежемесячной подписке.
Однако сейчас мы в небольшом отпуске и посты выходят не несколько раз в день, а 4-5 в неделю. Поэтому у вас есть возможность присмотреться к такому формату для себя и получить доступ до начала октября за 1000 рублей.
Обычно это цена за один месяц, но сейчас вы получите доступ на два месяца.
Всем хорошего дня и безопасного кода!
#bugs
Сегодня сделаю перерыв от Account Abstraction и напомню, что с февраля этого года у нас есть небольшой закрытый канал, где мы разбираем аудитоhские отчеты (найденные баги) и понемногу повышаем свою насмотренность в коде и поиске потенциальных проблем.
На канале делаются посты в формате:
Протокол Panoptic
[M-03] CREATE2 address collision during pool deployment allows for complete draining of the pool
Ссылка на репорт: https://code4rena.com/reports/2024-04-panoptic#m-03-create2-address-collision-during-pool-deployment-allows-for-complete-draining-of-the-pool
Описание: уязвимость, о которой я раньше и не знал. Есть некоторые нюансы в описании бага, точнее в вероятностях хеширования, поэтому если вы сможете разобраться в них, буду очень признателен, если напишите отдельный пост разбор.
Для начала посмотрите на код:
function deployNewPool(address token0, address token1, uint24 fee, bytes32 salt)
external
returns (PanopticPool newPoolContract)
{
...
// This creates a new Panoptic Pool (proxy to the PanopticPool implementation)
// Users can specify a salt, the aim is to incentivize the mining of addresses with leading zeros
@> newPoolContract = PanopticPool(POOL_REFERENCE.cloneDeterministic(salt));
...
}
Деплой нового пула происходит с помощью cloneDeterministic(), в которой пользователь указывает соль. Проблема именно в том, что этот аргумент может контролироваться, а не рассчитывается самостоятельно в функции.
Грубо говоря, cloneDeterministic() использует create2 опкод для расчета адреса нового пула. Также может сделать и хакер.
Он рассчитывает адрес пула (вот тут и нужно понимание хеширования, о котором я писал выше) и делает вот что.
1. Делает деплой контракта, раздает в различные токены апруву на перевод самому хакеру, и с помощью selfdestruct() уничтожает этот контракт.
2. Не смотря на обновление Денкун, selfdestruct все еще можно выполнить, если он происходит в одной транзакции с созданием контракта.
Таким образом, когда будет создан реальный пул и пользователи наполнят его токенами, хакер вызывает перевод с пула и опустошает его!
Такая вот необычная атака!
Как рекомендация от аудитора, было добавить в соль другие параметры от функции, типа block.timestamp или block.number, чтобы сделать соль более случайной.
Такие разборы могут быть интересны тем, кто хочет повысить свои знания в безопасности смарт контрактов или начать принимать участие в конкурсных аудитах.
Это закрытый канал для небольшого количества участников по ежемесячной подписке.
Однако сейчас мы в небольшом отпуске и посты выходят не несколько раз в день, а 4-5 в неделю. Поэтому у вас есть возможность присмотреться к такому формату для себя и получить доступ до начала октября за 1000 рублей.
Обычно это цена за один месяц, но сейчас вы получите доступ на два месяца.
Всем хорошего дня и безопасного кода!
#bugs
🔥6❤3👍1
Account abstraction (ERC-4337). Часть 14
Улучшение: Paymaster postOp
Мы можем сделать небольшое улучшение, чтобы позволить paymaster сделать больше. В данный момент paymaster вызываются только на этапе проверки, до того, как операция будет выполнена.
Но в зависимости от результата операции, от paymaster может потребоваться сделать что-то другое.
Например, платежной системе, позволяющей пользователям оплачивать газ в стейблкоинах, необходимо знать, сколько газа было фактически использовано в ходе операции, чтобы знать, сколько USDC взимать.
Таким образом, мы добавим в paymaster новый метод postOp, который будет вызываться после завершения операции и передавать ей данные о количестве использованного газа.
Мы также хотим, чтобы paymaster мог "передавать информацию самому себе" и использовать данные, которые он вычислил во время валидации, на этапе postOp. Именно поэтому мы позволим после валидации возвращать произвольные "контекстные" данные, которые позже будут переданы в postOp.
Наша первая попытка создать postOp будет выглядеть следующим образом:
Но для платежной системы, которая хочет в конце снять деньги в стейблкоинах, есть небольшие проблемы.
Допустим, перед тем как одобрить выполнение операции (в validatePaymasterOp), paymaster проверил, достаточно ли у пользователя, например, USDC для оплаты операции. Но вполне возможно, что во время выполнения операция отдаст все USDC кошелька, что будет означать, что платежная система не сможет извлечь оплату в конце.
P.S. Может ли paymaster взимать максимальную сумму USDC в начале, а затем возвращать неиспользованную часть в конце? Это вроде бы работает, но неудобно: требуется два вызова перевода вместо одного, что увеличивает стоимость газа и вызывает два разных события перевода.
Нам нужен способ, чтобы paymaster мог вызвать откат операции после ее выполнения, и если это произойдет, он должен иметь возможность получить платеж в любом случае, поскольку независимо от того, что произойдет, он уже согласился заплатить за газ во время validatePaymasterOp.
Способ реализации тут заключается в том, чтобы контракт потенциально вызывал postOp дважды.
Сначала он вызывает postOp в рамках того же выполнения, в котором он только что выполнил executeOp кошелька, и, таким образом, если postOp отменяется, это приводит к тому, что все эффекты executeOp также отменяются.
Если это происходит, то контракт снова вызывает postOp, но теперь мы оказываемся в ситуации, в которой были до выполнения executeOp, и поскольку в этой ситуации мы только что проверили validatePaymasterOp, paymaster должен быть в состоянии извлечь свою прибыль.
Чтобы придать postOp немного больше контекста, мы дадим ему еще один параметр: флаг, указывающий на то, что мы находимся во "втором запуске" после того, как он уже однажды отступил:
#accountabstraction
Улучшение: Paymaster postOp
Мы можем сделать небольшое улучшение, чтобы позволить paymaster сделать больше. В данный момент paymaster вызываются только на этапе проверки, до того, как операция будет выполнена.
Но в зависимости от результата операции, от paymaster может потребоваться сделать что-то другое.
Например, платежной системе, позволяющей пользователям оплачивать газ в стейблкоинах, необходимо знать, сколько газа было фактически использовано в ходе операции, чтобы знать, сколько USDC взимать.
Таким образом, мы добавим в paymaster новый метод postOp, который будет вызываться после завершения операции и передавать ей данные о количестве использованного газа.
Мы также хотим, чтобы paymaster мог "передавать информацию самому себе" и использовать данные, которые он вычислил во время валидации, на этапе postOp. Именно поэтому мы позволим после валидации возвращать произвольные "контекстные" данные, которые позже будут переданы в postOp.
Наша первая попытка создать postOp будет выглядеть следующим образом:
contract Paymaster {
function validatePaymasterOp(UserOperation op) returns (bytes context);
function postOp(bytes context, uint256 actualGasCost);
}Но для платежной системы, которая хочет в конце снять деньги в стейблкоинах, есть небольшие проблемы.
Допустим, перед тем как одобрить выполнение операции (в validatePaymasterOp), paymaster проверил, достаточно ли у пользователя, например, USDC для оплаты операции. Но вполне возможно, что во время выполнения операция отдаст все USDC кошелька, что будет означать, что платежная система не сможет извлечь оплату в конце.
P.S. Может ли paymaster взимать максимальную сумму USDC в начале, а затем возвращать неиспользованную часть в конце? Это вроде бы работает, но неудобно: требуется два вызова перевода вместо одного, что увеличивает стоимость газа и вызывает два разных события перевода.
Нам нужен способ, чтобы paymaster мог вызвать откат операции после ее выполнения, и если это произойдет, он должен иметь возможность получить платеж в любом случае, поскольку независимо от того, что произойдет, он уже согласился заплатить за газ во время validatePaymasterOp.
Способ реализации тут заключается в том, чтобы контракт потенциально вызывал postOp дважды.
Сначала он вызывает postOp в рамках того же выполнения, в котором он только что выполнил executeOp кошелька, и, таким образом, если postOp отменяется, это приводит к тому, что все эффекты executeOp также отменяются.
Если это происходит, то контракт снова вызывает postOp, но теперь мы оказываемся в ситуации, в которой были до выполнения executeOp, и поскольку в этой ситуации мы только что проверили validatePaymasterOp, paymaster должен быть в состоянии извлечь свою прибыль.
Чтобы придать postOp немного больше контекста, мы дадим ему еще один параметр: флаг, указывающий на то, что мы находимся во "втором запуске" после того, как он уже однажды отступил:
contract Paymaster {
function validatePaymasterOp(UserOperation op) returns (bytes context);
function postOp(bool hasAlreadyReverted, bytes context, uint256 actualGasCost);
}#accountabstraction
Account abstraction (ERC-4337). Часть 15
Подведем итоги.
Как paymaster позволяют выполнять спонсируемые транзакции?
Для того чтобы позволить кому-то, кроме владельца кошелька, оплачивать газ, мы вводим новый тип субъекта - paymaster, который развертывает смарт-контракт со следующим интерфейсом:
Мы добавили новые поля в пользовательские операции, чтобы кошельки могли указывать, какой именно paymaster им нужен:
Плательщики вносят ETH на контракт, чтобы имитировать кошелек, оплачивающий свой собственный газ.
Контракт обновляет свой метод handleOps таким образом, чтобы для каждой операции, помимо проверки кошелька через validateOp, она также проверяла paymaster операции (если таковые имеются) через validatePaymasterOp, затем выполняла операцию и, наконец, вызывала postOp.
И чтобы решить некоторые проблемы, связанные с симуляцией валидации paymaster, нам нужно ввести систему ставок, при которой paymaster фиксирует блокирует внесенный на контракт.
Для этого используется несколько новых методов:
Дальше больше! Мы переходим к третьей статье!
#accountabstraction
Подведем итоги.
Как paymaster позволяют выполнять спонсируемые транзакции?
Для того чтобы позволить кому-то, кроме владельца кошелька, оплачивать газ, мы вводим новый тип субъекта - paymaster, который развертывает смарт-контракт со следующим интерфейсом:
contract Paymaster {
function validatePaymasterOp(UserOperation op) returns (bytes context);
function postOp(bool hasAlreadyReverted, bytes context, uint256 actualGasCost);
}Мы добавили новые поля в пользовательские операции, чтобы кошельки могли указывать, какой именно paymaster им нужен:
struct UserOperation {
// ...
address paymaster;
bytes paymasterData;
}Плательщики вносят ETH на контракт, чтобы имитировать кошелек, оплачивающий свой собственный газ.
Контракт обновляет свой метод handleOps таким образом, чтобы для каждой операции, помимо проверки кошелька через validateOp, она также проверяла paymaster операции (если таковые имеются) через validatePaymasterOp, затем выполняла операцию и, наконец, вызывала postOp.
И чтобы решить некоторые проблемы, связанные с симуляцией валидации paymaster, нам нужно ввести систему ставок, при которой paymaster фиксирует блокирует внесенный на контракт.
Для этого используется несколько новых методов:
contract EntryPoint {
// ...
function addStake() payable;
function unlockStake();
function withdrawStake(address payable destination);
}Дальше больше! Мы переходим к третьей статье!
#accountabstraction