For the complete documentation index, see llms.txt. This page is also available as Markdown.

02. Deploy an ONCHAINID

Deploying an ONCHAINID creates the on-chain identity of a participant: a smart account that holds its keys, carries its claims, can hold tokens and has its first wallet linked. A participant creates its own identity, or an operator creates it on the participant's behalf: typically the agent that onboards investors for an Issuer, or a Claim Issuer, when it meets a wallet that has no identity. The identity is then created with that wallet as its only manager. It happens once per participant, before any claim can be issued to it and before it can hold any asset.

At a glance

Triggered by

Identity Owner, or a Deployer acting for it

Actors

Identity Owner, Deployer, Platform

Contracts

IdentityFactory, ONCHAINID, ERC734Validator

Preconditions

The identity type is registered in the IdentityFactory; to create for oneself, the type is self-deployable; to create for another account, the caller holds the creation role of that type; the account to link is not bound to any identity and was never revoked

Result

An ONCHAINID exists at a deterministic address, with its type recorded, its keys registered, the modules of its type installed, and its first account linked

Actors and roles

Actor
Role in this flow
What they hold

Identity Owner

Creates its own identity, or receives one created for it. Controls it from then on.

The account that becomes the first linked wallet, and the signers that become keys

Deployer

Creates an identity for another account, when the Identity Owner does not do it itself. For an investor, it is an Issuer's onboarding agent or a Claim Issuer.

The creation role of the identity type, in the AccessManager of the network

Platform

Assembles the keys, submits the transaction and records the address.

The key of the party it acts for

Sequence

Step by step

Step 1. Prepare the inputs

Who: Platform, on behalf of the caller

Call: Off-chain, using read calls

Before the call: The participant has the signer it will control the identity with.

What happens: The Platform assembles three inputs.

identityType is the type of the participant: INDIVIDUAL (2), CORPORATE (3), CLAIM_ISSUER (5) and so on. The Platform reads IdentityFactory.getIdentityTypePolicy(identityType) to learn whether the type is self-deployable and which role creates it for others.

salt is a string, not empty, chosen freely.

keys is the list of keys the identity starts with. Each key has:

Field
Meaning

signerData

The signer: the 20 bytes of an address, or a verifier address followed by a public key for a passkey or another scheme.

keyHash

keccak256 of signerData.

purpose

MANAGEMENT (1), ACTION (2), CLAIM_SIGNER (3), ENCRYPTION (4), CLAIM_ADDER (5) or PROPOSER (6).

keyType

A label for the scheme: ECDSA (1), RSA (2), WebAuthn (3).

clientData

Data a client needs to use the key, for example a WebAuthn credential identifier. Can be empty.

At least one key holds MANAGEMENT. The Platform reads IdentityFactory.getIdentity(account) for the account to link and expects zero.

What changes: Nothing on-chain.

Step 2. Create the identity

Who: Identity Owner, from the account that becomes its first wallet. Or a Deployer holding the creation role of the type.

Call: IdentityFactory.createIdentity(identityType, salt, keys) by the owner. IdentityFactory.createIdentityFor(account, identityType, salt, keys) by a Deployer.

Before the call: Step 1 is done.

What the contract checks:

  1. The type is registered. Otherwise the call reverts with UnknownIdentityType.

  2. For createIdentity: the type is self-deployable. Otherwise IdentityTypeNotSelfDeployable.

  3. For createIdentityFor: account is not zero, and the caller holds the creation role of the type, without an execution delay. Otherwise ZeroAddress, NotAuthorizedForIdentityType or DelayedRoleNotSupported.

  4. For createIdentityFor on a type whose owner can sign: every key belongs to account. Otherwise KeyNotForAccount.

  5. The salt is not empty and there is at least one key. Otherwise EmptyString or EmptyListOfKeys.

  6. No key is of the module type. Otherwise CallerKeyCannotBeModule.

  7. Each key is well-formed: its hash matches its signer data, the signer data has a valid length, the purpose is one of the six. Otherwise InvalidSignerData, InvalidSignerLength or InvalidPurpose.

  8. The identity ends up with at least one MANAGEMENT key that can sign. Otherwise NoManagementKeyInKeys.

  9. The first account is not an identity, was never revoked and is not bound to another identity. Otherwise CannotLinkFactoryIdentity, WalletAlreadyRevoked or WalletBoundToAnotherIdentity.

  10. For createIdentityFor on a type whose owner can sign: account is the only MANAGEMENT key. Otherwise AccountNotSoleManagementKey.

