[SNIP] Deployment interface between DApps and wallets

Hey community,
I’m Leonardo Lerer from StarkWare’s product team. This is a SNIP that specifies a standard interface between DApps and wallets around account deployment.

title: Deployment interface Dapps <-> wallets 
status: Draft
type: Standard
category: interface
created: 2023-10-23

Simple Summary

An interface for DApps ↔ wallet communication in order to enable a DApp to deploy an account on behalf of a user.

Abstract

In order for a third party (e.g. a DApp) to deploy an existing but non-deployed account in a wallet app, the third party needs to receive the relevant data from the wallet app. This proposal describes a standard interface for the communication between wallets and DApps for the purpose of deploying an account through the Universal Deployer Contract, instead of sending a DEPLOY_ACCOUNT transaction.

Motivation

In order to deploy a new account, a new user usually proceeds as follows:

  1. Generates data that will be needed to initialize and operate the account contract
  2. Computes the future address of the account
  3. Funds this address
  4. Sends a DEPLOY_ACCOUNT transaction that deploys the account contract at the pre-computed address

Steps 1, 2 and 4 are usually carried out by using a wallet application. Note that, because of account abstraction, the data generated at step 1 is different according to each wallet contract’s logic. With this proposal, we enable deployment of an existing but non-deployed wallet account through the Universal Deployer Contract. This is needed to enable a DApp to take on itself the burden of paying the DEPLOY_ACCOUNT transaction fees on behalf of the user, and therefore making steps 3 and 4 superfluous.

Specification

A method getDeploymentData associated to each non-deployed wallet account. The method returns the following data:

  • class_hash: The class hash of the contract to deploy (of type felt252)
  • salt: The salt used for the computation of the account address (of type felt252)
  • calldata_len: An integer equal to the length of “calldata” below (of type felt252)
  • calldata: An array of felts (of type Array<felt252>)

The integer calldata_len MUST be equal to the length of calldata. The OpenAPI specification for the result returned by the method getDeploymentData is:

{   
    "result": {
        "name": "result",
        "description": "The data needed in order to deploy a contract through the universal deployer contract",
        "schema": {
            "title": "Deployment data",
            "type": "object",
            "properties": {
                "class_hash": {
                    "title": "Class hash",
                    "$ref": "#/components/schemas/FELT"
                },
                "salt" : {
                    "title": "Salt",
                    "$ref": "#/components/schemas/FELT"
                },
                "calldata_len" : {
                    "title": "Calldata length",
                    "type": "#/components/schemas/FELT"
                },
                "calldata": {
                    "title": "Calldata",
                    "type": "array",
                    "items": "#/components/schemas/FELT"
                }
            }
        }
    }

    "components": {
        "schemas":[
            {
                "FELT": {
                    "type": "string",
                    "title": "Field element",
                    "description": "A field element, represented by at most 63 hex digits",
                    "pattern": "^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,62})$"
                }
            },
        ]
    }
}

These data MUST satisfy the following property. Let us call $class_hash, $salt, $calldata_len, $calldata the output values corresponding to the fields “class_hash”, “salt”, “calldata_len” and “calldata”, respectively. Then, a finalized INVOKE transaction that calls the function deployContract of the universal deployer contract with parameters

  • class_hash: $class_hash
  • salt: $salt
  • unique: 0x0
  • calldata_len: $calldata_len
  • calldata: $calldata

MUST have the same effect as a finalized DEPLOY_ACCOUNT transaction (sent by the pre-computed address) with inputs

  • class_hash: $class_hash
  • constructor_calldata: $calldata
  • contract_address_salt: $salt

i.e., the creation and suitable initialization of an instance of the class $class_hash at the precomputed address.

Copyright

Copyright and related rights waived via MIT.

In line with the StarkNet community’s discussions on standardizing wallet connections with Write API and JSON-RPC integration, the interface might be more suitably defined as a request method, similar to the request function in Ethereum’s API. This could look something like:

interface StarkNetDeploymentInterface {
    request(method: 'wallet_deploymentData', params?: never): Promise<GetDeploymentDataResult>;
}

