This page documents the non-custodial on-chain escrow used by hosted trading: what the escrow contracts can and cannot do, who controls them, what’s been audited, and the exact contract calls that get your funds back if trade.pmxt.dev is unreachable. Non-custodial by construction — every value-moving operator call requires an EIP-712 signature from the user’s wallet (or, for cross-chain buys, an additional independent oracle attestation); PMXT cannot unilaterally move user funds.
If anything here disagrees with what you read in Hosted trading or Escrow lifecycle, this page is the source of truth — those pages describe the happy path, this one describes the trust model.
Deployed contracts
There are two contracts, on two chains. Both are non-upgradeable, non-pausable, with no Ownable role — only an immutable operator set at deploy time.
| Contract | Chain | ChainId | Mainnet address | Explorer |
|---|
PreFundedEscrow (HomeEscrow) | Polygon | 137 | 0x3ad326f78b1390b9a5dc5f00e7f62f8632de23e2 | Polygonscan |
VenueEscrow | BSC | 56 | 0x6a273643d84edbb603b808d8a724fb963c7a298a | BscScan |
Verify each one on the explorer before you fund:
- Polygon
PreFundedEscrow: https://polygonscan.com/address/0x3ad326f78b1390b9a5dc5f00e7f62f8632de23e2
- BSC
VenueEscrow: https://bscscan.com/address/0x6a273643d84edbb603b808d8a724fb963c7a298a
The explorer pages currently show the deployed contract bytecode, but the source code is not explorer-verified yet. Public source links will be added here once verification/public source publication is complete; until then, treat the explorer addresses above as the public deployment identifiers and the unaudited status below as the security posture.
The SDK reads the live address from the trading API at runtime; if PMXT ever migrates the escrow, the SDK picks it up automatically. The mainnet addresses above are the current production deployment. Testnet deployments are environment-scoped and not listed here — check trade.pmxt.dev’s config endpoint, or your self-hosted trading-api’s PREFUNDED_ESCROW_ADDRESS / VENUE_ESCROW_ADDRESS env vars.
What the contracts can and cannot do
The trust property worth checking is “PMXT (the operator) cannot drain user funds.” It holds because every value-moving call requires either (a) the user’s EIP-712 signature, (b) the user as msg.sender, or (c) for cross-chain buys, a delivery attestation signed by an independent settlement oracle.
PreFundedEscrow (Polygon) — what the operator can do
| Function | What it moves | Required authorization |
|---|
settleBuy | USDC out of user balance → operator, real CTF in | User’s EIP-712 OrderParams signature + worst-price / max-cost check |
settleSellPay | User’s CTF out → operator; credits worst-price floor USDC | User’s EIP-712 SellOrderParams signature |
reimburseSell | Operator USDC → user (surplus above floor) | Prior settleSellPay on same nonce; cannot exceed user’s balance change |
settleCrossChainBuy | USDC out of user balance → operator | User’s EIP-712 CrossChainOrderParams signature + delivery attestation signed by settlementOracle |
settleCrossChainSellUSDC | Operator USDC → user | User’s EIP-712 CrossChainSellPayParams signature |
PreFundedEscrow — what only the user can do
| Function | Effect |
|---|
deposit(amount) | Pull USDC from msg.sender (after ERC-20 approve) |
withdraw(amount) | Start a timelocked withdrawal |
claimWithdrawal() | Pull matured USDC back to msg.sender |
cancelWithdrawal() | Re-credit a pending withdrawal back to balance |
cancelWithSig(...) | Burn a resting order nonce (permissionless relay against user signature) |
VenueEscrow (BSC) — what the operator can do
| Function | What it moves | Required authorization |
|---|
depositForUser | Operator’s outcome tokens → escrow, credited to user | Operator-only; only credits, never debits |
settleCrossChainSellTokens | User’s outcome tokens → operator | User’s EIP-712 CrossChainSellPullParams signature |
VenueEscrow never holds user USDC — it only holds Opinion outcome tokens, and only the user can move them back out. Withdrawing tokens back to your wallet (withdrawTokens → claimTokensWithdrawal) is user-only, mirroring the USDC withdrawal flow on Polygon.
Operator / admin model
There is no admin. The contracts have no Ownable, no Pausable, no upgrade proxy, no upgrade timelock, no fee setter. The entire privileged surface is the onlyOperator modifier in the shared OutcomeCustody base contract, and the operator address is immutable — set once in the constructor and unchangeable for the life of the contract.
What this means concretely:
- No one can pause withdrawals. Even if PMXT’s operator key is compromised,
withdraw / claimWithdrawal keep working.
- No one can change the operator. A compromised PMXT cannot rotate to a new operator and skip user signatures.
- No one can upgrade the contract. What’s deployed is what runs forever; bugs cannot be hot-patched, which is part of why this surface is intentionally minimal.
If a security flaw were discovered, the only remediation is deploying a new escrow at a new address and migrating users to it — there is no admin lever to flip.
Current operator addresses (immutable on the deployed contracts above):
- Polygon
PreFundedEscrow.operator: 0x84194eae9c63C1e3A769976bb762506d3443156d
- BSC
VenueEscrow.operator: 0x84194eae9c63C1e3A769976bb762506d3443156d
- Polygon
PreFundedEscrow.settlementOracle: 0x8aD93c0D15bC655b460dD700B1E25C86D1a728EE
The operator address is currently an EOA controlled by PMXT. TBD — multisig migration: a multisig replacement is planned but not yet deployed; we’ll update this page with the multisig address and signers when it lands.
Audit status
Unaudited as of 2026-06-09. The contracts have not undergone a third-party security review. Treat the escrow as you would any unaudited DeFi primitive: only fund what you can afford to lose, and prefer the timelock as a kill-switch (see below).
We will link the audit report here when one is completed.
Emergency exit: get your USDC out without PMXT
If trade.pmxt.dev is down, the operator is compromised, or you simply want to exit without using the SDK, your USDC is recoverable with two raw on-chain calls. PMXT’s server is not required — only a Polygon RPC and your wallet.
The full ABI you need from PreFundedEscrow:
function balances(address user) external view returns (uint256);
function withdraw(uint256 amount) external;
function pendingWithdrawals(address user) external view returns (uint256 amount, uint256 claimableAt);
function claimWithdrawal() external;
function cancelWithdrawal() external;
Step 1 — Check your balance
uint256 balance = PreFundedEscrow.balances(msg.sender); // USDC micro-units (6 decimals)
Step 2 — Request the withdrawal
PreFundedEscrow.withdraw(amount); // amount in USDC micro-units
This debits your in-escrow balance immediately and records a pending withdrawal claimable after WITHDRAWAL_DELAY seconds.
The deployed WITHDRAWAL_DELAY is 60 seconds. Older versions of Escrow lifecycle referred to “~1 hour” — that was a forward-looking value; the production contract uses 60 seconds today. We will lengthen the timelock before raising the value of funds the operator manages; this page will reflect the change.
Step 3 — Wait the timelock, then claim
(uint256 amount, uint256 claimableAt) = PreFundedEscrow.pendingWithdrawals(msg.sender);
require(block.timestamp >= claimableAt);
PreFundedEscrow.claimWithdrawal(); // USDC transferred to msg.sender
That’s it — your USDC is back in your wallet without any signature from or call to PMXT.
If you also hold Opinion outcome tokens on BSC
VenueEscrow exposes the same shape on BSC. Use withdrawTokens(tokenId, amount) → wait → claimTokensWithdrawal(tokenId).
Failure modes
Concrete behavior under specific failure conditions:
trade.pmxt.dev is offline. Your funds are unaffected. New orders cannot be built or submitted, but withdraw / claimWithdrawal work directly against Polygon — see the emergency exit above. Existing on-chain positions remain redeemable through the venue’s own redemption flow.
- The PMXT operator key is compromised. The operator still cannot move USDC without your EIP-712 signature; the worst they can do is force-settle resting orders that you already signed at prices no worse than your
worstPrice, or stall the system. Withdraw immediately via the raw contract calls above; the timelock window is your safety budget.
- Polygon RPC is down. Use a different Polygon RPC (Alchemy, Infura, Ankr,
polygon-rpc.com, etc.). The escrow lives on Polygon mainnet — any RPC pointing at chainId 137 reaches it.
- The contract is “paused.” It can’t be. There is no pause function. See the admin model section above.
- BSC
VenueEscrow has an issue mid-Opinion trade. Opinion buys settle with the dual signature user order + oracle delivery attestation; if the BSC side fails to deliver, the Polygon settleCrossChainBuy will never receive a valid attestation, and your USDC stays in your Polygon balance — the buy simply does not settle. Cross-chain sells use parallel signed legs; if the BSC pull leg stalls, you can burn the pull nonce permissionlessly via VenueEscrow.cancelPullWithSig against your own signature, then re-issue. The Polygon pay nonce can be burned via PreFundedEscrow.cancelWithSig(path=XCHAIN_SELL).
- Settlement oracle is compromised. A bad oracle signature alone is not enough to drain funds —
settleCrossChainBuy requires both the user’s signed order and an attestation whose (user, tokenId, destEscrow) match the order. A malicious oracle could only “confirm” deliveries you already signed for, and only up to your maxCostUsdc and worstPrice.
See also