fix: redeemer executes via Gnosis Safe proxy wallet (same as MM bot)

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 <noreply@anthropic.com>
This commit is contained in:
direkturcrypto
2026-02-25 03:32:01 +07:00
parent e82c67824c
commit ec2c948a00
2 changed files with 19 additions and 27 deletions
+1 -1
View File
@@ -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
+18 -26
View File
@@ -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) {