Community Plugin Account

starknet-plugin-account

Account abstraction opens a completely new design space for accounts.

This project is a community effort lead by Argent, Cartridge and Ledger, to explore the possibility to make accounts more flexible and modular by defining a plugin account architecture which lets users compose functionalities they want to enable when creating their account. The proposed architecture also aims to make the account extendable by letting users add or remove functionalities after the account has been created.

The idea of modular smart-contracts is not new and several architectures have been proposed for Ethereum [Argent smart-wallet, Diamond Pattern]. However, it is the first time that this is applied to accounts directly by leveraging account abstraction.

Account Abstraction:

In StarkNet accounts must comply to the IAccount interface:

@contract_interface
namespace IAccount {

    func supportsInterface(interfaceId: felt) -> (success: felt) {
    }

    func isValidSignature(hash: felt, signature_len: felt, signature: felt*) -> (isValid: felt) {
    }

    func __validate__(
        call_array_len: felt, call_array: CallArray*, calldata_len: felt, calldata: felt*
    ) {
    }

    func __validate_declare__(class_hash: felt) {
    }

    func __validate_deploy__(
        class_hash: felt, ctr_args_len: felt, ctr_args: felt*, salt: felt
    ) {
    }

    func __execute__(
        call_array_len: felt, call_array: CallArray*, calldata_len: felt, calldata: felt*
    ) -> (response_len: felt, response: felt*) {
    }
}

The two important methods are __validate__ which is called by the Starknet OS to verify that the transaction is valid and that the account will pay the fee before __execute__ is called by the OS to execute the transaction.

The __validate__ method has some constraints to protect the network. In particular, its logic must be implemented in a small number of steps and it cannot access the mutable state of any other contracts (i.e. it can only read the storage of the account).

PluginAccount:

The PluginAccount contract is the main account contract that supports the addition of plugins.

A plugin is a separate piece of logic that can extend the functionalities of the account.

In this first version we focus only on the validation of transactions so plugins can implement different validation logic. However, the architecture can be easily extended to let plugins handle the execution of transactions in the future.

The Plugin Account extends the base account interface with the following interface:

    func addPlugin(plugin: felt, plugin_calldata_len: felt, plugin_calldata: felt*) {
    }

    func removePlugin(plugin: felt) {
    }

    func setDefaultPlugin(plugin: felt) {
    }

    func isPlugin(plugin: felt) -> (success: felt) {
    }

    func readOnPlugin(plugin: felt, selector: felt, calldata_len: felt, calldata: felt*) {
    }

    func getDefaultPlugin() -> (plugin: felt) {
    }

A plugin must expose the following interface:

@contract_interface
namespace IPlugin {
    func initialize(
        calldata_len: felt,
        calldata: felt*) {}

    func validate(
        call_array_len: felt,
        call_array: CallArray*,
        calldata_len: felt,
        calldata: felt*) {}
}

Plugins can be enabled and disabled with the methods addPlugin and removePlugin respectively.

The presence of a plugin can be checked with the isPlugin method.

Validating with a plugin:

For every transaction the caller can instruct the account to validate the multi-call with a given plugin provided that the plugin has been registered in the account. Once the plugin is identified, the account will delegate the validation of the transaction to the plugin by calling the validate method of the plugin.

We note that the plugin must be called with a library_call to comply to the constraints of the __validate__ method, which prevents accessing the storage of other contracts. I.e. the logic of the plugin is executed in the context of the account and the state of the plugin, if any, must be stored in the account.

To instruct the account to use a specific plugin we leverage the transaction signature data. By convention, the first item in the signature data specifies the class hash of the plugin which should be used for validation. If no valid plugin is specified the default plugin is used. Any additional context necessary to validate the transaction, such as the signature itself, should be appended to the signature data.

So to validate a call using a specific plugin, the signature data should look like [pluginClassHash, ...]

Similarly, the isValidSignature will validate a signature using the provided plugin in the passed signature data.

The default Plugin:

If no plugin is specified, the default plugin is used to validate the transaction. There must always be a default plugin defined and this plugin must be properly initialised.

The default plugin is also used to implement the supportsInterface method of the account.

The default plugin can be changed with the method setDefaultPlugin.

