Skip to content

Commit b99c0a4

Browse files
committed
fix: bugs, logging and tests for rebalancer engines
1 parent abcacd2 commit b99c0a4

File tree

20 files changed

+1890
-935
lines changed

20 files changed

+1890
-935
lines changed

packages/adapters/rebalance/src/actions/dex-swap.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,15 @@ export class DexSwapActionHandler implements PostBridgeActionHandler {
149149
args: [sender as `0x${string}`, spender as `0x${string}`],
150150
});
151151

152-
if (allowance < swapAmount) {
152+
const approvalAmount = swapAmount + (swapAmount * BigInt(slippageBps)) / BigInt(10000);
153+
if (allowance < approvalAmount) {
153154
this.logger.info('DexSwap: building approval transaction', {
154155
sellToken,
155156
spender,
156157
currentAllowance: allowance.toString(),
157-
requiredAmount: swapAmount.toString(),
158+
approvalAmount: approvalAmount.toString(),
159+
swapAmount: swapAmount.toString(),
160+
slippageBps,
158161
destinationChainId,
159162
});
160163

@@ -165,7 +168,7 @@ export class DexSwapActionHandler implements PostBridgeActionHandler {
165168
data: encodeFunctionData({
166169
abi: erc20Abi,
167170
functionName: 'approve',
168-
args: [spender as `0x${string}`, swapAmount + (swapAmount * BigInt(slippageBps)) / BigInt(10000)],
171+
args: [spender as `0x${string}`, approvalAmount],
169172
}),
170173
value: BigInt(0),
171174
},
@@ -175,7 +178,7 @@ export class DexSwapActionHandler implements PostBridgeActionHandler {
175178
sellToken,
176179
spender,
177180
currentAllowance: allowance.toString(),
178-
requiredAmount: swapAmount.toString(),
181+
requiredAmount: approvalAmount.toString(),
179182
destinationChainId,
180183
});
181184
}

packages/adapters/rebalance/src/adapters/zircuit/zircuit.ts

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -408,40 +408,44 @@ export class ZircuitNativeBridgeAdapter implements BridgeAdapter {
408408
}
409409

410410
async isCallbackComplete(route: RebalanceRoute, originTransaction: TransactionReceipt): Promise<boolean> {
411-
const isL1ToL2 = route.origin === ETHEREUM_CHAIN_ID && route.destination === ZIRCUIT_CHAIN_ID;
412-
const isL2ToL1 = route.origin === ZIRCUIT_CHAIN_ID && route.destination === ETHEREUM_CHAIN_ID;
413-
if (!isL1ToL2 && !isL2ToL1) {
414-
throw new Error(`Unsupported Zircuit route: ${route.origin}->${route.destination}`);
415-
}
416-
if (isL1ToL2) {
417-
return true;
418-
}
411+
try {
412+
const isL1ToL2 = route.origin === ETHEREUM_CHAIN_ID && route.destination === ZIRCUIT_CHAIN_ID;
413+
const isL2ToL1 = route.origin === ZIRCUIT_CHAIN_ID && route.destination === ETHEREUM_CHAIN_ID;
414+
if (!isL1ToL2 && !isL2ToL1) {
415+
throw new Error(`Unsupported Zircuit route: ${route.origin}->${route.destination}`);
416+
}
417+
if (isL1ToL2) {
418+
return true;
419+
}
419420

420-
// L2→L1: complete only when finalized
421-
const l1Client = await this.getClient(ETHEREUM_CHAIN_ID);
422-
const l2Client = await this.getClient(ZIRCUIT_CHAIN_ID);
421+
// L2→L1: complete only when finalized
422+
const l1Client = await this.getClient(ETHEREUM_CHAIN_ID);
423+
const l2Client = await this.getClient(ZIRCUIT_CHAIN_ID);
423424

424-
const withdrawalTx = await this.extractWithdrawalTransaction(l2Client, originTransaction);
425-
if (!withdrawalTx) {
426-
// Cannot determine state — treat as complete to avoid stuck entries
427-
return true;
428-
}
425+
const withdrawalTx = await this.extractWithdrawalTransaction(l2Client, originTransaction);
426+
if (!withdrawalTx) {
427+
// Cannot determine state — treat as complete to avoid stuck entries
428+
return true;
429+
}
429430

430-
const withdrawalHash = this.hashWithdrawal(withdrawalTx);
431-
const isFinalized = await l1Client.readContract({
432-
address: ZIRCUIT_OPTIMISM_PORTAL as `0x${string}`,
433-
abi: zircuitOptimismPortalAbi,
434-
functionName: 'finalizedWithdrawals',
435-
args: [withdrawalHash],
436-
});
431+
const withdrawalHash = this.hashWithdrawal(withdrawalTx);
432+
const isFinalized = await l1Client.readContract({
433+
address: ZIRCUIT_OPTIMISM_PORTAL as `0x${string}`,
434+
abi: zircuitOptimismPortalAbi,
435+
functionName: 'finalizedWithdrawals',
436+
args: [withdrawalHash],
437+
});
437438

438-
this.logger.info('Zircuit isCallbackComplete check', {
439-
txHash: originTransaction.transactionHash,
440-
withdrawalHash,
441-
isFinalized,
442-
});
439+
this.logger.info('Zircuit isCallbackComplete check', {
440+
txHash: originTransaction.transactionHash,
441+
withdrawalHash,
442+
isFinalized,
443+
});
443444

444-
return isFinalized as boolean;
445+
return isFinalized as boolean;
446+
} catch (error) {
447+
this.handleError(error, 'check if callback is complete', { route, originTransaction });
448+
}
445449
}
446450

