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 theVALID
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)
)
}
}
}