Skip to content
This repository has been archived by the owner on Jun 20, 2024. It is now read-only.

Commit

Permalink
📝 update ch04-01-accounts.md (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
CyndieKamau authored Oct 19, 2023
1 parent 2ac0f4a commit 1006a1b
Showing 1 changed file with 264 additions and 36 deletions.
300 changes: 264 additions & 36 deletions src/ch04-01-accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,72 @@
Now that we know better the concept of AA, let’s actually code it in
Starknet.

## Account Contract Interface
## Account Contract Interface (SNIP-6)

Account contracts, while essentially being smart contracts,
differentiate themselves through unique methods. A smart contract gains
the status of an account contract when it implements the interface as
described by SNIP-6 ([StarkNet IMprovement Proposa-6: Standar Account
the status of an account contract when it implements the public interface as
described by SNIP-6 ([StarkNet Improvement Proposal-6: Standard Account
Interface](https://github.com/ericnordelo/SNIPs/blob/feat/standard-account/SNIPS/snip-6.md)).
This standard borrows from SRC-6 and SRC-5, which are akin to Ethereum’s
ERCs, setting application-level conventions and contract standards.

To initiate, let’s formulate the `ISRC6` (SNIP-6: Standard Account
Interface) trait, which outlines the requisite functions for an account
contract:

```rust
trait ISRC6 {
fn __validate__(calls: Array<Call>) -> felt252;
fn is_valid_signature(hash: felt252, signature: Array<felt252>) -> felt252;
fn __execute__(calls: Array<Call>) -> Array<Span<felt252>>;
}
/// @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 seen in the proposal above, an account contract must implement
at least the methods `__execute__`, `__validate__` and `is_valid_signature`,
formulated in the `ISRC6` trait.


The functions represented above serve the following purposes:

- `__validate__`: This function takes in the list of calls scheduled
for execution and validates them in line with the rules specified in
the contract. It returns a short string (e.g., VALID) encapsulated
within a `felt252` that signifies the validation outcome.
the contract. It returns a short string (e.g., VALID), as opposed to a
boolean, encapsulated within a `felt252` that signifies the validation outcome.
This is because in Cairo, a short string is simply the ASCII representation
of a single felt and not a real string. Which is why the return type is `felt252`.
If the signature verification fails, you can return literally any other felt that
is not the aforementioned short string. The number `0` is a common choice.

- `is_valid_signature`: This function is tasked with validating the
signature of a transaction. It receives a hash of the transaction
Expand All @@ -55,38 +93,210 @@ interface ID and returning a boolean.
}
```

Until now, we’ve mentioned contract calls without explicitly defining
them. Let’s remedy that.
In summary, when a user sends an `invoke` transaction, the first thing the
protocol does is to call the `__validate__` method to authenticate the signer
associated with the account. Note that there are restrictions on what you
can do inside the `__validate__` method to protect the Sequencer against
Denial of Service (DoS) attacks [1]. If the signature verification is
successful, it returns VALID felt252 element. If it fails, return 0.


Once the protocol authenticates the signer, it then calls the function `__execute__`.
It passes as an argument an array of all the "calls", or operations, the user wants to
perform as a multicall. Each contract call defines a target smart contract address (`to`), a
method to call (the `selector`) and the arguments expected by the method (the `calldata`).

The `Call` struct represents a single contract call:

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

trait ISRC6 {

....

fn __execute__(calls: Array<Call>) -> Array<Span<felt252>>;

....

}

```

Here’s what each field signifies:
The execution of each `Call` might result in a value being returned
from the target smart contract. This value could be a felt252, boolean, or a
complex data structure such as a struct or an array. The Starknet protocol
will then serialize this response using `Span<felt252>`. And since a `Span`
represents a snapshot of an Array [2], the `__execute__` method returns an array
of `Span<felt252>` elements, 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's instead an agreement between builders in the Starknet community as a way
to allow web3 apps to perform user authentication. Example, 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, then call the `is_valid_signature` method
to verify that the connected wallet address belongs to the user.


To allow other smart contracts to know that your account contract adheres to
the SNIP-6 public interface, you should implement `supports_interface`
method from the `ISRC5` introspection standard trait, with SNIP-6's Interface
ID passed as a parameter.


- `to`: The address of the target contract.
```rust
struct Call {
to: ContractAddress,
selector: felt252,
calldata: Array<felt252>
}

trait ISRC6 {

// __execute__, __validate__, is_valid_signature methods implemented here.
}


trait ISRC5 {

fn supports_interface(interface_id: felt252) -> bool;

}

```

The `interface_id` parameter is the combined hash of the trait's selectors as
defined by Ethereum's ERC165 [3]. You can calculate for yourself the id
by using the `src5-rs` utility [4], or trust that the id is
**1270010605630597976495846281167968799381097569185364931397797212080166453709.**


So far, we have created the basic structure for the account contract, as defined
by the SNIP-G Interface standard;


```rust
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;
}

```

- `selector`: The function’s selector to be invoked on the target
contract.

- `calldata`: An array that encapsulates the function parameters.
##Additional Interface

The elements described above are foundational to defining an account
contract and sufficient to implement the SNIP-6 standard. Nevertheless,
there are additional components that can be incorporated to bolster the
account contract’s functionality. For instance, the
`__validate_declare__` function might be added if the contract is used
to declare other contracts, providing a mechanism to validate the
contract declaration. Additionally, to counterfactually deploy a smart
contract (i.e., have it pay for its own deployment), one can include the
`__validate_deploy__` function. Detailed implementations of these
functions will be covered in the subsequent chapters.
account contract’s functionality.


For instance, the `__validate_declare__` function is added if the contract
is used to declare other contracts and pay the associated gas fees,
providing a mechanism to validate the contract declaration.
Additionally, to counterfactually deploy a smart contract one can include the
`__validate_deploy__` function.


Counterfactual deployment is a mechanism to deploy an account contract without
relying on another account contract to pay for the related gas fees.
It's important if we don't want to associate a new account contract with the
address that deployed it, but instead have a new beginning.


The deployment process starts by calculating locally the would-be-address of
our account contract without actually deploying it yet. This is possible
to achieve using the Starkli [5] tool. Once we know the account contract's
address, we then send enough ETH to that address to cover the costs of
deploying our account contract.


Once the precalculated account 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, its recommended to expose the `public_key` of the signer as a view
function as part of the public interface. With that in mind, the extended
account contract's interface is as follows;


```rust
/// @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.



```rust
// 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;
}

```



## Summary

Expand All @@ -104,15 +314,33 @@ verifying interface support in contracts.
Furthermore, we defined a single contract call using the `Call` struct,
explaining its fields— `to`, `selector`, and `calldata`. We also
discussed potential enhancements to account contracts using
`__validate_declare__` and `__validate_deploy__` functions. These
additional features, along with detailed function implementations, will
be explored in the chapters ahead.
`__validate_declare__` and `__validate_deploy__` functions..

In the next subchapter, we will implement a simple account contract and
learn how to deploy it on Starknet. This will provide a practical
understanding of how account contracts work and how to interact with
them.



## References:

- \[1\] Starknet Docs: Limitations on the validate function
<https://docs.starknet.io/documentation/architecture_and_concepts/Account_Abstraction/validate_and_execute/#validate_limitations>

- \[2\] Cairo Book: The span data type
<https://book.cairo-lang.org/ch02-06-common-collections.html#span>

- \[3\] ERC-165: Standard Interface Detection
<https://eips.ethereum.org/EIPS/eip-165>

- \[4\] Github: src5-rs
<https://github.com/ericnordelo/src5-rs>

- \[5\] Github: starkli
<https://github.com/xJonathanLEI/starkli>


The Book is a community-driven effort created for the community.

- If you’ve learned something, or not, please take a moment to provide
Expand Down

0 comments on commit 1006a1b

Please sign in to comment.