Solidity. Смарт контракты и аудит – Telegram
Solidity. Смарт контракты и аудит
2.62K subscribers
246 photos
7 videos
18 files
547 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
Account abstraction (ERC-4337). Часть 6

Разделение проверки и исполнения функции

Ранее мы определили интерфейс кошелька как:

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
Account abstraction (ERC-4337). Часть 8

Оплата газа непосредственно из кошелька

В настоящее время кошелек предоставляет 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

Стимулы для исполнителей

В настоящее время быть исполнителем - неблагодарное занятие. Им приходится проводить много симуляций, они не получают прибыли и иногда вынуждены платить за бензин, когда их симуляции фальсифицируются.

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

Мы добавим поле в пользовательские операции, чтобы выразить это:

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 газа за отправку транзакции, а также снизит плату за доступ к холодным хранилищам (доступ к одному и тому же хранилищу несколько раз в одной транзакции становится дешевле после первого раза).

Для этого потребуется совсем немного изменений.

Мы заменим:

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
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.

У него будет один метод, который смотрит на пользовательскую операцию и решает, хочет ли он заплатить за эту операцию или нет:

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 ставили на кон свой Эфир. Таким образом, наличие нескольких аккаунтов не принесет ему выгоды.

Давайте добавим новые методы для работы со стейкингом:

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

Описание: уязвимость, о которой я раньше и не знал. Есть некоторые нюансы в описании бага, точнее в вероятностях хеширования, поэтому если вы сможете разобраться в них, буду очень признателен, если напишите отдельный пост разбор.

Для начала посмотрите на код:


   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
🔥63👍1
Account abstraction (ERC-4337). Часть 14

Улучшение: 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, который развертывает смарт-контракт со следующим интерфейсом:

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
Account abstraction (ERC-4337). Часть 16

Создание кошелька

Мы еще не рассмотрели вопрос о том, как контракт кошелька каждого пользователя попадает в блокчейн. Традиционный" способ развертывания контракта - это использование EOA для отправки транзакции, содержащей код развертывания контракта. В данном случае это было бы весьма неудовлетворительно, поскольку мы проделали огромную работу, чтобы сделать так, чтобы кто-то мог взаимодействовать с блокчейном без EOA.

Мы хотим, чтобы тот, кто планирует завести кошелек, но у кого его еще нет, должен иметь возможность получить совершенно новый кошелек в сети, либо оплатив свой собственный газ с помощью ETH, либо найдя paymaster, который оплатит его газ (о чем мы говорили в части 2). Также он должен иметь возможность сделать это без создания EOA.

Есть и другая, менее очевидная цель, которая также довольно важна.

Когда я создаю новую EOA, я могу локально сгенерировать свой приватный ключ и заявить о своей учетной записи, не отправляя никаких транзакций.

Я могу сообщить людям свой адрес и начать получать ETH или токены еще до того, как сам отправлю транзакцию.

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

Определение адресов контрактов с помощью CREATE2

То, что мы можем получать активы по нашему адресу еще до того, как развернем наш контракт, - это намек на то, как нам нужно это реализовать. Он подразумевает, что, хотя мы, возможно, еще не развернули контракт нашего кошелька, нам нужно знать, по какому адресу он будет находиться, когда мы, наконец, приступим к его развертыванию.

P.S. Адрес, на который контракт в конечном итоге будет развернут, но еще этого не произошло, называется контрфактическим адресом (counterfactual address).

Ключевым компонентом, позволяющим добиться этого, является опкод CREATE2. Он разворачивает контракт по адресу, который можно детерминированно вычислить из следующих входных данных:

- Адрес контракта, вызывающего CREATE2;

- Соль, которая может быть любым 32-байтовым значением;

- Код инициации развертываемого контракта;

Интересный момент, о котором многие не догадываются: когда вы развертываете контракт, код, который вы отправляете, - это не тот же код, который в итоге окажется в вашем контракте. В частности, использование одного и того же кода инициации (code init) несколько раз не гарантирует, что развернутые контракты будут содержать один и тот же код, поскольку этот код может считываться из хранилища или использовать опкоды типа TIMESTAMP.

