Standard Account Contract
This section guides you through the creation of a standard account contract, adhering to the SNIP-6 and SRC-5 standards. Previously, we created a simple account contract that lacked signature validation and multicall execution. This time, we'll implement a more robust account contract that includes these features and adheres to the standards of an account contract.
Setting Up Your Project
For deploying an account contract to Starknet's testnet or mainnet, use Scarb version 2.3.1, which is compatible with the Sierra 1.3.0 target supported by both networks. For the latest information, review the Starknet Release Notes. As of November 2023, Scarb version 2.3.1 is the recommended choice.
To check your current Scarb version, run:
scarb --version
To install or update Scarb, refer to the Basic Installation instructions in Chapter 2, covering macOS and Linux environments:
curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh
Starting a New Scarb Project
Begin by creating a new project (more details in the Scarb subchapter in Chapter 2):
scarb new aa
Check the generated project structure:
$ tree .
.
└── aa
├── Scarb.toml
└── src
└── lib.cairo
By default, Scarb sets up for vanilla Cairo. Add Starknet capacities by editing Scarb.toml
to include the starknet
dependency:
[package]
name = "aa"
version = "0.1.0"
cairo-version = "2.3.0"
[dependencies]
starknet = ">=2.3.0"
[[target.starknet-contract]]
sierra = true
casm = true
Replace the code in src/lib.cairo
with an account contract scaffold:
#![allow(unused)] fn main() { #[starknet::contract] mod Account { #[storage] struct Storage { public_key: felt252 } } }
To validate signatures, store the public key associated with the signer's private key.
#![allow(unused)] fn main() { #[storage] struct Storage { public_key: felt252 } }
Compile your project to ensure the setup is correct:
scarb build
Implementing SNIP-6 Standard
To define an account contract, implement the ISRC6
trait:
#![allow(unused)] fn main() { 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; } }
The #[external(v0)]
attribute marks functions with unique selectors for external interaction. The Starknet protocol exclusively uses __execute__
and __validate__
, whereas is_valid_signature
is available for web3 applications to validate signatures.
The trait IAccount<T>
** with #[starknet::interface]
attribute groups publicly accessible functions, like is_valid_signature
. Functions __execute__
and __validate__
, though public, are accessible only indirectly.
#![allow(unused)] fn main() { use starknet::account::Call; #[starknet::interface] trait IAccount<T> { fn is_valid_signature(self: @T, hash: felt252, signature: Array<felt252>) -> felt252; } #[starknet::contract] mod Account { use super::Call; #[storage] struct Storage { public_key: felt252 } #[external(v0)] impl AccountImpl for super::IAccount<ContractState> { fn is_valid_signature(self: @ContractState, hash: felt252, signature: Array<felt252>) -> felt252 { ... } } // These functions are protocol-specific and not intended for direct external use. #[external(v0)] #[generate_trait] impl ProtocolImpl for ProtocolTrait { fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> { ... } fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 { ... } } } }
Restricted Function Access for Security
The __execute__
and __validate__
functions are designed for exclusive use by the Starknet protocol to enhance account security. Despite their public accessibility, only the Starknet protocol can invoke these functions, identified by using the zero address.
#![allow(unused)] fn main() { #[starknet::contract] mod Account { use starknet::get_caller_address; use zeroable::Zeroable; // Enforces Starknet protocol-only access to specific functions #[external(v0)] #[generate_trait] impl ProtocolImpl of ProtocolTrait { // Executes protocol-specific operations fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> { self.only_protocol(); // Verifies protocol-level caller // ... (implementation details) } // Validates protocol-specific operations fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 { self.only_protocol(); // Verifies protocol-level caller // ... (implementation details) } } // Defines a private function to check for protocol-level access #[generate_trait] impl PrivateImpl of PrivateTrait { fn only_protocol(self: @ContractState) { // ... (access validation logic) } } } }
Enhanced Security Through Protocol-Exclusive Functions
Starknet enhances the security of accounts by restricting the callability of certain functions. The __execute__
and __validate__
functions, though publicly visible, are callable solely by the Starknet protocol. This protocol asserts its unique calling rights by using a designated zero address—a special value that signifies protocol-level operations.
#![allow(unused)] fn main() { #[starknet::contract] mod Account { use starknet::get_caller_address; use zeroable::Zeroable; // Implements function access control for Starknet protocol #[external(v0)] #[generate_trait] impl ProtocolImpl of ProtocolTrait { // The __execute__ function is a protocol-exclusive operation fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> { self.only_protocol(); // Validates the caller as the Starknet protocol // ... (execution logic) } // The __validate__ function ensures the integrity of protocol-level calls fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 { self.only_protocol(); // Ensures the caller is the Starknet protocol // ... (validation logic) } } // A private function, only_protocol, to enforce protocol-level access #[generate_trait] impl PrivateImpl of PrivateTrait { // only_protocol checks the caller's address against the zero address fn only_protocol(self: @ContractState) { // If the caller is not the zero address, access is denied // This guarantees that only the Starknet protocol can call the function // ... (access control logic) } } } }
The is_valid_signature
function, by contrast, is not bounded by only_protocol
, maintaining its availability for broader use.
Transaction Signature Validation
To verify transaction signatures, the account contract stores the public key of the signer. The constructor
method initializes this public key during the contract's deployment.
#![allow(unused)] fn main() { #[starknet::contract] mod Account { // Persistent storage for account-related data #[storage] struct Storage { public_key: felt252 // Stores the public key for signature validation } // Sets the public key during contract deployment #[constructor] fn constructor(ref self: ContractState, public_key: felt252) { self.public_key.write(public_key); // Records the signer's public key } // ... Additional implementation details } }
The is_valid_signature
function outputs VALID
for an authentic signature and 0
for an invalid one. Additionally, the is_valid_signature_bool
internal function provides a Boolean result for the signature's validity.
#![allow(unused)] fn main() { #[starknet::contract] mod Account { // Import relevant cryptographic and data handling modules use array::ArrayTrait; use ecdsa::check_ecdsa_signature; use array::SpanTrait; // Facilitates the use of the span() method // External function to validate the transaction signature #[external(v0)] impl AccountImpl of super::IAccount<ContractState> { fn is_valid_signature(self: @ContractState, hash: felt252, signature: Array<felt252>) -> felt252 { // Converts the signature array into a span for processing let is_valid = self.is_valid_signature_bool(hash, signature.span()); if is_valid { 'VALID' } else { 0 } // Returns 'VALID' or '0' based on signature validity } } // Private function to check the signature validity and return a Boolean #[generate_trait] impl PrivateImpl of PrivateTrait { // Validates the signature using a span of elements fn is_valid_signature_bool(self: @ContractState, hash: felt252, signature: Span<felt252>) -> bool { // Checks if the signature has the correct length let is_valid_length = signature.len() == 2_u32; // If the signature length is incorrect, returns false if !is_valid_length { return false; } // Verifies the signature using the stored public key check_ecdsa_signature( hash, self.public_key.read(), *signature.at(0_u32), *signature.at(1_u32) ) } } // ... Additional implementation details } }
In the __validate__
function, the is_valid_signature_bool
method is utilized to confirm the integrity of transaction signatures.
#![allow(unused)] fn main() { #[starknet::contract] mod Account { // Import modules for transaction information retrieval use box::BoxTrait; use starknet::get_tx_info; // Protocol implementation for transaction validation #[external(v0)] #[generate_trait] impl ProtocolImpl of ProtocolTrait { // Validates the signature of a transaction fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 { self.only_protocol(); // Ensures protocol-only access // Retrieves transaction information and unpacks it let tx_info = get_tx_info().unbox(); let tx_hash = tx_info.transaction_hash; let signature = tx_info.signature; // Validates the signature and asserts its correctness let is_valid = self.is_valid_signature_bool(tx_hash, signature); assert(is_valid, 'Account: Incorrect tx signature'); // Stops execution if the signature is invalid 'VALID' // Indicates a valid signature } } // ... Additional implementation details } }
Unified Signature Validation for Contract Operations
The __validate_declare__
function is responsible for validating the signature
of the declare
function. On the other hand, __validate_deploy__
facilitates
counterfactual deployment,a method to deploy an account contract without
associating it to a specific deployer address.
To streamline the validation process, we'll unify the behavior of the three
validation functions __validate__
,__validate_declare__
and __validate_deploy__
.
The core logic from __validate__
is abstracted to validate_transaction
private
function, which is then invoked by the other two validation functions.
#![allow(unused)] fn main() { #[starknet::contract] mod Account { // Protocol implementation for the account contract #[external(v0)] #[generate_trait] impl ProtocolImpl of ProtocolTrait { // Validates general contract function calls fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 { self.only_protocol(); // Ensures only the Starknet protocol can call self.validate_transaction() // Centralized validation logic } // Validates the 'declare' function signature fn __validate_declare__(self: @ContractState, class_hash: felt252) -> felt252 { self.only_protocol(); // Ensures only the Starknet protocol can call self.validate_transaction() // Reuses the validation logic } // Validates counterfactual contract deployment fn __validate_deploy__(self: @ContractState, class_hash: felt252, salt: felt252, public_key: felt252) -> felt252 { self.only_protocol(); // Ensures only the Starknet protocol can call // Even though public_key is provided, it uses the one stored from the constructor self.validate_transaction() // Applies the same validation logic } } // Private trait implementation that contains shared validation logic #[generate_trait] impl PrivateImpl of PrivateTrait { // Abstracted core logic for validating transactions fn validate_transaction(self: @ContractState) -> felt252 { let tx_info = get_tx_info().unbox(); // Extracts transaction information let tx_hash = tx_info.transaction_hash; let signature = tx_info.signature; // Validates the transaction signature using an internal boolean function let is_valid = self.is_valid_signature_bool(tx_hash, signature); assert(is_valid, 'Account: Incorrect tx signature'); // Ensures signature correctness 'VALID' // Returns 'VALID' if the signature checks out } } } }
It's important to note that the __validate_deploy__
function receives the public key
as an argument. While this key is captured during the constructor phase before this function
is invoked, it remains crucial to provide it when initiating the transaction.
Alternatively, the public key can be directly utilized within the __validate_deploy__
function,
bypassing the constructor.
Efficient Multicall Transaction Execution
The __execute__
function within the Account
module of a Starknet contract is designed to process an array of Call
structures. This multicall feature consolidates several user operations into a single transaction, significantly improving the user experience by enabling batched operations.
#![allow(unused)] fn main() { ```rust #[starknet::contract] mod Account { // Protocol implementation to handle execution of calls #[external(v0)] #[generate_trait] impl ProtocolImpl of ProtocolTrait { // The __execute__ function processes an array of calls fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> { self.only_protocol(); // Ensures Starknet protocol level access self.execute_multiple_calls(calls) // Invokes batch processing of calls } // ... Additional implementation details } } }
Each Call
represents the details required for executing a single operation by the smart contract:
#![allow(unused)] fn main() { // Data structure encapsulating a contract call #[derive(Drop, Serde)] struct Call { to: ContractAddress, // The target contract address selector: felt252, // The function selector calldata: Array<felt252> // The parameters for the function call } }
The contract defines a private function execute_single_call
to handle individual calls. It utilizes the call_contract_syscall
to directly invoke a function on another contract:
#![allow(unused)] fn main() { #[starknet::contract] mod Account { // Import syscall for contract function invocation use starknet::call_contract_syscall; // Private trait implementation for individual call execution #[generate_trait] impl PrivateImpl of PrivateTrait { // Executes a single call to another contract fn execute_single_call(self: @ContractState, call: Call) -> Span<felt252> { let Call{to, selector, calldata} = call; // Destructures the Call struct call_contract_syscall(to, selector, calldata.span()).unwrap_syscall() // Performs the contract call } } // ... Additional implementation details } }
For the execution of multiple calls, execute_multiple_calls
iterates over the array of Call structures, invoking execute_single_call
for each and collecting the responses:
#![allow(unused)] fn main() { #[starknet::contract] mod Account { // Private trait implementation for batch call execution #[generate_trait] impl PrivateImpl of PrivateTrait { // Handles an array of calls and accumulates the results fn execute_multiple_calls(self: @ContractState, mut calls: Array<Call>) -> Array<Span<felt252>> { let mut res = ArrayTrait::new(); // Initializes the result array loop { match calls.pop_front() { Option::Some(call) => { let response = self.execute_single_call(call); // Executes each call individually res.append(response); // Appends the result of the call to the result array }, Option::None(_) => { break (); // Exits the loop when no more calls are left }, }; }; res // Returns the array of results } } // ... Additional implementation details } }
In summary, the __execute__
function orchestrates the execution of multiple calls within a single transaction. It leverages these internal functions to handle each call efficiently and return the collective results:
#![allow(unused)] fn main() { #[starknet::contract] mod Account { // External function definition within the protocol implementation #[external(v0)] #[generate_trait] impl ProtocolImpl of ProtocolTrait { // The __execute__ function takes an array of Call structures and processes them fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> { self.only_protocol(); // Verifies that the function caller is the Starknet protocol self.execute_multiple_calls(calls) // Delegates to a function for processing multiple calls } // ... Additional implementation details may follow } // ... Further module code may be present } }
The __execute__
function first ensures that it is being called by the Starknet protocol itself, a security measure to prevent unauthorized access. It then calls the execute_multiple_calls
function to handle the actual execution of the calls.
Ensuring Compatibility with Transaction Versioning
Starknet incorporates a versioning system for transactions to maintain backward compatibility while introducing new functionalities. The account contract tutorial showcases support for the latest transaction versions through a specific module, ensuring smooth operation of both legacy and updated transaction structures.
To accommodate the evolution of Starknet and its enhanced functionalities, a versioning system was introduced for transactions. This ensures backward compatibility, allowing both old and new transaction structures to operate concurrently.
- Version 1 for
invoke
transactions - Version 1 for
deploy_account
transactions - Version 2 for
declare
transactions
These supported versions are logically grouped in a module called SUPPORTED_TX_VERSION
:
#![allow(unused)] fn main() { // Module defining supported transaction versions mod SUPPORTED_TX_VERSION { // Constants representing the supported versions const DEPLOY_ACCOUNT: felt252 = 1; // Supported version for deploy_account transactions const DECLARE: felt252 = 2; // Supported version for declare transactions const INVOKE: felt252 = 1; // Supported version for invoke transactions } #[starknet::contract] mod Account { // The rest of the account contract module code ... } }
To handle the version checking, the account contract includes a private function only_supported_tx_version
. This function compares the version of an incoming transaction against the specified supported versions, halting execution with an error if a discrepancy is found.
The critical contract functions such as __execute__
, __validate__
, __validate_declare__
, and __validate_deploy__
implement this version check to confirm transaction compatibility.
#![allow(unused)] fn main() { #[starknet::contract] mod Account { // Importing constants from the SUPPORTED_TX_VERSION module use super::SUPPORTED_TX_VERSION; // Protocol implementation for Starknet functions #[external(v0)] #[generate_trait] impl ProtocolImpl of ProtocolTrait { // Function to execute multiple calls with version check fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> { self.only_protocol(); // Checks if the function caller is the Starknet protocol self.only_supported_tx_version(SUPPORTED_TX_VERSION::INVOKE); // Ensures the transaction is the supported version self.execute_multiple_calls(calls) // Processes the calls if version check passes } // Each of the following functions also includes the version check to ensure compatibility fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 { self.only_protocol(); self.only_supported_tx_version(SUPPORTED_TX_VERSION::INVOKE); self.validate_transaction() } fn __validate_declare__(self: @ContractState, class_hash: felt252) -> felt252 { self.only_protocol(); self.only_supported_tx_version(SUPPORTED_TX_VERSION::DECLARE); self.validate_transaction() } fn __validate_deploy__(self: @ContractState, class_hash: felt252, salt: felt252, public_key: felt252) -> felt252 { self.only_protocol(); self.only_supported_tx_version(SUPPORTED_TX_VERSION::DEPLOY_ACCOUNT); self.validate_transaction() } } // Private implementation for checking supported transaction versions #[generate_trait] impl PrivateImpl of PrivateTrait { // Function to assert the transaction version is supported fn only_supported_tx_version(self: @ContractState, supported_tx_version: felt252) { let tx_info = get_tx_info().unbox(); // Retrieves transaction details let version = tx_info.version; // Extracts the version from the transaction assert( version == supported_tx_version, 'Account: Unsupported tx version' // Error message for unsupported versions ); } // ... Additional private functions } } }
By integrating transaction version control, the contract ensures it operates consistently with the network's current standards, providing a clear path for upgrading and maintaining compatibility with Starknet's evolving ecosystem.
Handling Simulated Transactions
Starknet's simulation feature allows developers to estimate the gas cost of transactions without actually committing them to the network. This is particularly useful during development and testing phases. The estimate-only
flag available in tools like Starkli triggers the simulation process. To differentiate between actual transaction execution and simulation, Starknet uses a version offset strategy.
Simulated transactions are assigned a version number that is the sum of (2^{128}) and the version number of the actual transaction type. For example, if the latest version of a declare
transaction is 2, then a simulated declare
transaction would have a version number of (2^{128} + 2). The same logic applies to other transaction types like invoke
and deploy_account
.
Here's how the only_supported_tx_version
function is adjusted to accommodate both actual and simulated transaction versions:
#![allow(unused)] fn main() { #[starknet::contract] mod Account { // Constant representing the version offset for simulated transactions const SIMULATE_TX_VERSION_OFFSET: felt252 = 340282366920938463463374607431768211456; // This is 2^128 // Private trait implementation updated to validate transaction versions #[generate_trait] impl PrivateImpl of PrivateTrait { // Function to check for supported transaction versions, accounting for simulations fn only_supported_tx_version(self: @ContractState, supported_tx_version: felt252) { let tx_info = get_tx_info().unbox(); // Retrieves the transaction metadata let version = tx_info.version; // Extracts the version for comparison // Validates whether the transaction version matches either the supported actual version or the simulated version assert( version == supported_tx_version || version == SIMULATE_TX_VERSION_OFFSET + supported_tx_version, 'Account: Unsupported tx version' // Assertion message for version mismatch ); } // Additional private functions may follow } // Remaining contract code may continue here } }
The code snippet showcases the account contract's capability to recognize and process both actual and simulated versions of transactions by incorporating the large numerical offset. This ensures that the system can seamlessly operate with and adjust to the estimation process without affecting the actual transaction processing logic.
SRC-5 Standard and Contract Introspection
Contract introspection is a feature that allows Starknet contracts to self-report the interfaces they support, in compliance with the SRC-5 standard. The supports_interface
function is a fundamental part of this introspection process, enabling contracts to communicate their capabilities to others.
For a contract to be SRC-5 compliant, it must return true
when the supports_interface
function is called with a specific interface_id
. This unique identifier is chosen to represent the SRC-6 standard's interface, which the contract claims to support. The identifier is a large integer specifically chosen to minimize the chance of accidental collisions with other identifiers.
In the account contract, the supports_interface
function is part of the public interface, allowing other contracts to query its support for the SRC-6 standard:
#![allow(unused)] fn main() { // SRC-5 trait defining the introspection method trait ISRC5 { // Function to check interface support fn supports_interface(interface_id: felt252) -> bool; } // Extension of the account contract's interface for SRC-5 compliance #[starknet::interface] trait IAccount<T> { // ... Additional methods // Method to validate interface support fn supports_interface(self: @T, interface_id: felt252) -> bool; } #[starknet::contract] mod Account { // Constant identifier for the SRC-6 trait const SRC6_TRAIT_ID: felt252 = 1270010605630597976495846281167968799381097569185364931397797212080166453709; // Public interface implementation for the account contract #[external(v0)] impl AccountImpl of super::IAccount<ContractState> { // ... Other function implementations // Implementation of the interface support check fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { // Compares the provided interface ID with the SRC-6 trait ID interface_id == SRC6_TRAIT_ID } } // ... Additional account contract code } }
By implementing this function, the account contract declares its ability to interact with other contracts expecting SRC-6 features, thus adhering to the standards of the Starknet protocol and enhancing interoperability within the network.
Public Key Accessibility
For enhanced transparency and debugging purposes, it's recommended to make the public key of the account contract's signer accessible. This allows users to verify the correct deployment of the account contract by comparing the stored public key with the signer's public key offline.
#![allow(unused)] fn main() { ... #[starknet::contract] mod Account { ... #[external(v0)] impl AccountImpl of IAccount<ContractState> { ... fn public_key(self: @ContractState) -> felt252 { self.public_key.read() } } } }
Final Implementation
We now have a fully functional account contract. Here's the final implementation;
#![allow(unused)] fn main() { use starknet::account::Call; mod SUPPORTED_TX_VERSION { const DEPLOY_ACCOUNT: felt252 = 1; const DECLARE: felt252 = 2; const INVOKE: felt252 = 1; } #[starknet::interface] trait IAccount<T> { fn is_valid_signature(self: @T, hash: felt252, signature: Array<felt252>) -> felt252; fn supports_interface(self: @T, interface_id: felt252) -> bool; fn public_key(self: @T) -> felt252; } #[starknet::contract] mod Account { use super::{Call, IAccount, SUPPORTED_TX_VERSION}; use starknet::{get_caller_address, call_contract_syscall, get_tx_info, VALIDATED}; use zeroable::Zeroable; use array::{ArrayTrait, SpanTrait}; use ecdsa::check_ecdsa_signature; use box::BoxTrait; const SIMULATE_TX_VERSION_OFFSET: felt252 = 340282366920938463463374607431768211456; // 2**128 const SRC6_TRAIT_ID: felt252 = 1270010605630597976495846281167968799381097569185364931397797212080166453709; // hash of SNIP-6 trait #[storage] struct Storage { public_key: felt252 } #[constructor] fn constructor(ref self: ContractState, public_key: felt252) { self.public_key.write(public_key); } #[external(v0)] impl AccountImpl of IAccount<ContractState> { fn is_valid_signature(self: @ContractState, hash: felt252, signature: Array<felt252>) -> felt252 { let is_valid = self.is_valid_signature_bool(hash, signature.span()); if is_valid { VALIDATED } else { 0 } } fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { interface_id == SRC6_TRAIT_ID } fn public_key(self: @ContractState) -> felt252 { self.public_key.read() } } #[external(v0)] #[generate_trait] impl ProtocolImpl of ProtocolTrait { fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> { self.only_protocol(); self.only_supported_tx_version(SUPPORTED_TX_VERSION::INVOKE); self.execute_multiple_calls(calls) } fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 { self.only_protocol(); self.only_supported_tx_version(SUPPORTED_TX_VERSION::INVOKE); self.validate_transaction() } fn __validate_declare__(self: @ContractState, class_hash: felt252) -> felt252 { self.only_protocol(); self.only_supported_tx_version(SUPPORTED_TX_VERSION::DECLARE); self.validate_transaction() } fn __validate_deploy__(self: @ContractState, class_hash: felt252, salt: felt252, public_key: felt252) -> felt252 { self.only_protocol(); self.only_supported_tx_version(SUPPORTED_TX_VERSION::DEPLOY_ACCOUNT); self.validate_transaction() } } #[generate_trait] impl PrivateImpl of PrivateTrait { fn only_protocol(self: @ContractState) { let sender = get_caller_address(); assert(sender.is_zero(), 'Account: invalid caller'); } fn is_valid_signature_bool(self: @ContractState, hash: felt252, signature: Span<felt252>) -> bool { let is_valid_length = signature.len() == 2_u32; if !is_valid_length { return false; } check_ecdsa_signature( hash, self.public_key.read(), *signature.at(0_u32), *signature.at(1_u32) ) } fn validate_transaction(self: @ContractState) -> felt252 { let tx_info = get_tx_info().unbox(); let tx_hash = tx_info.transaction_hash; let signature = tx_info.signature; let is_valid = self.is_valid_signature_bool(tx_hash, signature); assert(is_valid, 'Account: Incorrect tx signature'); VALIDATED } fn execute_single_call(self: @ContractState, call: Call) -> Span<felt252> { let Call{to, selector, calldata} = call; call_contract_syscall(to, selector, calldata.span()).unwrap() } fn execute_multiple_calls(self: @ContractState, mut calls: Array<Call>) -> Array<Span<felt252>> { let mut res = ArrayTrait::new(); loop { match calls.pop_front() { Option::Some(call) => { let _res = self.execute_single_call(call); res.append(_res); }, Option::None(_) => { break (); }, }; }; res } fn only_supported_tx_version(self: @ContractState, supported_tx_version: felt252) { let tx_info = get_tx_info().unbox(); let version = tx_info.version; assert( version == supported_tx_version || version == SIMULATE_TX_VERSION_OFFSET + supported_tx_version, 'Account: Unsupported tx version' ); } } } }
Account Contract Creation Summary
-
SNIP-6 Implementation
- Implements the
ISRC6
trait, defining the account contract's structure.
- Implements the
-
Protocol-Only Function Access
- Restricts
__validate__
and__execute__
to StarkNet protocol access. - Makes
is_valid_signature
available for external calls. - Adds a
only_protocol
private function to enforce access rules.
- Restricts
-
Signature Validation Process
- Stores a public key to verify the signer's transactions.
- Initializes with a
constructor
to set the public key. - Validates signatures with
is_valid_signature
, returningVALID
or0
. - Uses
is_valid_signature_bool
to return a true or false validation result.
-
Declare and Deploy Function Validation
- Sets up
__validate_declare__
to check thedeclare
function's signature. - Designs
__validate_deploy__
for counterfactual deployments. - Abstracts core validation to
validate_transaction
.
- Sets up
-
Transaction Execution Logic
- Enables multicall capability with
__execute__
. - Handles calls individually with
execute_single_call
and in batches withexecute_multiple_calls
.
- Enables multicall capability with
-
Transaction Version Compatibility
- Ensures compatibility with StarkNet updates using a versioning system.
- Defines supported transaction types in
SUPPORTED_TX_VERSION
. - Checks transaction versions with
only_supported_tx_version
.
-
Simulated Transaction Handling
- Adapts
only_supported_tx_version
to recognize both actual and simulated versions.
- Adapts
-
Contract Self-Identification
- Allows self-identification with the SRC-5 standard via
supports_interface
.
- Allows self-identification with the SRC-5 standard via
-
Public Key Visibility
- Provides public key access for transparency.
-
Complete Implementation
- Presents the final account contract code.
Next, we will deploy the account using Starkli to the testnet and interact with other smart contracts.