Skip to main content

Overview

This guide provides security best practices for developers integrating with the Polymarket CTF Exchange and operators running exchange infrastructure. Following these guidelines will help ensure the security of your integration and protect your users.

For Integrators

Signature Validation

The CTF Exchange supports multiple signature types. Ensure you’re using the correct type for your use case.
Standard ECDSA signatures for externally owned accounts.Requirements (Signatures.sol:67-73):
  • Signer and maker must be the same address
  • Valid ECDSA signature using EIP-191 or EIP-712
Use when:
  • Users are signing with standard Ethereum wallets (MetaMask, etc.)
  • No proxy or smart contract wallet is involved
Example:
const order = {
  signer: userAddress,
  maker: userAddress,
  signatureType: SignatureType.EOA,
  signature: await wallet.signMessage(orderHash),
  // ... other fields
};
For Polymarket proxy wallets.Requirements (Signatures.sol:96-102):
  • Valid ECDSA signature from the wallet owner
  • Maker address must match the deterministic proxy address
  • Proxy address is computed from signer, factory, and implementation
Use when:
  • Users are trading through Polymarket proxy wallets
  • You need deterministic wallet addresses
Validation:
const proxyAddress = await exchange.getPolyProxyWalletAddress(signer);
// Ensure order.maker === proxyAddress
For multisig wallets using Gnosis Safe.Requirements (Signatures.sol:112-118):
  • Valid ECDSA signature from a Safe owner
  • Maker address must match the deterministic Safe address
Use when:
  • Implementing institutional trading with multisig
  • Users require multiple signers for security
Example:
const safeAddress = await exchange.getSafeAddress(signer);
// Ensure order.maker === safeAddress
For smart contracts implementing EIP-1271.Requirements (Signatures.sol:125-132):
  • Signer and maker must be the same address
  • Contract must have code deployed
  • Must implement EIP-1271 isValidSignature interface
Use when:
  • Integrating with smart contract wallets
  • Building automated trading strategies
  • Implementing custom signature logic
Important: Ensure the contract’s isValidSignature function cannot be exploited.

Order Construction

Always validate order parameters before signing to prevent users from signing malicious orders.

Essential Validations

  1. Token Validation
    // Verify token is registered
    const conditionId = await exchange.getConditionId(tokenId);
    if (conditionId === ethers.constants.HashZero) {
      throw new Error("Token not registered");
    }
    
    // Verify complement matches
    const complement = await exchange.getComplement(tokenId);
    if (complement !== expectedComplement) {
      throw new Error("Invalid complement");
    }
    
  2. Nonce Validation
    // Use current nonce for the order
    const currentNonce = await exchange.nonces(makerAddress);
    order.nonce = currentNonce;
    
  3. Amount Validation
    // Ensure amounts are reasonable
    if (order.makerAmount <= 0 || order.takerAmount <= 0) {
      throw new Error("Invalid amounts");
    }
    
    // Check for precision issues
    if (!Number.isInteger(order.makerAmount) || !Number.isInteger(order.takerAmount)) {
      throw new Error("Amounts must be integers");
    }
    
  4. Side Validation
    // Ensure side is valid (BUY or SELL)
    if (order.side !== Side.BUY && order.side !== Side.SELL) {
      throw new Error("Invalid side");
    }
    

Nonce Management

The exchange uses nonces to prevent replay attacks and allow order cancellation.
Each address has a single nonce value. Incrementing it invalidates all outstanding orders from that address.

Emergency Order Cancellation

// Cancel all outstanding orders by incrementing nonce
await exchange.incrementNonce();
Location: NonceManager.sol:9 Use cases:
  • User wants to cancel all orders at once
  • Suspected account compromise
  • Market conditions change drastically
Incrementing the nonce invalidates ALL outstanding orders. Users cannot selectively cancel individual orders this way.

Error Handling

Implement comprehensive error handling for all exchange interactions.
try {
  await exchange.fillOrder(order, fillAmount);
} catch (error) {
  if (error.message.includes("NotOperator")) {
    // Only operators can fill orders
    console.error("Caller is not an authorized operator");
  } else if (error.message.includes("Paused")) {
    // Trading is paused
    console.error("Exchange trading is currently paused");
  } else if (error.message.includes("InvalidSignature")) {
    // Signature validation failed
    console.error("Order signature is invalid");
  } else if (error.message.includes("InvalidTokenId")) {
    // Token not registered
    console.error("Token is not registered for trading");
  } else {
    // Handle other errors
    console.error("Order fill failed:", error);
  }
}

