Skip to content

Commit 0dcf904

Browse files
committed
feat: native bridges tested locally
1 parent 794cef2 commit 0dcf904

File tree

21 files changed

+2908
-87
lines changed

21 files changed

+2908
-87
lines changed

packages/adapters/rebalance/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
},
2020
"dependencies": {
2121
"@chainlink/ccip-sdk": "^0.93.0",
22+
"@consensys/linea-sdk": "^0.3.0",
2223
"@cowprotocol/cow-sdk": "^7.1.2-beta.0",
2324
"@defuse-protocol/one-click-sdk-typescript": "^0.1.5",
2425
"@mark/core": "workspace:*",
2526
"@mark/database": "workspace:*",
2627
"@mark/logger": "workspace:*",
2728
"@solana/web3.js": "^1.98.0",
2829
"@tonappchain/sdk": "0.7.1",
30+
"@zircuit/zircuit-viem": "^1.1.5",
2931
"axios": "1.9.0",
3032
"bs58": "^6.0.0",
3133
"commander": "12.0.0",

packages/adapters/rebalance/scripts/dev.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ async function testBridgeAdapter(
234234
assetCount: originChain.assets.length
235235
});
236236

237-
const asset = Object.values(originChain.assets).find(a => a.address.toLowerCase() === route.asset.toLowerCase());
237+
const isNativeETH = route.asset.toLowerCase() === '0x0000000000000000000000000000000000000000';
238+
const asset = Object.values(originChain.assets).find(a => a.address.toLowerCase() === route.asset.toLowerCase())
239+
?? (isNativeETH ? { address: route.asset, symbol: 'ETH', decimals: 18, tickerHash: '', isNative: true, balanceThreshold: '0' } : undefined);
238240
if (!asset) {
239241
throw new Error(`Asset ${route.asset} not found in origin chain ${route.origin}`);
240242
}
@@ -441,14 +443,12 @@ program
441443
} as unknown as MarkConfiguration, logger, database);
442444
const adapter = rebalancer.getAdapter(type as SupportedBridge);
443445

444-
// Find the asset to get decimals
446+
// Find the asset to get decimals (default to 18 if not found, amount is not used for claiming)
445447
const asset = Object.values(originChain.assets).find(a => a.address.toLowerCase() === route.asset.toLowerCase());
446-
if (!asset) {
447-
throw new Error(`Asset ${route.asset} not found in origin chain ${route.origin}`);
448-
}
448+
const decimals = asset?.decimals ?? 18;
449449

450450
// Convert amount to wei
451-
const amountInWei = parseUnits(options.amount, asset.decimals).toString();
451+
const amountInWei = parseUnits(options.amount, decimals).toString();
452452

