From 7373daa47cb0442be59e723cf58784e4527b4e90 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 15 Sep 2023 20:55:21 +0200 Subject: [PATCH] Update account docs (#709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: update format and add api * fix: typo * feat: add counterfactual deployment doc * feat: add API entries * feat: add events * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Andrew Fleming * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Andrew Fleming * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Andrew Fleming * Update docs/modules/ROOT/pages/guides/deployment.adoc Co-authored-by: Andrew Fleming * Update docs/modules/ROOT/pages/guides/deployment.adoc Co-authored-by: Andrew Fleming * feat: update from reviews * feat: apply review updates * feat: update docs * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/guides/deployment.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/guides/deployment.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/guides/deployment.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/guides/deployment.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * feat: apply review updates * fix: account casing * feat: add headers * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * feat: add link * feat: move API * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Andrew Fleming * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Andrew Fleming * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Andrew Fleming * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Andrew Fleming * refactor: update wording * Update docs/antora.yml Co-authored-by: Andrew Fleming * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * feat: apply update reviews * Update docs/modules/ROOT/pages/api/account.adoc Co-authored-by: Martín Triay * refactor: UI * fix: UI * feat: focus on SRC6 * Update docs/modules/ROOT/pages/accounts.adoc Co-authored-by: Martín Triay * feat: apply review updates --------- Co-authored-by: Andrew Fleming Co-authored-by: Martín Triay --- docs/antora.yml | 3 + docs/modules/ROOT/nav.adoc | 5 +- docs/modules/ROOT/pages/accounts.adoc | 655 ++---------------- docs/modules/ROOT/pages/api/account.adoc | 246 +++++++ .../modules/ROOT/pages/guides/deployment.adoc | 42 ++ src/account/account.cairo | 4 +- 6 files changed, 359 insertions(+), 596 deletions(-) create mode 100644 docs/modules/ROOT/pages/api/account.adoc create mode 100644 docs/modules/ROOT/pages/guides/deployment.adoc diff --git a/docs/antora.yml b/docs/antora.yml index ceea1e622..011b0fdf6 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -3,3 +3,6 @@ title: Contracts for Cairo version: 0.7.0 nav: - modules/ROOT/nav.adoc +asciidoc: + attributes: + page-sidebar-collapse-default: true diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 3c84b7d03..d78f8a2bd 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -4,6 +4,9 @@ * xref:proxies.adoc[Proxies and Upgrades] * xref:accounts.adoc[Accounts] +** xref:/guides/deployment.adoc[Counterfactual deployments] +** xref:/api/account.adoc[API Reference] + * xref:access.adoc[Access Control] * Tokens @@ -16,4 +19,4 @@ * xref:udc.adoc[Universal Deployer Contract] * xref:utilities.adoc[Utilities] -* xref:contracts::index.adoc[Contracts for Solidity] \ No newline at end of file +* xref:contracts::index.adoc[Contracts for Solidity] diff --git a/docs/modules/ROOT/pages/accounts.adoc b/docs/modules/ROOT/pages/accounts.adoc index 55cbbc5e4..101f71f9c 100644 --- a/docs/modules/ROOT/pages/accounts.adoc +++ b/docs/modules/ROOT/pages/accounts.adoc @@ -1,632 +1,101 @@ :test-signers: https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/tests/signers.py +:snip-5: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-5.md +:snip-6: https://github.com/ericnordelo/SNIPs/blob/feat/standard-account/SNIPS/snip-6.md +:counterfactual: xref:/guides/deployment.adoc[Counterfactual Deployments] = Accounts -Unlike Ethereum where accounts are directly derived from a private key, there's no native account concept on StarkNet. +Unlike Ethereum where accounts are derived from a private key, all Starknet accounts are contracts. This means there's no Externally Owned Account (EOA) +concept on Starknet. -Instead, signature validation has to be done at the contract level. -To relieve smart contract applications such as ERC20 tokens or exchanges from this responsibility, we make use of Account contracts to deal with transaction authentication. +Instead, the network features native account abstraction and signature validation happens at the contract level. -For a general overview of the account abstraction, see StarkWare's https://medium.com/starkware/starknet-alpha-0-10-0-923007290470[StarkNet Alpha 0.10]. -A more detailed discussion on the topic can be found in https://community.starknet.io/t/starknet-account-abstraction-model-part-1/781[StarkNet Account Abstraction Part 1]. +For a general overview of account abstraction, see +https://docs.starknet.io/documentation/architecture_and_concepts/Accounts/introduction/[Starknet's documentation]. +A more detailed discussion on the topic can be found in +https://community.starknet.io/t/starknet-account-abstraction-model-part-1/781[Starknet Shaman's forum]. -== Table of Contents +TIP: For detailed information on the usage and implementation check the xref:/api/account.adoc[API Reference] section. -* <> -* <> - ** <> -* <> -* <> -** <> - ** <> - ** <> - ** <> -* <> - ** <> - ** <> -* <> -* <> - ** <> - ** <> - ** <> - ** <> - ** <> - ** <> - ** <> - ** <> - ** <> -* <> - ** <> - ** <> -* <> -* <> -* <> -* <> +== Standard Account Interface -== Quickstart +Accounts in Starknet are smart contracts, and so they can be deployed and interacted +with like any other contract, and can be extended to implement any custom logic. However, an account is a special type +of contract that is used to validate and execute transactions. For this reason, it must implement a set of entrypoints +that the protocol uses for this execution flow. The {snip-6}[SNIP-6] proposal defines a standard interface for accounts, +supporting this execution flow and interoperability with DApps in the ecosystem. -The general workflow is: +=== ISRC6 Interface -. Account contract is deployed to StarkNet. -. Signed transactions can now be sent to the Account contract which validates and executes them. - -In Python, this would look as follows: - -[,python] ----- -from starkware.starknet.testing.starknet import Starknet -from utils import get_contract_class -from signers import MockSigner - -signer = MockSigner(123456789987654321) - -starknet = await Starknet.empty() - -# 1. Deploy Account -account = await starknet.deploy( - get_contract_class("Account"), - constructor_calldata=[signer.public_key] -) - -# 2. Send transaction through Account -await signer.send_transaction(account, some_contract_address, 'some_function', [some_parameter]) ----- - -== Account entrypoints - -Account contracts have only three entry points for all user interactions: - -1. <> validates the declaration signature prior to the declaration. -As of Cairo v0.10.0, contract classes should be declared from an Account contract. - -2. <> verifies the transaction signature before executing the transaction with `\\__execute__`. - -3. <> acts as the state-changing entry point for all user interaction with any contract, including managing the account contract itself. -That's why if you want to change the public key controlling the Account, you would send a transaction targeting the very Account contract: - -[,python] ----- -await signer.send_transaction( - account, - account.contract_address, - 'set_public_key', - [NEW_KEY] -) ----- - -Or if you want to update the Account's L1 address on the `AccountRegistry` contract, you would - -[,python] ----- -await signer.send_transaction(account, registry.contract_address, 'set_L1_address', [NEW_ADDRESS]) ----- - -NOTE: You can read more about how messages are structured and hashed in the https://github.com/OpenZeppelin/cairo-contracts/discussions/24[Account message scheme discussion]. -For more information on the design choices and implementation of multicall, you can read the https://github.com/OpenZeppelin/cairo-contracts/discussions/27[How should Account multicall work discussion]. - -The `\\__validate__` and `\\__execute__` methods accept the same arguments; however, `\\__execute__` returns a transaction response: - -[,cairo] ----- -func __validate__( - call_array_len: felt, call_array: AccountCallArray*, calldata_len: felt, calldata: felt*) { -} - -func __execute__( - call_array_len: felt, call_array: AccountCallArray*, calldata_len: felt, calldata: felt* -) -> (response_len: felt, response: felt*) { -} ----- - -Where: - -* `call_array_len` is the number of calls. -* `call_array` is an array representing each `Call`. -* `calldata_len` is the number of calldata parameters. -* `calldata` is an array representing the function parameters. - -NOTE: The scheme of building multicall transactions within the `\\__execute__` method will change once StarkNet allows for pointers in struct arrays. -In which case, multiple transactions can be passed to (as opposed to built within) `\\__execute__`. - -There's a fourth canonical entrypoint for accounts, the `\\__validate_deploy__` method. It is **only callable by the protocol** during the execution of a `DeployAccount` type of transaction, but not by any other contract. This entrypoint is for counterfactual deployments. - -=== Counterfactual Deployments - -Counterfactual means something that hasn't happened. - -A deployment is said to be counterfactual when the deployed contract pays for it. It's called like this because we need to send the funds to the address before deployment. A deployment that hasn't happened. - -The steps are the following: - -1. Precompute the `address` given a `class_hash`, `salt`, and constructor `calldata`. -2. Send funds to `address`. -3. Send a `DeployAccount` type transaction. -4. The protocol will then validate with `\\__validate_deploy__`. -5. If successful, the protocol deploys the contract and the contract itself pays for the transaction. - -Since `address` will ultimately depend on the `class_hash` and `calldata`, it's safe for the protocol to validate the signature and spend the funds on that address. - -== Standard interface - -The https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/account/IAccount.cairo[`IAccount.cairo`] contract interface contains the standard account interface proposed in https://github.com/OpenZeppelin/cairo-contracts/discussions/41[#41] and adopted by OpenZeppelin and Argent. -It implements https://eips.ethereum.org/EIPS/eip-1271[EIP-1271] and it is agnostic of signature validation. Further, nonce management is handled on the protocol level. - -NOTE: `\\__validate_deploy__` is not part of the interface since it's only callable by the protocol. Also contracts don't need to implement it to be considered accounts. - -[,cairo] +[,javascript] ---- +/// Represents a call to a target contract function. struct Call { - to: felt, - selector: felt, - calldata_len: felt, - calldata: felt*, -} - -// Tmp struct introduced while we wait for Cairo to support passing `[Call]` to __execute__ -struct CallArray { - to: felt, - selector: felt, - data_offset: felt, - data_len: felt, + to: ContractAddress, + selector: felt252, + calldata: Array } +/// Standard Account Interface +trait ISRC6 { + /// Executes a transaction through the account. + fn __execute__(calls: Array) -> Array>; -@contract_interface -namespace IAccount { - func supportsInterface(interfaceId: felt) -> (success: felt) { - } - - func isValidSignature(hash: felt, signature_len: felt, signature: felt*) -> (isValid: felt) { - } - - func __validate__( - call_array_len: felt, call_array: AccountCallArray*, calldata_len: felt, calldata: felt* - ) { - } - - func __validate_declare__(class_hash: felt) { - } - - func __execute__( - call_array_len: felt, call_array: AccountCallArray*, calldata_len: felt, calldata: felt* - ) -> (response_len: felt, response: felt*) { - } -} - ----- - -== Keys, signatures and signers - -While the interface is agnostic of signature validation schemes, this implementation assumes there's a public-private key pair controlling the Account. -That's why the `constructor` function expects a `public_key` parameter to set it. -Since there's also a `setPublicKey()` method, accounts can be effectively transferred. + /// Asserts whether the transaction is valid to be executed. + fn __validate__(calls: Array) -> felt252; -=== Signature validation - -Signature validation occurs separately from execution as of Cairo v0.10. -Upon receiving transactions, an account contract first calls `\\__validate__`. -An account will only execute a transaction if, and only if, the signature proves valid. -This decoupling allows for a protocol-level distinction between invalid and reverted transactions. -See <>. - -=== Signer - -The signer is responsible for creating a transaction signature with the user's private key for a given transaction. -This implementation utilizes https://github.com/OpenZeppelin/nile/blob/main/src/nile/signer.py[Nile's Signer] class to create transaction signatures through the `Signer` method `sign_transaction`. - -`sign_transaction` expects the following parameters per transaction: - -* `sender` the contract address invoking the tx. -* `calls` a list containing a sublist of each call to be sent. -Each sublist must consist of: - .. `to` the address of the target contract of the message. - .. `selector` the function to be called on the target contract. - .. `calldata` the parameters for the given `selector`. -* `nonce` an unique identifier of this message to prevent transaction replays. -* `max_fee` the maximum fee a user will pay. - -Which returns: - -* `calldata` a list of arguments for each call. -* `sig_r` the transaction signature. -* `sig_s` the transaction signature. - -While the `Signer` class performs much of the work for a transaction to be sent, it neither manages nonces nor invokes the actual transaction on the Account contract. -To simplify Account management, most of this is abstracted away with `MockSigner`. - -=== MockSigner utility - -The `MockSigner` class in {test-signers}[signers.py] is used to perform transactions on a given Account, crafting the transaction and managing nonces. - -NOTE: StarkNet's testing framework does not currently support transaction invocations from account contracts. `MockSigner` therefore utilizes StarkNet's API gateway to manually execute the `InvokeFunction` for testing. - -A `MockSigner` instance exposes the following methods: - -* `send_transaction(account, to, selector_name, calldata, nonce=None, max_fee=0)` returns a link:https://docs.python.org/3/library/asyncio-future.html[future] of a signed transaction, ready to be sent. -* `send_transactions(account, calls, nonce=None, max_fee=0)` returns a future of batched signed transactions, ready to be sent. -* `declare_class(account, contract_name, nonce=None, max_fee=0)` returns a future of a declaration transaction. -* `deploy_account(state, calldata, salt=0, nonce=0, max_fee=0)`: returns a future of a counterfactual deployment. - -To use `MockSigner`, pass a private key when instantiating the class: - -[,python] ----- -from utils import MockSigner - -PRIVATE_KEY = 123456789987654321 -signer = MockSigner(PRIVATE_KEY) ----- - -Then send single transactions with the `send_transaction` method. - -[,python] ----- -await signer.send_transaction(account, contract_address, 'method_name', []) ----- - -If utilizing multicall, send multiple transactions with the `send_transactions` method. - -[,python] ----- -await signer.send_transactions( - account, - [ - (contract_address, 'method_name', [param1, param2]), - (contract_address, 'another_method', []) - ] -) ----- - -Use `declare_class` to declare a contract: - -[,python] ----- -await signer.declare_class(account, "MyToken") ----- - -And `deploy_account` to <> deploy an account: - -[,python] ----- -await signer.deploy_account(state, [signer.public_key]) ----- - - -=== MockEthSigner utility - -The `MockEthSigner` class in {test-signers}[signers.py] is used to perform transactions on a given Account with a secp256k1 curve key pair, crafting the transaction and managing nonces. -It differs from the `MockSigner` implementation by: - -* Not using the public key but its derived address instead (the last 20 bytes of the keccak256 hash of the public key and adding `0x` to the beginning). -* Signing the message with a secp256k1 curve address. - -== `Call` and `AccountCallArray` format - -The idea is for all user intent to be encoded into a `Call` representing a smart contract call. -Users can also pack multiple messages into a single transaction (creating a multicall transaction). -Cairo currently does not support arrays of structs with pointers which means the `\\__execute__` function cannot properly iterate through multiple ``Call``s. -Instead, this implementation utilizes a workaround with the `AccountCallArray` struct. -See <>. - -=== `Call` - -A single `Call` is structured as follows: - -[,cairo] ----- -struct Call { - to: felt - selector: felt - calldata_len: felt - calldata: felt* + /// Asserts whether a given signature for a given hash is valid. + fn is_valid_signature(hash: felt252, signature: Array) -> felt252; } ---- -Where: - -* `to` is the address of the target contract of the message. -* `selector` is the selector of the function to be called on the target contract. -* `calldata_len` is the number of calldata parameters. -* `calldata` is an array representing the function parameters. +{snip-6}[SNIP-6] adds the `is_valid_signature` method. This method is not used by the protocol, but it's useful for +DApps to verify the validity of signatures, supporting features like Sign In with Starknet. -=== `AccountCallArray` +SNIP-6 also defines that compliant accounts must implement the SRC5 interface following {snip-5}[SNIP-5], as +a mechanism for detecting whether a contract is an account or not through introspection. -`AccountCallArray` is structured as: +=== ISRC5 Interface -[,cairo] +[,javascript] ---- -struct AccountCallArray { - to: felt - selector: felt - data_offset: felt - data_len: felt +/// Standard Interface Detection +trait ISRC5 { + /// Queries if a contract implements a given interface. + fn supports_interface(interface_id: felt252) -> bool; } ---- -Where: - -* `to` is the address of the target contract of the message. -* `selector` is the selector of the function to be called on the target contract. -* `data_offset` is the starting position of the calldata array that holds the ``Call``'s calldata. -* `data_len` is the number of calldata elements in the `Call`. - -== Multicall transactions - -A multicall transaction packs the `to`, `selector`, `calldata_offset`, and `calldata_len` of each call into the `AccountCallArray` struct and keeps the cumulative calldata for every call in a separate array. -The `\\__execute__` function rebuilds each message by combining the `AccountCallArray` with its calldata (demarcated by the offset and calldata length specified for that particular call). -The rebuilding logic is set in the internal `_from_call_array_to_call`. - -This is the basic flow: - -First, the user sends the messages for the transaction through a Signer instantiation which looks like this: - -[,python] ----- -await signer.send_transaction( - account, [ - (contract_address, 'contract_method', [arg_1]), - (contract_address, 'another_method', [arg_1, arg_2]) - ] -) ----- - -Then the `from_call_to_call_array` method in link:https://github.com/OpenZeppelin/nile/blob/main/src/nile/signer.py[Nile's signer] converts each call into the `AccountCallArray` format and cumulatively stores the calldata of every call into a single array. -Next, both arrays (as well as the `sender`, `nonce`, and `max_fee`) are used to create the transaction hash. -The Signer then invokes `\__execute__` with the signature and passes `AccountCallArray`, calldata, and nonce as arguments. - -Finally, the `\\__execute__` method takes the `AccountCallArray` and calldata and builds an array of ``Call``s (MultiCall). - -NOTE: Every transaction utilizes `AccountCallArray`. -A single `Call` is treated as a bundle with one message. - -== API Specification - -This in a nutshell is the Account contract public API: - -[,cairo] ----- -namespace Account { - func constructor(publicKey: felt) { - } - - func getPublicKey() -> (publicKey: felt) { - } - - func supportsInterface(interfaceId: felt) -> (success: felt) { - } - - func setPublicKey(newPublicKey: felt) { - } - - func isValidSignature(hash: felt, signature_len: felt, signature: felt*) -> (isValid: felt) { - } - - func __validate__( - call_array_len: felt, call_array: AccountCallArray*, calldata_len: felt, calldata: felt* - ) -> (response_len: felt, response: felt*) { - } - - func __validate_declare__( - call_array_len: felt, call_array: AccountCallArray*, calldata_len: felt, calldata: felt* - ) -> (response_len: felt, response: felt*) { - } - - func __execute__( - call_array_len: felt, call_array: AccountCallArray*, calldata_len: felt, calldata: felt* - ) -> (response_len: felt, response: felt*) { -} ----- - -=== `constructor` - -Initializes and sets the public key for the Account contract. - -Parameters: - -[,cairo] ----- -publicKey: felt ----- - -Returns: None. - -=== `getPublicKey` - -Returns the public key associated with the Account. - -Parameters: None. - -Returns: - -[,cairo] ----- -publicKey: felt ----- - -=== `supportsInterface` - -Returns `TRUE` if this contract implements the interface defined by `interfaceId`. -Account contracts now implement ERC165 through static support (see <>). - -Parameters: - -[,cairo] ----- -interfaceId: felt ----- - -Returns: - -[,cairo] ----- -success: felt ----- - -=== `setPublicKey` - -Sets the public key that will control this Account. -It can be used to rotate keys for security, change them in case of compromised keys or even transferring ownership of the account. - -Parameters: - -[,cairo] ----- -newPublicKey: felt ----- - -Returns: None. - -=== `isValidSignature` - -This function is inspired by https://eips.ethereum.org/EIPS/eip-1271[EIP-1271] and returns `TRUE` if a given signature is valid, otherwise it reverts. -In the future it will return `FALSE` if a given signature is invalid (for more info please check https://github.com/OpenZeppelin/cairo-contracts/issues/327[this issue]). - -Parameters: - -[,cairo] ----- -hash: felt -signature_len: felt -signature: felt* ----- - -Returns: - -[,cairo] ----- -isValid: felt ----- - -NOTE: It may return `FALSE` in the future if a given signature is invalid (follow the discussion on https://github.com/OpenZeppelin/cairo-contracts/issues/327[this issue]). - -=== `\\__validate__` - -Validates the transaction signature and is called prior to `\\__execute__`. - -Parameters: - -[,cairo] ----- -call_array_len: felt -call_array: AccountCallArray* -calldata_len: felt -calldata: felt* ----- - -Returns: None. - -=== `\\__validate_declare__` - -Validates the signature for declaration transactions. - -Parameters: - -[,cairo] ----- -class_hash: felt ----- - -Returns: None. - -=== `\\__validate_deploy__` - -Validates the signature for counterfactual deployment transactions. - -It takes the `class_hash` of the account being deployed along with the `salt` and `calldata`, the latter being expanded. For example if the account is deployed with calldata `[arg_1, ..., arg_n]`: - -Parameters: - -[,cairo] ----- -class_hash: felt -salt: felt -arg_1: felt -... -arg_n: felt ----- - -Returns: None. - -=== `\\__execute__` - -This is the only external entrypoint to interact with the Account contract. -It: - -. Calls the target contract with the intended function selector and calldata parameters. -. Forwards the contract call response data as return value. - -Parameters: - -[,cairo] ----- -call_array_len: felt -call_array: AccountCallArray* -calldata_len: felt -calldata: felt* ----- - -NOTE: The current signature scheme expects a 2-element array like `[sig_r, sig_s]`. - -Returns: - -[,cairo] ----- -response_len: felt -response: felt* ----- - -== Presets - -The following contract presets are ready to deploy and can be used as-is for quick prototyping and testing. -Each preset differs on the signature type being used by the Account. - -=== Account - -The https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/account/presets/Account.cairo[`Account`] preset uses StarkNet keys to validate transactions. - -=== Eth Account - -The https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/account/presets/EthAccount.cairo[`EthAccount`] preset supports Ethereum addresses, validating transactions with secp256k1 keys. - -== Account introspection with ERC165 - -Certain contracts like ERC721 or ERC1155 require a means to differentiate between account contracts and non-account contracts. -For a contract to declare itself as an account, it should implement https://eips.ethereum.org/EIPS/eip-165[ERC165] as proposed in https://github.com/OpenZeppelin/cairo-contracts/discussions/100[#100]. - -To be in compliance with ERC165 specifications, we calculate the account contract ID as the XOR of ``IAccount``'s equivalent EVM selectors (not StarkNet selectors). -This magic value has been tracking the changes of the still evolving Account interface standard, and **its current value is `0xa66bd575`**. - -Our ERC165 integration on StarkNet is inspired by OpenZeppelin's Solidity implementation of https://docs.openzeppelin.com/contracts/4.x/api/utils#ERC165Storage[ERC165Storage] which stores the interfaces that the implementing contract supports. -In the case of account contracts, querying `supportsInterface` of an account's address with the `IAccount` magic value should return `TRUE`. - -NOTE: For Account contracts, ERC165 support is static and does not require Account contracts to register. - -== Extending the Account contract +{snip-6}[SNIP-6] compliant accounts must return `true` when queried for the ISRC6 interface Id. -Account contracts can be extended by following the xref:extensibility.adoc#the_pattern[extensibility pattern]. +Even though these interfaces are not enforced by the protocol, it's recommended to implement them for enabling +interoperability with the ecosystem. -To implement custom account contracts, it's required by the StarkNet compiler that they include the three entrypoint functions `\\__validate__`, `\\__validate_declare__`, and `\\__execute__`. -`\\__validate__` and `\\__validate_declare__` should include the same signature validation method; whereas, `\\__execute__` should only handle the actual transaction. Incorporating a new validation scheme necessitates only that it's invoked by both `\\__validate__` and `\\__validate_declare__`. +== Protocol-level methods -This is why the Account library comes with different flavors of signature validation methods like `is_valid_eth_signature` and the vanilla `is_valid_signature`. +In this section we will describe the methods that the protocol uses for abstracting the accounts. The first two +are required for enabling accounts to be used for executing transactions. The rest are optional: -Account contract developers are encouraged to implement the https://github.com/OpenZeppelin/cairo-contracts/discussions/41[standard Account interface] and incorporate the custom logic thereafter. +1. `\\__validate__` verifies the validity of the transaction to be executed. This is usually used to validate signatures, +but the entrypoint implementation can be customized to feature any validation mechanism https://docs.starknet.io/documentation/architecture_and_concepts/Accounts/validate_and_execute/#validate_limitations[with some limitations]. -IMPORTANT: Due to current inconsistencies between the testing framework and the actual StarkNet network, extreme caution should be used when integrating new Account contracts. -Instances have occurred where account functionality tests pass and transactions execute correctly on the local node; yet, they fail on public networks. -For this reason, it's highly encouraged that new account contracts are also deployed and tested on the public testnet. -See https://github.com/OpenZeppelin/cairo-contracts/issues/386[issue #386] for more information. +2. `\\__execute__` executes the transaction if the validation is successful. -Some other validation schemes to look out for in the future: +3. `\\__validate_declare__` optional entrypoint similar to `\\__validate__` but for transactions +meant to declare other contracts. -* Multisig. -* Guardian logic like in https://github.com/argentlabs/argent-contracts-starknet/blob/de5654555309fa76160ba3d7393d32d2b12e7349/contracts/ArgentAccount.cairo[Argent's account]. +4. `\\__validate_deploy__` optional entrypoint similar to `\\__validate__` but meant for {counterfactual}. -== L1 escape hatch mechanism +NOTE: Although these entrypoints are available to the protocol for its regular transaction flow, they can also be called like any other method. -[unknown, to be defined] +== Deploying an account -== Paying for gas +In Starknet there are two ways of deploying smart contracts: using the `deploy_syscall` and doing +counterfactual deployments. +The former can be easily done with the xref:udc.adoc[Universal Deployer Contract (UDC)], a contract that +wraps and exposes the `deploy_syscall` to provide arbitrary deployments through regular contract calls. +But if you don't have an account to invoke it, you will probably want to use the latter. -[unknown, to be defined] +To do counterfactual deployments, you need to implement another protocol-level entrypoint named +`\\__validate_deploy__`. You can check the {counterfactual} guide to learn how. diff --git a/docs/modules/ROOT/pages/api/account.adoc b/docs/modules/ROOT/pages/api/account.adoc new file mode 100644 index 000000000..52ed25191 --- /dev/null +++ b/docs/modules/ROOT/pages/api/account.adoc @@ -0,0 +1,246 @@ +:github-icon: pass:[] +:snip6: https://github.com/ericnordelo/SNIPs/blob/feat/standard-account/SNIPS/snip-6.md[SNIP-6] + += Account + +Reference of interfaces, presets, and utilities related to account contracts. + +== Core + +[.contract] +[[ISRC6]] +=== `++ISRC6++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/cairo-2/src/account/interface.cairo#L12[{github-icon},role=heading-link] + +```javascript +use openzeppelin::account::interface::ISRC6; +``` + +Interface of the SRC6 Standard Account as defined in the {snip6}. + +[.contract-index] +.Functions +-- +* xref:#ISRC6-\\__execute__[`++__execute__(calls)++`] +* xref:#ISRC6-\\__validate__[`++__validate__(calls)++`] +* xref:#ISRC6-is_valid_signature[`++is_valid_signature(hash, signature)++`] +-- + +[#ISRC6-Functions] +==== Functions + +[.contract-item] +[[ISRC6-__execute__]] +==== `[.contract-item-name]#++__execute__++#++(calls: Array) → Array>++` [.item-kind]#external# + +Executes the list of calls as a transaction after validation. + +Returns an array with each call's output. + +NOTE: The `Call` struct is defined in https://github.com/starkware-libs/cairo/blob/main/corelib/src/starknet/account.cairo#L3[corelib]. + +[.contract-item] +[[ISRC6-__validate__]] +==== `[.contract-item-name]#++__validate__++#++(calls: Array) → felt252++` [.item-kind]#external# + +Validates a transaction before execution. + +Returns the short string `'VALID'` if valid, otherwise it reverts. + +[.contract-item] +[[ISRC6-is_valid_signature]] +==== `[.contract-item-name]#++is_valid_signature++#++(hash: felt252, signature: Array) → felt252++` [.item-kind]#external# + +Validates whether a signature is valid or not for the given message hash. + +Returns the short string `'VALID'` if valid, otherwise it reverts. + +[.contract] +[[Account]] +=== `++Account++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/cairo-2/src/account/account.cairo#L27[{github-icon},role=heading-link] + +:OwnerAdded: xref:Account-OwnerAdded[OwnerAdded] +:OwnerRemoved: xref:Account-OwnerRemoved[OwnerRemoved] + +```javascript +use openzeppelin::account::Account; +``` +Account contract implementation extending xref:ISRC6[`ISRC6`]. + +[.contract-index] +.Utilities +-- +* xref:#Account-assert_only_self[`++InternalImpl::assert_only_self(self)++`] +-- + +[.contract-index] +.External Functions +-- +* xref:#Account-\\__validate_deploy__[`++__validate_deploy__(self, hash, signature)++`] + +[.contract-subindex-inherited] +.SRC6Impl + +* xref:#Account-\\__execute__[`++__execute__(self, calls)++`] +* xref:#Account-\\__validate__[`++__validate__(self, calls)++`] +* xref:#Account-is_valid_signature[`++is_valid_signature(self, hash, signature)++`] + +[.contract-subindex-inherited] +.SRC5Impl + +* xref:#Account-supports_interface[`++supports_interface(self, interface_id)++`] + +[.contract-subindex-inherited] +.DeclarerImpl + +* xref:#Account-\\__validate_declare__[`++__validate_declare__(self, class_hash)++`] + +[.contract-subindex-inherited] +.PublicKeyImpl + +* xref:#Account-set_public_key[`++set_public_key(self, new_public_key)++`] +* xref:#Account-get_public_key[`++get_public_key(self)++`] +-- + +[.contract-index] +.Internal Functions +-- +* xref:#Account-constructor[`++constructor(self, _public_key)++`] + +[.contract-subindex-inherited] +.InternalImpl + +* xref:#Account-initializer[`++initializer(self, _public_key)++`] +* xref:#Account-validate_transaction[`++validate_transaction(self)++`] +* xref:#Account-_set_public_key[`++_set_public_key(self, new_public_key)++`] +* xref:#Account-_is_valid_signature[`++_is_valid_signature(self, hash, signature)++`] +-- + +[.contract-index] +.Events +-- +* xref:#Account-OwnerAdded[`++OwnerAdded(new_owner_guid)++`] +* xref:#Account-OwnerRemoved[`++OwnerRemoved(removed_owner_guid)++`] +-- + +[#Account-Utilities] +==== Utilities + +[.contract-item] +[[Account-assert_only_self]] +==== `[.contract-item-name]#++assert_only_self++#++(self: @ContractState)++` [.item-kind]#internal# + +Validates that the caller is the account itself. Otherwise it reverts. + +[#Account-Functions] +==== Functions + +[.contract-item] +[[Account-constructor]] +==== `[.contract-item-name]#++constructor++#++(ref self: ContractState, _public_key: felt252)++` [.item-kind]#constructor# + +Initializes the account with the given public key, and registers the ISRC6 interface ID. + +Emits an {OwnerAdded} event. + +[.contract-item] +[[Account-__validate_deploy__]] +==== `[.contract-item-name]#++__validate_deploy__++#++(self: @ContractState, class_hash: felt252, contract_address_salt: felt252, _public_key: felt252) → felt252++` [.item-kind]#external# + +Validates a https://docs.starknet.io/documentation/architecture_and_concepts/Network_Architecture/Blocks/transactions/#deploy_account_transaction[`DeployAccount` transaction]. +See xref:/guides/deployment.adoc[Counterfactual Deployments]. + +Returns the short string `'VALID'` if valid, otherwise it reverts. + +[.contract-item] +[[Account-__execute__]] +==== `[.contract-item-name]#++__execute__++#++(ref self: ContractState, calls: Array) → Array>++` [.item-kind]#external# + +See xref:ISRC6-\\__execute__[ISRC6::\\__execute__]. + +[.contract-item] +[[Account-__validate__]] +==== `[.contract-item-name]#++__validate__++#++(self: @ContractState, calls: Array) → felt252++` [.item-kind]#external# + +See xref:ISRC6-\\__validate__[ISRC6::\\__validate__]. + +[.contract-item] +[[Account-is_valid_signature]] +==== `[.contract-item-name]#++is_valid_signature++#++(self: @ContractState, hash: felt252, signature: Array) → felt252++` [.item-kind]#external# + +See xref:ISRC6-is_valid_signature[ISRC6::is_valid_signature]. + +[.contract-item] +[[Account-supports_interface]] +==== `[.contract-item-name]#++supports_interface++#++(self: @ContractState, interface_id: felt252) → bool++` [.item-kind]#external# + +Returns whether a contract implements a given interface or not. + +[.contract-item] +[[Account-__validate_declare__]] +==== `[.contract-item-name]#++__validate_declare__++#++(self: @ContractState, class_hash: felt252) → felt252++` [.item-kind]#external# + +Validates a https://docs.starknet.io/documentation/architecture_and_concepts/Network_Architecture/Blocks/transactions/#declare-transaction[`Declare` transaction]. + +Returns the short string `'VALID'` if valid, otherwise it reverts. + +[.contract-item] +[[Account-set_public_key]] +==== `[.contract-item-name]#++set_public_key++#++(ref self: ContractState, new_public_key: felt252)++` [.item-kind]#external# + +Sets a new public key for the account. Only accesible by the account calling itself through `\\__execute__`. + +Emits both an {OwnerRemoved} and an {OwnerAdded} event. + +[.contract-item] +[[Account-get_public_key]] +==== `[.contract-item-name]#++get_public_key++#++(self: @ContractState)++ → felt252` [.item-kind]#external# + +Returns the current public key of the account. + +[.contract-item] +[[Account-initializer]] +==== `[.contract-item-name]#++initializer++#++(ref self: ContractState, _public_key: felt252)++` [.item-kind]#internal# + +Initializes the account with the given public key, and registers the ISRC6 interface ID. + +Emits an {OwnerAdded} event. + +[.contract-item] +[[Account-validate_transaction]] +==== `[.contract-item-name]#++validate_transaction++#++(self: @ContractState)++ → felt252` [.item-kind]#internal# + +Validates a transaction signature from the +https://github.com/starkware-libs/cairo/blob/main/corelib/src/starknet/info.cairo#L61[global context]. + +Returns the short string `'VALID'` if valid, otherwise it reverts. + +[.contract-item] +[[Account-_set_public_key]] +==== `[.contract-item-name]#++_set_public_key++#++(ref self: ContractState, new_public_key: felt252)++` [.item-kind]#internal# + +Set the public key without validating the caller. + +Emits an {OwnerAdded} event. + +CAUTION: The usage of this method outside the `set_public_key` function is discouraged. + +[.contract-item] +[[Account-_is_valid_signature]] +==== `[.contract-item-name]#++_is_valid_signature++#++(self: @ContractState, hash: felt252, signature: Span)++ → bool` [.item-kind]#internal# + +Validates the provided `signature` for the `hash`, using the account current public key. + +[#Account-Events] +==== Events + +[.contract-item] +[[Account-OwnerAdded]] +==== `[.contract-item-name]#++OwnerAdded++#++(new_owner_guid: felt252)++` [.item-kind]#event# + +Emitted when a `public_key` is added. + +[.contract-item] +[[Account-OwnerRemoved]] +==== `[.contract-item-name]#++OwnerRemoved++#++(removed_owner_guid: felt252)++` [.item-kind]#event# + +Emitted when a `public_key` is removed. diff --git a/docs/modules/ROOT/pages/guides/deployment.adoc b/docs/modules/ROOT/pages/guides/deployment.adoc new file mode 100644 index 000000000..92f6a1bd2 --- /dev/null +++ b/docs/modules/ROOT/pages/guides/deployment.adoc @@ -0,0 +1,42 @@ +:foundry: https://foundry-rs.github.io/starknet-foundry/starknet/account.html[Starknet Foundry] +:starkli: https://book.starkli.rs/accounts#account-deployment[Starkli] + += Counterfactual deployments + +A counterfactual contract is a contract we can interact with even before actually deploying it on-chain. +For example, we can send funds or assign privileges to a contract that doesn't yet exist. +Why? Because deployments in Starknet are deterministic, allowing us to predict the address where our contract will be deployed. +We can leverage this property to make a contract pay for its own deployment by simply sending funds in advance. We call this a counterfactual deployment. + +This process can be described with the following steps: + +TIP: For testing this flow you can check the {foundry} or the {starkli} guides for deploying accounts. + +1. Deterministically precompute the `contract_address` given a `class_hash`, `salt`, and constructor `calldata`. +Note that the `class_hash` must be previously declared for the deployment to succeed. + +2. Send funds to the `contract_address`. Usually you will estimate the fee of the transaction first. Existing +tools usually do this for you. + +3. Send a `DeployAccount` type transaction to the network. + +4. The protocol will then validate the transaction with the `\\__validate_deploy__` entrypoint of the contract to be deployed. + +5. If the validation succeeds, the protocol will charge the fee and then register the contract as deployed. + +NOTE: Although this method is very popular to deploy accounts, this works for any kind of contract. + +== Deployment validation + +To be counterfactually deployed, the deploying contract must implement the `\\__validate_deploy__` entrypoint, +called by the protocol when a `DeployAccount` transaction is sent to the network. + +[,javascript] +---- +trait IDeployable { + /// Must return 'VALID' when the validation is successful. + fn __validate_deploy__( + class_hash: felt252, contract_address_salt: felt252, _public_key: felt252 + ) -> felt252; +} +---- diff --git a/src/account/account.cairo b/src/account/account.cairo index ce2eb8c2f..13b7e8d0e 100644 --- a/src/account/account.cairo +++ b/src/account/account.cairo @@ -228,7 +228,7 @@ mod Account { assert(self == caller, Errors::UNAUTHORIZED); } - #[internal] + #[private] fn _execute_calls(mut calls: Array) -> Array> { let mut res = ArrayTrait::new(); loop { @@ -245,7 +245,7 @@ mod Account { res } - #[internal] + #[private] fn _execute_single_call(call: Call) -> Span { let Call{to, selector, calldata } = call; starknet::call_contract_syscall(to, selector, calldata.span()).unwrap()