Universal Deployer Contract proposal 🪄

Overview

This standard describes a standard Universal Deployer Contract (UDC).

Motivation

Account contracts are very critical components of the StarkNet ecosystem, since a bug in any implementation —let alone a widespread one— could be disastrous. Therefore maximal caution is in order. In this spirit trimming account responsibilities should be considered to simplify implementations, minimizing their bug/attack surface.

To allow accounts to deploy contracts without compromising security, this standard proposes to move that functionality to an external, specialized deployer contract. And since it makes no sense to deploy a new deployer contract for each account, this should be a singleton Universal Deployer Contract (UDC).

Implementation

%lang starknet

from starkware.starknet.common.syscalls import get_caller_address, deploy
from starkware.cairo.common.cairo_builtins import HashBuiltin
from starkware.cairo.common.hash import hash2
from starkware.cairo.common.bool import FALSE

@event
func ContractDeployed(
        contractAddress: felt,
        deployer: felt,
        classHash: felt,
        salt: felt
    ):
end

@external
func deploy_contract{
        syscall_ptr: felt*,
        pedersen_ptr: HashBuiltin*,
        range_check_ptr,
    } (
        class_hash: felt,
        salt: felt,
        constructor_calldata_len: felt,
        constructor_calldata: felt*,
    ) -> (contract_address: felt):

    let (deployer) = get_caller_address()
    let (unique_salt) = hash2{hash_ptr=pedersen_ptr}(deployer, salt)

    let (contract_address) = deploy(
        class_hash=class_hash,
        contract_address_salt=unique_salt,
        constructor_calldata_size=constructor_calldata_len,
        constructor_calldata=constructor_calldata,
        deploy_from_zero=FALSE
    )

    ContractDeployed.emit(
        contractAddress=contract_address,
        deployer=deployer,
        classHash=class_hash,
        salt=salt
    )

    return (contract_address=contract_address)
end

Salt :salt:

A naive implementation of the UDC could simply expose the deploy syscall, but that would allow any adversarial party to abuse the fact that the UDC has its own address –a constant parameter for the syscall– making the target address space accessible to anyone.

To avoid address takeovers (a.k.a. squatting), the UDC hashes the caller contract address together with the salt parameter, reserving a portion of the target address space for each caller address.

Deployment

The UDC must be deployed using deploy_from_zero=TRUE to guarantee having the same deterministic address across all instances of StarkNet networks, facilitating tooling, interoperability and a schelling point where to find the right contract with the standard functionality.

50 Likes

Looking very good!

One addition I would add is that the UDC contract should be deployed using deploy_from_zero=TRUE.
This will result in it having a deterministic address across all instances of StarkNet which is very important for all the tooling (deploy transaction construction, deployed contract address calculation, etc…)

18 Likes

Thanks @martriay!

Could you please elaborate further on why this is needed? OZ-Based account contracts are not deploying contracts themselves but rather call them via call_contract syscall.

15 Likes

Could you please expand why there’s a need for the unique_salt? You mention address takeovers, how does that work?

You also write that UDC’s address is “a constant parameter for the syscall” but deploy doesn’t take such a parameter or use get_contract_address anywhere, so what did you mean by that?

13 Likes

I would add a check to ensure that the deployer (get_caller_address) isn’t 0
Maybe even the salt should be checked?

16 Likes

In order to deploy a contract from an account, you will need to either add that logic to the account or call a contract that knows how to deploy. This standard proposes how to approach the latter.

It’s implicit. Unless deploy_from_zero is 0, the caller contract address affects the deployment address. This explains the following point:

Since the UDC is a singleton contract, the “caller contract address” will be always the same from the syscall’s point of view. The unique salt is then needed to reserve a portion of the possible deployment addresses to yourself (a.k.a. to make the resulting address a function of the actual caller address).

What would that solve?

16 Likes

Regarding the account checking it is just that I see it often, and assumed it was just a good practice.
Regarding the salt part, I think that deploying a contract without a salt (or it being 0) would be bad practice since it could lead to some collision if it is omitted. So I’d enforce the user to specify one at least.

But correct me if I’m wrong, I’m still learning :slightly_smiling_face:

12 Likes

I think 0 is a valid salt value as any other.

10 Likes

I’m not (currently) developing an account contract, but if I would, I don’t think I would be using this. You mention simplifying implementation as the rationale for this standard. IMO it does the opposite.

UDC is just a syscall wrapped in a smart contract - to call deploy_contract, I’d have to have a declared const of its address, a defined @contract_interface and a external call in my account contract which is way more complicated than just calling the deploy syscall.

