Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"aws:setup": "npx ts-node scripts/aws-shard-setup.ts --manifest",
"build": "yarn workspaces foreach -Av --parallel --topological-dev run build",
"clean": "yarn workspaces foreach -A --parallel run clean",
"e2e:rebalance": "yarn workspace @mark/poller e2e",
"gcp:setup": "npx ts-node scripts/gcp-shard-setup.ts --manifest",
"lint": "yarn lint:package && yarn workspaces foreach -A --parallel run lint",
"lint:fix": "yarn workspaces foreach -A --parallel run lint:fix",
Expand All @@ -42,14 +43,6 @@
"dependencies": {
"@types/node": "20.17.12"
},
"dependenciesMeta": {
"@eth-optimism/contracts-bedrock": {
"built": false
},
"@eth-optimism/core-utils": {
"built": false
}
},
"devDependencies": {
"@commitlint/cli": "19.6.1",
"@commitlint/config-conventional": "19.6.0",
Expand All @@ -74,5 +67,13 @@
"tsc-alias": "1.8.10",
"typescript": "5.7.2"
},
"dependenciesMeta": {
"@eth-optimism/contracts-bedrock": {
"built": false
},
"@eth-optimism/core-utils": {
"built": false
}
},
"packageManager": "yarn@3.3.1"
}
11 changes: 7 additions & 4 deletions packages/adapters/rebalance/src/actions/dex-swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,15 @@ export class DexSwapActionHandler implements PostBridgeActionHandler {
args: [sender as `0x${string}`, spender as `0x${string}`],
});

