Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/__tests__/integration/ctf-client.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ethers } from 'ethers';
import {
CTF_CONTRACT,
USDC_CONTRACT,
NEG_RISK_CTF_EXCHANGE,
NEG_RISK_CTF_EXCHANGE_V2,
NEG_RISK_ADAPTER,
USDC_DECIMALS,
} from '../../clients/ctf-client.js';
Expand Down Expand Up @@ -104,12 +104,12 @@ describe('CTF Contract Verification', () => {
}, 30000);

it('should verify NegRisk CTF Exchange contract exists', async () => {
const code = await provider.getCode(NEG_RISK_CTF_EXCHANGE);
const code = await provider.getCode(NEG_RISK_CTF_EXCHANGE_V2);

expect(code).not.toBe('0x');
expect(code.length).toBeGreaterThan(10);

console.log(`✓ NegRisk CTF Exchange verified at ${NEG_RISK_CTF_EXCHANGE}`);
console.log(`✓ NegRisk CTF Exchange verified at ${NEG_RISK_CTF_EXCHANGE_V2}`);
console.log(` Contract code size: ${(code.length - 2) / 2} bytes`);
}, 30000);
});
Expand Down Expand Up @@ -322,7 +322,7 @@ describe('CTF Architecture Understanding', () => {
console.log(' - Handles NO → YES conversions');
console.log('');
console.log('NegRisk CTF Exchange:');
console.log(` ${NEG_RISK_CTF_EXCHANGE}`);
console.log(` ${NEG_RISK_CTF_EXCHANGE_V2}`);
console.log(' - Trading for NegRisk markets');

// This test always passes - it's documentation
Expand Down
91 changes: 19 additions & 72 deletions src/__tests__/unit/v2-relayer-token-routing.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
/**
* Unit tests for V2 Relayer collateral-token routing.
*
* After the 2026-04-28 V2 cutover, trading collateral is `pUSD`. The relayer
* helpers (`approveUsdc`, `transferUsdc`, `split`, `merge`, `redeem`,
* `redeemBatch`) used to hardcode V1 USDC.e for the collateral argument /
* `to` field, which would revert against V2 markets. The fix introduces a
* `CollateralToken` parameter on each function (default `'pUSD'`) that
* routes the call to the correct ERC-20 contract via
* `POLYGON_CONTRACTS_V2.{pUSD,usdcE}`.
* After the 2026-04-28 V2 cutover, trading collateral is `pUSD`. The
* trading-related relayer helpers (`split`, `merge`, `redeem`, `redeemBatch`)
* hardcode pUSD as the on-chain collateral argument. The off-exchange
* helpers (`approveUsdc`, `transferUsdc`) accept a `CollateralToken`
* parameter so they can route to either pUSD (V2 trading collateral, the
* default) or USDC.e (Onramp approval / fund-out collect path).
*
* These tests assert the routing behaviour by capturing the encoded
* transaction the service emits to a mocked relay client.
Expand Down Expand Up @@ -97,7 +96,7 @@ beforeEach(() => {
});

// ---------------------------------------------------------------------------
// approveUsdc
// approveUsdc — token routing (pUSD default + USDC.e for Onramp)
// ---------------------------------------------------------------------------

describe('RelayerService.approveUsdc — token routing', () => {
Expand All @@ -120,7 +119,7 @@ describe('RelayerService.approveUsdc — token routing', () => {
});

// ---------------------------------------------------------------------------
// transferUsdc
// transferUsdc — token routing (pUSD default + USDC.e for fund-out)
// ---------------------------------------------------------------------------

describe('RelayerService.transferUsdc — token routing', () => {
Expand All @@ -145,11 +144,11 @@ describe('RelayerService.transferUsdc — token routing', () => {
});

// ---------------------------------------------------------------------------
// split / merge
// split / merge — pUSD only (V2 trading collateral)
// ---------------------------------------------------------------------------

describe('RelayerService.split — token routing', () => {
it('defaults to pUSD as the splitPosition collateral arg', async () => {
describe('RelayerService.split — pUSD-only collateral', () => {
it('encodes pUSD as the splitPosition collateral arg (V2)', async () => {
const svc = makeService();
const r = await svc.split(TEST_CONDITION_ID, '10');
expect(r.success).toBe(true);
Expand All @@ -158,19 +157,10 @@ describe('RelayerService.split — token routing', () => {
const decoded = CTF_IFACE.decodeFunctionData('splitPosition', capturedExecute!.txs[0].data);
expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.pUSD.toLowerCase());
});

it("encodes USDC.e as collateral when token === 'USDC.e'", async () => {
const svc = makeService();
const r = await svc.split(TEST_CONDITION_ID, '10', false, 'USDC.e');
expect(r.success).toBe(true);

const decoded = CTF_IFACE.decodeFunctionData('splitPosition', capturedExecute!.txs[0].data);
expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.usdcE.toLowerCase());
});
});

describe('RelayerService.merge — token routing', () => {
it('defaults to pUSD as the mergePositions collateral arg', async () => {
describe('RelayerService.merge — pUSD-only collateral', () => {
it('encodes pUSD as the mergePositions collateral arg (V2)', async () => {
const svc = makeService();
const r = await svc.merge(TEST_CONDITION_ID, '10');
expect(r.success).toBe(true);
Expand All @@ -179,23 +169,14 @@ describe('RelayerService.merge — token routing', () => {
const decoded = CTF_IFACE.decodeFunctionData('mergePositions', capturedExecute!.txs[0].data);
expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.pUSD.toLowerCase());
});

it("encodes USDC.e as collateral when token === 'USDC.e'", async () => {
const svc = makeService();
const r = await svc.merge(TEST_CONDITION_ID, '10', false, 'USDC.e');
expect(r.success).toBe(true);

const decoded = CTF_IFACE.decodeFunctionData('mergePositions', capturedExecute!.txs[0].data);
expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.usdcE.toLowerCase());
});
});

// ---------------------------------------------------------------------------
// redeem / redeemBatch
// redeem / redeemBatch — pUSD only (V2 trading collateral)
// ---------------------------------------------------------------------------

describe('RelayerService.redeem — token routing', () => {
it('defaults to pUSD on the standard CTF redeem path', async () => {
describe('RelayerService.redeem — pUSD-only collateral', () => {
it('encodes pUSD on the standard CTF redeem path (V2)', async () => {
const svc = makeService();
const r = await svc.redeem(TEST_CONDITION_ID, 'YES');
expect(r.success).toBe(true);
Expand All @@ -208,30 +189,17 @@ describe('RelayerService.redeem — token routing', () => {
expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.pUSD.toLowerCase());
});

it("encodes USDC.e on the standard CTF redeem path when token === 'USDC.e'", async () => {
const svc = makeService();
const r = await svc.redeem(TEST_CONDITION_ID, 'NO', false, 'USDC.e');
expect(r.success).toBe(true);

const decoded = CTF_IFACE.decodeFunctionData(
'redeemPositions',
capturedExecute!.txs[0].data
);
expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.usdcE.toLowerCase());
});

it('routes through NEG_RISK_ADAPTER for negRisk markets (token arg ignored)', async () => {
it('routes through NEG_RISK_ADAPTER for negRisk markets (no collateral arg on adapter)', async () => {
// NegRisk adapter doesn't take a collateral arg in redeemPositions.
// We just assert routing target.
const svc = makeService();
const r = await svc.redeem(TEST_CONDITION_ID, 'YES', true);
expect(r.success).toBe(true);
expect(capturedExecute!.txs[0].to).toBe(NEG_RISK_ADAPTER);
});
});

describe('RelayerService.redeemBatch — token routing', () => {
it('defaults each standard CTF entry to pUSD', async () => {
describe('RelayerService.redeemBatch — pUSD-only collateral', () => {
it('encodes pUSD on every standard CTF entry', async () => {
const svc = makeService();
const r = await svc.redeemBatch([
{ conditionId: TEST_CONDITION_ID, outcome: 'YES' },
Expand All @@ -246,25 +214,4 @@ describe('RelayerService.redeemBatch — token routing', () => {
expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.pUSD.toLowerCase());
}
});

it('honors per-entry token override (mixed pUSD + USDC.e batch)', async () => {
const svc = makeService();
const r = await svc.redeemBatch([
{ conditionId: TEST_CONDITION_ID, outcome: 'YES' /* default pUSD */ },
{ conditionId: TEST_CONDITION_ID, outcome: 'NO', token: 'USDC.e' },
]);
expect(r.success).toBe(true);
expect(capturedExecute!.txs).toHaveLength(2);

