/** * Polymarket Proxy Wallet withdraw test script * * Uses Gnosis Safe execTransaction: EOA signs SafeTx EIP-712 hash -> call Proxy to transfer out USDC * Does not depend on the official Polymarket API, interacts directly with the on-chain Safe + USDC contracts * * Usage: * 1. Fill in CONFIG below (private key / Proxy / target address / withdraw amount) * 2. tsx scripts/test-withdraw.ts * * Safety: DRY_RUN=true by default, only prints, does not send transactions. Change to false after confirming the params are correct. */ import { ethers } from "ethers"; import { readFileSync, existsSync } from "fs"; import { resolve, dirname } from "path"; import { fileURLToPath } from "url"; // Simple .env loader (no third-party package) const __dirname = dirname(fileURLToPath(import.meta.url)); const ENV_FILE = resolve(__dirname, "..", ".env"); if (existsSync(ENV_FILE)) { for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/); if (!m) continue; let val = m[2]; if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { val = val.slice(1, -1); } if (process.env[m[1]] === undefined) process.env[m[1]] = val; } } // ── Config ────────────────────────────────────────────────── // Private key and Proxy are read from .env (POLYMARKET_PRIVATE_KEY / POLYMARKET_PROXY_ADDRESS) // This test script only tests one wallet; for production batch withdraws write a separate script that reads multi-wallet config const CONFIG = { // Withdraw target wallet (required) TO_ADDRESS: "0x__TARGET_ADDRESS__", // Withdraw amount (USDC, 6 decimals). Use 0.01 for testing AMOUNT_USDC: "0.01", // true = only print, do not send on-chain; false = actually send the transaction DRY_RUN: true, } as const; const EOA_PRIVATE_KEY = process.env.POLYMARKET_PRIVATE_KEY || ""; const PROXY_ADDRESS = process.env.POLYMARKET_PROXY_ADDRESS || ""; if (!EOA_PRIVATE_KEY || !PROXY_ADDRESS) { console.error("✗ .env is missing POLYMARKET_PRIVATE_KEY or POLYMARKET_PROXY_ADDRESS"); process.exit(1); } // ── Constants ──────────────────────────────────────────────────── const POLYGON_RPC = "https://polygon-bor-rpc.publicnode.com"; const POLYGON_CHAIN_ID = 137; // Polymarket uses USDC.e (PoS bridge USDC), not native USDC const USDC_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; const USDC_DECIMALS = 6; // Gnosis Safe ABI (only the parts used) const SAFE_ABI = [ "function nonce() view returns (uint256)", "function getThreshold() view returns (uint256)", "function getOwners() view returns (address[])", "function execTransaction(address to, uint256 value, bytes data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes signatures) payable returns (bool)", "function VERSION() view returns (string)", ]; const ERC20_ABI = [ "function balanceOf(address) view returns (uint256)", "function transfer(address,uint256)", "function decimals() view returns (uint8)", ]; // SafeTx EIP-712 type const EIP712_SAFE_TX_TYPE = { SafeTx: [ { type: "address", name: "to" }, { type: "uint256", name: "value" }, { type: "bytes", name: "data" }, { type: "uint8", name: "operation" }, { type: "uint256", name: "safeTxGas" }, { type: "uint256", name: "baseGas" }, { type: "uint256", name: "gasPrice" }, { type: "address", name: "gasToken" }, { type: "address", name: "refundReceiver" }, { type: "uint256", name: "nonce" }, ], }; async function main() { console.log("─── Polymarket Proxy withdraw test ───"); console.log(`DRY_RUN: ${CONFIG.DRY_RUN}`); const provider = new ethers.JsonRpcProvider(POLYGON_RPC, POLYGON_CHAIN_ID); const eoa = new ethers.Wallet(EOA_PRIVATE_KEY, provider); console.log(`EOA: ${eoa.address}`); console.log(`Proxy: ${PROXY_ADDRESS}`); console.log(`To: ${CONFIG.TO_ADDRESS}`); // 1. Check Safe status const safe = new ethers.Contract(PROXY_ADDRESS, SAFE_ABI, provider); const [owners, threshold, safeNonce] = await Promise.all([ safe.getOwners() as Promise, safe.getThreshold() as Promise, safe.nonce() as Promise, ]); console.log(`Owners: ${owners.join(", ")}`); console.log(`Threshold: ${threshold} Nonce: ${safeNonce}`); if (!owners.map(o => o.toLowerCase()).includes(eoa.address.toLowerCase())) { throw new Error("EOA is not an owner of the Proxy, misconfigured"); } if (threshold !== 1n) { throw new Error(`Proxy threshold=${threshold}, this script only supports 1/1 Safe`); } // 2. Check USDC balance const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider); const balance = await usdc.balanceOf(PROXY_ADDRESS) as bigint; const balanceHuman = ethers.formatUnits(balance, USDC_DECIMALS); console.log(`Proxy USDC balance: ${balanceHuman}`); const amountWei = ethers.parseUnits(CONFIG.AMOUNT_USDC, USDC_DECIMALS); if (amountWei > balance) { throw new Error(`Insufficient balance: trying to withdraw ${CONFIG.AMOUNT_USDC}, only have ${balanceHuman}`); } console.log(`Withdraw amount: ${CONFIG.AMOUNT_USDC} USDC (${amountWei} wei)`); // 3. Check EOA MATIC (pays gas) const maticBalance = await provider.getBalance(eoa.address); console.log(`EOA MATIC: ${ethers.formatEther(maticBalance)}`); if (maticBalance < ethers.parseEther("0.01")) { console.warn("⚠ EOA MATIC < 0.01, may not have enough gas"); } // 4. Build the callData for USDC.transfer(to, amount) const usdcIface = new ethers.Interface(ERC20_ABI); const transferData = usdcIface.encodeFunctionData("transfer", [CONFIG.TO_ADDRESS, amountWei]); console.log(`transferData: ${transferData}`); // 5. Assemble SafeTx const safeTx = { to: USDC_ADDRESS, value: 0n, data: transferData, operation: 0, // CALL safeTxGas: 0n, baseGas: 0n, gasPrice: 0n, gasToken: ethers.ZeroAddress, refundReceiver: ethers.ZeroAddress, nonce: safeNonce, }; // 6. EIP-712 signature const domain = { chainId: POLYGON_CHAIN_ID, verifyingContract: PROXY_ADDRESS, }; const signature = await eoa.signTypedData(domain, EIP712_SAFE_TX_TYPE, safeTx); console.log(`SafeTx signature: ${signature}`); if (CONFIG.DRY_RUN) { console.log("\n✓ DRY_RUN: all params OK, no transaction sent"); console.log(" Set CONFIG.DRY_RUN = false and run again to actually withdraw"); return; } // 7. Send on-chain: call Proxy.execTransaction(...) const safeWithSigner = safe.connect(eoa) as ethers.Contract; console.log("\nSending execTransaction…"); const tx = await safeWithSigner.execTransaction( safeTx.to, safeTx.value, safeTx.data, safeTx.operation, safeTx.safeTxGas, safeTx.baseGas, safeTx.gasPrice, safeTx.gasToken, safeTx.refundReceiver, signature ); console.log(`tx hash: ${tx.hash}`); console.log("Waiting for confirmation…"); const rec = await tx.wait(); if (rec?.status === 1) { console.log(`✓ Success block=${rec.blockNumber} gas=${rec.gasUsed}`); const newBal = await usdc.balanceOf(PROXY_ADDRESS) as bigint; console.log(`Proxy new balance: ${ethers.formatUnits(newBal, USDC_DECIMALS)} USDC`); } else { console.log("✗ tx mined but status=0 (execTransaction reverted internally)"); } } main().catch(err => { console.error("✗ Failed:", err instanceof Error ? err.message : err); process.exit(1); });