Changing the state of a plugin:

To manipulate the state of a plugin, the account has a __default__ method that will be called when a multi-call specifies a call to the account with an unknown selector. The plugin to call is the one used for the validation.

Reading the state of a plugin:

The view methods of a plugin can be accessed through the readOnPlugin method.

The view methods of the default plugin can also be accessed through the __default__ method.

Contributions

This project is still in development but we would love the hear the feedback of the StarkNet community. We would like to also encourage contributions to the repository.

For more info: https://github.com/argentlabs/starknet-plugin-account

54 Likes

Hey,

I’d rename the method isPlugin to hasPlugin because then the function isPlugin could be misunderstood as a check whether the given felt is a plugin or not. Not that the given felt is a plugin that is existing to the Account.

I get a rough idea of what the function readOnPlugin would do, but I’d like a confirmation. Could you elaborate on what is its purpose?

It means that when deploying an implementation a default plugin has to be specified, right?

Maybe having a count/list of all plugins an account has could be handy?
Or maybe it is best to keep it as simple as possible :slight_smile:

G.

31 Likes

@juniset , regarding:

Changing the state of a plugin:

To manipulate the state of a plugin, the account has a __default__ method that will be called when a multi-call specifies a call to the account with an unknown selector. The plugin to call is the one used for the validation.

  1. I don’t think that configuration should be implicit. Maybe consider adding an explicit “configure” method into the Plugin API with generic calldata_len and calldata?
  2. Wouldn’t this break in SN > v0.10 since selectors were deprecated in invoke transactions so you can’t choose a selector (__execute__ is chosen by default)
  3. In case you meant here that you will send a reentrant multicall with a non-existing entrypoint, again I think this is not a great design due to this implicit reentrancy.
24 Likes

Do we assume that a plugin is always connected to a specific application?
If not, then using a class hash as id might be problematic when there are multiple applications using the same plugin that received updates over time. Application would have to call hasPlugin for every known version of a plugin to see which one is supported by the user.
There would also be a problem when user updates a plugin before the application is updated to know about it - it won’t see that this functionality is available.

21 Likes

Thanks for the feedback! Def agree with your concern on the use of __default__, theres a PR here to remove it as per your suggestion: No Default Plugin by sgc-code ¡ Pull Request #10 ¡ argentlabs/starknet-plugin-account ¡ GitHub.

24 Likes

Currently, plugins aren’t connected to a particular app, they exist generically on the account. A plugin could implement its own logic to route signature validation differently based on the application being called, which is similar to what the session keys plugin does.

I think along with the account implementation, we’ll have to stand up some offchain infrastructure to manage the currently supported plugins for an account. An application could also do interface introspection using hasPlugin with a plugin hash.

Generally though, most applications wouldn’t need to be aware of plugins. A plugin developer could provide an implementation of the account interface, which would handle constructing transactions/signatures.

21 Likes

Do I understand correctly that with the current plugin architecture:

  1. only a single plugin can be specified (and hence executed) in one multicall?
  2. an app has to specifically include the plugin it wants to use in the calldata? So it has to be aware of the plugin’s existence and also if it’s installed in the account?
22 Likes

Currently, (1) is the case. We are experimenting with different architectures to support chaining plugins during execution:

For (2), the current implementation moves the plugin selection to the signature data. With the chainable architecture, you would be able to pass signed calldata to plugins too.

The account client does need to be aware of the implementation details to format calls / signatures correctly. This is usually handled by an account provider so the application developer doesn’t need to be concerned about it.

24 Likes

Ok, this is brilliant.

My mental model of plugins since I first learnt about them was HTTP middleware so I’m happy you’re thinking about it the same. I think this is the right way to go. It will enable a wave of innovation to the benefit of normal users.

Not sure I understand the finer points in (2). Say we have an on chain game like Realms. The Realms team develops a plugin to restrict interactions of an account only with approved contracts and functions. A player installs this plugin to their StarkNet account (one dedicated to playing Realms). Some other party develops a malicious client to play Realms. If this player uses the malicious client, will the plugin still execute (becuase it’s installed in their account) or not (because the malicious client naturally wouldn’t want to include it in the calldata)?

28 Likes