const first = CTF_IFACE.decodeFunctionData(
'redeemPositions',
capturedExecute!.txs[0].data
);
const second = CTF_IFACE.decodeFunctionData(
'redeemPositions',
capturedExecute!.txs[1].data
);
expect(first.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.pUSD.toLowerCase());
expect(second.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.usdcE.toLowerCase());
});
});
42 changes: 3 additions & 39 deletions src/clients/ctf-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,40 +32,22 @@ import { POLYGON_CONTRACTS_V2 } from '../constants/v2-contracts.js';
export const CTF_CONTRACT = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';

/**
* USDC.e (Bridged USDC) - V1 collateral / off-exchange rail.
* USDC.e (Bridged USDC) — off-exchange / fund-flow rail.
*
* ⚠️ WARNING: This is NOT native USDC (0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359)
*
* Post-2026-04-28 V2 cutover the on-exchange collateral is `pUSD`
* (`POLYGON_CONTRACTS_V2.pUSD`); USDC.e remains in use only for
* Onramp/Offramp wrap-unwrap and Safe-to-Safe transfers. Strategy code
* placing V2 orders MUST consume the pUSD address from
* `POLYGON_CONTRACTS_V2.pUSD` (or its alias `POLYMARKET_COLLATERAL_V2`
* exported below) — this constant only encodes the bridged-USDC rail.
* `POLYGON_CONTRACTS_V2.pUSD` — this constant only encodes the
* bridged-USDC rail.
*/
export const USDC_CONTRACT = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';

