[SNIP] Starknet Standard Account

With native Account Abstraction, Starknet has a lot of flexibility in the management of accounts rather than having their behavior determined at the protocol level. The potential for innovation is virtually limitless leveraging the scalability and capabilities of the STARK proofs, so different use cases are, and will continue to bring different implementations of accounts to the ecosystem.

With this said different dapps, protocols, and standard contracts (like tokens) often require interaction with accounts in a predictable way, either by recognizing them or by expecting certain features.

Currently, Starknet at the protocol level requires account contracts to implement at least two entry points for being able to send transactions: __execute__ and __validate__, but there is no mechanism (at a protocol level) for recognizing if a given contract is an account or not. Also, account implementors (like OZ, Argent, and Braavos) are adding an is_valid_signature method to the public interface for making ecosystem interoperability easier, by allowing off-chain and on-chain signature validation from other modules.

The intention of this post is to propose defining a SNIP (SRC) for the Starknet Standard Account, addressing two issues:

  • The first one: Define a minimal interface for Standard Accounts, supporting ecosystem interoperability, by making different implementations compatible at the core.
  • The second one: Define a standardized application-level mechanism for recognizing Standard Accounts, supporting discoverability and interoperability.

While the Starknet protocol allows deploying accounts that will be non-standard accounts, we expect this standard to be widely adopted by account implementors, again, with the goal of supporting interoperability in the ecosystem.

After some discussion among OZ, Argent, and Braavos, the proposal for the interface is the following:

trait IStandardAccount {
    fn __execute__(calls: Array<Call>) -> Array<Span<felt252>>;
    fn __validate__(calls: Array<Call>) -> felt252;
    // The return value is still in discussion at the moment of writing
    fn is_valid_signature(message: felt252, signature: Array<felt252>) -> [bool or felt252];

__execute__ and __validate__ entry points are required for sending transactions at the protocol level, while is_valid_signature is added for supporting interoperability with ecosystem dapps.

Notice that __validate_declare__ is not included, because allowing the account to declare is an extra feature we don’t consider need to be in the standard interface, and can be added separately.

The SRC will state that the Standard Account MUST also implement the ISRC5 interface:

trait ISRC5 {
    fn supports_interface(interface_id: felt252) -> bool;

And MUST expose the IStandardAccount interface id through this. Then StandardAccounts will be discoverable by introspection.

NOTE: If bool is selected as the return value for is_valid_signature, Accounts MUST NEVER return true from a fallback mechanism, when (if) such a mechanism is implemented for Starknet.

Quick update:

After some discussion among OZ, Starknet, Argent, and Braavos teams, we are leaning towards using felt252 as the return value of is_valid_signature, being the actual returned value VALID as felt252 (as it will be defined in the SNIP).

We think this offers better protection against default entry points (that we foresee can be allowed at some point in the future, supporting features like beacon proxies, etc…), than returning just a bool.

We are leaning towards this interface then:

trait IStandardAccount {
    fn __execute__(calls: Array<Call>) -> Array<Span<felt252>>;
    fn __validate__(calls: Array<Call>) -> felt252;
    fn is_valid_signature(message: felt252, signature: Array<felt252>) -> felt252;

With id (from SRC5): 0x2ceccef7f994940b3962a6c67e0ba4fcd37df7d131417c604f91e03caecc1cd

I created a draft of the standard SNIP that is available here:

I see SNIP-5 has been removed from Github. I wonder why? SNIP-6 is referencing (or trying to reference) 5.

I propose a small change here. The message shouldn’t be constrained to a felt252. I appreciate it is this way because a message is usually a hash of the data, but we shouldn’t restrict it to that.

fn is_valid_signature(message: Array<felt252>, signature: Array<felt252>) -> felt252;
fn is_valid_signature(message: Array<felt252>, signature: Array<felt252>) -> felt252;

This is how we currently implement it, we implemented native passkey, and I very much agree with it.