447451
private async getClient(chainId: number): Promise<PublicClient> {

packages/adapters/rebalance/test/actions/dex-swap.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ describe('DexSwapActionHandler', () => {
236236
const customEstOutput = '9999999999999999999';
237237
mockReadContract
238238
.mockResolvedValueOnce(BigInt(1000000) as never) // balanceOf
239-
.mockResolvedValueOnce(BigInt(1000000) as never); // allowance (sufficient)
239+
.mockResolvedValueOnce(BigInt(1010000) as never); // allowance (sufficient including slippage padding)
240240

241241
axiosPostMock.mockResolvedValueOnce({
242242
data: {

packages/agent/src/adapters.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export function initializeBaseAdapters(
7070
const fsSenderAddress =
7171
config.tacRebalance?.fillService?.senderAddress ??
7272
config.tacRebalance?.fillService?.address ??
73+
config.methRebalance?.fillService?.senderAddress ??
74+
config.methRebalance?.fillService?.address ??
7375
config.aManUsdeRebalance?.fillService?.senderAddress ??
7476
config.aManUsdeRebalance?.fillService?.address ??
7577
config.aMansyrupUsdtRebalance?.fillService?.senderAddress ??

packages/poller/src/rebalance/aaveTokenRebalancer.ts

Lines changed: 29 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ import {
1717
} from '@mark/core';
1818
import { ProcessingContext } from '../init';
1919
import { submitTransactionWithLogging } from '../helpers/transactions';
20-
import { RebalanceTransactionMemo, buildTransactionsForAction } from '@mark/rebalance';
21-
import { createRebalanceOperation, TransactionReceipt } from '@mark/database';
20+
import { buildTransactionsForAction } from '@mark/rebalance';
21+
import { TransactionReceipt } from '@mark/database';
2222
import { getBridgeTypeFromTag } from './helpers';
2323
import { RebalanceRunState } from './types';
2424
import { runThresholdRebalance, ThresholdRebalanceDescriptor } from './thresholdEngine';
2525
import { runCallbackLoop, RebalanceOperation } from './callbackEngine';
26+
import { executeEvmBridge } from './bridgeExecution';
2627

2728
/**
2829
* Descriptor that parameterizes the generic Aave token rebalancer for a specific flow.
@@ -252,13 +253,12 @@ export const executeStargateBridgeForAaveToken = async (
252253
const { config, chainService, fillServiceChainService, logger, requestId, rebalance } = context;
253254
const tokenConfig = descriptor.getConfig(config)!;
254255
const bridgeConfig = tokenConfig.bridge;
255-
const actions: RebalanceAction[] = [];
256256

257257
const bridgeType = SupportedBridge.Stargate;
258258
const adapter = rebalance.getAdapter(bridgeType);
259259
if (!adapter) {
260260
logger.error('Stargate adapter not found', { requestId });
261-
return actions;
261+
return [];
262262
}
263263

264264
// Select the correct chain service based on whether the sender is the fill service address
@@ -273,7 +273,7 @@ export const executeStargateBridgeForAaveToken = async (
273273
senderAddress,
274274
fillerSenderAddress,
275275
});
276-
return actions;
276+
return [];
277277
}
278278

279279
const sourceTokenAddress = getTokenAddressFromConfig(descriptor.sourceTokenTickerHash, MAINNET_CHAIN_ID, config)!;
@@ -300,122 +300,37 @@ export const executeStargateBridgeForAaveToken = async (
300300
});
301301

302302
try {
303-
// Get quote
304-
const receivedAmountStr = await adapter.getReceivedAmount(amount.toString(), route);
305-
logger.info('Received Stargate quote', {
306-
requestId,
307-
amountToBridge: amount.toString(),
308-
receivedAmount: receivedAmountStr,
309-
});
310-
311-
// Check slippage
312-
const receivedAmount = BigInt(receivedAmountStr);
313-
const slippage = BigInt(slippageDbps);
314-
const minimumAcceptableAmount = amount - (amount * slippage) / DBPS_MULTIPLIER;
315-
316-
if (receivedAmount < minimumAcceptableAmount) {
317-
logger.warn('Stargate quote does not meet slippage requirements', {
318-
requestId,
319-
amountToBridge: amount.toString(),
320-
receivedAmount: receivedAmount.toString(),
321-
minimumAcceptableAmount: minimumAcceptableAmount.toString(),
322-
slippageDbps,
323-
});
324-
return actions;
325-
}
326-
327-
// Get bridge transactions
328-
const bridgeTxRequests = await adapter.send(senderAddress, recipientAddress, amount.toString(), route);
329-
if (!bridgeTxRequests.length) {
330-
logger.error('No bridge transactions returned from Stargate adapter', { requestId });
331-
return actions;
332-
}
333-
334-
logger.info('Prepared Stargate bridge transactions', {
335-
requestId,
336-
transactionCount: bridgeTxRequests.length,
337-
});
338-
339-
// Execute bridge transactions
340-
let receipt: TransactionReceipt | undefined;
341-
let effectiveBridgedAmount = amount.toString();
342-
343-
for (const { transaction, memo, effectiveAmount } of bridgeTxRequests) {
344-
logger.info('Submitting Stargate bridge transaction', {
345-
requestId,
346-
memo,
347-
to: transaction.to,
348-
});
349-
350-
const result = await submitTransactionWithLogging({
351-
chainService: selectedChainService,
352-
logger,
353-
chainId: MAINNET_CHAIN_ID,
354-
txRequest: {
355-
to: transaction.to!,
356-
data: transaction.data!,
357-
value: (transaction.value || 0).toString(),
358-
chainId: Number(MAINNET_CHAIN_ID),
359-
from: senderAddress,
360-
funcSig: transaction.funcSig || '',
361-
},
362-
zodiacConfig: { walletType: WalletType.EOA },
363-
context: { requestId, route, bridgeType, transactionType: memo },
364-
});
365-
366-
logger.info('Successfully submitted Stargate bridge transaction', {
367-
requestId,
368-
memo,
369-
transactionHash: result.hash,
370-
});
371-
372-
if (memo === RebalanceTransactionMemo.Rebalance) {
373-
receipt = result.receipt! as unknown as TransactionReceipt;
374-
if (effectiveAmount) {
375-
effectiveBridgedAmount = effectiveAmount;
376-
}
377-
}
378-
}
379-
380-
// Create database record
381-
await createRebalanceOperation({
382-
earmarkId: null,
383-
originChainId: route.origin,
384-
destinationChainId: route.destination,
385-
tickerHash: descriptor.sourceTokenTickerHash,
386-
amount: effectiveBridgedAmount,
387-
slippage: slippageDbps,
388-
status: RebalanceOperationStatus.PENDING,
389-
bridge: descriptor.bridgeTag,
390-
transactions: receipt ? { [MAINNET_CHAIN_ID]: receipt } : undefined,
391-
recipient: recipientAddress,
392-
});
393-
394-
logger.info(`Successfully created ${descriptor.name} rebalance operation`, {
395-
requestId,
396-
originTxHash: receipt?.transactionHash,
397-
amountToBridge: effectiveBridgedAmount,
398-
bridge: descriptor.bridgeTag,
399-
});
400-
401-
actions.push({
402-
bridge: bridgeType,
403-
amount: amount.toString(),
404-
origin: route.origin,
405-
destination: route.destination,
406-
asset: route.asset,
407-
transaction: receipt?.transactionHash || '',
303+
const result = await executeEvmBridge({
304+
context,
305+
adapter,
306+
route,
307+
amount,
308+
sender: senderAddress,
408309
recipient: recipientAddress,
310+
slippageTolerance: BigInt(slippageDbps),
311+
slippageMultiplier: DBPS_MULTIPLIER,
312+
chainService: selectedChainService,
313+
senderConfig: {
314+
address: senderAddress,
315+
label: isFillerSender ? 'fill-service' : 'market-maker',
316+
},
317+
dbRecord: {
318+
earmarkId: null,
319+
tickerHash: descriptor.sourceTokenTickerHash,
320+
bridgeTag: descriptor.bridgeTag,
321+
status: RebalanceOperationStatus.PENDING,
322+
},
323+
label: `Stargate ${descriptor.name}`,
409324
});
325+
return result.actions;
410326
} catch (error) {
411327
logger.error(`Failed to execute Stargate bridge for ${descriptor.name}`, {
412328
requestId,
413329
route,
414330
error: jsonifyError(error),
415331
});
332+
return [];
416333
}
417-
418-
return actions;
419334
};
420335

421336
/**
@@ -468,11 +383,12 @@ async function processAaveTokenOperation(
468383
const selectedChainService = isForFillService && fillServiceChainService ? fillServiceChainService : chainService;
469384

470385
if (isForFillService && !fillServiceChainService) {
471-
logger.warn(`Fill service chain service not available for ${descriptor.name} callback, using main chain service`, {
386+
logger.error(`Fill service chain service not available for ${descriptor.name} callback, skipping`, {
472387
...logContext,
473388
recipient: operation.recipient,
474389
fsAddress,
475390
});
391+
return;
476392
}
477393

478394
const bridgeType = operation.bridge ? getBridgeTypeFromTag(operation.bridge) : undefined;

0 commit comments

Comments
 (0)