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

# Dual-signature flow

> Why withdrawals require two EIP-712 signatures and how the co-sign works in practice.

## The rule

No single key can move user funds on PredictStreet.
`VaultImplementation.withdrawERC20` on-chain checks **two signatures**:

1. **User signature** — EIP-712 from the vault owner EOA.
2. **Backend signature** — EIP-712 from the factory owner (backend key).

Both must cover the exact same typed-data struct. If either is
missing or invalid, the vault reverts. `msg.sender` of the tx is
**unconstrained** — a relayer, the user, or a third party can submit.

## Sequence

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant U as User wallet
    participant API as PredictStreet API
    participant V as Vault (on-chain)

    U->>API: POST /api/withdrawals/request<br/>{amount, destination, userSignature}
    Note over API: AML screen destination + source
    alt AML clean
        Note over API: lock balance; status=CO_SIGNED
        Note over API: backend co-signs
    else AML flagged
        Note over API: lock balance; status=MLRO_REVIEW
        Note over API: wait for MLRO decision; on approve → CO_SIGNED
    end
    API->>V: withdrawERC20(token, to, amount, userSig, backendSig, salt, deadline)
    V-->>API: tx hash; status=SUBMITTED
    Note over V: on-chain confirmation observed
    V-->>API: status=CONFIRMED, locked → 0 (permanent debit)
```

## Why two signatures

* **User signature.** Proves user authorised this specific withdrawal.
* **Backend signature.** Attests the request passed platform
  compliance (AML, banned-destination, return-to-source).
* **Unilateral backend moves are impossible.** The backend's key alone
  doesn't unlock the vault.

## What the on-chain verifies

```solidity theme={null}
function withdrawERC20(
    address token,
    address to,
    uint256 amount,
    uint256 salt,
    uint256 deadline,
    bytes calldata ownerSig,
    bytes calldata backendSig
) external {
    require(deadline >= block.timestamp, "expired");
    require(!usedDigests[digest], "replayed");

    bytes32 digest = _hashTypedDataV4(...);
    require(ECDSA.recover(digest, ownerSig) == vaultOwner, "bad owner sig");
    require(ECDSA.recover(digest, backendSig) == factory.owner(), "bad backend sig");

    usedDigests[digest] = true;
    IERC20(token).safeTransfer(to, amount);
}
```

Key invariants:

* **Digest-based replay protection** — single-use per digest.
* **No pre-signed cancellation** — if both parties want to abort,
  they simply don't submit.
* **Emergency withdraw** — user can invoke after a 7-day timelock if
  the backend is gone or refusing legitimate withdrawals.

## Backend co-signer security

The factory owner's key is the single most security-critical asset.
In mainnet it must be HSM-backed (AWS KMS / Azure Key Vault) or
threshold-signed (Gnosis Safe M-of-N).