#accountabstraction
Account abstraction (ERC-4337). Часть 17

На этой неделе мы заканчиваем с переводом и разбором статей Alchemy, посвященные Account Abstraction. И сегодня продолжим говорить о создании Кошельков.

Первая попытка: Развертывание произвольных контрактов

Теперь, когда мы знаем о CREATE2, наш изначальный план прост. Мы позволим пользователям передавать код инициации (init code), а точка входа будет разворачивать его. Для начала мы добавим еще одно поле в пользовательские операции:

struct UserOperation {
// ...
bytes initCode;
}


Затем мы обновим часть валидации в handleOps, чтобы сделать следующее:

В рамках проверки пользовательской операции, если операция имеет непустой initCode, то будет использоваться CREATE2 для развертывания контракта с этим init-кодом.

Затем выполняем остальную часть проверки:

1. Вызываем метод validateOp только что созданного кошелька;

2. Затем, если у операции есть paymaster, вызываем метод validatePaymasterOp в paymaster;

Так мы сможем выполнить наши изначальные задачи: пользователи смогут развернуть произвольные контракты и заранее знать их адреса, а само развертывание может спонсироваться или paymaster, или пользователем.

Тем не менее, есть несколько проблем, которые связаны с тем, что мы просим пользователя отправить, а контракт подтвердить произвольный блок байткода:

1. Когда paymaster просматривает пользовательские операции, он не может проанализировать байткод, чтобы решить, хочет ли он платить за него или нет;

2. Когда пользователь отправляет байткод для развертывания контракта, он не может легко проверить, что отправленный им байткод выполняет то, что он хочет. Если пользователь использует инструмент для развертывания контракта, то, если инструмент вредоносный или взломанный, он может предоставить код init, который установит бэкдор в развернутый контракт, и этот баг будет сложно обнаружить;

3. Вспомните из первой части, что bundler хочет имитировать проверку каждой операции, которую он включает в пакет, чтобы в итоге не включать операции, не прошедшие проверку, за которые ему потом придется платить газом из собственных средств. Но поскольку init code - это произвольный код, он может легко сработать во время симуляции, но провалиться во время выполнения.

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

Как обычно, когда мы хотим получить больше гарантий исполнения коды, приходит время ввести новый смарт-контракт...

#accountabstraction
Account abstraction (ERC-4337). Часть 18

Вторая попытка: Внедрение фабрик

Вместо того чтобы контракт принимал произвольный байткод и вызывал CREATE2, мы позволим пользователю выбрать любой контракт по своему усмотрению, который будет вызывать CREATE2.
Затем эти контракты, которые мы назовем фабриками, могут специализироваться на создании различных видов других кошельков, если они того пожелают.

Например, может быть одна фабрика, которая создает кошельки, защищающие токены Carbonated Courage, а другая - кошельки, требующие три из пяти ключей для подписания транзакций.

Фабрики будут иметь метод, который можно вызвать для создания контракта:

contract Factory {
function deployContract(bytes data) returns (address);
}


Мы также добавим поля в UserOp, для того чтобы, если операция пытается развернуть кошелек, то она указывала, какую фабрику использовать, а также передавала данные, которые фабрика получит на вход:

struct UserOperation {
// ...
address factory;
bytes factoryData;
}


Это решает первые две проблемы из предыдущих постов:

1. Если пользователь обращается на фабрику за кошельками, защищающими токены Carbonated Courage, то при условии, что контракт фабрики проходит аудит, он точно знает, что в итоге получит кошелек, защищающий NFT, не имеющий бэкдоров, и для этого ему не придется просматривать байткод;

2. Paymaster могут выбирать для оплаты развертывания определенные одобренные фабрики;

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

Это именно та проблема, с которой мы столкнулись в методе validatePaymasterOp у paymasters, и мы решим ее тем же способом.

