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

add fee router scripts / cli commands #17

Merged
merged 9 commits into from
Dec 14, 2023
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
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CHILD_CHAIN_PK=xyx
PARENT_CHAIN_PK=xyz
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"repository": "[email protected]:OffchainLabs/fund-distribution-contracts.git",
"author": "dzgoldman",
"license": "MIT",
"scripts":{
"gen-recipients": "make install && hardhat run src-ts/getRecipientData.ts"
"scripts": {
"gen-recipients": "make install && hardhat run src-ts/getRecipientData.ts"
},
"devDependencies": {
"@ethersproject/providers": "^5.7.2",
Expand All @@ -17,7 +17,7 @@
"@nomicfoundation/hardhat-toolbox": "^4.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.1",
"@typechain/ethers-v5": "^10.1.0",
"@typechain/hardhat":"^6.1.3",
"@typechain/hardhat": "^6.1.3",
"@types/chai": "^4.3.10",
"@types/mocha": "^10.0.4",
"chai": "^4.3.10",
Expand All @@ -30,6 +30,10 @@
"typescript": "^5.2.2"
},
"dependencies": {
"@openzeppelin/contracts": "^5.0.0"
"@arbitrum/sdk": "^3.1.13",
"@openzeppelin/contracts": "^5.0.0",
"@types/yargs": "^17.0.32",
"dotenv": "^16.3.1",
"yargs": "^17.7.2"
}
}
120 changes: 120 additions & 0 deletions src-ts/FeeRouter/ChildToParentMessageRedeemer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { JsonRpcProvider } from "@ethersproject/providers";
import { Wallet } from "ethers";
import {
ChildToParentRewardRouter__factory,
ChildToParentRewardRouter,
} from "../../typechain-types";
import { L2ToL1TxEvent } from "@arbitrum/sdk/dist/lib/abi/ArbSys";
import { EventArgs } from "@arbitrum/sdk/dist/lib/dataEntities/event";

import {
L2TransactionReceipt,
L2ToL1Message,
L2ToL1MessageStatus,
} from "@arbitrum/sdk";
const wait = async (ms: number) => new Promise((res) => setTimeout(res, ms));

export default class ChildToParentMessageRedeemer {
public startBlock: number;
public childToParentRewardRouter: ChildToParentRewardRouter;
constructor(
public readonly childChainProvider: JsonRpcProvider,
public readonly parentChainSigner: Wallet,
public readonly childToParentRewardRouterAddr: string,
public readonly blockLag: number,
initialStartBlock: number
) {
this.startBlock = initialStartBlock;
this.childToParentRewardRouter = ChildToParentRewardRouter__factory.connect(
childToParentRewardRouterAddr,
childChainProvider
);
}

public async redeemChildToParentMessages(oneOff = false) {
const toBlock =
(await this.childChainProvider.getBlockNumber()) - this.blockLag;
const logs = await this.childChainProvider.getLogs({
fromBlock: this.startBlock,
toBlock: toBlock,
...this.childToParentRewardRouter.filters.FundsRouted(),
});
if (logs.length) {
console.log(
`Found ${logs.length} route events between blocks ${this.startBlock} and ${toBlock}`
);
}

for (let log of logs) {
const arbTransactionRec = new L2TransactionReceipt(
await this.childChainProvider.getTransactionReceipt(log.transactionHash)
);
const l2ToL1Events = (
(await arbTransactionRec.getL2ToL1Events()) as EventArgs<L2ToL1TxEvent>[]
)
// Filter out any other L2 to L1 messages initiated in this transaction
.filter(
(l2ToL1Event) =>
l2ToL1Event.caller.toLowerCase() ===
this.childToParentRewardRouter.address.toLowerCase()
);
// sanity check
if (l2ToL1Events.length === 0) {
throw new Error("Impossible result: L2 to L1 msg not found");
}

for (let l2ToL1Event of l2ToL1Events) {
const l2ToL1Message = L2ToL1Message.fromEvent(
this.parentChainSigner,
l2ToL1Event
);
if (!oneOff) {
console.log(`Waiting for ${l2ToL1Event.hash} to be ready:`);
await l2ToL1Message.waitUntilReadyToExecute(
this.childChainProvider,
1000 * 60 * 30
);
}

const status = await l2ToL1Message.status(this.childChainProvider);
switch (status) {
case L2ToL1MessageStatus.CONFIRMED: {
console.log(l2ToL1Event.hash, "confirmed; executing:");
const rec = await (
await l2ToL1Message.execute(this.childChainProvider)
).wait(2);
console.log(`${l2ToL1Event.hash} executed:`, rec.transactionHash);
break;
}
case L2ToL1MessageStatus.EXECUTED: {
console.log(`${l2ToL1Event.hash} already executed`);
break;
}
case L2ToL1MessageStatus.UNCONFIRMED: {
DZGoldman marked this conversation as resolved.
Show resolved Hide resolved
console.log(`${l2ToL1Event.hash} not yet confirmed`);
break;
}
default: {
throw new Error(`Unhandled L2ToL1MessageStatus case: ${status}`);
}
}
}
}
this.startBlock = toBlock;
}

public async run(oneOff = false) {
while (true) {
try {
await this.redeemChildToParentMessages(oneOff);
} catch (err) {
console.log("err", err);
}
if (oneOff) {
break;
} else {
await wait(1000 * 60 * 60);
}
}
}
}
88 changes: 88 additions & 0 deletions src-ts/FeeRouter/ParentToChildRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Wallet, BigNumber } from "ethers";
import {
ParentToChildRewardRouter__factory,
ParentToChildRewardRouter,
} from "../../typechain-types";
import { Inbox__factory } from "@arbitrum/sdk/dist/lib/abi/factories/Inbox__factory";
import {
L1TransactionReceipt,
L1ToL2MessageStatus,
} from "@arbitrum/sdk";

