An account is an unique entity that can send transactions, users usually use wallets to manage their accounts. Historically, in Ethereum, all accounts were Externally Owned Accounts (EOA) and were controlled by private keys. This is a simple and secure way to manage accounts, but it has limitations as the account logic is hardcoded in the protocol. Account Abstraction (AA) is the concept behind abstracting parts of the account logic to allow for a more flexible account system. This replaces EOA with Account Contracts, which are smart contracts that implement the account logic. This opens up a lot of possibilities that can significantly improve the user experience when dealing with accounts. On Starknet, Account Abstraction is natively supported, and all accounts are Account Contracts.

Account Contract

A smart contract must follow the Standard Account Interface specification defined in the SNIP-6. In practice, this means that the contract must implement the SRC6 and SRC5 interfaces to be considered an account contract.

SNIP-6: SRC6 + SRC5

/// @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>
}
The Call struct is used to represent a call to a function (selector) in a target contract (to) with parameters (calldata). It is available under the starknet::account module.
/// @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;
}
A transaction can be represented as a list of calls Array<Call> to other contracts, with atleast one call.
  • __execute__: Executes a transaction after the validation phase. Returns an array of the serialized return of value (Span<felt252>) of each call.
  • __validate__: Validates a transaction by verifying some predefined rules, such as the signature of the transaction. Returns the VALID short string (as a felt252) if the transaction is valid.
  • is_valid_signature: Verify that a given signature is valid. This is mainly used by applications for authentication purposes.
Both __execute__ and __validate__ functions are exclusively called by the Starknet protocol.
/// @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;
}
The interface identifiers of both SRC5 and SRC6 must be published with supports_interface.

Minimal account contract Executing Transactions

In this example, we will implement a minimal account contract that can validate and execute transactions.
use starknet::account::Call;

#[starknet::interface]
trait ISRC6<TContractState> {
    fn execute_calls(self: @TContractState, calls: Array<Call>) -> Array<Span<felt252>>;
    fn validate_calls(self: @TContractState, calls: Array<Call>) -> felt252;
    fn is_valid_signature(
        self: @TContractState, hash: felt252, signature: Array<felt252>,
    ) -> felt252;
}

#[starknet::contract]
mod simpleAccount {
    use super::ISRC6;
    use starknet::account::Call;
    use core::num::traits::Zero;
    use core::ecdsa::check_ecdsa_signature;
    use starknet::storage::{StoragePointerWriteAccess, StoragePointerReadAccess};

    // Implement SRC5 with openzeppelin
    use openzeppelin_account::interface;
    use openzeppelin_introspection::src5::SRC5Component;
    component!(path: SRC5Component, storage: src5, event: SRC5Event);

    #[abi(embed_v0)]
    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
    impl SRC5InternalImpl = SRC5Component::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        src5: SRC5Component::Storage,
        public_key: felt252,
    }

    #[constructor]
    fn constructor(ref self: ContractState, public_key: felt252) {
        self.src5.register_interface(interface::ISRC6_ID);
        self.public_key.write(public_key);
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        SRC5Event: SRC5Component::Event,
    }

    #[abi(embed_v0)]
    impl SRC6 of ISRC6<ContractState> {
        fn execute_calls(self: @ContractState, calls: Array<Call>) -> Array<Span<felt252>> {
            assert(starknet::get_caller_address().is_zero(), "Not Starknet Protocol");
            let Call { to, selector, calldata } = calls.at(0);
            let res = starknet::syscalls::call_contract_syscall(*to, *selector, *calldata).unwrap();
            array![res]
        }

        fn validate_calls(self: @ContractState, calls: Array<Call>) -> felt252 {
            assert(starknet::get_caller_address().is_zero(), "Not Starknet Protocol");
            let tx_info = starknet::get_tx_info().unbox();
            let tx_hash = tx_info.transaction_hash;
            let signature = tx_info.signature;
            if self._is_valid_signature(tx_hash, signature) {
                starknet::VALIDATED
            } else {
                0
            }
        }

        fn is_valid_signature(
            self: @ContractState, hash: felt252, signature: Array<felt252>,
        ) -> felt252 {
            if self._is_valid_signature(hash, signature.span()) {
                starknet::VALIDATED
            } else {
                0
            }
        }
    }

    #[generate_trait]
    impl SignatureVerificationImpl of SignatureVerification {
        fn _is_valid_signature(
            self: @ContractState, hash: felt252, signature: Span<felt252>,
        ) -> bool {
            check_ecdsa_signature(
                hash, self.public_key.read(), *signature.at(0_u32), *signature.at(1_u32),
            )
        }
    }
}