ERC-6900 — это стандарт Ethereum, определяющий модульные абстрактные аккаунты (Modular Smart Contract Account — MSCA). Он расширяет функциональность абстрактных аккаунтов ERC-4337 (кто пропустил, мы уже писали о них тут), позволяя выносить дополнительную логику и проверки во внешние модули.
Ключевые аспекты ERC-6900:
-
Модульность: позволяет разделить логику аккаунта на отдельные плагины.
-
Расширяемость: упрощает добавление новых функций к аккаунтам без изменения основного кода.
-
Стандартизация: обеспечивает совместимость между различными реализациями аккаунтов и плагинов.
-
Интеграция с ERC-4337: совместим с инфраструктурой Account Abstraction.
Важно! Оба стандарта (ERC-4337 и ERC-6900) находятся в стадии черновика, поэтому возможны изменения. В статье рассматривается AA (ERC-4337) версии v0.6.0 и ERC-6900 (MSCA) версии v0.7.0 (на базе AA v0.6.0). Например, уже есть новая версия AA, в которой изменена работа с validateUserOp
, но MSCA пока этого не поддерживает.
Кроме того, ERC-6900 тесно связан с Alchemy, поэтому самые свежие обновления по этому стандарту, скорее всего, будут в их репозиториях, так как они разрабатывают архитектуру для работы с такими аккаунтами. Это один из главных недостатков стандарта — он создается с учетом нужд конкретного протокола, а не всего сообщества.
MSCA
Стандарт вдохновлен ERC-2535: Diamonds, Multi-Facet Proxy для маршрутизации логики выполнения на основе селекторов функций, хотя напрямую этот стандарт не используется. Все селекторы хранит MSCA — фактически это расширенная версия аккаунта ERC-4337, которая содержит логику установки/удаления плагинов и знает, по какому селектору куда перенаправить вызов.
На MSCA может выполняться два вида вызовов функций:
-
User operation — вызов через EntryPoint. Функции обрабатывают вызовы
validateUserOp
и проверяют действительность пользовательской операции ERC-4337. -
Runtime — прямой вызов на смарт-контракте аккаунта. Сюда входят служебные функции аккаунта (например,
execute
,executeBatch
,upgradeToAndCall
,installPlugin
и т.д.).
Для охвата всех возможных вызовов (включая прямые) у плагина может быть три типа callback-функций:
-
Validation — для проверки
userOp
или прямых вызовов. Схемы валидации определяют условия, при которых учетная запись смарт-контракта будет одобрять действия, выполняемые от ее имени. -
Execution — содержит логику выполнения бизнес-логики или проверки во время выполнения.
-
Hooks — хуки различаются в зависимости от места их вызова, позволяя контролировать процесс до и после выполнения.
-
Pre User Operation Validation Hook — запускается перед функцией
userOpValidationFunction
. -
Pre Runtime Validation Hook — запускается перед
runtimeValidationFunction
. -
Pre Execution Hook — запускается до выполнения бизнес-логики, может передавать данные функции Post Execution Hook.
-
Post Execution Hook — запускается после выполнения бизнес-логики и может обрабатывать данные Pre Execution Hook.
-
Идея в том, чтобы разделить вызовы на два типа из-за их различий: вызовы от EntryPoint и вызовы от EOA и смарт-контрактов. Это различие происходит на уровне валидации вызова, в то время как на уровне исполнения могут использоваться "общие" callback-функции. Получается следующая схема:
Существуют также вызовы executeFromPlugin
и executeFromPluginExternal
, которые обрабатываются иначе, но для начала лучше разобраться с первыми двумя типами вызовов и протестировать их на практике, прежде чем пробовать вызывать один плагин из другого.
Как создать MSCA из AA
Чтобы преобразовать классический Account Abstraction (AA) кошелек в MSCA, потребуются четыре обязательных интерфейса:
-
IAccount.sol — базовый интерфейс для всех AA (ERC-4337), описывающий функцию
validateUserOp
, которая вызывается смарт-контрактом EntryPoint. В классическом варианте здесь реализована проверка подписи и другая логика валидации. В MSCA вместо этого используется функцияuserOpValidationFunction
и хукpreUserOpValidationHook
, делегирующие эти проверки установленным плагинам.function validateUserOp( UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds ) external returns (uint256 validationData);
-
IPluginManager.sol — отвечает за установку и удаление плагинов с помощью двух функций:
function installPlugin( address plugin, bytes32 manifestHash, bytes calldata pluginInstallData, FunctionReference[] calldata dependencies ) external; function uninstallPlugin( address plugin, bytes calldata config, bytes calldata pluginUninstallData ) external;
-
IStandardExecutor.sol — включает стандартные функции выполнения вызовов AA, через них запрещено вызывать плагины напрямую.
function execute( address target, uint256 value, bytes calldata data ) external payable returns (bytes memory); function executeBatch( Call[] calldata calls // Call { target; value; data } ) external payable returns (bytes[] memory);
-
IPluginExecutor.sol - с помощью этого интерфейса плагин А может вызвать плагин Б, но вызов будет произведен через MSCA. Функция
executeFromPluginExternal
нужна, чтобы плагин мог вызвать внешний смарт-контракт через MSCA.function executeFromPlugin( bytes calldata data ) external payable returns (bytes memory); function executeFromPluginExternal( address target, uint256 value, bytes calldata data ) external payable returns (bytes memory);
Кроме этих обязательных интерфейсов, также существует IAccountLoupe.sol, который предоставляет информацию об установленных плагинах ончейн. Например, он включает функции getInstalledPlugins
, getPreValidationHooks
и другие.
Плагины
Плагин — это смарт-контракт-синглтон, разворачиваемый в единственном экземпляре для всех аккаунтов, которые будут его устанавливать. Плагин хранит настройки каждого аккаунта. Смарт-контракт плагина не должен быть обновляемым; для обновления необходимо удалить старую версию плагина и установить новую.
Плагин должен наследовать IPlugin.sol и реализовывать как минимум функции для установки и удаления плагина:
function onInstall(bytes calldata data) external;
function onUninstall(bytes calldata data) external;
Также плагин должен содержать манифест и метаданные.
function pluginManifest() external pure returns (PluginManifest memory);
function pluginMetadata() external pure returns (PluginMetadata memory);
Манифест необходим для установки плагина. Он описывает функции выполнения, функции проверки и хуки, которые будут настроены на MSCA во время установки плагина. Кроме того, манифест содержит требования к зависимостям (где зависимостью может выступать другой плагин) и разрешения на использование определенных функций.
Стоит подробнее рассмотреть структуру манифеста.
Манифест плагина
Манифест — это спецификация плагина, определяющая, как MSCA должен взаимодействовать с плагином, какие функции плагина следует вызывать при обращении к определённым селекторам и как обрабатывать зависимости от других плагинов.
struct PluginManifest {
// Список интерфейсов ERC-165 который следует добавить к аккаунту MSCA.
// Не должен включать интерфейс IPlugin
bytes4[] interfaceIds;
// Если какие-то функции плагина зависят от валидации через другие плагины,
// то их интерфейсы должны быть добавлены в этот массив
bytes4[] dependencyInterfaceIds;
// Это функции плагина, которые устанавливаются на MSCA
// и расширяют его функционал
bytes4[] executionFunctions;
// Функции, уже установленные на MSCA, к которым есть доступ у этого плагина
bytes4[] permittedExecutionSelectors;
// Флаг определяющий может ли плагин вызывать внешние смарт-контракты
bool permitAnyExternalAddress;
// Флаг определяющий может ли плагин тратить нативные токены сети
bool canSpendNativeToken;
// Спецификация функций
ManifestExternalCallPermission[] permittedExternalCalls;
ManifestAssociatedFunction[] userOpValidationFunctions;
ManifestAssociatedFunction[] runtimeValidationFunctions;
ManifestAssociatedFunction[] preUserOpValidationHooks;
ManifestAssociatedFunction[] preRuntimeValidationHooks;
ManifestExecutionHook[] executionHooks;
}
interfaceIds
Например, если ваш аккаунт не поддерживает работу с ERC721 и не может принимать NFT, вы можете добавить плагин с функцией onERC721Received
, а в interfaceIds
указать интерфейс IERC721Receiver
. В результате supportInterface
аккаунта MSCA будет возвращать true
при проверке этого интерфейса.
dependencyInterfaceIds
Необходимо указывать, когда целевой плагин зависит от валидации на другом плагине. Например возьмем плагин, который я написал для тестов. Его основная задача проверять есть ли токен ERC20 в вайтлисте при вызовах функций transfer
и approve
. У него есть служебная функция updateTokens
, которая добавляет и удаляет токены из вайтлиста. Логично, что доступ к такой функции должен быть ограничен, но плагин могут использовать тысячи аккаунтов и отдавать управление вайтлистом какому-то одному кошельку-админу нецелесообразно. В связи с этим каждый аккаунт MSCA сам управляет списком токенов с которыми он может работать. Чтобы доступ к изменению вайтлиста был только у MSCA - понадобиться добавить зависимость в виде плагина который будет отвечать за проверку доступа. В моем случае это MultiOwnerPlugin. Настройка этой проверки будет выполнена далее.
function pluginManifest() external pure override returns (PluginManifest memory) {
PluginManifest memory manifest;
// dependency
manifest.dependencyInterfaceIds = new bytes4[](1);
manifest.dependencyInterfaceIds[0] = type(IMultiOwnerPlugin).interfaceId;
// ...
}
executionFunctions
Это функции которые устанавливаются на MSCA при установке плагина, тем самым расширяя его. В моем случае это функции updateTokens
, isAllowedToken
и getTokens
. Расширяют, означает, что они, как и служебные функции аккаунта, будут вызываться на аккаунте "напрямую", например так - account.updateTokens()
. Т.к. функции плагина будут вызваны через fallback
функцию аккаунта, то если мы не добавим их селекторы в executionFunctions
, такой вызов будет аккаунтом отклонен.
permittedExecutionSelectors
В этот массив добавляются селекторы функций, которые могут быть вызваны плагином на MSCA через функцию executeFromPlugin
.
permitAnyExternalAddress
Флаг, который разрешает или запрещает вызовы через executeFromPluginExternal
.
canSpendNativeToken
Флаг, который определяет, может ли плагин использовать нативные токены сети.
Спецификация функций с которыми работает плагин
Этот раздел манифеста описывает, как именно плагин будет взаимодействовать с различными функциями и хуками в MSCA.
permittedExternalCalls
Определяет разрешения на вызовы внешних адресов. Имеет два варианта: либо разрешить вызовы любого селектора, либо передать массив разрешённых селекторов.
struct ManifestExternalCallPermission {
address externalAddress;
bool permitAnySelector;
bytes4[] selectors;
}
userOpValidationFunctions, runtimeValidationFunctions
Если ваш плагин должен подключаться на этапе вызова userOpValidationFunction
или runtimeValidationFunctions
, то необходимо описать, для каких селекторов это будет работать. Важно отметить, что функции (userOpValidationFunctions
и runtimeValidationFunctions
) могут быть только по одной на на каждый селектор аккаунта, независимо от количества установленных плагинов.
К примеру плагин MultiOwnerPlugin отвечает за валидацию всех транзакций при вызове селекторов аккаунта. Это означает, что если на аккаунте уже установлен MultiOwnerPlugin, то только он отвечает за валидацию селектора IStandardExecutor.execute.selector
в функциях userOpValidationFunctions
и runtimeValidationFunctions
. Поэтому не получится установить плагин, в котором проверка для селектора execute
будет обрабатываться этими же функциями, будет ошибка вроде этой UserOpValidationFunctionAlreadySet(0xb61d27f6,0xc7183455a4c133ae270771860664b6b7ec320bb100)
.
Решение в такой ситуации - добавить необходимую проверку в хук preUserOpValidationHooks
или preRuntimeValidationHooks
, а не в функцию.
Если вам нужна валидация нового селектора, устанавливаемого в MSCA, который не обрабатывается через MultiOwnerPlugin, вы можете добавить его через зависимость. Для этого используются структуры ManifestAssociatedFunction
и ManifestFunction
.
Разберем на примере моего плагина TokenWhitelistPlugin
и функции updateTokens
, которую я хочу добавить к MSCA. Ранее мы уже добавили IMultiOwnerPlugin
в dependencyInterfaceIds
, это был шаг номер 1. Теперь необходимо добавить селектор в executionFunctions
.
function pluginManifest() external pure override returns (PluginManifest memory) {
PluginManifest memory manifest;
// dependency
manifest.dependencyInterfaceIds = new bytes4[](1);
manifest.dependencyInterfaceIds[0] = type(IMultiOwnerPlugin).interfaceId;
// runtime execution functions
manifest.executionFunctions = new bytes4[](1);
manifest.executionFunctions[0] = this.updateTokens.selector;
// ...
}
Данная функция плагина будет вызываться только в runtime, потому что аккаунт не может вызывать плагин через userOp
, следовательно обрабатывать вызов будем через соответствующую функцию. Для начала создаем новый массив ManifestAssociatedFunction
и добавляем туда селектор функции:
struct ManifestAssociatedFunction {
bytes4 executionSelector; // селектор аккаунта (в данном случае он добавлен с плагина)
ManifestFunction associatedFunction; // функция отвечающая за обработку
}
manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](1);
manifest.runtimeValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.updateTokens.selector,
associatedFunction: // ...
});
Далее в associatedFunction
необходимо добавить структуру ManifestFunction
чтобы описать что плагин должен с этим селектором:
struct ManifestFunction {
ManifestAssociatedFunctionType functionType; // Это enum который определен стандартом erc-6900
uint8 functionId; // Это enum который определяется в плагине и помогает идентифицировать функцию
uint256 dependencyIndex; // Индекс зависимости в массиве dependencyInterfaceIds
}
Первое, с чем нужно определиться - это functionType
, флаг, по которому аккаунт поймет куда направить обработку этого селектора:
enum ManifestAssociatedFunctionType {
// Функция не определена
NONE,
// Функция принадлежит этому плагину
SELF,
// Функция принадлежит внешнему плагину, предоставляемому
// в качестве зависимости при установке плагина.
// Плагины МОГУТ зависеть от внешних функций проверки.
// Он НЕ ДОЛЖЕН зависеть от внешних хуков, иначе установка будет неудачной.
DEPENDENCY,
// Устанавливает магическое значение, чтобы всегда пропускать проверку
// в режиме runtime для данной функции.
// Только для режима runtime, если установить для validationFunction,
// это будет впустую потраченный газ.
// Если использовать с хуком, равносильно тому что хук не задан.
RUNTIME_VALIDATION_ALWAYS_ALLOW,
// Устанавливает магическое значение, чтобы всегда вызывать сбой в хуке для данной функции.
// Используется только с хуками предварительного выполнения.
// Но не следует использовать с функциями проверки -
// равносильно тому, что проверка отсутствует.
// Его не следует использовать в post-exec хуках, потому что если известно,
// что хук всегда будет возвращаться, это должно происходить как можно раньше,
// чтобы сэкономить газ.
PRE_HOOK_ALWAYS_DENY
}
В нашем случае необходимо использовать флаг DEPENDENCY
. Когда добавляется зависимость functionId
не имеет значения, т.к. его назначит сама зависимость, dependencyIndex
должен соответствовать позиции плагина-зависимости в dependencyInterfaceIds
:
manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](1);
manifest.runtimeValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.updateTokens.selector,
associatedFunction: ManifestFunction({
functionType: ManifestAssociatedFunctionType.DEPENDENCY,
functionId: 0, // в случае с зависимостью ни на что не влияет
dependencyIndex: 0 // в нашем случае 0, потому что по этому индексу лежит интерфейс IMultiOwnerPlugin
})
});
Почти готово. Чтобы все сработало нужно правильно установить плагин TokenWhitelistPlugin
с описанием того, какая будет зависимость и как она должна работать. Зависимость указывается как bytes21
, где первые 20 байт содержат адрес плагина-зависимости, а последний байт указывает functionId
который нужно передать зависимости.
// Пользовательский тип для упаковки данных о плагине
type FunctionReference is bytes21;
FunctionReference[] memory dependencies = new FunctionReference[](1);
// здесь указываем адрес зависимости и functionId
dependencies[0] = FunctionReferenceLib.pack(
address(multiOwnerPlugin),
uint8(IMultiOwnerPlugin.FunctionId.RUNTIME_VALIDATION_OWNER_OR_SELF)
);
vm.prank(owner);
account1.installPlugin({
plugin: address(tokenWhitelistPlugin),
manifestHash: manifestHash,
pluginInstallData: tokenWhitelistPluginInstallData,
dependencies: dependencies // сюда передаем зависимости
});
Готово. Если все сделать правильно, то теперь при вызове на аккаунте updateTokens
аккаунт будет видеть, что этот селектор обрабатывает зависимость MultiOwnerPlugin
, поэтому передаст управление ему в функцию runtimeValidationFunction
, где будет определен functionId
указанный при установке зависимости вместе с плагином:
function runtimeValidationFunction(
uint8 functionId,
address sender,
uint256,
bytes calldata
) external view override {
if (functionId == uint8(FunctionId.RUNTIME_VALIDATION_OWNER_OR_SELF)) {
// Validate that the sender is an owner of the account, or self.
if (sender != msg.sender && !isOwnerOf(msg.sender, sender)) {
revert NotAuthorized();
}
return;
}
revert NotImplemented(msg.sig, functionId);
}
Важно! В зависимости не получится указать внешний хук, только внешнюю функцию.
preUserOpValidationHooks, preRuntimeValidationHooks
Обрабатываются также через ManifestAssociatedFunction. Выполняются перед соответствующими функциями. Хуков может быть несколько на один селектор в одном аккаунте MSCA. Не могут быть зависимостями.
executionHooks
Хуки, которые задеиствуется на этапе выполнения кода, независимо от того, каким способом был сделан вызов - через userOp или напрямую. Например для плагина TokenWhitelistPlugin
это хорошее место чтобы охватить все вызовы и выполнить проверку есть ли токен в вайтлисте.
Т.к есть два хука на этапе выполнения: pre- и postExecHook, то и структура для описания обработки хуков немного другая - ManifestExecutionHook
:
struct ManifestExecutionHook {
bytes4 selector; // селектор функции, выполнение которой нужно проверить
ManifestFunction preExecHook; // функция для предварительной проверки
ManifestFunction postExecHook; // функция для пост-проверки
}
Дальше используется все та же ManifestFunction
, например мне на плагине нужно обрабатывать вызовы через функции аккаунта MSCA execute
или executeBatch
.
-
Тип функции будет
SELF
, потому вызов обрабатывается в этом же плагине. -
functionId
указываем для того, чтобы плагин понимал с каким селектором производится вызов. -
dependencyIndex
не указываем, потому что у хуков не может быть зависимостей.
manifest.executionHooks = new ManifestExecutionHook[](2);
manifest.executionHooks[0] = ManifestExecutionHook({
executionSelector: IStandardExecutor.execute.selector,
preExecHook: ManifestFunction({
functionType: ManifestAssociatedFunctionType.SELF,
functionId: uint8(FunctionId.EXECUTE_FUNCTION),
dependencyIndex: 0
}),
postExecHook: none
});
manifest.executionHooks[1] = ManifestExecutionHook({
executionSelector: IStandardExecutor.executeBatch.selector,
preExecHook: ManifestFunction({
functionType: ManifestAssociatedFunctionType.SELF,
functionId: uint8(FunctionId.EXECUTE_BATCH_FUNCTION),
dependencyIndex: 0
}),
postExecHook: none
});
Манифест готов. Далее в плагине нужно будет реализовать функцию preExecutionHook
, для обработки таких вызовов. Реализацию можно посмотреть здесь.
function preExecutionHook(
uint8 functionId,
address sender,
uint256 value,
bytes calldata data
) external view override returns (bytes memory) {}
Разработка плагина
Перед тем как сделать плагин, необходимо четко определить, с какими селекторами он будет работать и где нужно выполнять проверки. Примерный чек-лист, который я составил для себя, выглядит так (есть вероятность, что я что-то упустил):
-
Что делает плагин? Какова его главная задача?
-
Какие функции нужно будет добавить аккаунту?
-
Понадобится ли обрабатывать данные
userOp
или достаточно данных вызова? -
Будет ли плагин использоваться как зависимость?
-
Нет ли конфликта селекторов с другими плагинами?
-
Где выполнить проверку как можно раньше? Имеется в виду поток проверки и выполнения вызова и выбор функции или хука в соответствии с этим потоком.
-
Обрабатывает ли выбранная функция/хук все возможные вызовы на аккаунте?
Для примера возьмем другой плагин — TransferLimitPlugin и пройдем по этому списку.
-
Плагин должен устанавливать лимиты неснижаемого остатка токенов ERC20 на аккаунте MSCA. Например, вы боитесь потратить по ошибке большую сумму USDT, поэтому ставите на него лимит. К примеру, на кошельке лежит 5500 USDT, ставим лимит на 5000, а 500 можем спокойно тратить, заодно плагин оградит вас от бесконечных апрувов.
-
Основная функция для такого плагина — обновление лимитов, так ее и назовем —
updateLimit
. Эту функцию сможет вызывать только тот, кто может управлять настройками аккаунта MSCA. Помимо этого понадобятся некоторые view-функции, в нашем случае этоgetCurrentLimit
иgetTokensForAccount
. -
В данном случае не важно, каким способом был сделан вызов, важно обрабатывать любые вызовы MSCA — userOp и runtime.
-
Выглядит так, что нет нужды использовать плагин как зависимость.
-
Так как мы не собираемся делать этот плагин в качестве зависимости, означает, что мы будем использовать хуки, а значит, конфликтовать они не будут.
-
Раньше всего проверку можно выполнить в хуках валидации (preUserOpValidationHooks или preRuntimeValidationHooks), но в таком случае придется на плагине писать две отдельные функции-обработчики, да и информация из userOp нам ничем не поможет.
-
Определенно нужно обрабатывать все возможные переводы токенов, поэтому в данном случае целесообразно сделать это в
preExecutionHook
.
Для наглядности я также накидал схемку, по которой будет проще выбрать необходимые функции или хуки для плагина.
Далее необходимо написать манифест в соответствии с правилами, о которых я рассказывал выше (он будет похож на манифест плагина TokenWhitelistPlugin
) и реализовать сами функции.
Функции view и pure
Функции на чтение данных нужно также добавить в executionFunctions
и отнести их к типу RUNTIME_VALIDATION_ALWAYS_ALLOW
, чтобы они также стали частью MSCA, хотя возможно в некоторых случаях проще будет их вызывать непосредственно на плагине.
function pluginManifest() external pure override returns (PluginManifest memory) {
// ...
// runtime execution functions
manifest.executionFunctions = new bytes4[](3);
manifest.executionFunctions[0] = this.updateLimit.selector;
manifest.executionFunctions[1] = this.getTokensForAccount.selector;
manifest.executionFunctions[2] = this.getCurrentLimit.selector;
ManifestFunction memory runtimeAlwaysAllow = ManifestFunction({
functionType: ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW,
functionId: 0,
dependencyIndex: 0
});
manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](3);
manifest.runtimeValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.updateLimit.selector,
// ...
});
manifest.runtimeValidationFunctions[1] = ManifestAssociatedFunction({
executionSelector: this.getTokensForAccount.selector,
associatedFunction: runtimeAlwaysAllow
});
manifest.runtimeValidationFunctions[2] = ManifestAssociatedFunction({
executionSelector: this.getCurrentLimit.selector,
associatedFunction: runtimeAlwaysAllow
});
Полный пример можно посмотреть здесь.
Установка и удаление плагина
Чтобы плагин можно было установить на MSCA, необходимо реализовать функции onInstall
и onUninstall
. Например, в случае с плагином TransferLimitPlugin в процессе установки можно сразу задать все необходимые лимиты, а при удалении — их очистить:
function onInstall(bytes calldata data) external override {
(ERC20SpendLimit[] memory spendLimits) = abi.decode(data, (ERC20SpendLimit[]));
uint256 length = spendLimits.length;
for (uint8 i = 0; i < length; i++) {
_tokenList.tryAdd(msg.sender, SetValue.wrap(bytes30(bytes20(spendLimits[i].token))));
_limits[msg.sender][spendLimits[i].token] = spendLimits[i].limit;
}
}
function onUninstall(bytes calldata data) external override {
(address[] memory tokens) = abi.decode(data, (address[]));
uint256 length = tokens.length;
for (uint8 i = 0; i < length; i++) {
delete _limits[msg.sender][tokens[i]];
}
_tokenList.clear(msg.sender);
}
Для установки плагина потребуется:
-
Адрес плагина.
-
Хеш
keccak256
от манифеста плагина. -
Данные, которые необходимо передать в функцию
onInstall
(в нашем случае это адреса токенов и их лимиты). -
Зависимости, например MultiOwnerPlugin.
function _installTransferLimitPlugin() private {
// Получаем хеш манифеста
bytes32 transferLimitManifestHash = keccak256(abi.encode(transferLimitPlugin.pluginManifest()));
// Настраиваем параметры установки
TransferLimitPlugin.ERC20SpendLimit[] memory spendLimits = new TransferLimitPlugin.ERC20SpendLimit[](1);
spendLimits[0] = TransferLimitPlugin.ERC20SpendLimit({token: address(token), limit: TRANSFER_LIMIT});
bytes memory transferLimitPluginInstallData = abi.encode(spendLimits);
// Настраиваем зависимости
FunctionReference[] memory dependencies = new FunctionReference[](1);
dependencies[0] = FunctionReferenceLib.pack(
address(multiOwnerPlugin), uint8(IMultiOwnerPlugin.FunctionId.RUNTIME_VALIDATION_OWNER_OR_SELF)
);
// Устанавливаем плагин на MSCA
vm.prank(owner);
account1.installPlugin(
address(transferLimitPlugin),
transferLimitManifestHash,
transferLimitPluginInstallData,
dependencies
);
}
Для удаления плагина потребуется настроить конфигурацию UninstallPluginConfig
:
struct UninstallPluginConfig {
// Манифест плагина
bytes serializedManifest;
// Флаг для принудительного удаления плагина.
// Удаляет плагин, даже если функция onUninstall попытается остановить транзакцию
bool forceUninstall;
// Максимальное количество газа, допустимое для каждой функции обратного вызова деинсталляции
// (`onUninstall`), или ноль, чтобы не устанавливать ограничения.
// Обычно используется вместе с `forceUninstall` для удаления плагинов, которые
// препятствуют деинсталляции, потребляя весь оставшийся газ.
uint256 callbackGasLimit;
}
Пример удаления плагина:
function test_uninstallPlugin() external {
// Получаем манифест
bytes memory serializedManifest = abi.encode(transferLimitPlugin.pluginManifest());
// Настраиваем конфигурацию для удаления
bytes memory config = abi.encode(
UpgradeableModularAccount.UninstallPluginConfig({
serializedManifest: serializedManifest,
forceUninstall: false,
callbackGasLimit: 0
})
);
// Формируем данные для функции onUninstall
address[] memory tokens = new address[](1);
tokens[0] = address(token1);
bytes memory uninstallData = abi.encode(tokens);
// Удаляем плагин
vm.prank(owner);
account1.uninstallPlugin({
plugin: address(transferLimitPlugin),
config: config,
pluginUninstallData: uninstallData
});
}
Особенности плагинов
Порядок выполнения плагинов на одинаковых селекторах
К примеру, у вас есть два плагина: А и Б. Оба плагина обрабатывают через хуки селектор execute
. В каком порядке они будут вызваны?
Логично предположить, что плагины будут вызываться в порядке их установки на MSCA, от самого "старого" до последнего установленного. На деле всё происходит наоборот.
Пример: мы добавили плагин А, затем Б, затем В. Хуки будут вызываться в порядке В -> Б -> А. Все хуки аккаунта хранятся в связном списке, и из-за особенностей его обработки значения извлекаются с конца. При этом, если, например, хук плагина В ревертнет вызов, то дальше хуки выполняться не будут. Такое же поведение будет и в функции AccountLoupe::getInstalledPlugins
, которая вернёт массив плагинов в обратном порядке их установки.
Сложность разработки
Основная сложность кроется не в написании функций и хуков для валидации вызовов, а в том, что плагин должен хорошо встраиваться в MSCA, не конфликтуя с другими плагинами, а иногда ещё и иметь возможность с ними взаимодействовать и работать в связке.
Тестирование
Для тестирования и ознакомления со стандартом подойдёт репозиторий plugin-template. Там уже настроены тесты для работы с EntryPoint.
Вывод
На мой взгляд, стандарт получился неоднозначным. С одной стороны, он открывает неплохие возможности для расширения абстрактного аккаунта. С другой — есть чувство, что он спотыкается сам о себя, иногда давая слишком много инструментов и возможностей их комбинировать.
Было бы лучше, если бы этот стандарт был более "легковесным". Плохо также и то, что на него оказывает большое влияние Alchemy и интересы их протокола. Я думаю этот стандарт можно воспринимать, как ещё одну ступень эволюции абстрактных аккаунтов. К тому же сообщество не очень хорошо его приняло и отталкиваясь от проблем ERC-6900 был разработан другой стандарт - ERC-7579, о нем я тоже планирую сделать подробный обзор.
Ссылки
Делимся инсайтами и новостями из мира web3 в нашем телеграм-канале. Присоединяйтесь:)
Автор: yarlykovrv