diff --git a/package-lock.json b/package-lock.json index a94cb80..636a1c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "dotenv": "^16.4.7", "ethers": "^5.8.0", "https-proxy-agent": "^7.0.6", + "undici": "^7.22.0", "ws": "^8.19.0" }, "devDependencies": { @@ -1828,6 +1829,15 @@ "dev": true, "license": "MIT" }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index aaf7d1e..9986f31 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "dotenv": "^16.4.7", "ethers": "^5.8.0", "https-proxy-agent": "^7.0.6", + "undici": "^7.22.0", "ws": "^8.19.0" }, "devDependencies": { diff --git a/src/services/ctf.js b/src/services/ctf.js index 8672158..1592dd1 100644 --- a/src/services/ctf.js +++ b/src/services/ctf.js @@ -118,6 +118,9 @@ async function _doExecSafeCall(to, data, description = '') { if (description) logger.info(`MM: exec safe tx — ${description}`); let lastErr; + // Track gas price multiplier for replacement transactions + let gasMultiplier = 1; + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { const provider = await getPolygonProvider(); @@ -149,9 +152,15 @@ async function _doExecSafeCall(to, data, description = '') { // Polygon requires maxPriorityFeePerGas ≥ 25 Gwei. // Some RPC nodes (e.g. lava.build) return a stale low estimate, so we enforce a floor. const feeData = await provider.getFeeData(); - const MIN_TIP = ethers.utils.parseUnits('30', 'gwei'); - const gasTip = feeData.maxPriorityFeePerGas?.gt(MIN_TIP) ? feeData.maxPriorityFeePerGas : MIN_TIP; - const gasFeeCap = feeData.maxFeePerGas ?? ethers.utils.parseUnits('500', 'gwei'); + + // Increase gas price on retry to replace pending transaction + // Multiplier: 1x → 2x → 4x on subsequent attempts + const MIN_TIP = ethers.utils.parseUnits('30', 'gwei').mul(Math.ceil(gasMultiplier)); + const gasTip = feeData.maxPriorityFeePerGas?.mul(Math.ceil(gasMultiplier)).gt(MIN_TIP) + ? feeData.maxPriorityFeePerGas.mul(Math.ceil(gasMultiplier)) + : MIN_TIP; + const gasFeeCap = (feeData.maxFeePerGas ?? ethers.utils.parseUnits('500', 'gwei')) + .mul(Math.ceil(gasMultiplier * 1.5)); const tx = await safe.execTransaction( to, 0, data, 0, 0, 0, 0, @@ -169,7 +178,14 @@ async function _doExecSafeCall(to, data, description = '') { const friendly = parseOnchainError(err); if (attempt < MAX_RETRIES) { - logger.warn(`MM: transaction failed (attempt ${attempt}/${MAX_RETRIES}): ${friendly} — retrying in ${RETRY_DELAY / 1000}s...`); + // Increase gas multiplier for replacement transaction + if (err?.message?.includes('replacement transaction underpriced') || + err?.message?.includes('Gas price too low to replace')) { + gasMultiplier *= 2; + logger.warn(`MM: transaction failed (attempt ${attempt}/${MAX_RETRIES}): ${friendly} — increasing gas ${gasMultiplier}x and retrying...`); + } else { + logger.warn(`MM: transaction failed (attempt ${attempt}/${MAX_RETRIES}): ${friendly} — retrying in ${RETRY_DELAY / 1000}s...`); + } await sleep(RETRY_DELAY); } }