Vesting · Flows

The vesting flows.

Every confidential vesting integration boils down to these paths, each with hooks, a recipe, and a Sepolia demo.

Prerequisite: operator approval

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. 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.

    1. 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)
    2. Pass splitEnabled / pausableEnabled to route through createManagerWithOptions when a clone should ship without split or pause
    3. Receipt contains the ManagerCreated event with the clone address
    4. 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. 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.

    1. 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
    2. Resolve an encryptor — lazy in React, eager in Node
    3. manager.createVesting({ params: { recipient, startTimestamp, endTimestamp, ... }, amount, encryptor }) - SDK wraps the encrypt+proof
    4. Receipt's VestingCreated event carries the vestingId (branded VestingId)
  3. 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.

    1. Read fee config: useManagerFeeInfo({ address })
    2. Build the correct ClaimArgs variant: { vestingId, feeType: FeeType.Gas, value } | { vestingId, feeType: FeeType.DistributionToken }
    3. useClaim.mutate(args) — receipt grants recipient ACL on the new handle
  4. 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.

    1. Pick the handles to disclose (vesting handles, claim handles, fee handles)
    2. useDiscloseToParty({ vestingId, party, disclosureType }) - single grant
    3. useBatchDiscloseHandlesToParty for N grants in one tx
  5. 5

    Transfer + recover

    Handoff a vesting to a new recipient (two-step accept), revoke a vesting, or recover from a stale subwallet.

    1. useInitiateVestingTransfer({ vestingId, newRecipient, transferDurationSeconds }) - old recipient signs the intent
    2. New recipient picks it up via useAcceptVestingTransfer
    3. Either side can usePendingVestingTransfer to poll status
    4. Operator side: useRevokeVesting claws back a revocable schedule; useWithdrawAdmin pulls the recovered balance out of the manager