Skip to content

Commit

Permalink
feat: Support AlephZero manual withdrawals
Browse files Browse the repository at this point in the history
Similar to #1866 but for AlephZero which is an Arbitrum Orbit chain
  • Loading branch information
nicholaspai committed Nov 6, 2024
1 parent 30b7003 commit 6255334
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 4 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"node": ">=20"
},
"dependencies": {
"@across-protocol/constants": "^3.1.16",
"@across-protocol/constants": "^3.1.18",
"@across-protocol/contracts": "^3.0.11",
"@across-protocol/sdk": "^3.2.11",
"@arbitrum/sdk": "^3.1.3",
Expand Down
14 changes: 14 additions & 0 deletions scripts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,17 @@ export async function getOvmSpokePoolContract(chainId: number, signer?: Signer):
const contract = new Contract(spokePoolAddr, contracts.Ovm_SpokePool__factory.abi, signer);
return contract;
}

/**
* @description Instantiate an Across Arbitrum SpokePool contract instance.
* @param chainId Chain ID for the SpokePool deployment.
* @returns SpokePool contract instance.
*/
export async function getArbSpokePoolContract(chainId: number, signer?: Signer): Promise<Contract> {
const hubChainId = resolveHubChainId(chainId);
const hubPool = await getContract(hubChainId, "HubPool");
const spokePoolAddr = (await hubPool.crossChainContracts(chainId))[1];

const contract = new Contract(spokePoolAddr, contracts.Arbitrum_SpokePool__factory.abi, signer);
return contract;
}
108 changes: 108 additions & 0 deletions scripts/withdrawFromArbitrumOrbit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Submits a bridge from Arbitrum Orbit L2 to L1.
// For now, this script only supports WETH withdrawals on AlephZero.

import {
ethers,
retrieveSignerFromCLIArgs,
getProvider,
ERC20,
TOKEN_SYMBOLS_MAP,
assert,
getL1TokenInfo,
Contract,
fromWei,
blockExplorerLink,
CHAIN_IDs,
} from "../src/utils";
import { CONTRACT_ADDRESSES } from "../src/common";
import { askYesNoQuestion, getArbSpokePoolContract } from "./utils";

import minimist from "minimist";

const cliArgs = ["amount", "chainId"];
const args = minimist(process.argv.slice(2), {
string: cliArgs,
});

// Example run:
// ts-node ./scripts/withdrawFromArbitrumOrbit.ts
// \ --amount 3000000000000000000
// \ --chainId 41455
// \ --wallet gckms
// \ --keys bot1

export async function run(): Promise<void> {
assert(
cliArgs.every((cliArg) => Object.keys(args).includes(cliArg)),
`Missing cliArg, expected: ${cliArgs}`
);
const baseSigner = await retrieveSignerFromCLIArgs();
const signerAddr = await baseSigner.getAddress();
const chainId = parseInt(args.chainId);
const connectedSigner = baseSigner.connect(await getProvider(chainId));
const l2Token = TOKEN_SYMBOLS_MAP.WETH?.addresses[chainId];
assert(l2Token, `WETH not found on chain ${chainId} in TOKEN_SYMBOLS_MAP`);
const l1TokenInfo = getL1TokenInfo(l2Token, chainId);
console.log("Fetched L1 token info:", l1TokenInfo);
assert(l1TokenInfo.symbol === "ETH", "Only WETH withdrawals are supported for now.");
const amount = args.amount;
const amountFromWei = ethers.utils.formatUnits(amount, l1TokenInfo.decimals);
console.log(`Amount to bridge from chain ${chainId}: ${amountFromWei} ${l2Token}`);

const erc20 = new Contract(l2Token, ERC20.abi, connectedSigner);
const currentBalance = await erc20.balanceOf(signerAddr);
const currentEthBalance = await connectedSigner.getBalance();
console.log(
`Current WETH balance for account ${signerAddr}: ${fromWei(currentBalance, l1TokenInfo.decimals)} ${l2Token}`
);
console.log(`Current ETH balance for account ${signerAddr}: ${fromWei(currentEthBalance, l1TokenInfo.decimals)}`);

// Now, submit a withdrawal:
// - Example WETH: 0xB3f0eE446723f4258862D949B4c9688e7e7d35d3
// - Example ERC20GatewayRouter: https://evm-explorer.alephzero.org/address/0xD296d45171B97720D3aBdb68B0232be01F1A9216?tab=read_proxy
// - Example Txn: https://evm-explorer.alephzero.org/tx/0xb493174af0822c1a5a5983c2cbd4fe74055ee70409c777b9c665f417f89bde92
const arbErc20GatewayRouterObj = CONTRACT_ADDRESSES[chainId].erc20GatewayRouter;
assert(arbErc20GatewayRouterObj, "erc20GatewayRouter for chain not found in CONTRACT_ADDRESSES");
const erc20GatewayRouter = new Contract(arbErc20GatewayRouterObj.address, arbErc20GatewayRouterObj.abi, connectedSigner);
const outboundTransferArgs = [
TOKEN_SYMBOLS_MAP.WETH?.addresses[CHAIN_IDs.MAINNET], // l1Token
signerAddr, // to
amount, // amount
"0x", // data
];

console.log(
`Submitting outboundTransfer on the Arbitrum ERC20 gateway router @ ${erc20GatewayRouter.address} with the following args: `,
...outboundTransferArgs
);

// Sanity check that the ovmStandardBridge contract is the one we expect by comparing its stored addresses
// with the ones we have recorded.
const spokePool = await getArbSpokePoolContract(chainId, connectedSigner);
const expectedErc20GatewayRouter = await spokePool.l2GatewayRouter();
assert(
expectedErc20GatewayRouter === arbErc20GatewayRouterObj.address,
`Unexpected L2 erc20 gateway router address in ArbitrumSpokePool contract, expected: ${expectedErc20GatewayRouter}, got: ${arbErc20GatewayRouterObj.address}`
);
if (!(await askYesNoQuestion("\nDo you want to proceed?"))) {
return;
}
const withdrawal = await erc20GatewayRouter.outboundTransfer(...outboundTransferArgs);
console.log(`Submitted withdrawal: ${blockExplorerLink(withdrawal.hash, chainId)}.`);
const receipt = await withdrawal.wait();
console.log("Receipt", receipt);
}

