feat: CTF Exchange V2 migration - #24
Open
dkeysil wants to merge 8 commits into
Open
Conversation
Rewrites the order struct, EIP-712 domain version, and contract addresses to match CTF Exchange V2 (live 2026-04-22). After cutover V1 signatures will be rejected by the operator. Signed struct now has 11 fields: salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata, builder. ORDER_TYPEHASH = 0xbb86318a2138f5fa8ae32fbe8e659f8fcf13cc6ae4014a707893055433818589. Removed from OrderData: Taker, Expiration, Nonce, FeeRateBps. Added to OrderData: Timestamp (unix ms, defaults to now), Metadata (bytes32), Builder (bytes32 builder code). Domain version bumped "1" -> "2" (name unchanged). Exchange addresses replaced with V2 deployments (same on mainnet and Amoy): CTFExchange V2 0xE111180000d2663C0091e4f400237545B87B996B NegRisk CTFExchange V2 0xe2222d279d744050d28e00520010520000310F59 Collateral updated to pUSD (0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB). SignatureType enum extended with POLY_1271 (EIP-1271 smart-contract wallets). Adds utils.GenerateTimestampMs for the default timestamp source and WithTimestampGenerator for deterministic tests. Golden signature/hash vectors pinned to py-clob-client-v2==1.0.0 output via scripts/gen_v2_vectors.py; ORDER_TYPEHASH is independently asserted against the Solidity constant.
- Drop Bytes32Zero alias; use common.Hash{} directly.
- Public OrderData uses native types: Timestamp int64, Metadata/Builder
common.Hash. Eliminates string parsing + parseBytes32 helper.
- Order.Side and Order.SignatureType are now uint8 (ABI-native), avoiding
two per-call big.Int allocations and the Uint64() casts.
- Cache EIP-712 domain separator per (chainId, contract) at constructor
time. BuildOrderHash becomes a map lookup + one ABI-pack + keccak.
- Drop functional-options for the timestamp generator; plain third
positional arg (nil for default) is symmetric with saltGenerator.
- Parse Maker once in BuildOrder (was twice when Signer was empty).
- Merge TestBuildOrderHash_V2Goldens + TestBuildSignedOrder_V2Goldens
into TestV2Goldens with a single table.
- Delete unreachable POLY_GNOSIS_SAFE fallback branch (Maker == Signer
in all test cases; ValidateSignature never errors).
- Prune migration-era narrating comments; keep WHY-only ones.
All py-clob-client-v2 golden vectors (order hash + signature) still match
byte-for-byte; ORDER_TYPEHASH invariant still holds.
- Pin golangci-lint to v2.11.4 (v1.54.2 from 2023 won't compile under
modern Go toolchains; pre-existing upstream issue).
- Migrate .golangci.yaml to v2 schema. Configure revive var-naming with
upperCaseConst: true so SignatureType consts (EOA, POLY_PROXY,
POLY_GNOSIS_SAFE, POLY_1271) match the TS/Python SDK naming.
- Allow OrderData.TokenId (intentional, mirrors tokenId on the wire).
- Exclude pkg/contracts (auto-generated abigen bindings) from lint.
- Rename package-private _ALL_CAPS vars to camelCase (eip712, builder).
- Rename chainId -> chainID and tokenId -> tokenID in internals.
- Add package doc comments to builder, config, eip712, signer, model,
utils, and doc comments to every exported type, const, var, func,
method, and interface.
- Add trailing newlines to two auto-generated contract files to satisfy
gofmt.
- Replace interface{} with any and fmt.Sprintf+[]byte with fmt.Appendf
in eip712.
make lint -> 0 issues. make test -> all pass including py-clob-client-v2
golden vectors (byte-for-byte equality preserved across rename).
Two gaps that would break real consumers of the V2 migration:
1. The V1 abigen bindings in pkg/contracts/{exchange,exchange-fees,
neg-risk,neg-risk-fees} still encode the V1 Order tuple (taker,
expiration, nonce, feeRateBps) and V1-only methods (IncrementNonce,
HashOrder, etc.). With config.Exchange / NegRiskExchange now pointing
at the V2 deployments, any caller that combined GetContracts with
those bindings would send malformed calldata to the V2 contracts.
Remove the bindings; nothing in go-order-utils itself imports them,
and consumers needing V2 bindings should regenerate from the V2 ABI.
2. SignatureType POLY_1271 was exposed by the enum but BuildSignedOrder
always ran signer.ValidateSignature (ecrecover) against order.Signer,
which for the real 1271 case (Signer is a smart contract) can never
match the recovered EOA — every 1271 attempt returned "signature
error". Skip the local check for POLY_1271; the signature is
validated on-chain via isValidSignature at fill time. Matches
py-clob-client-v2 / clob-client-v2 behavior (neither locally verifies).
Adds a POLY_1271 golden vector from py-clob-client-v2 and a regression
test (TestBuildSignedOrder_POLY1271SkipsEcrecover) that exercises the
Signer != EOA path which errored pre-fix.
Drops pkg/contracts exclusion from .golangci.yaml and runs go mod tidy
to remove the now-unused transitive dependencies.
The Makefile pins golangci-lint v2.11.4 which requires Go >= 1.25 at install time (go install builds from source). The workflow was pinned to Go 1.21 (EOL since 2024-08) and would fail the lint step with "module requires Go 1.25.0 or later". Bumping CI to 1.25; the module's own go directive stays at 1.21 so library consumers aren't forced to upgrade.
- Regenerate pkg/contracts/exchange with abigen from the V2 CTFExchange ABI (source: @polymarket/clob-client-v2 src/order-utils/abi/ExchangeV2.ts). The same bytecode is deployed at both V2 addresses (standard 0xE111... and neg-risk 0xe222...), so a single binding package covers both — pass the address returned by config.GetContracts(...).Exchange or .NegRiskExchange. The generated Order struct matches the V2 signed layout exactly (salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata, builder, signature). V2 has no standalone fee-module contract — fees are operator-set at match time — so the V1 *-fees packages are not regenerated. - Bump module go directive to 1.26 (latest stable) and CI GO_VERSION to '1.26'. The previous 1.21 pin was EOL since 2024-08 and blocked installing the modern golangci-lint.
Renames the fork's module path so bg-prediction-market-api can pull it alongside the upstream V1 release (github.com/polymarket/go-order-utils v1.22.6) during the pre-cutover dual-stack window. All internal imports updated to the new path; no behavioral changes.
|
Thanks for putting together this PR @dkeysil, any idea when it'll be ready to merge? |
Author
|
@rchernobelskiy I don't work for Polymarket but we are using this fork in our product, so I can assure that it's working in our case. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migrates
go-order-utilsto CTF Exchange V2 ahead of the 2026-04-22 cutover. After cutover, V1 order signatures will be rejected by the operator (newORDER_TYPEHASH, new domain version, new contract addresses), so this library must be updated or it becomes non-functional.What changed
V2 order struct (EIP-712)
11 signed fields in this exact order:
ORDER_TYPEHASH = 0xbb86318a2138f5fa8ae32fbe8e659f8fcf13cc6ae4014a707893055433818589— pinned byTestOrderStructureHashV2against the Solidity constant inctf-exchange-v2.taker,expiration,nonce,feeRateBpstimestamp(unix ms, replacesnoncefor uniqueness),metadata(bytes32),builder(bytes32 builder code)"1"→"2"; name unchangedSignatureTypegainedPOLY_1271. For POLY_1271 ordersBuildSignedOrderskips the localecrecovercheck — the signature is validated on-chain viaisValidSignatureat fill time, matchingpy-clob-client-v2/@polymarket/clob-client-v2behavior.Contract addresses (same on Polygon mainnet and Amoy)
CTFExchange V2:0xE111180000d2663C0091e4f400237545B87B996BNegRisk CTFExchange V2:0xe2222d279d744050d28e00520010520000310F59Collateral→ pUSD0xC011a7E12a19f7B1f670d46F03B03f3342E82DFBContract bindings
pkg/contracts/exchangeregenerated viaabigenfrom the V2CTFExchangeABI in@polymarket/clob-client-v2(src/order-utils/abi/ExchangeV2.ts). Both V2 exchanges deploy the same bytecode, so one binding covers both deployments — callers pass the address fromconfig.GetContracts(...).Exchangeor.NegRiskExchange. V1*-feespackages are not regenerated: V2 removed the standalone fee-module contracts (fees are set at match time by the operator).Public API changes beyond the V2 struct shape
OrderData.Timestamp:string→int64(zero → use builder's timestamp generator)OrderData.Metadata/.Builder:string→common.HashOrder.Side/.SignatureType:*big.Int→uint8(ABI-native; saves two allocations per call)NewExchangeOrderBuilderImplgained a third positional argtimestampGenerator func() int64(nil defaults toutils.GenerateTimestampMs)Contracts.FeeModule/.NegRiskFeeModuleremoved (no V2 equivalent)Performance
EIP-712 domain separators are precomputed at builder construction and cached per
VerifyingContract, saving anabi.Pack+ keccak per signing call.Tooling
make lintpinsgolangci-lint/v2@v2.11.4(v1.54.2 no longer compiles on modern Go);.golangci.yamlmigrated to v2 schema.errcheck,govet,ineffassign,staticcheck[absorbedgosimplein v2],unused) +gofmt; addsgocyclo,misspell,revive. Two narrow revive exclusions:upperCaseConst: true(for existingPOLY_*/EOA/BUY/SELLconst names, which match the TS/Py SDKs) and a path-scoped"TokenId should be TokenID"suppression inpkg/model/order.go(field mirrors the JSON wire name).go.modand CIGO_VERSIONbumped to1.26. 1.21 was EOL since 2024-08 and blocked the modern linter install.Verification
TestOrderStructureHashV2pins the Go-computed hash to the Solidity constant.(order_hash, signature)vectors generated viascripts/gen_v2_vectors.pyand baked intoTestV2Goldens: CTF EOA, NegRisk EOA, CTF POLY_GNOSIS_SAFE, CTF POLY_1271, CTF EOA with non-zero metadata + builder. All pass.TestBuildSignedOrder_POLY1271SkipsEcrecoverexercises theSigner ≠ EOApath that errored before the local-validation fix.make lint→0 issues.make test→ green.go test -race -count=1 ./...→ green.go mod verify→ clean.V2 contracts aren't deployed yet (cutover is 2026-04-22), so end-to-end testing against a live exchange isn't possible pre-merge; parity with the reference Python SDK + independent
ORDER_TYPEHASHvalidation against Solidity are the strongest pre-deployment checks available.Test plan
lint-testpasses on Go 1.26ORDER_TYPEHASHvsctf-exchange-v2sourcepkg/contracts/exchangebindingsclob-v2.polymarket.compreprod (reviewer with Amoy creds)Consumer migration note
Downstream code using
OrderData{Taker, FeeRateBps, Nonce, Expiration}won't compile;timestamp/metadata/builderare optional (defaults: now-ms / zero / zero), so migration is mostly a field rename pass.Note
High Risk
Changes the cryptographic signing/hashing schema and public order/config types, so any mismatch with the on-chain V2 spec will invalidate signatures and break downstream integrations.
Overview
Updates the order builder to CTF Exchange V2 by changing the EIP-712 domain/version and signed struct fields (removing
taker/expiration/nonce/feeRateBps, addingtimestamp/metadata/builder, and switchingSide/SignatureTypetouint8), plus caching domain separators per verifying contract and skipping localecrecovervalidation forPOLY_1271signatures.Refreshes protocol configuration to V2 contract addresses (same exchange addresses on Polygon + Amoy), removes fee-module fields from
Contracts, and drops the generated V1exchange-feesbinding. Adds new tests that pinORDER_TYPEHASH, verify byte-for-byte golden vectors against the Python V2 client, and cover thePOLY_1271path + unsupported chain behavior.Modernizes tooling by bumping Go to
1.26, switching togolangci-lintv2 with a new config/formatter setup, updating CI accordingly, and changing the module path/imports togithub.com/dkeysil/go-order-utils.Reviewed by Cursor Bugbot for commit 9f2faa6. Bugbot is set up for automated code reviews on this repo. Configure here.