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
deployVaultis not idempotent — it reverts withVaultAlreadyExists(user)(selector0x4293864f) if the wallet already has a vault. The platform deploys partner vaults automatically (Backend auto-deploy), so integration code should only readvaultOf(user)(or pollGET /api/me/vault) and never calldeployVaultitself.- At creation the vault is initialised with
vaultOwner = userand max USDC + ERC-1155 approvals toCTFExchange,ConditionalTokens,NegRiskAdapterandNegRiskCtfExchange.
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:
- Backend co-signature. POST
/api/vault/split-signaturewith{ marketId, amount }. Body of response carries the dual-sig payload the vault expects: - Owner signature. Sign the same
SplitPositiontyped-data with the vault EIP-712 domain (see below) using the EOA key. - On-chain submit. Call
vault.splitPosition(kind, collateral, conditionId, partition, amount, salt, deadline, ownerSig, backendSig).
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:
- Backend co-signature. POST
/api/vault/merge-signaturewith{ marketId, amount }. Response shape is identical to split’s: - Owner signature. Sign the same
MergePositionstyped-data with the vault EIP-712 domain. The struct field shape is identical toSplitPosition(same 7 fields); only the EIP-712 primary type changes fromSplitPosition→MergePositions. Use type table below. - On-chain submit. Call
vault.mergePositions(kind, collateral, conditionId, partition, amount, salt, deadline, ownerSig, backendSig).
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
initiateEmergencyWithdraw() again.
Digest invalidation
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
verifyingContract is the vault address, not the factory.