Solidity. Смарт контракты и аудит – Telegram
Solidity. Смарт контракты и аудит
2.62K subscribers
246 photos
7 videos
18 files
547 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
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
Solidity hints. Часть 17

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

А на прошлой неделе мы закончили большой, трехнедельный цикл по Account Abstraction, и дальше мы снова некоторое время будем разбирать пункты из прекрасного репо Chinmaya, посвященное нюансам Solidity.

Мы остановились на 27 пункте:

The code of free functions is included in all contracts that call them, similar to internal library functions (free functions are those that exist at file level, outside of a contract).

который переводится как:

Код свободных функций включается во все контракты, которые их вызывают, подобно внутренним библиотечным функциям (свободные функции - это те, которые существуют на уровне файлов, вне контракта). Что же имеется тут ввиду?

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

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

function sum(uint[] memory arr) pure returns (uint s) {
for (uint i = 0; i < arr.length; i++)
s += arr[i];
}

contract ArrayExample {
bool found;
function f(uint[] memory arr) public {
// This calls the free function internally.
// The compiler will add its code to the contract.
uint s = sum(arr);
require(s >= 10);
found = true;
}
}


Вот это вполне рабочий код, где функция sum() находится вне рамок контракта.

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

function sum(uint[] memory arr) private pure returns (uint s)

то компилятор подсветит ошибку.

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

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

#freefunctions
🔥5👍1
Solidity hints. Часть 18

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

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

28. Functions defined outside a contract are still always executed in the context of a contract. They still can call other contracts, send them Ether and destroy the contract that called them

что в переводе:

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

Я об этом и так написал в том посте. Поэтому мы просто перейдем к следующему:

29. the opcode STATICCALL is used when view functions are called, For library view functions DELEGATECALL is used(if interacting with already deployed library). This means library view functions do not have run-time checks that prevent state modifications ?? Omg

и перевод:

при вызове view функций используется опкод STATICCALL, для библиотечных функций представления используется DELEGATECALL (при взаимодействии с уже развернутой библиотекой). Это означает, что библиотечные view функции не имеют проверок во время выполнения, которые предотвращают модификацию состояния ? Омб

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

Как вы можете знать, view функции в Solidity не могут вносить изменения в память контракта, т.е. они не могут, например, изменить значение переменной или массива.

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

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

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

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

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

1. Порождение событий;
2. Создание новых контрактов;
3. Использование selfdestruct;
4. Трансфер Эфира;
5. Вызов функций (не view / pure);
6. Использование низкоуровневых вызовов;
7. Использование assembly с некоторыми опкодами;

Будьте аккуратны при работе с библиотеками и памятью контракта!

#library #solidity #storage
👍31
Solidity hints. Часть 19

Смотрим следующий пункт в репо и звучит он как:

30. For pure functions, the opcode STATICCALL is used, which does not guarantee that the state is not read, but at least that it is not modified. It is not possible to prevent functions from reading the state at the level of the EVM.

что в переводе:

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

Когда вы пишите функции в контракте и помечаете их как view или pure, возможно, вы порой замечали, что компилятор может ругаться на некоторые варианты:

1. Когда вы с view функцией хотите изменить какую-либо переменную состояния;
2. Когда в с pure функцией хотите прочитать переменную состояния;

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

Тут я пока не нашел объяснения, на каком уровне идет запрет для pure чтения памяти контракта, если вы знаете ответ, поделитесь в комментариях.

И когда искал ответ но этот вопрос, наткнулся на интересный рабочий пример с pure функцией, где разработчик задавался вопросом, почему в этом случае pure может читать из памяти:

pragma solidity ^0.8.15;

contract Demo {

string internal _thing = "I am a string";

function thing() external pure returns (string memory) {

string storage ref;

assembly ("memory-safe") {
ref.slot := _thing.slot
}

return ref;
}
}


В обсуждениях пришли к выводу, что все дело в блоке assembly, который неявно конвертирует из storage в memory, как view:

The assembly in this function is pure. The problem is that the implicit conversion from storage to memory is view.

В последней версии Solidity 0.8.26 это все еще работает!

#assembly #pure #view
🤔3
Solidity hints. Часть 20

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

The data returned from fallback function will not be ABI-encoded. Instead it will be returned without modifications (not even padding) WTF

что в переводе:

Данные, возвращаемые функцией fallback, не будут кодироваться ABI. Вместо этого они будут возвращены без изменений (даже без падинга) ОМБ

Думаю, для начала напомню, что такое паддинг.

Допустим мы создали байтовый массив с фиксированной длиной bytes32. Если мы запишем туда значение, которое будет меньше 32 байтов, то его конец будет заполнен нулями, например:

bytes32: 0x5b38da6a701c568545dcfcb03fcb875f56beddc4000000000000000000000000

Тут мы записали адрес 5b38da6a701c568545dcfcb03fcb875f56beddc4, и так как размерность была 32, то оставшееся место заняли нули. Это и называется паддингом.

Он разделяется на два вида:

Right-padded - к нему относят такие типы данных как, string, bytes and bytesN (N - указание на размерность).

Left-padded - intN / uintN, address and другие типы.

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

Теперь перейдем к самим fallback функциям.

Во многих источниках в сети, я встречал следующие описания:

1. Она являются неименованными функциями.
2. Она не может принимать аргументы.
3. Она не может ничего возвращать.
3. В смарт-контракте может быть только одна такая функция.
4. Обязательно должна быть external.
5. Для приема Эфира должна иметь модификатор payable.
6. Ограничено лимитом в 2300 газа, если ее вызывают другие функции.
7. Исполняется, когда в контракте не найдена вызываемая функция.

И если 5 пунктов верны для всех случаем с fallback, то 2 и 3 не совсем верны, так как есть следующая форма функции:

fallback (bytes calldata input) external [payable] returns (bytes memory output)

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

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

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

contract FallbackInputOutput {
address immutable target;

constructor(address _target) {
target = _target;
}

fallback(bytes calldata data) external payable returns (bytes memory) {
(bool ok, bytes memory res) = target.call{value: msg.value}(data);
//require(ok, "call failed");
return res;
//return abi.encode(res);
}
}

contract Counter {
uint256 public count;

function get() external view returns (uint256) {
return count;
}

}

contract TestFallbackInputOutput {
event Log(bytes res);

function test(address _fallback, bytes calldata data) external {
(bool ok, bytes memory res) = _fallback.call(data);
require(ok, "call failed");
emit Log(res);
}

function getTestData() external pure returns (bytes memory) {
return
(abi.encodeCall(Counter.get, ()));
}
}


Сначала разворачиваются контракты TestFallbackInputOutput и Counter, а затем FallbackInputOutput от адреса Counter.

После деплоя вызываете getTestData() для получения банных для отправки, а затем test() с адресом FallbackInputOutput.

В первый раз попробуйте с простым return, а после с return abi.encode(res), и в логах посмотрите, какие данные возвращаются.

В первом случае вернуться незашифрованные данные, о чем и говорится в пункте. Если хотите как-то контролировать их получение, то нужно дополнительно использовать abi.encode для этого.

#fallback #output
🤯1
Solidity hints. Часть 21

А сегодня мы поговорим про Function Overloading - перегрузку функций.

32. Return parameters are not taken into account for overload resolution for function calls to overloaded functions, only function arguments are matched. (Overload resolution is nothing in practice because function dispatcher will match the selectors)

что в переводе:

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

Звучит запутано, но на практике все куда проще.

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

contract A {
function f(uint value) public pure returns (uint out) {
out = value;
}

function f(uint value, bool really) public pure returns (uint out) {
if (really)
out = value;
}
}


Перегрузка функция также может применяться и во внешних интерфесах, однако тут нужно быть аккуратным, так как то, что "разное" для Solidity, может быть "одинаковым" для ABI. Наприммер,

contract A {
function f(B value) public pure returns (B out) {
out = value;
}

function f(address value) public pure returns (address out) {
out = value;
}
}

contract B {
}


Для Solidity объект контракта B, который принимает первая функция абсолютно отличается от аргумента типа address, который присутствует во второй функции. Однако на более "низком уровне", и объект контракта и address - это просто адреса. И поэтому компилятор будет выдавать ошибку при попытке собрать ваш контракт.

Также вам нужно быть аккуратными с размерностью принимаемых аргументов и возвращаемыми значениями. Например,

contract A {
function f(uint8 val) public pure returns (uint8 out) {
out = val;
}
function f(uint256 val) public pure returns (uint256 out) {
out = val;
}
}


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

При этом, если мы попробует выполнить f(50), то возникнет ошибка. По сути, число 50 может быть конвертировано, как в uint256, так и в uint8, и программа просто не поймет, какую функцию требуется вызвать. Но с запросом f(256) все будет ок, так как для этого числа подойдет только вторая функция.

Об этом и говорится в обозначенном в начале поста пункте.

#overloading
1👍1👌1
Скамы с ботами и арбитражом

Вчера смотрел видео с канала Патрика Коллинса и одно из них сподвигло написать этот пост.

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

Вот само видео, если захотите его посмотреть:

https://youtu.be/xJaPrnI7LAA

В чем собственно проблема?

На Ютуб и в различных статьях в сети можно встретить огромное количество материала о том, как легко и просто зарабатывать $ 1000 - $ 10 000 в день на специальных блокчейн ботах, занимающихся арбитражом или флешзаймами. Там говорят, что "с помощью этого простого контракта вы можете делать нереальные деньги каждый день"! Многие из низ даже показывают на видео как выполняется транзакция и как увеличивается счет на кошельке.

Не ведитесь на это! Это 100% развод!

Конечно, есть люди, которые пишут своих ботов для таких операций, но код там настолько уникальный, что выкладывать его в открытый доступ чревато. Это настолько конкурентная среда, что каждый пользователь, по сути, борется со множеством других таких же пользователей за несколько сотен долларов. Если вы хотите также писать своих ботов, то нужно погружаться в сферу и прицельно изучать не только сам язык Solidity, но и работу Defi протоколов, MEV ботов, и финансовую часть.

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

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

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