What changes: The factory deploys the identity as a beacon proxy, at an address derived from the type, the salt, the first account and the keys. The identity records its type, installs the modules registered for that type and registers its keys. The factory records the type, makes the identity resolve to itself, and links the first account: the caller for createIdentity, account for createIdentityFor. The call returns the address of the identity.

Events: KeyAdded for each key, IdentityTypeRecorded(identity, identityType), AccountLinked(account, identity, caller) and IdentityDeployed(identity, account, identityType, deployer).

Step 3. Record and verify

Who: Platform

Call: Off-chain, using read calls

Before the call: The transaction of step 2 is confirmed.

What happens: The Platform reads the address from IdentityDeployed. It confirms IdentityFactory.identityTypeOf(identity), IdentityFactory.getIdentity(account), which returns the identity, and Identity.getKeysByPurpose(1), which returns the MANAGEMENT keys.

What changes: Nothing on-chain.

State after the flow

  • The identity exists, and IdentityFactory.isFactoryIdentity(identity) is true.

  • IdentityFactory.identityTypeOf(identity) returns its type, which never changes.

  • IdentityFactory.getIdentity returns the identity for its first account, and for the identity's own address.

  • The identity holds the keys given at creation, and at least one MANAGEMENT key that can sign.

  • The modules of its type are installed.

  • The identity holds no claim. It is not eligible for any asset that requires one.

  • The party that deployed an identity for another account holds no key on it.

Failure modes

Symptom
Cause
Fix

Reverts with UnknownIdentityType

The type is not registered in the factory (check 1)

Use a registered type

Reverts with IdentityTypeNotSelfDeployable

The type cannot be created by its own owner, for example CLAIM_ISSUER (check 2)

Have a party with the creation role use createIdentityFor

Reverts with NotAuthorizedForIdentityType

The caller does not hold the creation role of the type (check 3)

Use an account that holds the role

Reverts with DelayedRoleNotSupported

The caller holds the role with an execution delay (check 3)

Use a holder with no delay on this role

Reverts with KeyNotForAccount or AccountNotSoleManagementKey

A Deployer passed a key that is not the account's, or a second manager (checks 4 and 10)

Pass keys of the account only. The owner adds other keys afterward (22)

Reverts with EmptyString or EmptyListOfKeys

Empty salt, or no key (check 5)

Provide both

Reverts with InvalidSignerData, InvalidSignerLength or InvalidPurpose

A key is malformed (check 7)

Compute keyHash as keccak256(signerData) and use a purpose from 1 to 6

Reverts with NoManagementKeyInKeys

No key holds MANAGEMENT (check 8)

Add a MANAGEMENT key

Reverts with WalletBoundToAnotherIdentity

The first account is already linked to an identity (check 9)

The participant already has an identity. Use it, or use another account

Reverts with WalletAlreadyRevoked

The first account was revoked from an identity (check 9)

Use another account. A revoked wallet is never linked again

Reverts in the deployment itself

An identity already exists for the same type, salt, account and keys

Use that identity, or another salt

Common misunderstandings

Wrong: "The Issuer creates the investor's identity for its token."

Right: An identity belongs to its owner and serves every asset. No Issuer owns it. A party can create one on a participant's behalf, and the factory guarantees that the participant's account is then its only manager. The party that paid for the deployment holds no key.

Wrong: "Having an ONCHAINID makes an investor eligible."

Right: A new identity holds no claim. Eligibility comes from claims signed by Claim Issuers that an asset trusts, for the topics the asset requires.

Wrong: "The identity only stores keys and claims. Tokens stay in the wallet."

Right: An ONCHAINID is a smart account. It holds tokens at its own address and transacts through its keys. The wallet linked at creation is one address of the identity, not the only place where it can hold a position.

Wrong: "The same person can have one identity per chain."

Right: The identity lives on the reference chain and links wallets on any chain. The same inputs also give the same identity address on every EVM chain where the factory sits at the same address.

Integration notes

  • Store the salt with the participant. With the type, the first account and the keys, it determines the address.

  • For an investor without a gas token, a party with the creation role deploys the identity with createIdentityFor. The investor's account is the sole manager from the first block.

  • An onboarding agent or a Claim Issuer that meets a wallet reads IdentityFactory.getIdentityIncludingRevoked(account) first. It creates an identity only when the wallet was never bound to one.

  • With createIdentityFor, pass only keys of the named account. A passkey or a second device is added by the owner afterward.

  • With createIdentity, the owner can register a passkey at creation. The calling account is still linked as the first wallet.

  • Index IdentityDeployed and IdentityTypeRecorded to maintain the list of identities and their types.

  • Read the type from IdentityFactory.identityTypeOf(identity), never from the identity.

Last updated