Skip to main content

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

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

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