Reentrancy Protection

All state-changing functions in the exchange are protected by OpenZeppelin’s ReentrancyGuard.
While the exchange itself is protected, your integration should also follow best practices:
// ✅ Good: Check state before external calls
const balance = await token.balanceOf(user);
if (balance >= amount) {
  await exchange.fillOrder(order, amount);
}

// ❌ Bad: Making assumptions after external calls
await exchange.fillOrder(order, amount);
const balance = await token.balanceOf(user); // State may have changed

Gas Optimization

Batch Operations

Use batch functions when filling multiple orders:
// ✅ Efficient: Single transaction for multiple orders
await exchange.fillOrders(orders, fillAmounts);

// ❌ Inefficient: Multiple transactions
for (const order of orders) {
  await exchange.fillOrder(order, fillAmount);
}
Benefits:
  • Lower gas costs per order
  • Atomic execution (all or nothing)
  • Better UX for users

Frontend Security

Always show users exactly what they’re signing:
const orderSummary = {
  market: marketName,
  outcome: outcomeName,
  side: order.side === Side.BUY ? "Buy" : "Sell",
  amount: formatAmount(order.makerAmount),
  price: calculatePrice(order.makerAmount, order.takerAmount),
  expiration: new Date(order.expiration * 1000),
};

// Display to user before requesting signature
Sanitize and validate all user inputs:
function validateTradeAmount(amount: string): number {
  // Remove non-numeric characters
  const cleaned = amount.replace(/[^0-9.]/g, "");
  
  // Parse as number
  const parsed = parseFloat(cleaned);
  
  // Validate
  if (isNaN(parsed) || parsed <= 0) {
    throw new Error("Invalid amount");
  }
  
  // Convert to wei/smallest unit
  return ethers.utils.parseUnits(cleaned, decimals);
}
  • Verify contract addresses match expected values
  • Display contract addresses to users
  • Implement address whitelisting for production
  • Use ENS names where appropriate
const EXPECTED_EXCHANGE = "0x..."; // From config

if (exchange.address.toLowerCase() !== EXPECTED_EXCHANGE.toLowerCase()) {
  throw new Error("Exchange address mismatch - possible phishing attempt");
}
Track and alert on unusual patterns:
// Example: Detect rapid order creation
const recentOrders = userOrders.filter(
  (o) => o.timestamp > Date.now() - 60000 // Last minute
);

if (recentOrders.length > 10) {
  showWarning("Unusual activity detected. Please verify your account security.");
}

For Operators

Access Control

Operators can execute trades on behalf of users. Secure your operator keys carefully.
Compromised operator keys can be used to execute unauthorized trades. Implement defense in depth.

Key Management

  1. Use Hardware Security Modules (HSMs)
    • Store operator keys in HSMs or secure enclaves
    • Never store keys in plain text
    • Rotate keys periodically
  2. Implement Rate Limiting
    // Example: Limit orders per second
    const MAX_ORDERS_PER_SECOND = 10;
    const orderTimestamps = [];
    
    function checkRateLimit() {
      const now = Date.now();
      const recent = orderTimestamps.filter((t) => now - t < 1000);
      
      if (recent.length >= MAX_ORDERS_PER_SECOND) {
        throw new Error("Rate limit exceeded");
      }
      
      orderTimestamps.push(now);
    }
    
  3. Monitor Operator Activity
    • Log all operator transactions
    • Alert on unusual patterns
    • Implement automated circuit breakers
  4. Use Multiple Operators
    • Distribute load across multiple operator addresses
    • Limit exposure if one key is compromised
    • Implement operator rotation

Order Validation

Operators should validate orders before submission:
function validateOrderBeforeFill(order: Order): void {
  // 1. Verify signature
  const isValid = await exchange.validateOrderSignature(
    orderHash,
    order
  );
  if (!isValid) throw new Error("Invalid signature");

  // 2. Check nonce
  const currentNonce = await exchange.nonces(order.maker);
  if (order.nonce !== currentNonce) {
    throw new Error("Invalid nonce - order may be cancelled");
  }

  // 3. Verify expiration
  if (order.expiration < Math.floor(Date.now() / 1000)) {
    throw new Error("Order expired");
  }

  // 4. Check token registration
  await exchange.validateTokenId(order.tokenId);

  // 5. Verify maker has sufficient balance
  const balance = await ctf.balanceOf(order.maker, order.tokenId);
  if (balance < order.makerAmount) {
    throw new Error("Insufficient maker balance");
  }
}

