-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from OffchainLabs/fund-router-scripts
add fee router scripts / cli commands
- Loading branch information
Showing
7 changed files
with
393 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
CHILD_CHAIN_PK=xyx | ||
PARENT_CHAIN_PK=xyz |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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", | ||
|
@@ -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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: { | ||
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); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
|
||
|
||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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( | ||
parentChildSigner, | ||
childChainSigner, | ||
options.parentToChildRewardRouterAddr, | ||
utils.parseEther(String(options.minBalanceEther)) | ||
); | ||
console.log("done"); | ||
})(); |
Oops, something went wrong.