453453
// Poll for transaction readiness
454454
await pollForTransactionReady(adapter, amountInWei, route, receipt as TransactionReceipt);
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { createPublicClient, http, encodeFunctionData, parseEventLogs, keccak256, encodeAbiParameters, parseAbiParameters, parseAbi } from 'viem';
2+
import { buildProveZircuitWithdrawal, getWithdrawals } from '@zircuit/zircuit-viem/op-stack';
3+
4+
const ZIRCUIT_OPTIMISM_PORTAL = '0x17bfAfA932d2e23Bd9B909Fd5B4D2e2a27043fb1';
5+
const ZIRCUIT_L2_OUTPUT_ORACLE = '0x92Ef6Af472b39F1b363da45E35530c24619245A4';
6+
7+
const zircuitOptimismPortalAbi = parseAbi([
8+
'function proveWithdrawalTransaction((uint256 nonce, address sender, address target, uint256 value, uint256 gasLimit, bytes data) _tx, uint256 _l2OutputIndex, (bytes32 version, bytes32 stateRoot, bytes32 messagePasserStorageRoot, bytes32 latestBlockhash) _outputRootProof, bytes[] calldata _withdrawalProof)',
9+
]);
10+
const zircuitL2ToL1MessagePasserAbi = parseAbi([
11+
'event MessagePassed(uint256 indexed nonce, address indexed sender, address indexed target, uint256 value, uint256 gasLimit, bytes data, bytes32 withdrawalHash)',
12+
]);
13+
14+
async function main() {
15+
const l2Client = createPublicClient({ transport: http('https://zircuit-mainnet.drpc.org') });
16+
const l1Client = createPublicClient({ transport: http('https://ethereum.publicnode.com') });
17+
18+
// Get the original receipt
19+
const receipt = await l2Client.getTransactionReceipt({
20+
hash: '0x4a5203d25bbe1fd6aa3536e013f017d5d2f21c5996167173d3ec03bdeb977426'
21+
});
22+
console.log('Got receipt, block:', receipt.blockNumber);
23+
24+
// Extract withdrawal using our method
25+
const logs = parseEventLogs({ abi: zircuitL2ToL1MessagePasserAbi, logs: receipt.logs });
26+
const messagePassedEvent = logs.find((log) => log.eventName === 'MessagePassed');
27+
if (!messagePassedEvent) {
28+
console.error('No MessagePassed event found');
29+
return;
30+
}
31+
const args = (messagePassedEvent as any).args;
32+
const withdrawalTx = {
33+
nonce: args.nonce,
34+
sender: args.sender,
35+
target: args.target,
36+
value: args.value,
37+
gasLimit: args.gasLimit,
38+
data: args.data,
39+
};
40+
console.log('Withdrawal nonce:', withdrawalTx.nonce.toString());
41+
console.log('Withdrawal nonce (hex):', '0x' + withdrawalTx.nonce.toString(16));
42+
console.log('Withdrawal sender:', withdrawalTx.sender);
43+
console.log('Withdrawal target:', withdrawalTx.target);
44+
console.log('Withdrawal value:', withdrawalTx.value.toString());
45+
46+
// Also get withdrawal from the library
47+
const libWithdrawals = getWithdrawals(receipt);
48+
console.log('\nLibrary withdrawal nonce:', libWithdrawals[0]?.nonce.toString());
49+
console.log('Library withdrawal hash:', libWithdrawals[0]?.withdrawalHash);
50+
51+
// Our computed hash
52+
const ourHash = keccak256(
53+
encodeAbiParameters(
54+
parseAbiParameters('uint256, address, address, uint256, uint256, bytes'),
55+
[withdrawalTx.nonce, withdrawalTx.sender, withdrawalTx.target, withdrawalTx.value, withdrawalTx.gasLimit, withdrawalTx.data],
56+
),
57+
);
58+
console.log('\nOur computed hash:', ourHash);
59+
console.log('Library hash:', libWithdrawals[0]?.withdrawalHash);
60+
console.log('Hash match:', ourHash === libWithdrawals[0]?.withdrawalHash);
61+
62+
// Build proof
63+
console.log('\nBuilding proof...');
64+
try {
65+
const proofResult = await buildProveZircuitWithdrawal(l2Client as any, {
66+
receipt: receipt as any,
67+
l1Client: l1Client as any,
68+
l2OutputOracleAddress: ZIRCUIT_L2_OUTPUT_ORACLE as `0x${string}`,
69+
} as any);
70+
71+
console.log('Proof built successfully');
72+
console.log('l2OutputIndex:', (proofResult.l2OutputIndex as bigint).toString());
73+
console.log('withdrawalProof length:', proofResult.withdrawalProof.length);
74+
console.log('withdrawalProof[0] length:', (proofResult.withdrawalProof[0] as string).length);
75+
console.log('outputRootProof:', JSON.stringify({
76+
version: proofResult.outputRootProof.version,
77+
stateRoot: proofResult.outputRootProof.stateRoot,
78+
messagePasserStorageRoot: proofResult.outputRootProof.messagePasserStorageRoot,
79+
latestBlockhash: proofResult.outputRootProof.latestBlockhash,
80+
}, null, 2));
81+
82+
// Encode the calldata
83+
const calldata = encodeFunctionData({
84+
abi: zircuitOptimismPortalAbi,
85+
functionName: 'proveWithdrawalTransaction',
86+
args: [
87+
withdrawalTx,
88+
proofResult.l2OutputIndex as bigint,
89+
proofResult.outputRootProof as any,
90+
proofResult.withdrawalProof as `0x${string}`[],
91+
],
92+
});
93+
console.log('\nCalldata length:', calldata.length);
94+
console.log('Function selector:', calldata.slice(0, 10));
95+
96+
// Simulate the call
97+
console.log('\nSimulating call on L1...');
98+
try {
99+
await l1Client.call({
100+
to: ZIRCUIT_OPTIMISM_PORTAL as `0x${string}`,
101+
data: calldata,
102+
});
103+
console.log('*** Simulation SUCCEEDED ***');
104+
} catch (e: any) {
105+
console.error('*** Simulation FAILED ***');
106+
console.error('Error:', e.message?.slice(0, 1000));
107+
}
108+
} catch (e: any) {
109+
console.error('Proof building failed:', e.message?.slice(0, 1000));
110+
}
111+
}
112+
113+
main().catch(console.error);