interface GetDeploymentDataResult {
    address: string; // Represented as 'felt252'
    class_hash: string; // Represented as 'felt252'
    salt: string; // Represented as 'felt252'
    calldata: string[]; // Array of 'felt252', length := calldata_len
}

// possible errors:
Error("No account selected")

Additionally to moving the method into request, I would also suggest to drop the calldata_len field, and rely on the calldata array length.I would also add a field called address containing the expected address where the account should be deployed. This can be used to double check calculated addresses, or to make sure that these are the deployment parameters for the expected account.

Hello Leonardo and the StarkWare team,

Thank you for introducing this SNIP regarding the standard interface between DApps and wallets for account deployment. This initiative seems like a significant step forward in streamlining user experience and reducing the barriers for new users joining the StarkNet ecosystem. The approach of allowing DApps to deploy accounts on behalf of users, thereby handling the intricacies of data generation and transaction fees, is particularly noteworthy. It promises to simplify the onboarding process and could potentially attract more users to the platform.

I have a few questions and observations:

  1. Security Considerations: How does this proposal ensure the security of the account deployment process, especially since the DApp is handling sensitive deployment data?
  2. User Control and Transparency: Will there be mechanisms in place for users to review and approve the data being used for deployment by the DApp?
  3. Compatibility with Different Wallets: How will this interface accommodate the varied logic of different wallet contracts, given the account abstraction in StarkNet?
  4. Implementation and Adoption: What steps are planned to encourage DApps and wallet providers to adopt this new standard?

This proposal’s focus on reducing the complexity of steps 3 and 4 of the account deployment process is commendable. It’s a thoughtful approach to making the StarkNet more accessible, especially for newcomers who might find the current process daunting. The detailed specification, particularly the structure and properties of getDeploymentData, provides a clear framework for developers. This clarity should aid in smooth adoption and implementation.

Looking forward to seeing how this develops and impacts the StarkNet community.

Best regards,
Tudor.

Following feedback from the community, we describe an extension to the above interface that allows more flexibility for the wallets’ different account abstractions.
It might still be premature to adopt this as a standard, so it’s more of a recommendation. Other directions are possible and can be explored, for example adding more features to the UDC.

Specification

The getDeploymentData method will return the following fields:

  • class_hash: The class hash of the contract to deploy (of type felt252)
  • salt: The salt used for the computation of the account address (of type felt252)
  • calldata: An array of felts (of type Array<felt252>)
  • sigdata: A (possibly empty) array of felts to be added in the signature (of type Array<felt252>)
  • version: Cairo version (an integer, of type u8).

Let us call $class_hash, $salt, $calldata, $sigdata the values corresponding to the fields “class_hash”, “salt”, “calldata”, “sigdata” respectively. Then, the above parameters have to satisfy the condition that the following two transactions have the same effect.

  1. An INVOKE transaction that calls the function deployContract of the universal deployer contract with parameters

    • class_hash: $class_hash
    • salt: $salt
    • unique: 0x0
    • calldata_len: len($calldata)
    • calldata: $calldata

    and signature array given by: DApp_sig + $sigdata, where the ‘+’ means concatenation of arrays. Here, DApp_sig is an array of felts computed by the DApp, according to the DApp’s account logic.

  2. A DEPLOY_ACCOUNT transaction (sent by the pre-computed address) with inputs

    • class_hash: $class_hash
    • constructor_calldata: $calldata
    • contract_address_salt: $salt

    and signed according to the account logic.

Remarks

  1. The case where ‘sigdata’ is an empty array corresponds to the initial proposal.
  2. Note that, typically, ‘sigdata’ is different from the user’s signature on the deploy_account transaction.
  3. If ‘sigdata’ is non-empty, the DApp’s account abstraction must be flexible enough for the INVOKE transaction to successfully pass the DApp’s own __validate__ function. The amount of flexibility required by the DApp depends on which variety sigdata it chooses to allow.
  4. Similarly, on the wallet side, the account must have a constructor which is flexible enough to allow a signature of type DApp_sig + sigdata to go through successfully.
  5. The latter two points effectively mean that the DApp and the wallet should be aligned with respect to signature formats. For example, a wallet’s account abstraction that requires a signature length of 2 on the deployment transaction cannot be deployed by a DApp whose signature is always of length 1.