Бандлеры ограничат доступ фабрик только к их собственному связанному хранилищу и хранилищу кошелька, который они развертывают, и не позволят им вызывать запрещенные методы вроде TIMESTAMP.

Мы также попросим фабрики разместить некоторое количество ETH с помощью метода addStake, а затем бандлеры смогут дросселировать или запрещать фабрики в зависимости от того, как часто их симуляции фальсифицировались в последнее время.

P.S. Как и в случае с paymaster, фабрике не нужно делать ETH депозит, если ее метод развертывания обращается только к хранилищу развертываемого кошелька, а не к собственному хранилищу фабрики.

На данный момент созданная нами архитектура может выполнять все функции настоящего ERC-4337!

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

#accountabstraction
Account abstraction (ERC-4337). Часть 19

Переходим к заключительной части статей от Alchemy, которые разберем в течение этих двух дней. И сегодня мы поговорим о совокупности подписей (Aggregate signatures).

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

Разве не было бы здорово проверять множество операций одновременно с помощью одной подписи вместо многих?

Для этого используется концепция из криптографии - агрегированные подписи.

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

Частым примером схемы подписи, поддерживающей агрегирование, является BLS.

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

Введение в агрегаторы

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

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

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

Схема агрегации определяется тем, как она объединяет несколько подписей в одну и как она проверяет объединенную подпись, поэтому агрегатор раскрывает эти две функции как методы:

contract Aggregator {
function aggregateSignatures(UserOperation[] ops)
returns (bytes aggregatedSignature);

function validateSignatures(UserOperation[] ops, bytes signature);
}


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

Если кошелек хочет участвовать в агрегации, он предоставляет метод для выбора агрегатора:

contract Wallet {
// ...

function getAggregator() returns (address);
}


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

Такая группа может выглядеть следующим образом:

struct UserOpsPerAggregator {
UserOperation[] ops;
address aggregator;
bytes combinedSignature;
}


P.S. Если бандлер знает о конкретном агрегаторе, то он может оптимизировать работу, жестко закодировав (hardcode) собственную версию алгоритма агрегации подписей, вместо того чтобы запускать aggregateSignatures.

Вспомните, что у нашего контракта есть метод handleOps, который принимает список агрегаторов.

Мы создадим новый метод, handleAggregatedOps, который будет делать то же самое, но будет принимать операции, сгруппированные по агрегаторам:

contract EntryPoint {
function handleOps(UserOperation[] ops);

function handleAggregatedOps(UserOpsPerAggregator[] ops);

// ...
}


Новый метод, handleAggregatedOps, работает в основном так же, как и handleOps. Единственное отличие заключается в шаге проверки.

Если handleOps выполняет проверку путем вызова метода validateOp каждого кошелька, то handleAggregatedOps вместо этого вызывает метод validateSignatures агрегатора для объединенной подписи каждой группы, используя агрегатор этой группы.

Но здесь есть одна проблема, которая нам уже хорошо знакома.
👍1
Bundler хочет имитировать валидацию и проверить, что агрегатор проверит группу операций, прежде чем включить их в пакет, потому что если валидация не пройдет, то bundler будет вынужден платить за газ. Но агрегатор с произвольной логикой может легко добиться успеха во время симуляции, но потерпеть неудачу во время выполнения.

Мы решим эту проблему точно так же, как и в случае с paymaster'ами и фабриками: мы ограничим, к каким хранилищам может обращаться агрегатор, какие опкоды он может использовать, и потребуем, чтобы он делал депозит ETH в контракте, если он не обращается к хранилищу.

Вот и все для агрегированных подписей! Завтра мы уже подведем итоги!

#accountabstraction
Account abstraction (ERC-4337). Часть 20

Подведение итогов

То, что мы создали здесь, является более или менее полной архитектурой ERC-4337! Есть некоторые различия в деталях, например, в названиях и аргументах некоторых методов, но нет ничего, что я бы счел архитектурным различием.

