Account Abstraction on Starknet, Part I

David Barreto
Starknet Edu
Published in
7 min readJul 31, 2023

--

TL;DR; Account Abstraction is a win for usability in web3 and on Starknet it has been supported from the beginning at the protocol level. For a smart contract to be considered an account contract it must at least implement the interface defined by SNIP-6. Additional methods might be required for advanced account functionality.

// Cheat sheet

struct Call {
to: ContractAddress,
selector: felt252,
calldata: Array<felt252>
}

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

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

trait IAccountAddon {
fn __validate_declare__(class_hash: felt252) -> felt252;
fn __validate_deploy__(class_hash: felt252, salt: felt252, public_key: felt252) -> felt252;
fn public_key() -> felt252;
}

Much has been said about the need to improve the user experience (UX) of web3 if we want to increase adoption. Account Abstraction (AA) is one of the most powerful tools on Starknet to improve UX as it enables users to sign transactions with FaceID or TouchID, to execute multiple operations in a single transaction and to allow for third party services to perform operations on behalf of the user with fine grain control. No wonder why Visa has been so interested in exploring Starknet for auto payments [1].

With Account Abstraction, and in contrast to Externally Owned Accounts (EOA), the signer is decoupled from the account. The signer is the piece of code that signs transactions using a private key and elliptic curve cryptography to uniquely identify a user. The account is a smart contract on Starknet that defines how signature verification is performed, executes the transactions signed by the user and ultimately owns the user’s assets (aka tokens) on L2.

Note: Using an Elliptic Curve Digital Signature Algorithtm (ECDSA) is not the only way to authenticate a signer, other mechanisms are possible but they come with tradeoffs of performance, cost and ecosystem support. ECDSA remains the most widely used algorithm on Starknet and different curves are supported.

This is a three part article. In part I (this article you are reading) I’ll go over some of the theory behind account abstraction and its public interface. In part II I’ll create a new account contract from scratch. Finally, in part III, I’ll deploy it to testnet using Starkli and then use it to interact with Starknet.

SNIP-6

For a smart contract to be considered an account (aka account contract) it must adhere to a specific public interface defined by the Starknet Improvement Proposal number 6 (SNIP-6) [2].

/// @title Represents a call to a target contract
/// @param to The target contract address
/// @param selector The target function selector
/// @param calldata The serialized function parameters
struct Call {
to: ContractAddress,
selector: felt252,
calldata: Array<felt252>
}

/// @title SRC-6 Standard Account
trait ISRC6 {
/// @notice Execute a transaction through the account
/// @param calls The list of calls to execute
/// @return The list of each call's serialized return value
fn __execute__(calls: Array<Call>) -> Array<Span<felt252>>;

/// @notice Assert whether the transaction is valid to be executed
/// @param calls The list of calls to execute
/// @return The string 'VALID' represented as felt when is valid
fn __validate__(calls: Array<Call>) -> felt252;

/// @notice Assert whether a given signature for a given hash is valid
/// @param hash The hash of the data
/// @param signature The signature to validate
/// @return The string 'VALID' represented as felt when the signature is valid
fn is_valid_signature(hash: felt252, signature: Array<felt252>) -> felt252;
}

/// @title SRC-5 Standard Interface Detection
trait ISRC5 {
/// @notice Query if a contract implements an interface
/// @param interface_id The interface identifier, as specified in SRC-5
/// @return `true` if the contract implements `interface_id`, `false` otherwise
fn supports_interface(interface_id: felt252) -> bool;
}

As you can see in the proposal, an account contract must implement at least the methods __execute__, __validate__ and is_valid_signature.

The methods __execute__ and __validate__ are meant to be called by the Starknet protocol during different stages of the lifecycle of a transaction. This doesn’t mean that only the Starknet protocol can use those methods, as a matter of fact, anyone can call those methods even if the contract account doesn’t belong to them. Something to keep an eye on when securing our account.

When a user sends an invoke transaction, the first thing that the protocol does is to call the __validate__ method to check the signature of the transaction. In other words, to authenticate the signer associated with the account. There are restrictions on what you can do inside the __validate__ method to protect the Sequencer against Denial of Service (DoS) attacks [3].

Notice that if the signature verification is successful, the __validate__ method should return the short string VALID as opposed to a boolean. In Cairo, a short string is simply the ASCII representation of a single felt and not a real string. This is why the return type of the method is felt252. If the signature verification fails, you can stop execution with an assert or return literally any other felt that is not the aforementioned short string.

