Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sendTokensToHubPool script to test finalizations #574

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions deploy/consts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../utils";

export const WETH = TOKEN_SYMBOLS_MAP.WETH.addresses;
export const MOCK_ADMIN = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D";

export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string } } = {
1: {
Expand Down
2 changes: 2 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ require("./tasks/enableL1TokenAcrossEcosystem");
require("./tasks/finalizeScrollClaims");
// eslint-disable-next-line node/no-missing-require
require("./tasks/rescueStuckScrollTxn");
// eslint-disable-next-line node/no-missing-require
require("./tasks/tokenTraversal");

dotenv.config();

Expand Down
132 changes: 132 additions & 0 deletions tasks/tokenTraversal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { getNodeUrl, EMPTY_MERKLE_ROOT } from "@uma/common";
import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants";
import { task } from "hardhat/config";
import { minimalSpokePoolInterface, minimalAdapterInterface } from "./utils";
import { Event, Wallet, providers, Contract, ethers, bnZero } from "ethers";

/**
* ```
* yarn hardhat token-traversal \
* --network base --adapter [adapter_address] --spokePool [spoke_pool_address] --value 0.02
* ```
* This REQUIRES a spoke pool to be deployed to the specified network AND for the
* spoke pool to have the signer as the `crossDomainAdmin`.
*/

task("token-traversal", "Test L1 <-> L2 communication between a deployed L1 adapter and a L2 spoke pool")
.addParam("adapter", "address of the L1 adapter to use")
.addParam("spoke", "address of the L1 spoke pool to use")
.addOptionalParam(
"value",
"amount of ETH to send with transaction. This should only be used in special cases since improper use could nuke funds"
)
.addOptionalParam("l2token", "The l2 token address to send over via the adapter. Defaults to weth.")
.addOptionalParam("amountToSend", "amount of token to send over via the adapter. Defaults to 1000")
.setAction(async function (taskArguments, hre_) {
const hre = hre_ as any;
const msgValue = ethers.utils.parseEther(taskArguments.value === undefined ? "0" : value);

/*
* Setup: Need to obtain all contract addresses involved and instantiate L1/L2 providers and contracts.
*/
// Instantiate providers/signers/chainIds
const l2ChainId = parseInt(await hre.getChainId());
const l1ChainId = parseInt(await hre.companionNetworks.l1.getChainId());

const l2ProviderUrl = hre.network.config.url;

const baseNetwork = l1ChainId === CHAIN_IDs.MAINNET ? "mainnet" : "sepolia";
const l1ProviderUrl = hre.config.networks[`${baseNetwork}`].url;

const l2Provider = new ethers.providers.JsonRpcProvider(l2ProviderUrl);
const l2Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l2Provider);
const l1Provider = new ethers.providers.JsonRpcProvider(l1ProviderUrl);
const l1Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l1Provider);

const tokenAddress = taskArguments.token ?? TOKEN_SYMBOLS_MAP.WETH.addresses[l2ChainId];
if (!tokenAddress) {
throw new Error(
"[-] No token address specified and cannot default to WETH. Please set the `l2token` parameter to the l2 token address to send back to the hub pool"
);
}
const amountToReturn = taskArguments.amountToSend ?? 1000;

// Construct the contracts
const spokePool = new Contract(taskArguments.spokePool, minimalSpokePoolInterface, l2Signer);
const adapter = new Contract(taskArguments.adapter, minimalAdapterInterface, l1Signer);

console.log("[+] Successfully constructed all contracts. Determining root bundle Id to use.");
let rootBundleId = 0;
try {
while (1) {
await spokePool.rootBundles(rootBundleId);
rootBundleId++;
}
} catch (e) {
console.log(`[+] Obtained latest root bundle Id ${rootBundleId}`);
}
/*
* Step 1: Craft and send a message to be sent to the provided L1 chain adapter contract. This message should be used to call `relayRootBundle` on the
* associated L2 contract
*/
const rootBundleType = "tuple(uint256,uint256,uint256[],uint32,address,address[])";
// Construct the root bundle
const encodedRootBundle = ethers.utils.defaultAbiCoder.encode(
[rootBundleType],
[[amountToReturn, l2ChainId, [], 0, tokenAddress, []]]
);
const rootBundleHash = ethers.utils.keccak256(encodedRootBundle);
// Submit the root bundle to chain.
const relayRootBundleTxnData = spokePool.interface.encodeFunctionData("relayRootBundle", [
rootBundleHash,
EMPTY_MERKLE_ROOT,
]);
const adapterTxn = await adapter.relayMessage(spokePool.address, relayRootBundleTxnData, { value });
const txn = await adapterTxn.wait();
console.log(
`[+] Called L1 adapter (${adapter.address}) to relay refund leaf message to mock spoke pool at ${spokePool.address}. Txn: ${txn.transactionHash}`
);
/*
* Step 2: Spin until we observe the message to be executed on the L2. This should take ~3 minutes.
*/
// It would be nice to just have a websocket listening for the `RelayedRootBundle` event, but I don't want to assume a websocket provider.
console.log(
"[i] Optimistically waiting 5 minutes for L1 message to propagate. If root bundle is not observed, will check spoke every minute thereafter."
);
const fiveMins = 1000 * 60 * 5;
await delay(fiveMins);

// We should be able to query the canonical messenger to see if our L1 message was propagated, but this requires us to instantiate a unique L2 messenger contract
// for each new chain we make, which is not scalable. Instead, we query whether our root bundle is in the spoke pool contract, as this is generalizable and does not
// require us to instantiate any new contract.
while (1) {
try {
// Check the root bundle
await spokePool.rootBundles(rootBundleId);
break;
} catch (e) {
// No root bundle made it yet. Continue to spin
console.log("[-] Root bundle not found on L2. Waiting another 60 seconds.");
await delay(1000 * 60);
}
}
console.log("[+] Root bundle observed on L2 spoke pool. Attempting to execute.");
/*
* Step 3: Call `executeRelayerRefund` on the target spoke pool to send funds back to the hub pool (or, whatever was initialized as the `hubPool` in the deploy
* script, which is likely the dev EOA).
*/
const executeRelayerRefundLeaf = await spokePool.executeRelayerRefundLeaf(
rootBundleId,
[amountToReturn, l2ChainId, [], 0, tokenAddress, []],
[]
);
const l2Txn = await executeRelayerRefundLeaf.wait();
console.log(
`[+] Executed root bundle with transaction hash ${l2Txn.transactionHash}. You can now test the finalizer in the relayer repository.`
);
});

