Skip to content

Instantly share code, notes, and snippets.

@vincfurc
Last active May 29, 2025 08:00
Show Gist options
  • Select an option

  • Save vincfurc/7fd3b88e95d6d7105fac3ca2dca075d5 to your computer and use it in GitHub Desktop.

Select an option

Save vincfurc/7fd3b88e95d6d7105fac3ca2dca075d5 to your computer and use it in GitHub Desktop.
Arbitrum time to withdrawal calculation
import { ethers } from "ethers";
import dotenv from "dotenv";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
dotenv.config({ path: join(__dirname, "../.env") });
// --- Constants ---
const ARBYS_ADDRESS = "0x0000000000000000000000000000000000000064";
const L1_OUTBOX_ADDRESS = "0x0B9857ae2D4A3DBe74ffE1d7DF045bb7F96E4840";
// --- Event ABIs ---
const L2ToL1Tx_EVENT_ABI = ["event L2ToL1Tx(address caller, address indexed destination, uint256 indexed hash, uint256 indexed position, uint256 arbBlockNum, uint256 ethBlockNum, uint256 timestamp, uint256 callvalue, bytes data)"];
const SEND_ROOT_UPDATED_EVENT_ABI = ["event SendRootUpdated(bytes32 indexed outputRoot, bytes32 indexed l2BlockHash)"];
// --- Interfaces for parsing logs ---
const arbSysInterface = new ethers.Interface(L2ToL1Tx_EVENT_ABI);
const outboxInterface = new ethers.Interface(SEND_ROOT_UPDATED_EVENT_ABI);
// Helper function to format seconds into human-readable string
function formatDuration(totalSeconds) {
if (totalSeconds === null || totalSeconds < 0) return "N/A";
const days = Math.floor(totalSeconds / (3600 * 24));
const hours = Math.floor((totalSeconds % (3600 * 24)) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = Math.floor(totalSeconds % 60);
return `${days}d ${hours}h ${minutes}m ${seconds}s`;
}
async function main() {
const l2TxHashArg = process.argv[2] || "0x55f2093b852e415c4de49b34c7d9901c5bbcf97950e772dab4ba846e2867c9d6";
if (!l2TxHashArg || !ethers.isHexString(l2TxHashArg, 32)) {
console.error("Please provide a valid L2 transaction hash");
process.exit(1);
}
console.log(`Analyzing L2 transaction: ${l2TxHashArg}\n`);
const l2RpcUrl = process.env.ARBITRUM_RPC_URL;
const l1RpcUrl = process.env.ETHEREUM_RPC_URL;
if (!l2RpcUrl || !l1RpcUrl) {
console.error("Please set ARBITRUM_RPC_URL and ETHEREUM_RPC_URL in your .env file.");
process.exit(1);
}
const l2Provider = new ethers.JsonRpcProvider(l2RpcUrl);
const l1Provider = new ethers.JsonRpcProvider(l1RpcUrl);
// --- Step 1: Get L2ToL1Tx Event Data ---
console.log("Step 1: Fetching L2ToL1Tx event data...");
const l2TxReceipt = await l2Provider.getTransactionReceipt(l2TxHashArg);
if (!l2TxReceipt) {
console.error(`Failed to get L2 transaction receipt for ${l2TxHashArg}`);
process.exit(1);
}
let l2ToL1Log;
for (const log of l2TxReceipt.logs) {
if (log.address.toLowerCase() === ARBYS_ADDRESS.toLowerCase()) {
try {
const parsedLog = arbSysInterface.parseLog(log);
if (parsedLog && parsedLog.name === "L2ToL1Tx") {
l2ToL1Log = parsedLog;
break;
}
} catch (e) {
// Not the event we're looking for
}
}
}
if (!l2ToL1Log) {
console.error(`L2ToL1Tx event not found in transaction ${l2TxHashArg}`);
process.exit(1);
}
const l2ArbBlockNum = BigInt(l2ToL1Log.args.arbBlockNum.toString());
const l2Timestamp = Number(l2ToL1Log.args.timestamp); // Get L2 timestamp for duration calculation
console.log(`L2 Block Number: ${l2ArbBlockNum}`);
console.log(`L2 Time: ${new Date(l2Timestamp * 1000).toUTCString()}`);
// --- Step 2: Search for SendRootUpdated events ---
console.log("\nStep 2: Searching for SendRootUpdated events...");
const currentL1Block = await l1Provider.getBlockNumber();
const BATCH_SIZE = 10_000;
const ONE_MONTH_IN_SECONDS = 30 * 24 * 60 * 60;
const BLOCKS_PER_MONTH = Math.floor(ONE_MONTH_IN_SECONDS / 12); // 12-second block time
const startBlock = Math.max(0, currentL1Block - BLOCKS_PER_MONTH);
console.log(`Scanning L1 blocks ${startBlock} to ${currentL1Block} (last month)...`);
let executableEvents = []; // Track all matching events
for (let endBlock = currentL1Block; endBlock > startBlock; endBlock -= BATCH_SIZE) {
const batchStartBlock = Math.max(startBlock, endBlock - BATCH_SIZE);
process.stdout.write(`\rScanning blocks ${batchStartBlock} to ${endBlock}...`);
try {
const sendRootUpdatedEvents = await l1Provider.getLogs({
address: L1_OUTBOX_ADDRESS,
fromBlock: batchStartBlock,
toBlock: endBlock,
topics: [outboxInterface.getEvent("SendRootUpdated").topicHash]
});
for (const log of sendRootUpdatedEvents) {
const parsedLog = outboxInterface.parseLog(log);
const l2BlockHashFromEvent = parsedLog.args.l2BlockHash;
// Get the L2 block number for this hash
const l2BlockInfo = await l2Provider.getBlock(l2BlockHashFromEvent);
if (!l2BlockInfo) continue;
const l2BlockNumberFromEvent = BigInt(l2BlockInfo.number);
console.log(`\nFound SendRootUpdated at L1 block ${log.blockNumber}:`);
console.log(`L2 Block Hash: ${l2BlockHashFromEvent}`);
console.log(`L2 Block Number: ${l2BlockNumberFromEvent}`);
// Check if this root covers our withdrawal
if (l2ArbBlockNum <= l2BlockNumberFromEvent) {
const timestamp = (await l1Provider.getBlock(log.blockNumber)).timestamp;
executableEvents.push({
l1Block: log.blockNumber,
l1Timestamp: timestamp,
l2BlockNumber: l2BlockNumberFromEvent,
outputRoot: parsedLog.args.outputRoot
});
}
}
} catch (err) {
console.error(`\nError scanning blocks ${batchStartBlock}-${endBlock}: ${err.message}`);
}
}
if (executableEvents.length === 0) {
console.log("\n❌ No SendRootUpdated event found that makes this withdrawal executable yet.");
} else {
// Sort by timestamp to find the earliest one
executableEvents.sort((a, b) => a.l1Timestamp - b.l1Timestamp);
const earliest = executableEvents[0];
console.log("\n🎉 Found earliest time withdrawal became executable:");
console.log(`L1 Block: ${earliest.l1Block}`);
console.log(`Time: ${new Date(earliest.l1Timestamp * 1000).toUTCString()}`);
console.log(`Output Root: ${earliest.outputRoot}`);
console.log(`L2 Block Number: ${earliest.l2BlockNumber}`);
// Calculate and display time to withdrawal
const timeToWithdrawal = earliest.l1Timestamp - l2Timestamp;
console.log(`\nTime from L2 withdrawal to L1 executability: ${formatDuration(timeToWithdrawal)}`);
if (executableEvents.length > 1) {
console.log(`\nFound ${executableEvents.length} total SendRootUpdated events covering this withdrawal.`);
}
}
}
main().catch(err => {
console.error("\nUnhandled error:", err);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment