[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.

Great to see the direction toward `Array` for signatures. @Xiang, the native passkey implementation is exactly the kind of use case that validates this change.

We’ve been building session key infrastructure on top of SNIP-6 and wanted to connect a thread here. We think passkeys and session keys are complementary layers of the same UX stack.

Passkeys solve authentication: prove who owns the account, biometrics, no seed phrase. Session keys solve authorization: define what a delegate can do, with time limits, call limits, and selector restrictions. Paymasters solve sponsorship: who pays for gas via SNIP-9 and SNIP-29. Together these three layers make self-custodial wallets feel like Web2 apps. Cartridge’s Flippy Flop already showed this in production: passkey login, session key gameplay, paymaster gas, 127 TPS with zero friction.

We have a session key account contract live on mainnet ( GitHub - chipi-pay/sessions-smart-contract: Session key standard for Starknet smart accounts. Time-limited, call-limited, selector-restricted delegation with paymaster compatibility. Reference implementation for a proposed SNIP. Live on mainnet (v33), 65 tests, 4 Nethermind AuditAgent scans (final: 0 findings). ), scanned four times by Nethermind’s AuditAgent, and we’re drafting a SNIP to standardize the `ISessionKeyManager` interface. Our 4-element session signature `[session_pubkey, r, s, valid_until]` is already an `Array`, so it’s fully compatible with where SNIP-6 is heading.

The important thing is that the session key interface is independent of the owner’s auth method. A passkey account, a multisig, or a standard ECDSA account can all implement `ISessionKeyManager` without modification. As passkey wallets become the default way people onboard, every one of those new users will need scoped, revocable delegation for dApps, agents, and automated workflows. That’s what session keys provide.

Will share the full SNIP draft in a separate thread soon. Feedback from wallet teams and account authors would be really welcome.