/** Native USDC on Polygon - NOT compatible with CTF */
export const NATIVE_USDC_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';

/**
* V1 NegRisk CTF Exchange address.
*
* @deprecated Replaced by `POLYGON_CONTRACTS_V2.negRiskExchange` (or its
* alias {@link NEG_RISK_CTF_EXCHANGE_V2}) at the 2026-04-28 V2 cutover.
* V1 CLOB rejects all V1-signed orders since cutover. This export is
* retained ONLY so legacy call sites compile; new code MUST consume the
* V2 address.
*/
export const NEG_RISK_CTF_EXCHANGE_V1_DEPRECATED = '0xC5d563A36AE78145C45a50134d48A1215220f80a';

/**
* @deprecated Alias for {@link NEG_RISK_CTF_EXCHANGE_V1_DEPRECATED}.
* Will keep value-equal to the V1 address until the alias is removed in a
* follow-up major. New code should import {@link NEG_RISK_CTF_EXCHANGE_V2}.
*/
export const NEG_RISK_CTF_EXCHANGE = NEG_RISK_CTF_EXCHANGE_V1_DEPRECATED;

/**
* V2 NegRisk CTF Exchange (canonical post-cutover).
*
Expand All @@ -78,24 +60,6 @@ export const NEG_RISK_CTF_EXCHANGE_V2 = POLYGON_CONTRACTS_V2.negRiskExchange;
/** NegRisk Adapter — wraps multi-outcome markets. UNCHANGED from V1. */
export const NEG_RISK_ADAPTER = '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296';

/**
* V1 CTF Exchange address — Polymarket order matching contract for standard
* markets, pre-V2 era.
*
* @deprecated Replaced by `POLYGON_CONTRACTS_V2.ctfExchange` (or its alias
* {@link CTF_EXCHANGE_V2}) at the 2026-04-28 V2 cutover. V1 CLOB rejects
* all V1-signed orders since cutover. This export is retained ONLY so
* legacy call sites compile; new code MUST consume the V2 address.
*/
export const CTF_EXCHANGE_V1_DEPRECATED = '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E';

