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
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ jobs:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 9

- uses: actions/setup-node@v4
with:
Expand All @@ -30,4 +28,3 @@ jobs:

- name: Test
run: pnpm run test

26 changes: 20 additions & 6 deletions src/__tests__/unit/v2-relayer-token-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});
});
Expand All @@ -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());
});
});
Expand All @@ -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);
Expand All @@ -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 () => {
Expand All @@ -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' },
Expand All @@ -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());
}
});
});
13 changes: 11 additions & 2 deletions src/services/relayer-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading