Build a Plugin
Incorrectly implemented plugins can brick your wallet or otherwise result in loss of funds. If you are building a plugin, we strongly encourage that you get in touch with our team on Discord so we can provide a review.
If you haven't already, make sure you read the overview so you understand how Kernel works from a high level. This document will dive into the details of writing a plugin.
As mentioned in the overview, there are two kinds of plugins: validators and executors.
Writing a Validator
A validator modifies how transactions are validated. Validators must implement the IKernelValidator
interface:
interface IKernelValidator {
function enable(bytes calldata _data) external;
function disable(bytes calldata _data) external;
function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingFunds)
external
returns (uint256);
function validateSignature(bytes32 hash, bytes calldata signature) external view returns (uint256);
}
Here's an explanation of each function:
enable
: used in "enable mode" (see the overview) to enable the validator. The_data
should be whatever data you need to initialize the plugin. For example, in the ECDSA plugin, the data is simply the owner address.disable
: used to disable the validator. To use this function, the user calls it directly on the validator contract.validateUserOp
: implements custom validation logic for this plugin. The difference between thisvalidateUserOp
and the Kernel'svalidateUserOp
is that the Kernel will "strip" the extra data (e.g. the "mode") fromuserOp.signature
before passing it to thisvalidateUserOp
.validateSignature
: validates signatures dynamically. Only relevant if you want to use this validator as a "default validator," which we will explain later.
Default Validator
Each Kernel account must have one "default validator." A default validator is special in a few ways:
- It's the validator used in "sudo mode" (see overview). In other words, when other validators are not used, the default validator is used by default (duh).
- It's the validator used in "enable mode" to check if it approves of the enabling of another validator.
- It's the validator used for checking signatures for ERC-1271.
The second and third points are the reasons why a default validator must implement a validateSignature
method.
Validator Storage
Since validators are invoked with call
, it has its own storage and doesn't share storage with Kernel.
However, since validator storage is accessed during the validation phase, ERC-4337's "storage rules" apply. In particular, it means that you need to index storage by msg.sender
.
For example, you cannot do this:
bool enabled;
function validateUserOp(UserOperation calldata _op, ...) external {
require(enabled);
}
But you can do this:
mapping(address => bool) enabled;
function validateUserOp(UserOperation calldata _op, ...) external {
require(enabled[_op.sender]);
}
Validator Opcodes
Since validators are used during the validation phase, they cannot use any forbidden opcodes defined in ERC-4337.
Validator Examples
We strongly recommend that you read some of the following examples and make sure you understand how and why they work.
ECDSA validator: the default "default validator" for ZeroDev accounts. Mimics the behavior of an EOA -- simply checking that the account owner is signing correctly using ECDSA.
Kill switch validator: just like the ECDSA validator except that it allows for a "kill switch" -- a designated guardian can "turn off" the account and set a new owner, presumably because the original owner's key was compromised.
ERC165 session key validator: designed to work with a specific executor (explained later) that transfers NFTs. This validator verifies that the executor is indeed interacting with a contract that implements the ERC721 interface.
Writing an Executor
You have significantly more freedom when writing an executor than writing a validator, since they are just custom functions that you want to add "on top" of what Kernel already defines. Furthermore, since executors are used during the execution phase, there are no opcode restrictions.
Executor Storage
Executors are invoked with delegatecall
, which means executors need to be extra careful to not touch any storage area used by Kernel. Therefore, it's strongly encouraged that you use "unstructured stroage" (aka "diamond storage").
Executor Examples
- ERC721 transfer: a simple example of a custom function that transfers NFTs. This executor is designed to work with the ERC165 session key validator.