/**
* @deprecated Alias for {@link CTF_EXCHANGE_V1_DEPRECATED}.
* Will keep value-equal to the V1 address until the alias is removed in a
* follow-up major. New code should import {@link CTF_EXCHANGE_V2}.
*/
export const CTF_EXCHANGE = CTF_EXCHANGE_V1_DEPRECATED;

/**
* V2 CTF Exchange (canonical post-cutover).
*
Expand Down
32 changes: 8 additions & 24 deletions src/constants/v2-contracts.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/**
* Polymarket CLOB V2 Contract Constants (SSOT)
* ----------------------------------------------------------------------------
* V2 cutover: 2026-04-28 (UTC). All legacy V1 CTF Exchange / NegRisk Exchange
* addresses below are kept ONLY as `@deprecated` references for migration
* traceability — production callers MUST consume the V2 addresses.
* V2 cutover: 2026-04-28 (UTC). V1 surfaces have been removed — production
* code MUST consume the V2 addresses below. The only remaining V1 traces
* live in `utils/calldata-decoder.ts` (CTF_ROUTER / NEG_RISK_ROUTER /
* MATCH_ORDERS_SELECTOR_V1) and exist solely to decode pre-cutover
* settlement TXs that may still surface in mempool tail / historical replay.
*
* Verified sources:
* - V2 SDK source (`@polymarket/clob-client-v2@1.0.3`):
Expand All @@ -12,7 +14,6 @@
*
* Migration plan: see `earning-engine/.claude/skills/guide-polymarket-v2-migration/`
* - `plans/02-poly-sdk-migration.md` — overall workstream
* - `plans/12-poly-sdk-pr-templates.md` §PR-A — this PR's scope
* - `audits/01-poly-sdk-audit.md` — provenance for each address
*/

Expand Down Expand Up @@ -97,28 +98,14 @@ export const POLYGON_CONTRACTS_V2 = {
collateralOfframp: '0x2957922Eb93258b93368531d39fAcCA3B4dC5854',
} as const;

/**
* Legacy V1 addresses (for reference / grep traceability).
*
* @deprecated Do not consume from production paths. V1 CLOB rejects all
* V1-signed orders since 2026-04-28. These are kept here so that searches
* for old addresses still surface a single SSOT result.
*/
export const POLYGON_CONTRACTS_V1_LEGACY = {
/** @deprecated Replaced by `POLYGON_CONTRACTS_V2.ctfExchange`. */
ctfExchange: '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E',
/** @deprecated Replaced by `POLYGON_CONTRACTS_V2.negRiskExchange`. */
negRiskExchange: '0xC5d563A36AE78145C45a50134d48A1215220f80a',
} as const;

/**
* EIP-712 domain version constants.
*
* The Polymarket protocol exposes TWO independent EIP-712 domains:
*
* 1. CTF Exchange domain — used to sign on-chain order structs.
* Bumped from "1" → "2" at the V2 cutover. Order signing MUST use
* `domainVersionV2`.
* 1. CTF Exchange domain — used to sign on-chain order structs. V2 uses
* `domainVersionV2 = "2"` (the V1 `"1"` domain has been retired and is
* not surfaced here).
*
* 2. ClobAuthDomain — used to sign API auth challenges (HMAC bootstrap
* and L1 header generation). UNCHANGED at V2: still `"1"`.
Expand All @@ -132,9 +119,6 @@ export const EIP_712 = {
/** Domain `name` for CTF Exchange (UNCHANGED across V1/V2). */
domainName: 'Polymarket CTF Exchange',

/** @deprecated V1 CTF Exchange domain version — kept for hash parity tests only. */
domainVersionV1: '1',

/** V2 CTF Exchange domain version — production order signing MUST use this. */
domainVersionV2: '2',