Monitoring and Alerting

Implement comprehensive monitoring for exchange operations:
exchange.on("TradingPaused", (pauser) => {
  console.error(`ALERT: Trading paused by ${pauser}`);
  // Stop accepting new orders
  // Notify operations team
});

exchange.on("TradingUnpaused", (unpauser) => {
  console.info(`Trading resumed by ${unpauser}`);
  // Resume normal operations
});
exchange.on("RemovedOperator", (operator, admin) => {
  if (operator === operatorAddress) {
    console.error(`ALERT: Operator privileges revoked by ${admin}`);
    // Stop operations immediately
  }
});

exchange.on("NewOperator", (operator, admin) => {
  console.info(`New operator added: ${operator} by ${admin}`);
});
exchange.on("ProxyFactoryUpdated", (oldFactory, newFactory) => {
  console.warn(`Proxy factory updated: ${oldFactory} -> ${newFactory}`);
  // May need to update signature verification logic
});

exchange.on("SafeFactoryUpdated", (oldFactory, newFactory) => {
  console.warn(`Safe factory updated: ${oldFactory} -> ${newFactory}`);
  // May need to update signature verification logic
});
const failureThreshold = 10;
let recentFailures = 0;

async function executeTrade(order: Order, amount: number) {
  try {
    await exchange.fillOrder(order, amount);
    recentFailures = 0; // Reset on success
  } catch (error) {
    recentFailures++;
    
    if (recentFailures >= failureThreshold) {
      console.error("ALERT: High failure rate detected");
      // Trigger circuit breaker
    }
    
    throw error;
  }
}

Infrastructure Security

Network Security

  • Use private RPC endpoints
  • Implement request authentication
  • Rate limit RPC calls
  • Monitor for unusual network activity

Database Security

  • Encrypt sensitive data at rest
  • Use parameterized queries
  • Implement access controls
  • Regular security audits

API Security

  • Require authentication for all endpoints
  • Implement request signing
  • Use HTTPS exclusively
  • Rate limit all endpoints

Deployment Security

  • Use infrastructure as code
  • Implement least privilege access
  • Regular security patches
  • Audit logging for all access

Emergency Procedures

For Admins

In case of a security incident:
1

Pause Trading

Immediately call pauseTrading() to halt all operations:
await exchange.pauseTrading();
2

Assess the Situation

  • Identify the nature of the incident
  • Determine scope of impact
  • Review recent transactions
  • Check if any operators are compromised
3

Revoke Compromised Access

Remove any compromised operators or admins:
await exchange.removeOperator(compromisedOperator);
4

Implement Mitigation

  • Deploy fixes if necessary
  • Update configurations
  • Rotate keys
  • Notify affected users
5

Resume Operations

Once the issue is resolved:
await exchange.unpauseTrading();
6

Post-Incident Review

  • Document the incident
  • Identify root cause
  • Implement preventive measures
  • Update security procedures

For Users

If you suspect your account is compromised:
1

Cancel All Orders

Increment your nonce to invalidate all outstanding orders:
await exchange.incrementNonce();
2

Revoke Approvals

Remove token approvals if possible:
await ctf.setApprovalForAll(exchange.address, false);
3

Transfer Assets

Move assets to a secure address if possible.
4

Contact Support

Report the incident through official channels.

Security Checklist

Integration Checklist

  • All order parameters validated before signing
  • Signature types correctly implemented
  • Error handling for all exchange interactions
  • User inputs sanitized and validated
  • Contract addresses verified against expected values
  • Reentrancy protection in custom contracts
  • Gas estimation implemented
  • Comprehensive logging and monitoring
  • Security audit completed (for production)
  • Incident response plan documented

Operator Checklist

  • Operator keys stored in HSM or secure enclave
  • Rate limiting implemented
  • Order validation before submission
  • Monitoring for all exchange events
  • Automated alerting configured
  • Circuit breakers implemented
  • Key rotation procedures documented
  • Multiple operators for redundancy
  • Regular security audits scheduled
  • Incident response procedures tested

Additional Resources

Security Audit

Review the ChainSecurity audit report

Admin Controls

Learn about administrative functions

Order Structure

Understand how orders work

Signature Types

Deep dive into signature validation
Security is an ongoing process. Stay informed about updates to the protocol and regularly review your security practices.

Build docs developers (and LLMs) love