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:
+1
-1
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user