fix(polymarket): 3 deposit-wallet path bugs in v0.6.0 (redeem / auth / rfq)#3
Merged
skylavis-sky merged 3 commits intoMay 5, 2026
Conversation
…ding pUSD
Bug: redeem.rs `run` (L493) and `run_all` (L606) hardcoded
`Contracts::PUSD` as the collateral_addr passed into ctf_redeem_via_proxy /
ctf_redeem_positions / ctf_redeem_via_deposit_wallet. The original code had
a comment "Auto-detect collateral: V2 markets use pUSD, V1 use USDC.e"
followed by a literal `let collateral_addr = Contracts::PUSD;` — i.e. the
auto-detect was never implemented.
Why this silently no-ops on chain (status=0x1, gas ~35k, but 0 burn / 0 payout):
CTF derives positionId = keccak256(collateralToken, collectionId).
Polymarket V2 markets keep CTF collateral as USDC.e; the V2 cutover was at
the CLOB layer (pUSD pricing), not at the CTF layer. With pUSD passed in,
CTF computes a positionId the wallet does not hold, balanceOf returns 0,
redeemPositions silently no-ops without reverting. Plugin reports success
based on tx receipt status=0x1 alone, leaving winning shares unredeemed
and ~0.05 POL of gas spent per call.
Live evidence — before fix: 7 redeem(--all) txs all status=0x1, all
~35k gas, zero PayoutRedemption / Transfer events emitted, all winning
shares still in proxy. After fix: re-ran the same redeems, txs status=0x1,
~150k gas, TransferSingle (CTF burn) + ERC-20 Transfer (USDC.e payout) —
$7 of winnings recovered. Sample tx 0xbf3f1d72...3012 (BTC $150k April,
proxy redeem) and 0x441c6398...b173 (BTC 5m 04-28, proxy redeem).
Fix:
src/onchainos.rs (+143 lines, no behavior change to existing fns):
+ get_ctf_balance_hex(owner, position_id_hex_64chars) — variant of
get_ctf_balance taking hex token id (no decimal conversion churn)
+ ctf_get_collection_id_hex(parent, cond, idx) — eth_call CTF view fn
(CTF's collectionId derivation uses BN254 curve point math
internally; on-chain delegation avoids a heavy local crypto dep)
+ ctf_get_position_id_hex(collateral, collection_id) — eth_call CTF
view fn returning the canonical positionId for a (collateral,
collection) pair
src/commands/redeem.rs:
+ detect_collateral_for_position(condition_id, candidate_wallets) —
probes USDC.e and pUSD CTF positionIds against each candidate
wallet via balanceOf; returns the collateral whose computed
positionId yields a non-zero balance on at least one wallet.
Bails with NO_BALANCE_FOUND if neither matches.
- run_all (L606): replaces hardcoded PUSD with detect_collateral_for_position,
passes detected collateral to redeem_one. Detection failure is
reported per-position with COLLATERAL_NOT_DETECTED error_code so the
surrounding loop continues for other positions.
- run (L493): same replacement for the single-market path.
+ Logs "[polymarket] Detected CTF collateral: <symbol> (<address>)"
after detection so the resolved collateral is auditable in stderr
without parsing tx logs.
Test plan:
- cargo build --release: clean (27 warnings unchanged from baseline,
0 errors, 17.5s incremental).
- Live redeem of 7 historical winning positions (proxy + EOA paths) —
all confirmed real CTF burn + USDC.e payout to the holding wallet.
- Live redeem of 1 deposit-wallet winning position (V2 deposit-wallet
path via ctf_redeem_via_deposit_wallet) — confirmed gasless
relayer-batch redeem, USDC.e payout to deposit wallet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…obbering with PolyProxy
Bug: ensure_credentials_for() in auth.rs runs auto-detection of
proxy_wallet / deposit_wallet on every call, then assigns
creds.mode based on what was found in this run, then save_credentials.
The detection at L438 had `creds.proxy_wallet.is_none() &&
creds.deposit_wallet.is_none()` (AND) — so if the user had a proxy,
deposit_wallet detection was skipped. Mode then defaulted to PolyProxy.
Effect: any wallet that has both a proxy (from V1 era) AND a deposit
wallet (set up via setup-deposit-wallet) silently has its
creds.mode reset to PolyProxy on every call to buy / sell / redeem /
orders / cancel / rfq / balance — i.e. the deposit wallet is never
actually used for trading. setup-deposit-wallet's mode write is wiped
on the next ensure_credentials call. switch-mode --mode deposit-wallet
also gets clobbered the same way.
Live evidence — before fix:
$ polymarket-plugin setup-deposit-wallet
... mode set to deposit_wallet, persisted ...
$ polymarket-plugin balance # invokes ensure_credentials_for
$ cat ~/.config/polymarket/creds.json | jq .mode
"poly_proxy" # ← mode silently reset
$ polymarket-plugin buy --market-id ... --outcome Up --amount 1
[polymarket] Using POLY_PROXY mode — maker: 0x3f8a...aa7e
# ← Used proxy, not deposit_wallet. Deposit wallet is dead.
After fix:
$ polymarket-plugin switch-mode --mode deposit-wallet
$ polymarket-plugin buy --market-id ... --outcome Up --amount 1
[polymarket] Using DEPOSIT_WALLET mode — maker: 0x99ad...4ae4
[polymarket] Wrapping via WALLET batch (gasless)...
... order matched ... # ← Real deposit_wallet path engaged
Fix:
src/auth.rs::ensure_credentials_for:
+ Load existing saved mode (if any) before auto-detection so we
can respect explicit user choice.
- Decouple deposit_wallet detection from proxy_wallet absence —
a wallet can hold both. Detect each independently.
- Mode selection becomes:
match saved_mode {
DepositWallet if deposit_wallet present => DepositWallet,
PolyProxy if proxy present => PolyProxy,
Eoa => Eoa,
_ /* no compatible saved mode */ => auto-pick,
}
Auto-pick prefers DepositWallet over PolyProxy when both are
detected, since deposit_wallet is the v0.6.0+ default for new
users and is gasless.
Test plan:
- cargo build --release: clean.
- Verified saved mode survives across ensure_credentials calls:
setup-deposit-wallet → balance → buy now correctly stays in
DepositWallet mode end-to-end.
- PolyProxy users (no deposit wallet) unaffected — saved_mode =
PolyProxy + proxy present → returns PolyProxy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t (sig_type=3)
Bug: rfq.rs `confirm` flow constructs OrderParamsV2 with
signer = signer_addr (EOA) regardless of trading mode and always
calls sign_order_v2_via_onchainos (plain EIP-712). This breaks
DepositWallet mode in two distinct ways:
1. signer mismatch: CLOB validates `order.signer ==
deposit_wallet_of(API_KEY)` for sig_type=3, so EOA != deposit
wallet causes the CLOB to reject before any signature check.
2. signature format mismatch: even if signer matched, the deposit
wallet's isValidSignature (Solady ERC-1271 implementation) re-hashes
via the ERC-7739 TypedDataSign envelope internally. A plain EIP-712
signature over the raw order hash does not validate. Per v0.6.0
CHANGELOG: "Plain EIP-712 over the order hash fails because the
deposit wallet's isValidSignature re-hashes via the TypedDataSign
envelope internally."
sell.rs / buy.rs already implement the correct conditional pattern at
L509-534 (sell). rfq.rs was missed in the v0.6.0 deposit-wallet rollout
(no live regression observed because RFQ minimum size is typically large
enough that few users have hit this path).
Additional cleanup: line 140 had `let _clob_version = ...` (unused
variable) followed by a comment "RFQ is a V2-only feature" but no
guard. If the CLOB reports V1 (e.g. during cutover delay), the code
still produces a V2-signed order the V1 server cannot validate. Replace
with an explicit `bail!` when version != 2 so the error surfaces clearly.
Fix:
src/commands/rfq.rs:
+ bail! if get_clob_version() != 2 instead of silently routing
a V2-signed order to a V1 server.
+ order_signer = if mode == DepositWallet { maker_addr (= deposit
wallet) } else { signer_addr (= EOA) }, mirroring sell.rs L509.
+ Conditional sign function: sign_order_v2_poly1271_via_onchainos
for DepositWallet (ERC-7739 envelope), sign_order_v2_via_onchainos
otherwise (plain EIP-712), mirroring sell.rs L530.
- OrderParamsV2.signer and OrderBodyV2.signer now use order_signer
(was hardcoded signer_addr).
Test plan:
- cargo build --release: clean.
- Cannot live-validate cheaply (RFQ block trades have large minimums
and quotes expire in ~30s). Verified by code inspection that the
fixed rfq.rs branch logic is identical to sell.rs's already-tested
deposit-wallet path. The Solady ERC-7739 wire format used by
sign_order_v2_poly1271_via_onchainos is the same one
setup-deposit-wallet's batch signing exercises successfully on
chain (deposit wallet 0x99ad...4ae4 was deployed + 5 approvals
batched + signature verified by ERC-1271).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e3ac876
into
skylavis-sky:feat/polymarket-v0.6.0-deposit-wallet
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
End-to-end validation of v0.6.0 deposit-wallet flow surfaced 3 separate bugs where the deposit-wallet code path silently degrades or fails. All 3 are scoped narrowly and don't touch the EOA / PolyProxy paths. Live-tested on Polygon mainnet against my own wallet — see "Live evidence" sections below for tx hashes and on-chain proof.
The 3 bugs are independent — each commit can be reviewed and merged separately.
Bug 1 —
redeem.rshardcodedContracts::PUSD→ silent on-chain no-opSymptom:
redeem/redeem --allreturnsredeemed_count: N, error_count: 0, all txs status=0x1, ~35k gas each, but no PayoutRedemption / Transfer events emit, no winning shares get burned, no collateral is paid out.Root cause: The
Auto-detect collateralcomment atredeem.rs:81andredeem.rs:492was followed bylet collateral_addr = Contracts::PUSD;— the auto-detect was never implemented. CTF derivespositionId = keccak256(collateralToken, collectionId). Polymarket V2's pUSD is a CLOB-layer wrapper; the underlying CTF positions still use USDC.e collateral. With pUSD passed in, CTF computes a positionId the wallet doesn't hold, balanceOf returns 0, redeemPositions silently no-ops without reverting.Live evidence — pre-fix (7 redeem txs all silently no-op):
0x5e6128e8...a97c5(BTC $150k April EOA)0xcbcea252...5980d(BTC $150k April proxy)balanceOf(proxy, winning_token_id)post-tx still showed4_999_280and2_000_000raw — shares were never burnt. ~0.36 POL of gas spent for nothing.Live evidence — post-fix (re-ran same redemptions):
0xbf3f1d72...3012(BTC $150k April proxy)0x441c6398...b173(BTC 5m 04-28 proxy)balanceOf(proxy, winning_token_id)post-tx: 0. $7 of winnings recovered (no real loss because shares were preserved on chain — only the wasted gas was lost).Bug 2 —
auth.rsignores saved DepositWallet mode → wallet has both proxy+deposit_wallet → mode silently reset to PolyProxy on every commandSymptom: After
setup-deposit-wallet,creds.jsonhasmode: deposit_wallet. Runpolymarket-plugin balance(or any command),creds.jsonnow hasmode: poly_proxy.polymarket-plugin buyuses proxy as maker, not deposit wallet.switch-mode --mode deposit-walletis also clobbered on next command. Deposit wallet is effectively dead for any user who already has a proxy (i.e. every existing V1 user).Root cause:
ensure_credentials_for()re-runs auto-detection on every call. Detection at L438 hadif creds.proxy_wallet.is_none() && creds.deposit_wallet.is_none()(AND), so if a proxy is detected, deposit_wallet detection is skipped. Then mode is computed from in-memory state and saved — overwriting whatever mode the user set viasetup-deposit-walletorswitch-mode.Live evidence — pre-fix:
Live evidence — post-fix:
Bug 3 —
rfq.rsDepositWallet sign path wrong (signer + sign function)Symptom: RFQ confirm in DepositWallet mode produces an order rejected by CLOB (signature invalid).
Root cause: Two issues compound:
signer: signer_addr.clone()— hardcoded EOA. CLOB sig_type=3 requiresorder.signer == deposit_wallet_of(API_KEY). EOA != deposit wallet, so CLOB rejects before any signature check.sign_order_v2_via_onchainos(plain EIP-712). Per v0.6.0 CHANGELOG: "Plain EIP-712 over the order hash fails because the deposit wallet'sisValidSignaturere-hashes via the TypedDataSign envelope internally." Should usesign_order_v2_poly1271_via_onchainos(Solady ERC-7739 wire format) for sig_type=3.sell.rsandbuy.rsalready have the correct conditional pattern (sell.rs L509-534).rfq.rswas missed during the v0.6.0 deposit-wallet rollout — likely no live regression observed because RFQ has large minimum sizes and few users have hit this path yet.Bonus cleanup: L140 had
let _clob_version = ...(unused) with a comment "RFQ is a V2-only feature" but no actual version guard. Fixed by adding an explicitbail!when version != 2.Test plan: Cannot live-validate cheaply (RFQ block trade minimums are large + quotes expire in ~30s). Verified by code inspection that the post-fix branch logic is identical to sell.rs's already-tested deposit-wallet path. The Solady ERC-7739 envelope used by
sign_order_v2_poly1271_via_onchainosis the same onesetup-deposit-walletexercises successfully on chain (deposit wallet0x99ad...4ae4was deployed + 5 approvals batched + signatures verified by ERC-1271).Complete e2e validation cycle
Live-tested the full deposit-wallet flow after applying all 3 fixes:
Total POL spent by user EOA: ~$0.02 (one ERC-20 transfer). All other operations (wrap, buy, sell, redeem) were gasless via relayer.
Files changed
src/onchainos.rssrc/commands/redeem.rssrc/auth.rssrc/commands/rfq.rscargo build --release: clean (27 warnings unchanged from baseline, 0 errors, 17.5s incremental from atouchrebuild).Suggested merge strategy
The 3 commits are independent. Suggested order: merge Bug 2 first (auth.rs is foundational — without it, Bug 1's deposit-wallet path can't even be reached); then Bug 1 (most live-tested); then Bug 3 (code-inspection only).