Отличия данного цикла постов от ERC-4337

Несмотря на то, что общая архитектура account abstraction у нас уже есть, умные люди, стоящие за ERC-4337, придумали некоторые вещи, которые несколько отличаются от того, что мы описали выше.

Давайте рассмотрим некоторые из них!

1. Временные диапазоны валидации

Выше я довольно нечетко описал тип возвращаемы данных validateOp кошелька и validatePaymasterOp от paymaster. В ERC-4337 есть хороший способ использовать это.

Кошелек лучше бы реализовать так, чтобы пользовательская операция была действительна только в течение определенного времени. В противном случае недобросовестный бандлер может продержать эту операцию очень долго, а затем включить ее в бандл гораздо позже, в выгодное для бандлера время.

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

Таким образом, ERC-4337 дает validateOp возвращаемое значение, которое кошелек может использовать для выбора временного диапазона:

contract Wallet {
function validateOp(UserOperation op, uint256 requiredPayment)
returns (uint256 sigTimeRange);
// ...
}


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

Еще одно замечание из ERC-4337: кошельки должны возвращать значение sentinel из validateOp, и не делать этого в случае неудачи валидации, что помогает при оценке газа, поскольку eth_estimateGas не сообщает, сколько газа было использовано в транзакции, которая откатывается.

2. Произвольные вызовы для кошельков и фабрик

Ранее мы разбирали, что интерфейс нашего кошелька:

contract Wallet {
function validateOp(UserOperation op, uint256 requiredPayment);
function executeOp(UserOperation op);
}


В ERC-4337 у кошельков на самом деле нет метода с именем executeOp.

Вместо этого у пользовательской операции есть поле CallData:

struct UserOperation {
// ...
bytes callData;
}


Эти данные передаются кошельку в качестве данных вызова.

Для типичного смарт-контракта первые четыре байта этих данных будут интерпретироваться как селектор функции, а остальные - как аргументы функции.

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

Аналогично, в ERC-4337 фабричные контракты фактически не имеют метода deployContract. Они тоже получают произвольные данные для вызова, в данном случае из поля initCode операции.

3. Компактные данные для paymasters и фабрик

Выше мы говорили, что пользовательская операция содержит поля, в которых можно указать paymasters, а также данные, которые ему нужно передать:

struct UserOperation {
// ...
address paymaster;
bytes paymasterData;
}


В ERC-4337 они объединены в одно поле в качестве оптимизации, где первые 20 байт поля - это адрес paymaster, а остальные - данные:

struct UserOperation {
// ...
address paymaster;
bytes paymasterData;
}


То же самое касается фабрик и отправляемых им данных: если мы использовали два поля factory и factoryData, то ERC-4337 объединяет их в одно поле initCode.

Ну, вот собственно и все! За 20 постов мы перевели 4 прекрасные статьи от Alchemy! А завтра посмотрим видео от Патрика на эту тему!

Понимаю, что новичкам в Solidity это может быть сложно, но мне все равно хотелось бы иногда устраивать месяц "задротства" разбирая интересные статьи или контракты более профессионального уровня.
А так, с понедельника мы снова на некоторое время вернумся к разборам пунктов от Chinmaya, которые идеально подходят для начинающих.

#accountabstraction
🔥6
Account abstraction (ERC-4337). Финал

Ну, и как вишенка на торте, в последнем посте про Account Abstraction предлагаю посмотреть 4 часовое видео от Патрика Коллинса!

У меня возникло несколько вопросов в момент просмотра данного видео, но они, скорее, про какие-то нюансы его реализации, чем общее понимание АА.

Если вы также внимательно прочитали все предыдущие посты, то видео будет намного понятнее, так как уже будет представление, зачем нужна та или иная функция.

Account Abstraction - ULTIMATE Tutorial (Updraft Excerpt)

Приятного просмотра!

P.S. Субтитры на русском языке вполне сносны к чтению!

#accountabstraction