packages/adapters/rebalance/src/adapters/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import { StargateBridgeAdapter } from './stargate';
1414
import { TacInnerBridgeAdapter, TacNetwork } from './tac';
1515
import { PendleBridgeAdapter } from './pendle';
1616
import { CCIPBridgeAdapter } from './ccip';
17-
import { ZKSyncNativeBridgeAdapter } from './zksync/zksync';
17+
import { ZKSyncNativeBridgeAdapter } from './zksync';
18+
import { LineaNativeBridgeAdapter } from './linea';
19+
import { ZircuitNativeBridgeAdapter } from './zircuit';
1820

1921
export class RebalanceAdapter {
2022
constructor(
@@ -97,6 +99,10 @@ export class RebalanceAdapter {
9799
return new CCIPBridgeAdapter(this.config.chains, this.logger);
98100
case SupportedBridge.Zksync:
99101
return new ZKSyncNativeBridgeAdapter(this.config.chains, this.logger);
102+
case SupportedBridge.Linea:
103+
return new LineaNativeBridgeAdapter(this.config.chains, this.logger);
104+
case SupportedBridge.Zircuit:
105+
return new ZircuitNativeBridgeAdapter(this.config.chains, this.logger);
100106
default:
101107
throw new Error(`Unsupported adapter type: ${type}`);
102108
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { parseAbi } from 'viem';
2+
3+
// Contract addresses
4+
export const LINEA_L1_MESSAGE_SERVICE = '0xd19d4B5d358258f05D7B411E21A1460D11B0876F';
5+
export const LINEA_L2_MESSAGE_SERVICE = '0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec';
6+
export const LINEA_L1_TOKEN_BRIDGE = '0x051F1D88f0aF5763fB888eC4378b4D8B29ea3319';
7+
export const LINEA_L2_TOKEN_BRIDGE = '0x353012dc4a9A6cF55c941bADC267f82004A8ceB9';
8+
9+
// Chain IDs
10+
export const ETHEREUM_CHAIN_ID = 1;
11+
export const LINEA_CHAIN_ID = 59144;
12+
13+
// Anti-DDoS fee for L2→L1 messages (in wei) - approximately 0.001 ETH
14+
export const L2_TO_L1_FEE = BigInt('1000000000000000');
15+
16+
// Finality window for L2→L1 messages (24 hours in seconds)
17+
export const FINALITY_WINDOW_SECONDS = 24 * 60 * 60;
18+
19+
// Linea Message Service ABI
20+
export const lineaMessageServiceAbi = parseAbi([
21+
// L1 Message Service
22+
'function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) payable',
23+
'function claimMessageWithProof((bytes32[] proof, uint256 messageNumber, uint32 leafIndex, address from, address to, uint256 fee, uint256 value, address feeRecipient, bytes32 merkleRoot, bytes data) _params)',
24+
'event MessageSent(address indexed _from, address indexed _to, uint256 _fee, uint256 _value, uint256 _nonce, bytes _calldata, bytes32 indexed _messageHash)',
25+
'event MessageClaimed(bytes32 indexed _messageHash)',
26+
// L2 Message Service
27+
'function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) payable',
28+
]);
29+
30+
// Linea Token Bridge ABI
31+
export const lineaTokenBridgeAbi = parseAbi([
32+
'function bridgeToken(address _token, uint256 _amount, address _recipient) payable',
33+
'function bridgeTokenWithPermit(address _token, uint256 _amount, address _recipient, bytes calldata _permitData) payable',
34+
'event BridgingInitiated(address indexed sender, address indexed recipient, address indexed token, uint256 amount)',
35+
'event BridgingFinalized(address indexed nativeToken, address indexed bridgedToken, uint256 amount, address indexed recipient)',
36+
]);
37+
38+
// L1 MessageService deployment block (avoids scanning from genesis)
39+
export const LINEA_L1_MESSAGE_SERVICE_DEPLOY_BLOCK = BigInt(17614000);
40+
41+
// Status anchor events for proof retrieval
42+
export const STATUS_ANCHOR_EVENT_SIGNATURE = '0x7ec0c4a1ce1ec0d8b871e88b71f4f2b26a6ce6bb2c6d9e1c8a00f8efcf87b87e';
43+
44+
// Public L1 RPCs that support wide-range eth_getLogs queries.
45+
// The Linea SDK queries from block 0 to latest, which commercial
46+
// providers (Alchemy, Infura) reject due to block range limits.
47+
export const LINEA_SDK_FALLBACK_L1_RPCS = [
48+
'https://ethereum.publicnode.com',
49+
'https://eth.llamarpc.com',
50+
'https://rpc.ankr.com/eth',
51+
];
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './linea';
2+
export * from './constants';

0 commit comments

Comments
 (0)