export const checkAndRouteFunds = async (
parentChainSigner: Wallet,
childChainSigner: Wallet,
parentToChildRewardRouterAddr: string,
minBalance: BigNumber
) => {
if (
(await parentChainSigner.provider.getBalance(parentToChildRewardRouterAddr)).lt(
minBalance
)
) {
return;
}

const parentToChildRewardRouter: ParentToChildRewardRouter =
ParentToChildRewardRouter__factory.connect(
parentToChildRewardRouterAddr,
parentChainSigner
);

// check if it's time to trigger
if (!(await parentToChildRewardRouter.canDistribute())) {
return;
}
console.log("Calling parent to child router:");

const inbox = Inbox__factory.connect(
await parentToChildRewardRouter.inbox(),
parentChainSigner.provider
);
// retryable has 0 calldata (simple transfer). 0 in second paramt uses current L1 basefee
const _submissionFee = await inbox.calculateRetryableSubmissionFee(0, 0);

// add a 10% increase for insurance
const submissionFee = _submissionFee.add(_submissionFee.mul(.1))

const gasPrice = await childChainSigner.getGasPrice();

// we use the minimum gas limit set in the contract (we presume it's more than enough)
const gasLimit = await parentToChildRewardRouter.minGasLimit();

const value = submissionFee.add(gasPrice.mul(gasLimit));

const rec = await (
await parentToChildRewardRouter.routeFunds(
submissionFee,
gasLimit,
gasPrice,
{
value,
}
)
).wait(1);
console.log('Retryable created', rec.transactionHash);

const l1TxRec = new L1TransactionReceipt(rec);
const l1ToL2Msgs = await l1TxRec.getL1ToL2Messages(childChainSigner);
if (l1ToL2Msgs.length != 1) throw new Error("Unexpected messages length");

const l1ToL2Msg = l1ToL2Msgs[0]
console.log('Waiting for result:');
const result = await l1ToL2Msg.waitForStatus()
if(result.status == L1ToL2MessageStatus.FUNDS_DEPOSITED_ON_L2 ){
console.log('Retryable failed; retrying:');

const rec = await (await l1ToL2Msg.redeem()).wait()
console.log('Successfully redeemed:', rec.transactionHash);

} else if (result.status == L1ToL2MessageStatus.REDEEMED){
console.log('Successfully redeemed');

} else {
throw new Error("Error: unexpected retryable status")
}


};
53 changes: 53 additions & 0 deletions src-ts/cli/childToParentRedeemer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import dotenv from "dotenv";
import yargs from "yargs";
import ChildToParentMessageRedeemer from "../FeeRouter/ChildToParentMessageRedeemer";
import { JsonRpcProvider } from "@ethersproject/providers";
import { Wallet } from "ethers";

