From a2811bea3b9ec996a4dc9baf2c18c37f9f8a28d1 Mon Sep 17 00:00:00 2001 From: hhh0x Date: Sun, 10 May 2026 16:06:07 +0000 Subject: [PATCH 1/2] fix(v2-redeem): use USDC.e as collateral instead of pUSD (B14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V2 binary CTF markets are registered with USDC.e as underlying collateral — pUSD is only the trading-collateral wrapper for orders, not the CTF split/merge collateral. Calling Relayer.redeem with pUSD caused Safe.execTransaction to revert silently. Empirical proof via variant matrix on strategy-02: pUSD/[1] FAIL, pUSD/[1,2] FAIL, NegRiskAdapter FAIL, USDC.e/[1] SUCCESS Recovered $15.92 of stuck winning tokens (s-01 $4.81 + s-02 $11.11). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../unit/v2-relayer-token-routing.test.ts | 26 ++++++++++++++----- src/services/relayer-service.ts | 13 ++++++++-- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/__tests__/unit/v2-relayer-token-routing.test.ts b/src/__tests__/unit/v2-relayer-token-routing.test.ts index 34f386c..fbd2ded 100644 --- a/src/__tests__/unit/v2-relayer-token-routing.test.ts +++ b/src/__tests__/unit/v2-relayer-token-routing.test.ts @@ -155,6 +155,11 @@ describe('RelayerService.split — pUSD-only collateral', () => { expect(capturedExecute!.txs[0].to).toBe(CTF_CONTRACT); const decoded = CTF_IFACE.decodeFunctionData('splitPosition', capturedExecute!.txs[0].data); + // split/merge use pUSD (V2 trading-layer wrapper). Note: this is a + // theoretical path — s1 strategy never calls split/merge via Relayer + // directly; CLOB Exchange handles split internally during fills. If we + // ever DO use split/merge via Relayer, this might need empirical + // verification (similar to B14 redeem investigation). expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.pUSD.toLowerCase()); }); }); @@ -167,6 +172,7 @@ describe('RelayerService.merge — pUSD-only collateral', () => { expect(capturedExecute!.txs[0].to).toBe(CTF_CONTRACT); const decoded = CTF_IFACE.decodeFunctionData('mergePositions', capturedExecute!.txs[0].data); + // See split note above re: pUSD vs USDC.e. expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.pUSD.toLowerCase()); }); }); @@ -175,8 +181,8 @@ describe('RelayerService.merge — pUSD-only collateral', () => { // redeem / redeemBatch — pUSD only (V2 trading collateral) // --------------------------------------------------------------------------- -describe('RelayerService.redeem — pUSD-only collateral', () => { - it('encodes pUSD on the standard CTF redeem path (V2)', async () => { +describe('RelayerService.redeem — V2 standard CTF redeem uses USDC.e collateral', () => { + it('encodes USDC.e on the standard CTF redeem path (B14, 2026-05-10)', async () => { const svc = makeService(); const r = await svc.redeem(TEST_CONDITION_ID, 'YES'); expect(r.success).toBe(true); @@ -186,7 +192,11 @@ describe('RelayerService.redeem — pUSD-only collateral', () => { 'redeemPositions', capturedExecute!.txs[0].data ); - expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.pUSD.toLowerCase()); + // 2026-05-10 (B14): V2 binary CTF markets register USDC.e as the underlying + // CTF collateral; pUSD is only the V2 trading-layer wrapper. CTF.redeemPositions + // must reference USDC.e to find the split records. See + // task-polymarket-v2-redeem-investigation/4-redeem/gap-analysis.md + expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.usdcE.toLowerCase()); }); it('routes through NEG_RISK_ADAPTER for negRisk markets (no collateral arg on adapter)', async () => { @@ -198,8 +208,8 @@ describe('RelayerService.redeem — pUSD-only collateral', () => { }); }); -describe('RelayerService.redeemBatch — pUSD-only collateral', () => { - it('encodes pUSD on every standard CTF entry', async () => { +describe('RelayerService.redeemBatch — V2 standard CTF redeem uses USDC.e collateral', () => { + it('encodes USDC.e on every standard CTF entry (B14, 2026-05-10)', async () => { const svc = makeService(); const r = await svc.redeemBatch([ { conditionId: TEST_CONDITION_ID, outcome: 'YES' }, @@ -211,7 +221,11 @@ describe('RelayerService.redeemBatch — pUSD-only collateral', () => { for (const tx of capturedExecute!.txs) { expect(tx.to).toBe(CTF_CONTRACT); const decoded = CTF_IFACE.decodeFunctionData('redeemPositions', tx.data); - expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.pUSD.toLowerCase()); + // 2026-05-10 (B14): V2 binary CTF markets register USDC.e as the underlying + // CTF collateral; pUSD is only the V2 trading-layer wrapper. CTF.redeemPositions + // must reference USDC.e to find the split records. See + // task-polymarket-v2-redeem-investigation/4-redeem/gap-analysis.md + expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.usdcE.toLowerCase()); } }); }); diff --git a/src/services/relayer-service.ts b/src/services/relayer-service.ts index 299a2e9..5f93ec9 100644 --- a/src/services/relayer-service.ts +++ b/src/services/relayer-service.ts @@ -711,10 +711,17 @@ export class RelayerService { to = NEG_RISK_ADAPTER; } else { // Standard CTF: redeemPositions(collateral, parentCollectionId, conditionId, indexSets) + // 2026-05-10: V2 binary CTF markets are still registered with USDC.e as the + // underlying collateral (pUSD is the trading wrapper, but the conditional + // splits/merges happen at the USDC.e layer). Using pUSD here causes the Safe + // call to revert with no on-chain tx (relay returns STATE_FAILED + empty hash). + // Empirical proof: probe-relayer-failure.ts experiment 2026-05-10 showed + // USDC.e param succeeded (tx 0x488dba6868...) where pUSD param failed across + // [1] / [1,2] index sets and NegRiskAdapter. const indexSets = outcome === 'YES' ? [1] : [2]; const ctfInterface = new ethers.utils.Interface(CTF_ABI); data = ctfInterface.encodeFunctionData('redeemPositions', [ - POLYGON_CONTRACTS_V2.pUSD, + POLYGON_CONTRACTS_V2.usdcE, ethers.constants.HashZero, conditionId, indexSets, @@ -837,9 +844,11 @@ export class RelayerService { data = negRiskInterface.encodeFunctionData('redeemPositions', [conditionId, amounts]); to = NEG_RISK_ADAPTER; } else { + // 2026-05-10 (B14): V2 standard CTF redeem uses USDC.e collateral. + // See task-polymarket-v2-redeem-investigation/4-redeem/gap-analysis.md const indexSets = outcome === 'YES' ? [1] : [2]; data = ctfInterface.encodeFunctionData('redeemPositions', [ - POLYGON_CONTRACTS_V2.pUSD, + POLYGON_CONTRACTS_V2.usdcE, ethers.constants.HashZero, conditionId, indexSets, From a946871cb8283b78e642f672605d60340cd6b4fc Mon Sep 17 00:00:00 2001 From: hhh0x Date: Tue, 12 May 2026 09:04:21 +0000 Subject: [PATCH 2/2] ci: use packageManager pnpm version --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4342fce..49749ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: @@ -30,4 +28,3 @@ jobs: - name: Test run: pnpm run test -