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
| Selector | Error | Cause | Recovery |
|---|---|---|---|
0x0819bdcd | SignatureExpired() | deadline < block.timestamp | Re-sign with a fresh deadline |
0xac94b822 | InvalidInvestorSignature() | EIP-712 fields, chain ID, or verifyingContract don't match the recovered signer | Confirm operator matches msg.sender, token matches registrar.token(), nonce matches operatorNonce(investor, operator), chainId matches the target chain, verifyingContract matches the proxy address |
0xfed39497 | InvestorNotFound(address wallet) | Investor wallet not in DS registry | Drip from the faucet first (auto-registers) or have the registry admin add the wallet |
0x38bfcc16 | VaultAlreadyRegistered(address vault) | Vault is already bound to this investor | Skip — registration is idempotent for the same pair; treat as success |
0x8df63830 | VaultBelongsToDifferentInvestor(address vault, string vaultInvestorId) | Vault is bound to a different investor | Use a different vault address; reusing custody across investors is not allowed |
0xe6c4247b | InvalidAddress() | Vault or investor address is the zero address | Pass valid addresses |
0x308e16dd | NotAnOperator(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 paused | Wait for the admin to call unpause() |
| n/a (OZ) | AccessControlUnauthorizedAccount(account, role) | Direct admin function called by non-admin | Use 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 — aninvalidateOperatorPermissioncall by the investor invalidates a stale value). - You read
token()from the registrar instead of hard-coding the DST address. - You set
deadlinefar 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) == trueafter the tx confirms. - You captured the tx hash for audit.