> ## Documentation Index
> Fetch the complete documentation index at: https://docs.testnet.dev.adipredictstreet.com/llms.txt
> Use this file to discover all available pages before exploring further.

# VaultFactory + VaultImplementation

> Per-user custody with dual-sig withdrawals and emergency path.

Every user gets their own `VaultImplementation` clone (EIP-1167
minimal proxy) deployed by `VaultFactory` on first deposit.

## VaultFactory

```solidity theme={null}
function deployVault(address user) external returns (address vault);
function vaultOf(address user) external view returns (address);
```

* `deployVault` is idempotent — calling it for a wallet that already
  has a vault is a no-op that returns the existing clone address.
* At creation the clone is initialised with `vaultOwner = user` and
  auto-approvals to `CTFExchange`, `NegRiskAdapter`,
  `PredictStreetNegRiskCtfExchange`, `ConditionalTokens`.
* `deployVault` consumes \~4M gas for the very first clone (storage
  initialisation of the auto-approvals). Send a `gasLimit` of **5M**
  to avoid `out of gas` reverts. Subsequent re-calls are no-ops and
  cheap.

## Bootstrap: vault → deposit

```typescript theme={null}
// 1. Make sure vault exists
await (await vaultFactory.deployVault(eoa, { gasLimit: 5_000_000 })).wait();
const vault = await vaultFactory.vaultOf(eoa);

// 2. Approve USDC and deposit into the vault
await (await usdc.connect(user).approve(vault, parseUnits('1000', 6))).wait();
await (await new Contract(
  vault,
  ['function depositERC20(address token, uint256 amount)'],
  user,
).depositERC20(usdc.target, parseUnits('1000', 6))).wait();
```

<Note>
  Per-EOA deposit limits in `DepositLimitRegistry` are initialised by
  the platform automatically in response to the `VaultCreated` event
  — partners do **not** call `initializeLimits` themselves. If you
  fire `depositERC20` in the same block as `deployVault`, the deposit
  can briefly revert with selector `0x87138d5c` = `NotInitialized()`;
  wait one block and retry.
</Note>

## VaultImplementation entry points

All mutating calls require **two EIP-712 signatures** (vault owner +
factory owner). `msg.sender` is unconstrained.

| Function                                                                                                | Purpose                                                                                                                                                                                                  |
| ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withdrawERC20(token, to, amount, salt, deadline, ownerSig, backendSig)`                                | External withdrawal                                                                                                                                                                                      |
| `withdrawERC1155(token, to, id, amount, salt, deadline, ownerSig, backendSig)`                          | Withdraw outcome tokens                                                                                                                                                                                  |
| `splitPosition(kind, collateral, conditionId, partition, amount, salt, deadline, ownerSig, backendSig)` | Mint full outcome set inside the vault                                                                                                                                                                   |
| `mergePositions(...)`                                                                                   | Collapse outcome set → collateral                                                                                                                                                                        |
| `convertPositions(marketId, indexSet, amount, salt, deadline, ownerSig, backendSig)`                    | Neg-risk NO-basket conversion. The `negRiskAdapter` reference is held as a constructor-set immutable on the vault clone, so it is **not** part of the signed struct — only the seven typed fields above. |

### Splitting USDC into YES + NO inside the vault

`SELL` orders need the outcome ERC-1155 to live in the **vault** (the
on-chain `_verifyVault` check looks up the position by `maker = vault`).
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:
   ```json theme={null}
   {
     "pendingSplitId":  "...",
     "kind":            0,
     "collateralToken": "0x9bC8...241e1",
     "conditionId":     "0x...",
     "partition":       ["1", "2"],
     "amount":          "30000000",
     "salt":            "...",
     "deadline":        1776999999,
     "vaultAddress":    "0x...",
     "backendSig":      "0x..."
   }
   ```
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,
   only the field name changes (`pendingMergeId` instead of
   `pendingSplitId`):
   ```json theme={null}
   {
     "pendingMergeId":  "...",
     "kind":            0,
     "collateralToken": "0x9bC8...241e1",
     "conditionId":     "0x...",
     "partition":       ["1", "2"],
     "amount":          "30000000",
     "salt":            "...",
     "deadline":        1776999999,
     "vaultAddress":    "0x...",
     "backendSig":      "0x..."
   }
   ```
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:

```json theme={null}
{
  "<SplitPosition or MergePositions>": [
    { "name": "kind",            "type": "uint8"     },
    { "name": "collateralToken", "type": "address"   },
    { "name": "conditionId",     "type": "bytes32"   },
    { "name": "partition",       "type": "uint256[]" },
    { "name": "amount",          "type": "uint256"   },
    { "name": "salt",            "type": "uint256"   },
    { "name": "deadline",        "type": "uint256"   }
  ]
}
```

<Note>
  Binary-market only in MVP (`outcomes == 2`, partition `[1, 2]`).
  Neg-risk N-outcome merges land on the `NegRiskAdapter` path later.
</Note>

## Emergency withdraw

```solidity theme={null}
initiateEmergencyWithdraw(ownerSig)       // starts 7-day timelock
// ... wait 7 days ...
emergencyWithdrawERC20(token, to, amount, ownerSig)
emergencyWithdrawERC1155(token, to, id, amount, ownerSig)
```

Backend can abort:

```solidity theme={null}
cancelEmergencyWithdraw()
```

## Digest invalidation

```solidity theme={null}
invalidateDigest(bytes32 digest) external onlyVaultOwner;
invalidateDigests(bytes32[] calldata digests) external onlyVaultOwner;
```

## Why EIP-1167 minimal proxy

* **Gas-cheap deployment** (\~45k gas vs \~2M for a full copy).
* **Non-upgradeable** — implementation pinned, admin can't hot-patch.
* **Bug mitigation** requires new factory + user-driven emergency
  withdraw.

## EIP-712 domain

```
name: "PredictStreetVault"
version: "1"
chainId: <chain>
verifyingContract: <your vault clone address>
```

Note `verifyingContract` is the **vault address**, not the factory.