If the protocol is able to authenticate the signer, it will then call the function __execute__ passing as an argument an array of all the operations or “calls” the user wants to perform as a multicall. Each one of these calls define a target smart contract, a method to call (the “selector”) and the arguments expected by the method.

The execution of each Call might result in a value being returned from the target smart contract. This value could be a simple scalar like a felt252 or a boolean, or a complex data structure like a struct or an array. In any case, the Starknet protocol serializes the response using a Span of felt252 elements. Remember that Span represents a snapshot of an Array [4]. This is why the return type of the __execute__ method is an Array of Spans which represents a serialized response from each call in the multicall.

The method is_valid_signature is not defined or used by the Starknet protocol. It was instead an agreement between builders in the Starknet community as a way to allow web3 apps to perform user authentication. Think of a user trying to authenticate to an NFT marketplace using their wallet. The web app will ask the user to sign a message and then it will call the function is_valid_signature to verify that the connected wallet address belongs to the user.

To allow other smart contracts to know if your account contract adheres to the SNIP-6 interface, you should implement the method supports_interface from the SRC5 introspection standard. The interface_id for the SNIP-6 interface is the combined hash of the trait’s selectors as defined by Ethereum’s ERC165 [5]. You can calculate the id yourself by using the src5-rs utility [6] or you can take my word for it that the id is 1270010605630597976495846281167968799381097569185364931397797212080166453709.

Additional Interface

Although the interface defined by the SNIP-6 is enough to guarantee that a smart contract is in fact an account contract, it is the minimum requirement and not the whole story. For an account to be able to declare other smart contracts and pay for the associated gas fees it will need to also implement the method __validate_declare__. If we also want to be able to deploy our account contract using the counterfactual deployment method then it also needs to implement the __validate_deploy__ method.

Counterfactual deployment is a mechanism to deploy an account contract without relying on another account contract to pay for the related gas fees. This is important if we don’t want to associate a new account contract with its deployer address and instead have a “pristine” beginning.

This deployment process starts by calculating locally the would-be-address of our account contract without actually deploying it yet. This is possible to do with tools like Starkli [7]. Once we know the address, we then send enough ETH to that address to cover the costs of deploying our account contract. Once the address is funded we can finally send a deploy_account transaction to Starknet with the compiled code of our account contract. The Sequencer will deploy the account contract to the precalculated address and pay itself gas fees with the ETH we sent there. There’s no need to declare an account contract before deploying it.

To allow tools like Starkli to easily integrate with our smart contract in the future, it is recommended to expose the public_key of the signer as a view function as part of the public interface. With all this in mind, the extended interface of an account contract is shown below.

/// @title IAccount Additional account contract interface
trait IAccountAddon {
/// @notice Assert whether a declare transaction is valid to be executed
/// @param class_hash The class hash of the smart contract to be declared
/// @return The string 'VALID' represented as felt when is valid
fn __validate_declare__(class_hash: felt252) -> felt252;

/// @notice Assert whether counterfactual deployment is valid to be executed
/// @param class_hash The class hash of the account contract to be deployed
/// @param salt Account address randomizer
/// @param public_key The public key of the account signer
/// @return The string 'VALID' represented as felt when is valid
fn __validate_deploy__(class_hash: felt252, salt: felt252, public_key: felt252) -> felt252;

/// @notice Exposes the signer's public key
/// @return The public key
fn public_key() -> felt252;
}

In summary, a fully fledged account contract should implement the SNIP-5, SNIP-6 and the Addon interface.

Conclusion

Account Abstraction is a win for usability in web3 and on Starknet it has been supported from the beginning at the protocol level. For a smart contract to be considered an account contract it must at least implement the interface defined by SNIP-6. Additional methods might be required for advanced account functionality.

An account contract can be deployed using another account contract to pay for gas fees or doing a counterfactual deployment for a pristine start.

In part II of this series [8] I’ll show you how to create an account contract from scratch and then on part III how to deploy it to Starknet’s testnet using Starkli. Stay tuned.

References

[1] Auto Payments for Self-Custodial Wallets

[2] SNIP-6 Standard Account Interface

[3] Starknet Docs: Limitations on the validate function

[4] Cairo Book: The Span data type

[5] ERC-165: Standard Interface Detection

[6] Github: src5-rs

[7] Github: starkli

[8] Account Abstraction on Starknet, Part II

--

--

David Barreto
Starknet Edu

Starknet Developer Advocate. Find me on Twitter as @barretodavid