From ec2c948a00cd0c7ef80074ec5fffd01accd98580 Mon Sep 17 00:00:00 2001 From: direkturcrypto Date: Wed, 25 Feb 2026 03:32:01 +0700 Subject: [PATCH] fix: redeemer executes via Gnosis Safe proxy wallet (same as MM bot) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit redeemer.js was calling redeemPositions() directly from the EOA private key wallet. This is wrong for two reasons: 1. Conditional tokens are held in config.proxyWallet (Gnosis Safe), not in the EOA — so the EOA has nothing to redeem 2. No maxPriorityFeePerGas was set, causing Polygon's "gas tip below minimum" error (1.5 Gwei sent, 25 Gwei required) Fix: replace the direct contract call with execSafeCall() (exported from ctf.js), which is identical to the MM bot's redemption pattern: - encodes calldata and executes via Safe.execTransaction() - signed by the EOA, but msg.sender on-chain = proxy wallet - enforces 30 Gwei minimum priority fee for Polygon - retries up to 3x on transient RPC errors Also remove the now-unused NEG_RISK_CTF_ADDRESS local constant (CTF_ADDRESS and USDC_ADDRESS imported from ctf.js). Co-Authored-By: Claude Sonnet 4.6 --- src/services/ctf.js | 2 +- src/services/redeemer.js | 44 ++++++++++++++++------------------------ 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/services/ctf.js b/src/services/ctf.js index ee2a6fd..45db1ea 100644 --- a/src/services/ctf.js +++ b/src/services/ctf.js @@ -105,7 +105,7 @@ let _txQueue = Promise.resolve(); * Calls are serialized via an internal queue so nonces never collide. * Retries up to MAX_RETRIES times on transient errors. */ -function execSafeCall(to, data, description = '') { +export function execSafeCall(to, data, description = '') { // Enqueue: this call will only start after the previous one resolves/rejects const result = _txQueue.then(() => _doExecSafeCall(to, data, description)); // Don't let a failure poison the queue for subsequent calls diff --git a/src/services/redeemer.js b/src/services/redeemer.js index 02ed359..5271b00 100644 --- a/src/services/redeemer.js +++ b/src/services/redeemer.js @@ -1,16 +1,12 @@ import { ethers } from 'ethers'; import config from '../config/index.js'; import { getPolygonProvider } from './client.js'; +import { execSafeCall, CTF_ADDRESS, USDC_ADDRESS } from './ctf.js'; import { getOpenPositions, removePosition } from './position.js'; import { recordSimResult } from '../utils/simStats.js'; import logger from '../utils/logger.js'; -// Contract addresses on Polygon -const CTF_ADDRESS = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'; -const NEG_RISK_CTF_ADDRESS = '0xC5d563A36AE78145C45a50134d48A1215220f80a'; -const USDC_ADDRESS = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'; - -// CTF ABI (minimal for redeemPositions & balanceOf) +// CTF ABI (minimal — read-only calls only; writes go through execSafeCall) const CTF_ABI = [ 'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)', 'function balanceOf(address owner, uint256 tokenId) view returns (uint256)', @@ -67,33 +63,29 @@ async function checkOnChainPayout(conditionId) { } /** - * Redeem winning position on-chain (real mode only) + * Redeem winning position on-chain via the Gnosis Safe proxy wallet. + * Uses execSafeCall (same as MM bot) so: + * - tx is signed by the EOA but executed FROM the proxy wallet + * - Polygon 30 Gwei minimum tip is enforced + * - automatic retry on transient errors */ -async function redeemPosition(conditionId, isNegRisk = false) { +async function redeemPosition(conditionId) { try { - const provider = await getPolygonProvider(); - const wallet = new ethers.Wallet(config.privateKey, provider); - const ctfAddress = isNegRisk ? NEG_RISK_CTF_ADDRESS : CTF_ADDRESS; - const ctf = new ethers.Contract(ctfAddress, CTF_ABI, wallet); - - const parentCollectionId = ethers.constants.HashZero; - const indexSets = [1, 2]; - - logger.info(`Redeeming position: ${conditionId}`); - const tx = await ctf.redeemPositions( + const ctfIface = new ethers.utils.Interface(CTF_ABI); + const data = ctfIface.encodeFunctionData('redeemPositions', [ USDC_ADDRESS, - parentCollectionId, + ethers.constants.HashZero, conditionId, - indexSets, - { gasLimit: 300000 }, - ); + [1, 2], + ]); - logger.info(`Redeem tx: ${tx.hash}`); - const receipt = await tx.wait(); + const label = conditionId.slice(0, 12) + '...'; + logger.info(`Redeeming position: ${label}`); + const receipt = await execSafeCall(CTF_ADDRESS, data, `redeemPositions ${label}`); logger.success(`Redeemed in block ${receipt.blockNumber}`); return true; } catch (err) { - logger.error('Failed to redeem:', err.message); + logger.error(`Failed to redeem: ${err.message}`); return false; } } @@ -168,7 +160,7 @@ export async function checkAndRedeemPositions() { const success = await redeemPosition(position.conditionId); if (success) { removePosition(position.conditionId); - logger.money(`Redeemed: ${position.market}`); + logger.money(`Redeemed: ${position.market} → USDC recovered`); } } } catch (err) {