Besides that, how would this work on an L3? Would the UDC’s address be the same on each new L3 instance? If so, who’s the authority that deploys it?

13 Likes

no, you as the user simply call the UCD as you would call any other contract. you target the UCD with __execute__ when crafting a transaction, even from your wallet. the only thing you need to do as an account developer, is not to implement anything.

14 Likes

yes, you simply deploy it from the zero address so you’ll get always the same address, as long as it’s the same implementation. no authority needed.

17 Likes

I think it’s a great idea to have this sort singleton contract, regardless of whether account developers chose to include a deploy function in there contract.

Seems that using unique_salt will likely miss the point of having deploy_from_zero=TRUE If I want a contract to be deployed at the same address cross all SN instances, I would have to own the same account on all SN instances, which is not guaranteed.

Squatting should be considered, but perhaps some of it’s negativities are mitigated by the constructor_calldata, e.g. I can’t own your account on a different SN instance if you initially created it with your public key

Perhaps an additional version can be deployed without this address reservation, similar to EIP-2470: Singleton Factory

19 Likes

I would err on the side of having this separate contract (rather than requiring account providers implement it). It seems more intuitive and SRP is preserved. For people who come from previous versions or another blockchain tech stack, having a separate deployer for each new project would just be a hassle and unnecessary boilerplate.

Good job on the proposal!

12 Likes

Agree, this is important to make the life of wallets and tooling simpler.

Agree that it is great to have the capability of deploying a contract to the same address on different networks. But since accounts are deployed with a specific transaction I think it is not that complicated to have the same account address on different network.

12 Likes

Why would completely miss the point if it’s already solved in the proposal due to constructor parameters “reserving” your account address as you described?

I think this would work because if I initialize a contract on a given network, even though that tx can be replayed on a new one by a squatter, it’s the same contract code with the same constructor arguments, therefore the squatter shouldn’t be able to control that account.

How would that differ from simply making deployed_from_zero a parameter of the UDC deploy method?

12 Likes

I think it would be great if the UDC is also deployed to the same address across all instances. To minimize tooling complexity etc. Then deploy_from_zero is not as critical :smiley: , but I think keeping it at TRUE is preferred if it is not the case that it is deployed to the same address.

This could depend on if and how the account factory was deployed and other factors, so this might be complicated. One won’t necessarily keep their params for an account deployment, and won’t necessarily have access to the same account class on a different instance.

Perhaps we should add a flag, use_uniqe_salt.
If TRUE, the salt is hashed with the caller’s address as suggested, if FALSE, the salt is taken as-is.

This way we can have the option of both

  • Reserving an address space per account if use_uniqe_salt=TRUE
  • Deploying a contract to a deterministic address across all instances, regardless of who is deploying it

The deployer can choose what is preferred

13 Likes

I agree with this modification to the proposal and I think it makes sense (I know I said otherwise in the past @juniset and @martriay ).

My only comment is that for it to work, we can’t take the salt as-is, as this will mean that users don’t have their own address space (I can always choose a salt which is equal to some unique_salt).
My suggestion, in case the unique_salt flag is set to FALSE, hash the salt with some prefix that will ensure the address spaces are separate.

12 Likes

Mh as long as you deploy your account manually (without factories) you should be able to have the same account across StarkNet instances, because the deployment should be reproducible and cannot be squatted. But it’s true that if the account comes from a third party factory, there’s no guarantees the same factory will be available on other chains.

isn’t the purpose of the suggestion to have a flag for users to share the address space? to deploy contracts independently of their address. It would basically be a proxy for deploy_from_zero, which is “taken” by the UDC.

11 Likes

I like that! It combines the best of both worlds: each user has a separate address space that cannot be squatted, while at the same time you can deploy to an address that does not depend on the account.

14 Likes

Yes this is the idea. My suggestion is how to ensure that this “shared” space is separated from each user’s unique address space.

Concretely, I suggest changing -


    let (deployer) = get_caller_address()
    let (unique_salt) = hash2{hash_ptr=pedersen_ptr}(deployer, salt)

to

    if (use_unique_salt == 1) {
        let (deployer) = get_caller_address();
        let (unique_salt) = hash2{hash_ptr=pedersen_ptr}(deployer, salt);
    } else {
        let (unique_salt) = hash2{hash_ptr=pedersen_ptr}("SOME_CONSTANT", salt);
    }

(and change to boolean once Cairo 1.0 is out :grin:)

Exactly

14 Likes