Expand Down
17 changes: 5 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,23 +435,19 @@ export type {
// TradingService provides all trading functionality with proper type exports

// CTF (Conditional Token Framework)
// NOTE: USDC_CONTRACT is USDC.e (bridged) — V1 collateral / off-exchange rail.
// NOTE: USDC_CONTRACT is USDC.e (bridged) — off-exchange / fund-flow rail.
// V2 trading collateral is pUSD (POLYGON_CONTRACTS_V2.pUSD).
// NATIVE_USDC_CONTRACT is native USDC, NOT compatible with CTF.
// CTF_EXCHANGE / NEG_RISK_CTF_EXCHANGE are V1 addresses (kept @deprecated
// for back-compat); new code must consume CTF_EXCHANGE_V2 /
// NEG_RISK_CTF_EXCHANGE_V2 (or POLYGON_CONTRACTS_V2 directly).
// V1 exchange aliases (CTF_EXCHANGE / NEG_RISK_CTF_EXCHANGE) have been
// removed at the V2-only cutover; consume CTF_EXCHANGE_V2 /
// NEG_RISK_CTF_EXCHANGE_V2 or POLYGON_CONTRACTS_V2 directly.
export {
CTFClient,
CTF_CONTRACT,
USDC_CONTRACT, // USDC.e (0x2791...) - V1 collateral / off-exchange rail
USDC_CONTRACT, // USDC.e (0x2791...) - off-exchange rail
NATIVE_USDC_CONTRACT, // Native USDC (0x3c49...) - NOT for CTF
NEG_RISK_CTF_EXCHANGE, // @deprecated alias for NEG_RISK_CTF_EXCHANGE_V1_DEPRECATED
NEG_RISK_CTF_EXCHANGE_V1_DEPRECATED, // V1 (pre-cutover)
NEG_RISK_CTF_EXCHANGE_V2, // V2 canonical
NEG_RISK_ADAPTER,
CTF_EXCHANGE, // @deprecated alias for CTF_EXCHANGE_V1_DEPRECATED
CTF_EXCHANGE_V1_DEPRECATED, // V1 (pre-cutover)
CTF_EXCHANGE_V2, // V2 canonical
USDC_DECIMALS,
calculateConditionId,
Expand All @@ -476,7 +472,6 @@ export { RevertReason } from './clients/ctf-client.js';
// without reaching into the constants/ module path.
export {
POLYGON_CONTRACTS_V2,
POLYGON_CONTRACTS_V1_LEGACY,
EIP_712,
POLYGON_CHAIN_ID,
POLYGON_AMOY_CHAIN_ID,
Expand Down Expand Up @@ -576,8 +571,6 @@ export {
extractTraderAddresses,
CTF_ROUTER,
NEG_RISK_ROUTER,
/** @deprecated alias of MATCH_ORDERS_SELECTOR_V1 — use the explicit version. */
MATCH_ORDERS_SELECTOR,
MATCH_ORDERS_SELECTOR_V1,
MATCH_ORDERS_SELECTOR_V2,
ROUTER_ADDRESSES,
Expand Down
8 changes: 0 additions & 8 deletions src/services/authorization-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,6 @@ export interface AllowancesResult {
pusdBalance: string;
/** Wallet's USDC.e balance (off-exchange rail). */
usdcEBalance: string;
/**
* @deprecated Use `pusdBalance` for V2 trading-readiness. Kept as an alias of
* `pusdBalance` so older readers don't break; the field name predates the
* V2 cutover where USDC.e was the trading collateral.
*/
usdcBalance: string;
erc20Allowances: AllowanceInfo[];
erc1155Approvals: AllowanceInfo[];
tradingReady: boolean;
Expand Down Expand Up @@ -250,8 +244,6 @@ export class AuthorizationService {
wallet: walletAddress,
pusdBalance,
usdcEBalance,
// Backward-compat: alias of `pusdBalance` (V2 trading collateral).
usdcBalance: pusdBalance,
erc20Allowances,
erc1155Approvals,
tradingReady,
Expand Down
Loading
Loading