Skip to content

Commit 9ed239c

Browse files
committed
fix: guard against threshold underflow, stale CCIP amounts, and undefined FS chain service
1 parent a69b293 commit 9ed239c

File tree

4 files changed

+79
-10
lines changed

4 files changed

+79
-10
lines changed

packages/poller/src/init.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,21 @@ export const initPoller = async (): Promise<{ statusCode: number; body: string }
112112

113113
logger.debug('Logging run mode of the instance', { runMode: process.env.RUN_MODE });
114114

115-
const rebalancer = getRegisteredRebalancers().find((r) => r.runMode === process.env.RUN_MODE);
115+
const runMode = process.env.RUN_MODE;
116+
const rebalancer = runMode ? getRegisteredRebalancers().find((r) => r.runMode === runMode) : undefined;
117+
118+
if (runMode && !rebalancer && runMode !== 'rebalanceOnly') {
119+
const validModes = getRegisteredRebalancers().map((r) => r.runMode);
120+
logger.error(`Unknown RUN_MODE "${runMode}". Valid modes: ${validModes.join(', ')}, rebalanceOnly`, {
121+
runMode,
122+
validModes,
123+
});
124+
return {
125+
statusCode: 400,
126+
body: JSON.stringify({ error: `Unknown RUN_MODE: ${runMode}` }),
127+
};
128+
}
129+
116130
if (rebalancer) {
117131
logger.info(`Starting ${rebalancer.displayName} rebalancing`, {
118132
stage: config.stage,

packages/poller/src/rebalance/mantleEth.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,8 +942,18 @@ async function processMethOperation(operation: RebalanceOperation, context: Proc
942942
operation.recipient!.toLowerCase() === config.methRebalance?.fillService?.address?.toLowerCase();
943943
const fillerSenderAddress =
944944
config.methRebalance?.fillService?.senderAddress ?? config.methRebalance?.fillService?.address;
945+
946+
if (isForFillService && !fillServiceChainService) {
947+
logger.warn('Fill service chain service not available for FS operation callback, skipping', {
948+
...logContext,
949+
recipientAddress: operation.recipient,
950+
note: 'fillServiceChainService may not be configured in this deployment',
951+
});
952+
return;
953+
}
954+
945955
const evmSender = isForFillService ? fillerSenderAddress! : config.ownAddress;
946-
const selectedChainService = isForFillService ? fillServiceChainService : chainService;
956+
const selectedChainService = isForFillService ? fillServiceChainService! : chainService;
947957

948958
// Check if ready for callback
949959
if (operation.status === RebalanceOperationStatus.PENDING) {

packages/poller/src/rebalance/solanaUsdc.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TransactionReceipt as ViemTransactionReceipt } from 'viem';
2-
import { safeParseBigInt } from '../helpers';
2+
import { safeParseBigInt, getEvmBalance } from '../helpers';
33
import { jsonifyError } from '@mark/logger';
44
import {
55
RebalanceOperationStatus,
@@ -709,14 +709,44 @@ async function executeLeg2And3(
709709

710710
const ptUsdeAddress = tokenPair.ptUSDe;
711711

712+
// Use actual USDC balance on Mainnet instead of operation.amount to account for
713+
// potential differences from CCIP fees or rounding during the cross-chain transfer
714+
let swapAmount = operation.amount;
715+
try {
716+
const usdcBalance = await getEvmBalance(
717+
rebalanceConfig,
718+
MAINNET_CHAIN_ID.toString(),
719+
recipient,
720+
usdcAddress,
721+
USDC_SOLANA_DECIMALS,
722+
context.prometheus,
723+
);
724+
const operationAmount = safeParseBigInt(operation.amount);
725+
if (usdcBalance < operationAmount) {
726+
logger.warn('Actual USDC balance on Mainnet is less than operation amount (CCIP fees/rounding)', {
727+
...logContext,
728+
operationAmount: operation.amount,
729+
actualBalance: usdcBalance.toString(),
730+
difference: (operationAmount - usdcBalance).toString(),
731+
});
732+
swapAmount = usdcBalance.toString();
733+
}
734+
} catch (balanceError) {
735+
logger.warn('Failed to check actual USDC balance on Mainnet, using operation amount', {
736+
...logContext,
737+
error: jsonifyError(balanceError),
738+
fallbackAmount: operation.amount,
739+
});
740+
}
741+
712742
logger.debug('Leg 2 Pendle swap details', {
713743
...logContext,
714744
storedRecipient,
715745
fallbackRecipient: rebalanceConfig.ownAddress,
716746
finalRecipient: recipient,
717747
usdcAddress,
718748
ptUsdeAddress,
719-
amountToSwap: operation.amount,
749+
amountToSwap: swapAmount,
720750
});
721751

722752
// Create route for USDC → ptUSDe swap on mainnet (same chain swap)
@@ -728,17 +758,17 @@ async function executeLeg2And3(
728758
};
729759

730760
// Get quote from Pendle for USDC → ptUSDe
731-
const receivedAmountStr = await pendleAdapter.getReceivedAmount(operation.amount, pendleRoute);
761+
const receivedAmountStr = await pendleAdapter.getReceivedAmount(swapAmount, pendleRoute);
732762

733763
logger.info('Received Pendle quote for USDC → ptUSDe swap', {
734764
...logContext,
735-
amountToSwap: operation.amount,
765+
amountToSwap: swapAmount,
736766
expectedPtUsde: receivedAmountStr,
737767
route: pendleRoute,
738768
});
739769

740770
// Execute the Pendle swap transactions
741-
const swapTxRequests = await pendleAdapter.send(recipient, recipient, operation.amount, pendleRoute);
771+
const swapTxRequests = await pendleAdapter.send(recipient, recipient, swapAmount, pendleRoute);
742772

743773
if (!swapTxRequests.length) {
744774
logger.error('No swap transactions returned from Pendle adapter', logContext);
@@ -874,9 +904,9 @@ async function executeLeg2And3(
874904
status: RebalanceOperationStatus.CANCELLED,
875905
});
876906

877-
logger.info('Marked operation as FAILED due to Leg 2/3 failure', {
907+
logger.info('Marked operation as CANCELLED due to Leg 2/3 failure', {
878908
...logContext,
879-
note: 'Funds are on Mainnet as USDC - manual intervention may be required',
909+
note: 'Funds are on Mainnet as USDC or ptUSDe (depending on which leg failed) - manual intervention required',
880910
});
881911
}
882912
}

packages/poller/src/rebalance/thresholdEngine.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ export async function runThresholdRebalance(
8888

8989
// 4. Threshold comparison
9090
const { threshold, target } = descriptor.getThresholds();
91+
92+
if (target < threshold) {
93+
logger.error(`${name} misconfiguration: target (${target.toString()}) is less than threshold (${threshold.toString()})`, {
94+
requestId,
95+
threshold: threshold.toString(),
96+
target: target.toString(),
97+
});
98+
return [];
99+
}
100+
91101
logger.info(`${name} threshold check`, {
92102
requestId,
93103
recipientBalance: recipientBalance.toString(),
@@ -106,7 +116,12 @@ export async function runThresholdRebalance(
106116
}
107117

108118
// 5. Compute shortfall and convert to bridge amount
109-
const shortfall = target - recipientBalance;
119+
// Clamp to 0n to guard against edge cases where recipientBalance > target but < threshold
120+
const shortfall = recipientBalance < target ? target - recipientBalance : 0n;
121+
if (shortfall === 0n) {
122+
logger.info(`${name} recipient balance above target, no shortfall`, { requestId });
123+
return [];
124+
}
110125
let bridgeAmount: bigint;
111126
try {
112127
bridgeAmount = await descriptor.convertShortfallToBridgeAmount(shortfall, context);

0 commit comments

Comments
 (0)