dotenv.config();

const PARENT_CHAIN_PK = process.env.PARENT_CHAIN_PK;

if (!PARENT_CHAIN_PK) throw new Error("Need PARENT_CHAIN_PK");

const options = yargs(process.argv.slice(2))
.options({
parentRPCUrl: { type: "string", demandOption: true },
childRPCUrl: { type: "string", demandOption: true },
childToParentRewardRouterAddr: { type: "string", demandOption: true },
blockLag: { type: "number", demandOption: false, default: 5 },
childChainStartBlock: { type: "number", demandOption: false, default: 0 },
oneOff: {
type: "boolean",
demandOption: false,
default: false,
description:
"Runs continuously if false, runs once and terminates if true",
},
})
.parseSync() as {
parentRPCUrl: string;
childRPCUrl: string;
childToParentRewardRouterAddr: string;
blockLag: number;
childChainStartBlock: number;
oneOff: boolean;
};

(async () => {
const parentChildSigner = new Wallet(
PARENT_CHAIN_PK,
new JsonRpcProvider(options.parentRPCUrl)
);
console.log(`Signing with ${parentChildSigner.address} on parent chain
${(await parentChildSigner.provider.getNetwork()).chainId}'`);

const redeemer = new ChildToParentMessageRedeemer(
new JsonRpcProvider(options.childRPCUrl),
parentChildSigner,
options.childToParentRewardRouterAddr,
options.blockLag,
options.childChainStartBlock
);
await redeemer.run(options.oneOff);
})();
51 changes: 51 additions & 0 deletions src-ts/cli/routeParentToChild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import dotenv from "dotenv";
import yargs from "yargs";
import { JsonRpcProvider } from "@ethersproject/providers";
import { Wallet, utils } from "ethers";
import { checkAndRouteFunds } from "../FeeRouter/ParentToChildRouter";

dotenv.config();

const PARENT_CHAIN_PK = process.env.PARENT_CHAIN_PK;
if (!PARENT_CHAIN_PK) throw new Error("Need PARENT_CHAIN_PK");

const CHILD_CHAIN_PK = process.env.CHILD_CHAIN_PK;
if (!CHILD_CHAIN_PK) throw new Error("Need CHILD_CHAIN_PK");

const options = yargs(process.argv.slice(2))
.options({
parentRPCUrl: { type: "string", demandOption: true },
childRPCUrl: { type: "string", demandOption: true },
parentToChildRewardRouterAddr: { type: "string", demandOption: true },
minBalanceEther: { type: "number", demandOption: false, default: 0 },
})
.parseSync() as {
parentRPCUrl: string;
childRPCUrl: string;
parentToChildRewardRouterAddr: string;
minBalanceEther: number;
};

(async () => {
const parentChildSigner = new Wallet(
PARENT_CHAIN_PK,
new JsonRpcProvider(options.parentRPCUrl)
);
console.log(`Signing with ${parentChildSigner.address} on parent chain
${(await parentChildSigner.provider.getNetwork()).chainId}'`);

const childChainSigner = new Wallet(
PARENT_CHAIN_PK,
new JsonRpcProvider(options.childRPCUrl)
);

console.log(`Signing with ${childChainSigner.address} on child chain
${(await childChainSigner.provider.getNetwork()).chainId}'`);
await checkAndRouteFunds(
yahgwai marked this conversation as resolved.
Show resolved Hide resolved
parentChildSigner,
childChainSigner,
options.parentToChildRewardRouterAddr,
utils.parseEther(String(options.minBalanceEther))
);
console.log("done");
})();
Loading
Loading