Skip to main content

Step 4: Register Vault

This is the compliance bridge step. The investor signs once, and the operator uses that signature to bind one or more vault addresses to the investor identity in the DS registry. The signature is a standing permission: it remains valid until the deadline expires or the investor revokes it.

Flow at a glance

The nonce does not increment — the same signature can register more vaults for the same (investor, operator) pair until the deadline or until the investor calls invalidateOperatorPermission(operator).

What the contract does on success

One thing to internalize: the nonce does not increment. The same signature can be used to register more vaults later under the same (investor, operator) pair, until the deadline or until the investor calls invalidateOperatorPermission(operator).

EIP-712 typed data

{
"domain": {
"name": "VaultRegistrar",
"version": "1",
"chainId": 11155111,
"verifyingContract": "0xVaultRegistrar"
},
"primaryType": "RegisterVault",
"types": {
"RegisterVault": [
{ "name": "investor", "type": "address" },
{ "name": "operator", "type": "address" },
{ "name": "token", "type": "address" },
{ "name": "nonce", "type": "uint256" },
{ "name": "deadline", "type": "uint256" }
]
}
}

The on-chain typehash is:

keccak256("RegisterVault(address investor,address operator,address token,uint256 nonce,uint256 deadline)")

See EIP-712 details for the verbatim domain values, the worked digest example, and equivalent ethers/wagmi snippets.

Signing and submitting

import { parseUnits } from "viem";

// 1. Read inputs from chain
const nonce = await publicClient.readContract({
address: VAULT_REGISTRAR,
abi: vaultRegistrarAbi,
functionName: "operatorNonce",
args: [investorAddress, operatorAddress],
});

const token = await publicClient.readContract({
address: VAULT_REGISTRAR,
abi: vaultRegistrarAbi,
functionName: "token",
});

const deadline = BigInt(Math.floor(Date.now() / 1000) + 600); // 10 min

// 2. Investor signs (off-chain, in their wallet)
const signature = await investorWalletClient.signTypedData({
domain: {
name: "VaultRegistrar",
version: "1",
chainId: 11155111,
verifyingContract: VAULT_REGISTRAR,
},
types: {
RegisterVault: [
{ name: "investor", type: "address" },
{ name: "operator", type: "address" },
{ name: "token", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
},
primaryType: "RegisterVault",
message: {
investor: investorAddress,
operator: operatorAddress,
token,
nonce,
deadline,
},
});

// 3. Operator submits the registration
const txHash = await operatorWalletClient.writeContract({
address: VAULT_REGISTRAR,
abi: vaultRegistrarAbi,
functionName: "registerVault",
args: [vaultAddress, investorAddress, deadline, signature],
});

Errors and how to recover

SelectorErrorCauseRecovery
0x0819bdcdSignatureExpired()deadline < block.timestampRe-sign with a fresh deadline
0xac94b822InvalidInvestorSignature()EIP-712 fields, chain ID, or verifyingContract don't match the recovered signerConfirm operator matches msg.sender, token matches registrar.token(), nonce matches operatorNonce(investor, operator), chainId matches the target chain, verifyingContract matches the proxy address
0xfed39497InvestorNotFound(address wallet)Investor wallet not in DS registryDrip from the faucet first (auto-registers) or have the registry admin add the wallet
0x38bfcc16VaultAlreadyRegistered(address vault)Vault is already bound to this investorSkip — registration is idempotent for the same pair; treat as success
0x8df63830VaultBelongsToDifferentInvestor(address vault, string vaultInvestorId)Vault is bound to a different investorUse a different vault address; reusing custody across investors is not allowed
0xe6c4247bInvalidAddress()Vault or investor address is the zero addressPass valid addresses
0x308e16ddNotAnOperator(address account)Caller does not hold OPERATOR_ROLE (also surfaced for admin operations)Have the registrar admin call addOperator(yourCaller)
n/a (OZ)EnforcedPause()Registrar is pausedWait for the admin to call unpause()
n/a (OZ)AccessControlUnauthorizedAccount(account, role)Direct admin function called by non-adminUse the correct admin key

For the full reference table see API Reference: Errors.

Operator checklist

  • You read operatorNonce(investor, operator) immediately before signing (do not cache it across sessions — an invalidateOperatorPermission call by the investor invalidates a stale value).
  • You read token() from the registrar instead of hard-coding the DST address.
  • You set deadline far enough in the future to cover signature collection + tx confirmation, but short enough that a leaked signature has limited blast radius. The frontend default is 10 minutes (vaultRegistrarConfig.defaultDeadlineSeconds = 600).
  • You verified isRegistered(vault, investor) == true after the tx confirms.
  • You captured the tx hash for audit.

Next step

Step 5: Transfer Tokens