Будьте аккуратны!

#scam
👍13👌2
Пара слов о современном аудите

Уже более полугода я веду отдельный канал, где каждый день публикую разборы уязвимостей по аудиторским отчетам с конкурсных платформ, типа code4rena, codehawks, sherlock и других. И хочу сказать пару слов тем, кто планирует начать свой путь в аудитах и конкурсах.

Не надейтесь на легкие деньги!

Это года два назад можно было получить пару сот долларов за то, что в контракте был использован transferFrom вместо safeTransferFrom и подобные популярные баги. Сейчас это уже прекрасно отлавливают статические анализаторы и боты. Посмотрите на один из лучших ботов сейчас - lightchaser. Он может отловить более 700 популярных багов! Это значит, что как минимум это количество просто уйдет в "невалид" при аудите.

Я бы разбил поиск багов на несколько уровней:

1. Проход ботами и детекторами. Базовая вещь, которая находит мелкие и средние недочеты.

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

3. Аудит на "нишевые" места. Тут помогает знание других протоколов и их работы. Например, вы прекрасно знаете как работает Uniswap V3: тики, twap, дельта - и можете найти в аудируемом протоколе места, где есть ошибки в коде.

4. Аудит с пониманием кода. Место откуда начинается настоящий аудит. Для того, что понимать код потребуется потрать некоторое время. И я говорю не просто о том, "как работает эта функция", а о том, что вообще происходит внутри нее.

Так, обычный transfer() можно понимать, как перевод токенов с одного адреса на другой. Но аудитор должен учитывать многие другие параметры: как изменятся балансы пользователей, totalSupply, награды, комиссии, изменения в других переменных состояния и многое другое. И чем сложнее функция, тем больше времени уйдет на ее разбор.

5. Аудит с деталями и математикой. Тут аудитор должен понимать не только, что делает код, но и то, что он делать не должен, или что он вообще не делает. Это то место, когда аудитор буквально занимает место разработчика и говорит ему, почему код работает не верно.

2 и 3 пункты этого списка можно усвоить и регулярно практиковать на любых конкурсных аудитах, выделяя по 5-6 часов. Помогут тут и чтение отчетов, и разборы багов, и простое внимание в код. Но и заработок тут будет никакой: 20-30 долларов в лучшем случае за весь конкурс.

На 4 пункт нужно будет потратить достаточно количество времени, более 20-30 часов. В итоге, можно будет заработать пару сотен.

На 5 пункт вы затратите максимум времени: возможно, весь период конкурса. И только тут сможете получить достаточную прибыль.

Аудит - это полноценная работа. Та работа, которая будет занимать у вас 8-9 часов каждый день.

Это уже не легкие деньги, и на курсах этому не научат. Только практика и вникание в код.

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

Аудиторы хорошо получают. Но они и времени отдают в профессию очень много.

#audit
🤔7👍31🔥1
Подведение итогов этого лета

Вот и лето прошло, словно и не бывало...

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

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

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

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

А пока, вот содержание летних постов, на случай, если вы что-то пропустили или просто захотите перечитать:

Какие проекты написать для портфолио?

Токен написать и косточкой не подавиться

Небольшой роадмап для начинающих

Контракты и паттерны

Пара слов о современном аудите


Разбираем темы по Solidity

Solidity hints. Часть 1. Работа falkback()

Solidity hints. Часть 2. Send() return value

Solidity hints. Часть 3. Low-level functions

Solidity hints. Часть 4. Delegatecall

Solidity hints. Часть 5. Enum types

Solidity hints. Часть 6. Function existence check

Solidity hints. Часть 7. Extcodesize

Solidity hints. Часть 7. Selfdestruct

Solidity hints. Часть 8. Precompile

Solidity hints. Часть 9. Overflow

Solidity hints. Часть 10. Call & Delegatecall

Solidity hints. Часть 11. Creationcode

Solidity hints. Часть 12. Constructor

Solidity hints. Часть 13. Array

Solidity hints. Часть 14. Payable modifier

Solidity hints. Часть 15. Modifiers

Solidity hints. Часть 16. Immutable / Constant

Solidity hints. Часть 17. Free functions

Solidity hints. Часть 18. Library & storage

Solidity hints. Часть 19. Assembly, pure functions

Solidity hints. Часть 20. Fallback output

Solidity hints. Часть 21. Function overloading


Большой цикл постов по Account Abstraction

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


Всем приятного отдыха и легкого обучения!

#summer
3🔥144👍2
Первый донат и 2000 участников

Только увидел, что кто-то поставил лайк-звездочку, это мой самый первый донат вообще) очень приятно) спасибо, кто бы ты не был)

А еще, на канале теперь 2000+ участников! Если не ошибаюсь, мы теперь самый большой канал (не чат) посвященный Solidity в ру сегменте! Это очень здорово!

Всем спасибо за доверие!

#thanks
2027❤‍🔥7👍3