if (allowance < swapAmount) {
const approvalAmount = swapAmount + (swapAmount * BigInt(slippageBps)) / BigInt(10000);
if (allowance < approvalAmount) {
this.logger.info('DexSwap: building approval transaction', {
sellToken,
spender,
currentAllowance: allowance.toString(),
requiredAmount: swapAmount.toString(),
approvalAmount: approvalAmount.toString(),
swapAmount: swapAmount.toString(),
slippageBps,
destinationChainId,
});

Expand All @@ -165,7 +168,7 @@ export class DexSwapActionHandler implements PostBridgeActionHandler {
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [spender as `0x${string}`, swapAmount + (swapAmount * BigInt(slippageBps)) / BigInt(10000)],
args: [spender as `0x${string}`, approvalAmount],
}),
value: BigInt(0),
},
Expand All @@ -175,7 +178,7 @@ export class DexSwapActionHandler implements PostBridgeActionHandler {
sellToken,
spender,
currentAllowance: allowance.toString(),
requiredAmount: swapAmount.toString(),
requiredAmount: approvalAmount.toString(),
destinationChainId,
});
}
Expand Down
5 changes: 4 additions & 1 deletion packages/adapters/rebalance/src/adapters/linea/linea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,10 @@ export class LineaNativeBridgeAdapter implements BridgeAdapter {
// The Linea SDK issues wide-range eth_getLogs on both L1 and L2.
// Commercial free-tier providers (Alchemy, DRPC) reject block ranges >10k.
// Try configured providers first, then fall back to public RPCs for both chains.
const l1Candidates = [...(this.chains[ETHEREUM_CHAIN_ID.toString()]?.providers ?? []), ...LINEA_SDK_FALLBACK_L1_RPCS];
const l1Candidates = [
...(this.chains[ETHEREUM_CHAIN_ID.toString()]?.providers ?? []),
...LINEA_SDK_FALLBACK_L1_RPCS,
];
const l2Candidates = [...(this.chains[LINEA_CHAIN_ID.toString()]?.providers ?? []), ...LINEA_SDK_FALLBACK_L2_RPCS];

if (l2Candidates.length === 0) {
Expand Down
2 changes: 1 addition & 1 deletion packages/adapters/rebalance/src/adapters/pendle/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ export const PENDLE_SUPPORTED_CHAINS = {
export const USDC_PTUSDE_PAIRS: Record<number, { usdc: string; ptUSDe: string }> = {
1: {
usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
ptUSDe: '0xE8483517077afa11A9B07f849cee2552f040d7b2',
ptUSDe: '0x3de0ff76e8b528c092d47b9dac775931cef80f49',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ export class StargateBridgeAdapter implements BridgeAdapter {
*/
private resolveDstToken(route: RebalanceRoute): string | null {
if (route.destination === 30826) return USDT_TON_STARGATE;
return (
getDestinationAssetAddress(route.asset, route.origin, route.destination, this.chains, this.logger) ?? null
);
return getDestinationAssetAddress(route.asset, route.origin, route.destination, this.chains, this.logger) ?? null;
}

/**
Expand Down
69 changes: 34 additions & 35 deletions packages/adapters/rebalance/src/adapters/zircuit/zircuit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,12 +396,7 @@ export class ZircuitNativeBridgeAdapter implements BridgeAdapter {
data: encodeFunctionData({
abi: zircuitOptimismPortalAbi,
functionName: 'proveWithdrawalTransaction',
args: [
withdrawalTx,
proofResult.l2OutputIndex,
proofResult.outputRootProof,
proofResult.withdrawalProof,
],
args: [withdrawalTx, proofResult.l2OutputIndex, proofResult.outputRootProof, proofResult.withdrawalProof],
}),
value: BigInt(0),
},
Expand All @@ -413,40 +408,44 @@ export class ZircuitNativeBridgeAdapter implements BridgeAdapter {
}

async isCallbackComplete(route: RebalanceRoute, originTransaction: TransactionReceipt): Promise<boolean> {
const isL1ToL2 = route.origin === ETHEREUM_CHAIN_ID && route.destination === ZIRCUIT_CHAIN_ID;
const isL2ToL1 = route.origin === ZIRCUIT_CHAIN_ID && route.destination === ETHEREUM_CHAIN_ID;
if (!isL1ToL2 && !isL2ToL1) {
throw new Error(`Unsupported Zircuit route: ${route.origin}->${route.destination}`);
}
if (isL1ToL2) {
return true;
}
try {
const isL1ToL2 = route.origin === ETHEREUM_CHAIN_ID && route.destination === ZIRCUIT_CHAIN_ID;
const isL2ToL1 = route.origin === ZIRCUIT_CHAIN_ID && route.destination === ETHEREUM_CHAIN_ID;
if (!isL1ToL2 && !isL2ToL1) {
throw new Error(`Unsupported Zircuit route: ${route.origin}->${route.destination}`);
}
if (isL1ToL2) {
return true;
}

// L2→L1: complete only when finalized
const l1Client = await this.getClient(ETHEREUM_CHAIN_ID);
const l2Client = await this.getClient(ZIRCUIT_CHAIN_ID);
// L2→L1: complete only when finalized
const l1Client = await this.getClient(ETHEREUM_CHAIN_ID);
const l2Client = await this.getClient(ZIRCUIT_CHAIN_ID);

const withdrawalTx = await this.extractWithdrawalTransaction(l2Client, originTransaction);
if (!withdrawalTx) {
// Cannot determine state — treat as complete to avoid stuck entries
return true;
}
const withdrawalTx = await this.extractWithdrawalTransaction(l2Client, originTransaction);
if (!withdrawalTx) {
// Cannot determine state — treat as complete to avoid stuck entries
return true;
}

const withdrawalHash = this.hashWithdrawal(withdrawalTx);
const isFinalized = await l1Client.readContract({
address: ZIRCUIT_OPTIMISM_PORTAL as `0x${string}`,
abi: zircuitOptimismPortalAbi,
functionName: 'finalizedWithdrawals',
args: [withdrawalHash],
});
const withdrawalHash = this.hashWithdrawal(withdrawalTx);
const isFinalized = await l1Client.readContract({
address: ZIRCUIT_OPTIMISM_PORTAL as `0x${string}`,
abi: zircuitOptimismPortalAbi,
functionName: 'finalizedWithdrawals',
args: [withdrawalHash],
});

this.logger.info('Zircuit isCallbackComplete check', {
txHash: originTransaction.transactionHash,
withdrawalHash,
isFinalized,
});
this.logger.info('Zircuit isCallbackComplete check', {
txHash: originTransaction.transactionHash,
withdrawalHash,
isFinalized,
});

return isFinalized as boolean;
return isFinalized as boolean;
} catch (error) {
this.handleError(error, 'check if callback is complete', { route, originTransaction });
}
}

private async getClient(chainId: number): Promise<PublicClient> {
Expand Down
2 changes: 1 addition & 1 deletion packages/adapters/rebalance/test/actions/dex-swap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ describe('DexSwapActionHandler', () => {
const customEstOutput = '9999999999999999999';
mockReadContract
.mockResolvedValueOnce(BigInt(1000000) as never) // balanceOf
.mockResolvedValueOnce(BigInt(1000000) as never); // allowance (sufficient)
.mockResolvedValueOnce(BigInt(1010000) as never); // allowance (sufficient including slippage padding)

axiosPostMock.mockResolvedValueOnce({
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ describe('PendleBridgeAdapter', () => {
it('has USDC/ptUSDe pair for mainnet', () => {
expect(USDC_PTUSDE_PAIRS[1]).toBeDefined();
expect(USDC_PTUSDE_PAIRS[1].usdc).toBe('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48');
expect(USDC_PTUSDE_PAIRS[1].ptUSDe).toBe('0xE8483517077afa11A9B07f849cee2552f040d7b2');
expect(USDC_PTUSDE_PAIRS[1].ptUSDe).toBe('0x3de0ff76e8b528c092d47b9dac775931cef80f49');
});

it('has mainnet in supported chains', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/agent/src/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export function initializeBaseAdapters(
const fsSenderAddress =
config.tacRebalance?.fillService?.senderAddress ??
config.tacRebalance?.fillService?.address ??
config.methRebalance?.fillService?.senderAddress ??
config.methRebalance?.fillService?.address ??
config.aManUsdeRebalance?.fillService?.senderAddress ??
config.aManUsdeRebalance?.fillService?.address ??
config.aMansyrupUsdtRebalance?.fillService?.senderAddress ??
Expand Down
61 changes: 55 additions & 6 deletions packages/agent/src/validation.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Logger } from '@mark/logger';
import { MarkConfiguration, TokenRebalanceConfig } from '@mark/core';
import {
MarkConfiguration,
TokenRebalanceConfig,
TOKEN_REBALANCER_KEYS,
TokenRebalancerKey,
SolanaRebalanceConfig,
} from '@mark/core';

/**
* Validates a single token rebalance configuration.
* Helper function used by validateTokenRebalanceConfig.
*/
function validateSingleTokenRebalanceConfig(
tokenConfig: TokenRebalanceConfig | undefined,
configName: 'tacRebalance' | 'methRebalance' | 'aManUsdeRebalance' | 'aMansyrupUsdtRebalance',
configName: TokenRebalancerKey,
config: MarkConfiguration,
logger: Logger,
): void {
Expand Down Expand Up @@ -99,13 +105,56 @@ function validateSingleTokenRebalanceConfig(
});
}

/**
* Validates Solana ptUSDe rebalance configuration.
* Solana uses a different config shape (SolanaRebalanceConfig) than the EVM token rebalancers.
*/
function validateSolanaRebalanceConfig(
solanaConfig: SolanaRebalanceConfig | undefined,
config: MarkConfiguration,
logger: Logger,
): void {
if (!solanaConfig?.enabled) {
logger.debug('solanaPtusdeRebalance disabled, skipping config validation');
return;
}

const errors: string[] = [];

if (!solanaConfig.ptUsdeThreshold) {
errors.push('solanaPtusdeRebalance.ptUsdeThreshold is required when enabled');
}
if (!solanaConfig.ptUsdeTarget) {
errors.push('solanaPtusdeRebalance.ptUsdeTarget is required when enabled');
}
if (!solanaConfig.bridge?.minRebalanceAmount) {
errors.push('solanaPtusdeRebalance.bridge.minRebalanceAmount is required');
}
if (!config.solana?.privateKey) {
errors.push('solana.privateKey (SOLANA_PRIVATE_KEY) is required for Solana rebalancing');
}

if (errors.length > 0) {
const errorMessage = `solanaPtusdeRebalance config validation failed:\n - ${errors.join('\n - ')}`;
logger.error('solanaPtusdeRebalance config validation failed', { errors });
throw new Error(errorMessage);
}

logger.info('solanaPtusdeRebalance config validated successfully', {
ptUsdeThreshold: solanaConfig.ptUsdeThreshold,
ptUsdeTarget: solanaConfig.ptUsdeTarget,
minRebalanceAmount: solanaConfig.bridge.minRebalanceAmount,
maxRebalanceAmount: solanaConfig.bridge.maxRebalanceAmount,
});
}

/**
* Validates token rebalance configuration for production readiness.
* Throws if required fields are missing when token rebalancing is enabled.
*/
export function validateTokenRebalanceConfig(config: MarkConfiguration, logger: Logger): void {
validateSingleTokenRebalanceConfig(config.tacRebalance, 'tacRebalance', config, logger);
validateSingleTokenRebalanceConfig(config.methRebalance, 'methRebalance', config, logger);
validateSingleTokenRebalanceConfig(config.aManUsdeRebalance, 'aManUsdeRebalance', config, logger);
validateSingleTokenRebalanceConfig(config.aMansyrupUsdtRebalance, 'aMansyrupUsdtRebalance', config, logger);
for (const key of TOKEN_REBALANCER_KEYS) {
validateSingleTokenRebalanceConfig(config[key], key, config, logger);
}
validateSolanaRebalanceConfig(config.solanaPtusdeRebalance, config, logger);
}
Loading
Loading