The vesting flows.
Every confidential vesting integration boils down to these paths, each with hooks, a recipe, and a Sepolia demo.
Before creating, funding, or dispersing, the token must approve this contract as an operator: call token.setOperator(<contract address>, deadline) first. Without it the transaction reverts with ERC7984UnauthorizedSpender (0x79f2cb38).
The SDK surfaces the missing approval as: “The token has not approved this contract as an operator. Call setOperator() (see /fhe operators) before this transaction.”
Do not route these calls through Multicall3. aggregate3 makes the Multicall3 contract the msg.sender, which breaks operator approvals and per-user clone authorization. Use the built-in batch functions instead: batchCreateVesting, disperse, batchDiscloseToParty.
- 1
Deploy a manager
Aria's wallet creates a per-user manager clone via the factory. The clone owns Aria's vestings; she keeps DEFAULT_ADMIN_ROLE.
- factory.createManager({ token, userSalt }) — EIP-1167 minimal-proxy clone at a deterministic address derived via LibClone.cloneDeterministic (predicted with predictDeterministicAddress; CREATE3 is only for the system contracts, not the per-user clone)
- Pass splitEnabled / pausableEnabled to route through createManagerWithOptions when a clone should ship without split or pause
- Receipt contains the ManagerCreated event with the clone address
- Grant manager roles (VESTING_CREATOR_ROLE, REVOKER_ROLE, WITHDRAWER_ROLE, CLAIMER_ROLE, PAUSER_ROLE, DISCLOSURE_ADMIN_ROLE) as needed - WITHDRAWER_ROLE gates withdrawing unreserved tokens (e.g. a treasury address), CLAIMER_ROLE gates gas-sponsored claims on behalf of recipients (e.g. a relayer). FEE_MANAGER_ROLE lives on the factory, not the manager
- 2
Open a vesting
Encrypt the plaintext amount client-side, submit it with the {start, end, cliff, interval} schedule. Recipient gets read ACL on the handle.
- Operator approval first: the operator's wallet calls token.setOperator(managerAddress, deadline) on the distribution token (one-time per chain) — see the Prerequisite callout above
- Resolve an encryptor — lazy in React, eager in Node
- manager.createVesting({ params: { recipient, startTimestamp, endTimestamp, ... }, amount, encryptor }) - SDK wraps the encrypt+proof
- Receipt's VestingCreated event carries the vestingId (branded VestingId)
- 3
Claim what's vested
Recipient (or admin on their behalf) pulls the vested portion. ClaimArgs branches on the manager's feeType — useManagerFeeInfo resolves it once.
- Read fee config: useManagerFeeInfo({ address })
- Build the correct ClaimArgs variant: { vestingId, feeType: FeeType.Gas, value } | { vestingId, feeType: FeeType.DistributionToken }
- useClaim.mutate(args) — receipt grants recipient ACL on the new handle
- 4
Disclose to a third party
Add a tax authority, auditor, or finance team as a viewer on specific handles. ACL is append-only — disclosure doesn't unlock the funds.
- Pick the handles to disclose (vesting handles, claim handles, fee handles)
- useDiscloseToParty({ vestingId, party, disclosureType }) - single grant
- useBatchDiscloseHandlesToParty for N grants in one tx
- 5
Transfer + recover
Handoff a vesting to a new recipient (two-step accept), revoke a vesting, or recover from a stale subwallet.
- useInitiateVestingTransfer({ vestingId, newRecipient, transferDurationSeconds }) - old recipient signs the intent
- New recipient picks it up via useAcceptVestingTransfer
- Either side can usePendingVestingTransfer to poll status
- Operator side: useRevokeVesting claws back a revocable schedule; useWithdrawAdmin pulls the recovered balance out of the manager
Hooks