Example

A straightforward example of usage of ‘sigdata’ to guarantee integrity :

  • calldata = [pub_key] (with corresponding private key priv_key),
  • sigdata = [sigdata_1, sigdata_2, sigdata_3]
    • sigdata_3 = sign([sigdata_1, sigdata_2], priv_key).

In the constructor of the wallet account, run verify(sigdata_3, [sig_data_1, sig_data_2], pub_key).

I think this extension to the proposal is a very bad idea and should be avoided. It basically encourages abusing the signature array to pass arbitrary data that can be used in the constructor of an account without being commited to the address.

Starknet supports a CREATE2 like deployment of accounts which is needed to enable a good UX for wallets in an account abstraction environment: wallets can show the pre-computed address to users, asks the users to fund the address, then transparently deploy the account with the first transaction. For this flow to be non-custodial and protect the user against a malicious wallet it is essential that all the parameters of the account (class hash, signers, etc) are commited in the address so that the user knows with 100% certainty the logic and the ownership of the account that will be deployed at the pre-computed address.

Abusing the signature array to pass parameters to the constructor breaks that CREATE2 pattern and will harm the trust users have on Starknet.

Account abstraction gives a lot of flexibility and possibilities, but we should be very careful about the ones that we want to leverage. Today, nothing in the protocol prevents abusing the signature array as described above, but we should work on how to prevent it instead of encouraging it as this proposal does.

One partial solution to this problem that I have already suggested to @bbrandtom and @FeedTheFed would be to limit access to the signature array to the outer frame of execution (the account validating the transaction). In this way it would not be possible for an account abusing the signature to be deployed at the same address using DEPLOY_ACCOUNT and the UDC.

Hi 0xjanek, tudorpintea999 and juniset, thank you for the feedback! Here are some belated replies.

In the above proposals we tried to keep the interface language-agnostic. I agree with you that in practice the interface would be implemented along the lines that you propose. In the second post, we dropped the length fields as you suggested, and in the same vein we didn’t include the address, to keep things down to non-redundant inputs as a starting point.

For the calldata field (i.e. arguments to the constructor), it’s not necessary to provide integrity proofs because those arguments determine the final address. If a malicious actor tries to mess with those parameters, they will end up deploying an account at some other address, and the user’s account will still show ‘not deployed’. In the latest proposal, the sigdata field would need to have an integrity proof in order to provide security (even though it’s not enforced in the interface), because those parameters don’t affect the final address. The example at the end is one way to do that.

The interface only specifies the bare minimum for communication and there is no mechanism for users to review and approve the data. If the contract is implemented correctly, the mechanism is secure.

That’s a good point. It’s hard to design an interface that accomodates all possible account abstractions (both proposed interfaces fail this test, although they do give a fair amount of flexibility), and I’m not sure it’s reasonable to try to do it in this way. This is very related to the remarks by juniset, that are addressed later in this post.

At the moment, this is only a proposal for an interface. As I address below, it shouldn’t be regarded as a standard.

In the latest proposal, we suggested that it might be premature to adopt it as a standard. It should be stated more clearly: this is not a standard, it is a proposed interface that dapps and wallets can choose to use, in the current state of the protocol. It is also not intended as encouragement to use the signature field as metadata. As you point out, this is a protocol matter and I agree with you that long-term solutions should be explored.

@0xjanek , how about another error -

Error("Account already deployed")

Hey @avimak

I think there is no reason to check if the account was already deployed.
Imo this method should just return the deploymentData even if the account is already deployed.

  1. the wallet already knows whether the account is deployed or not, and the response is constructed over there.
  2. what’s the use-case for returning deployment-data for already-deployed accounts?