Skip to main content
Every user gets their own Vault contract, deployed by VaultFactory. The platform deploys it for you (see Backend auto-deploy); a deposit does not create it, because depositERC20 is called on the vault itself. Each vault is a full, non-upgradeable contract, not a proxy. Its address is assigned at deploy time (plain CREATE, not CREATE2), so read it from vaultOf(user) rather than computing it.

VaultFactory

  • deployVault is not idempotent — it reverts with VaultAlreadyExists(user) (selector 0x4293864f) if the wallet already has a vault. The platform deploys partner vaults automatically (Backend auto-deploy), so integration code should only read vaultOf(user) (or poll GET /api/me/vault) and never call deployVault itself.
  • At creation the vault is initialised with vaultOwner = user and max USDC + ERC-1155 approvals to CTFExchange, ConditionalTokens, NegRiskAdapter and NegRiskCtfExchange.

Bootstrap: vault → deposit

Per-EOA deposit limits in DepositLimitRegistry are set by the platform — partners do not initialise them. This is a separate step from the vault deploy, so deployed: true alone does not mean the vault accepts deposits: until the platform has assigned your EOA a deposit tier, depositERC20 reverts with KycRequired() (selector 0x709ea86e). Before the first deposit, poll GET /api/me/deposit-limits until restrictions.depositBlocked is false.

Vault entry points

The calls below require two EIP-712 signatures (the vault owner’s plus a platform backend co-signature). msg.sender is unconstrained. There is no ERC-1155 withdraw. Outcome tokens leave the vault by trading, by mergePositions followed by withdrawERC20, by redemption after resolution, or through the emergency withdraw.

Splitting USDC into YES + NO inside the vault

SELL orders need the outcome ERC-1155 to live in the vault: at settlement the exchange debits the tokens from the order’s maker vault, so they must be there, not in your EOA. The supported flow is:
  1. Backend co-signature. POST /api/vault/split-signature with { marketId, amount }. Body of response carries the dual-sig payload the vault expects:
  2. Owner signature. Sign the same SplitPosition typed-data with the vault EIP-712 domain (see below) using the EOA key.
  3. On-chain submit. Call vault.splitPosition(kind, collateral, conditionId, partition, amount, salt, deadline, ownerSig, backendSig).
After the tx confirms, the vault holds equal balances of YES and NO ERC-1155 for conditionId, and SELL orders on either outcome become possible.

Merging YES + NO back into USDC

mergePositions is the inverse of splitPosition: it burns equal units of every outcome ERC-1155 inside the vault and returns the matching USDC. Useful for closing a hedged position before resolution, freeing locked outcome tokens back into spendable USDC, or just exiting a market early without waiting for the oracle. The flow mirrors split:
  1. Backend co-signature. POST /api/vault/merge-signature with { marketId, amount }. Response shape is identical to split’s:
  2. Owner signature. Sign the same MergePositions typed-data with the vault EIP-712 domain. The struct field shape is identical to SplitPosition (same 7 fields); only the EIP-712 primary type changes from SplitPosition → MergePositions. Use type table below.
  3. On-chain submit. Call vault.mergePositions(kind, collateral, conditionId, partition, amount, salt, deadline, ownerSig, backendSig).
After the tx confirms, the vault burns amount of each outcome’s ERC-1155 and credits amount USDC; the platform reflects the balance change with ~1-block lag and your /api/me/balances shows available += amount. Both SplitPosition and MergePositions use the same EIP-712 type shape on chain:
The partition is always [1, 2] (YES + NO of one condition). kind is 0 on binary markets and 1 on neg-risk markets. On a neg-risk market each outcome is its own YES / NO question: send outcomeIndex to POST /api/vault/split-signature / merge-signature to pick it (without it: 400 outcome_index_required), and the signed conditionId is that outcome’s condition.

Emergency withdraw

Call these from the vault-owner EOA. No signatures are involved. The emergency is one-way: neither the platform nor anyone else can cancel it. While the timelock or the execution window is running, the exchange rejects orders from your vault. If the 7-day execution window lapses, call initiateEmergencyWithdraw() again.

Digest invalidation

Cancels a dual-signed operation (withdraw, split, merge, convert) before it is submitted by marking its digest used. Either signer can do it: the vault owner or the platform backend; anyone else gets NotOwner(). invalidateDigest reverts DigestAlreadyUsed() on a digest that was already consumed; the batch form skips those.

Why a non-upgradeable per-user vault

  • Non-upgradeable — the vault bytecode is fixed in the factory when the factory is deployed; no admin can hot-patch or swap it.
  • Bug mitigation requires a new factory + user-driven emergency withdraw.

EIP-712 domain

Note verifyingContract is the vault address, not the factory.