if (require.main === module) {
run()
.then(async () => {
// eslint-disable-next-line no-process-exit
process.exit(0);
})
.catch(async (error) => {
console.error("Process exited with", error);
// eslint-disable-next-line no-process-exit
process.exit(1);
});
}

7 changes: 7 additions & 0 deletions src/common/ContractAddresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ZK_SYNC_DEFAULT_ERC20_BRIDGE_L2_ABI from "./abi/ZkSyncDefaultErc20BridgeL
import ZK_SYNC_MAILBOX_ABI from "./abi/ZkSyncMailbox.json";
import ARBITRUM_ERC20_GATEWAY_ROUTER_L1_ABI from "./abi/ArbitrumErc20GatewayRouterL1.json";
import ARBITRUM_ERC20_GATEWAY_L2_ABI from "./abi/ArbitrumErc20GatewayL2.json";
import ARBITRUM_ERC20_GATEWAY_ROUTER_L2_ABI from "./abi/ArbitrumErc20GatewayRouterL2.json";
import ARBITRUM_OUTBOX_ABI from "./abi/ArbitrumOutbox.json";
import LINEA_MESSAGE_SERVICE_ABI from "./abi/LineaMessageService.json";
import LINEA_TOKEN_BRIDGE_ABI from "./abi/LineaTokenBridge.json";
Expand Down Expand Up @@ -321,6 +322,12 @@ export const CONTRACT_ADDRESSES: {
abi: CCTP_TOKEN_MESSENGER_ABI,
},
},
41455: {
erc20GatewayRouter: {
abi: ARBITRUM_ERC20_GATEWAY_ROUTER_L2_ABI,
address: "0xD296d45171B97720D3aBdb68B0232be01F1A9216",
},
},
59144: {
l2MessageService: {
address: "0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec",
Expand Down
43 changes: 43 additions & 0 deletions src/common/abi/ArbitrumErc20GatewayL2.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,48 @@
],
"name": "DepositFinalized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "l1Token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "_from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "_to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "_l2ToL1Id",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "_exitNum",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
}
],
"name": "WithdrawalInitiated",
"type": "event"
}
]
56 changes: 56 additions & 0 deletions src/common/abi/ArbitrumErc20GatewayRouterL2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "_l1Token",
"type": "address"
},
{
"internalType": "address",
"name": "_to",
"type": "address"
},
{
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "outboundTransfer",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_token",
"type": "address"
}
],
"name": "getGateway",
"outputs": [
{
"internalType": "address",
"name": "gateway",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
]

4 changes: 4 additions & 0 deletions src/finalizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ const chainFinalizers: { [chainId: number]: { finalizeOnL2: ChainFinalizer[]; fi
finalizeOnL1: [opStackFinalizer],
finalizeOnL2: [],
},
[CHAIN_IDs.ALEPH_ZERO]: {
finalizeOnL1: [arbitrumOneFinalizer],
finalizeOnL2: [],
},
// Testnets
[CHAIN_IDs.BASE_SEPOLIA]: {
finalizeOnL1: [cctpL2toL1Finalizer],
Expand Down
52 changes: 52 additions & 0 deletions src/finalizer/utils/arbitrum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
compareAddressesSimple,
TOKEN_SYMBOLS_MAP,
CHAIN_IDs,
ethers,
paginatedEventQuery,
} from "../../utils";
import { TokensBridged } from "../../interfaces";
import { HubPoolClient, SpokePoolClient } from "../../clients";
Expand Down Expand Up @@ -51,6 +53,56 @@ export async function arbitrumOneFinalizer(
!compareAddressesSimple(e.l2TokenAddress, TOKEN_SYMBOLS_MAP["USDC"].addresses[CHAIN_ID])
);

// Experimental feature: Add in all ETH withdrawals from Arbitrum Orbit chain to the finalizer. This will help us
// in the short term to automate ETH withdrawals from Lite chains, which can build up ETH balances over time
// and because they are lite chains, our only way to withdraw them is to initiate a slow bridge of ETH from the
// the lite chain to Ethereum.
const withdrawalToAddresses: string[] = process.env.FINALIZER_WITHDRAWAL_TO_ADDRESSES
? JSON.parse(process.env.FINALIZER_WITHDRAWAL_TO_ADDRESSES).map((address) => ethers.utils.getAddress(address))
: [];
if (!CONTRACT_ADDRESSES[chainId].erc20GatewayRouter) {
logger.warn({
at: "ArbitrumFinalizer",
message: `No erc20GatewayRouter contract found for chain ${chainId} in CONTRACT_ADDRESSES`,
});
} else if (withdrawalToAddresses.length > 0) {
const arbitrumGatewayRouter = new Contract(
CONTRACT_ADDRESSES[chainId].erc20GatewayRouter.address,
CONTRACT_ADDRESSES[chainId].erc20GatewayRouter.abi,
spokePoolClient.spokePool.provider
);
const arbitrumGateway = await arbitrumGatewayRouter.getGateway(TOKEN_SYMBOLS_MAP.WETH.addresses[chainId]);
// TODO: For this to work for ArbitrumOrbit, we need to first query ERC20GatewayRouter.getGateway(l2Token) to
// get the ERC20 Gateway. Then, on the ERC20 Gateway, query the WithdrawalInitiated event.
// See example txn: https://evm-explorer.alephzero.org/tx/0xb493174af0822c1a5a5983c2cbd4fe74055ee70409c777b9c665f417f89bde92
// which withdraws WETH to mainnet using dev wallet.
const withdrawalEvents = await paginatedEventQuery(
arbitrumGateway,
arbitrumGateway.filters.WithdrawalInitiated(
null, // from
withdrawalToAddresses // to
),
{
...spokePoolClient.eventSearchConfig,
toBlock: spokePoolClient.latestBlockSearched,
}
);
// If there are any found withdrawal initiated events, then add them to the list of TokenBridged events we'll
// submit proofs and finalizations for.
withdrawalEvents.forEach((event) => {
const tokenBridgedEvent: TokensBridged = {
...event,
amountToReturn: event.args.amount,
chainId,
leafId: 0,
l2TokenAddress: TOKEN_SYMBOLS_MAP.WETH.addresses[chainId],
};
if (event.blockNumber <= latestBlockToFinalize) {
olderTokensBridgedEvents.push(tokenBridgedEvent);
}
});
}

return await multicallArbitrumFinalizations(olderTokensBridgedEvents, signer, hubPoolClient, logger);
}

Expand Down
4 changes: 4 additions & 0 deletions src/finalizer/utils/opStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ export async function opStackFinalizer(
CONTRACT_ADDRESSES[chainId].ovmStandardBridge.abi,
spokePoolClient.spokePool.provider
);
// TODO: For this to work for ArbitrumOrbit, we need to first query ERC20GatewayRouter.getGateway(l2Token) to
// get the ERC20 Gateway. Then, on the ERC20 Gateway, query the WithdrawalInitiated event.
// See example txn: https://evm-explorer.alephzero.org/tx/0xb493174af0822c1a5a5983c2cbd4fe74055ee70409c777b9c665f417f89bde92
// which withdraws WETH to mainnet using dev wallet.
const withdrawalEvents = await paginatedEventQuery(
ovmStandardBridge,
ovmStandardBridge.filters.ETHBridgeInitiated(
Expand Down
Loading

0 comments on commit 6255334

Please sign in to comment.