// Sleep
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
118 changes: 118 additions & 0 deletions tasks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,124 @@ export const minimalSpokePoolInterface = [
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "uint32",
name: "rootBundleId",
type: "uint32",
},
{
components: [
{
internalType: "uint256",
name: "amountToReturn",
type: "uint256",
},
{
internalType: "uint256",
name: "chainId",
type: "uint256",
},
{
internalType: "uint256[]",
name: "refundAmounts",
type: "uint256[]",
},
{
internalType: "uint32",
name: "leafId",
type: "uint32",
},
{
internalType: "address",
name: "l2TokenAddress",
type: "address",
},
{
internalType: "address[]",
name: "refundAddresses",
type: "address[]",
},
],
internalType: "struct SpokePoolInterface.RelayerRefundLeaf",
name: "relayerRefundLeaf",
type: "tuple",
},
{
internalType: "bytes32[]",
name: "proof",
type: "bytes32[]",
},
],
name: "executeRelayerRefundLeaf",
outputs: [],
stateMutability: "payable",
type: "function",
},
{
inputs: [
{
internalType: "bytes32",
name: "relayerRefundRoot",
type: "bytes32",
},
{
internalType: "bytes32",
name: "slowRelayRoot",
type: "bytes32",
},
],
name: "relayRootBundle",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
name: "rootBundles",
outputs: [
{
internalType: "bytes32",
name: "slowRelayRoot",
type: "bytes32",
},
{
internalType: "bytes32",
name: "relayerRefundRoot",
type: "bytes32",
},
],
stateMutability: "view",
type: "function",
},
];

export const minimalAdapterInterface = [
{
inputs: [
{
internalType: "address",
name: "target",
type: "address",
},
{
internalType: "bytes",
name: "message",
type: "bytes",
},
],
name: "relayMessage",
outputs: [],
stateMutability: "payable",
type: "function",
},
];

async function askQuestion(query: string) {
Expand Down
Loading