Skip to content

Commit 41b2b09

Browse files
feat(Scroll): add rescue script for stuck txns (#406)
1 parent e1cf4f0 commit 41b2b09

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

hardhat.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import "@openzeppelin/hardhat-upgrades";
1717
// Custom tasks to add to HRE.
1818
// eslint-disable-next-line node/no-missing-require
1919
require("./tasks/enableL1TokenAcrossEcosystem");
20+
// eslint-disable-next-line node/no-missing-require
2021
require("./tasks/finalizeScrollClaims");
22+
// eslint-disable-next-line node/no-missing-require
23+
require("./tasks/rescueStuckScrollTxn");
2124

2225
dotenv.config();
2326

tasks/rescueStuckScrollTxn.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/* eslint-disable camelcase */
2+
import { task } from "hardhat/config";
3+
import { Contract, Signer, ethers } from "ethers";
4+
import { L1_ADDRESS_MAP } from "../deploy/consts";
5+
6+
require("dotenv").config();
7+
8+
const relayMessengerAbi = [
9+
{
10+
anonymous: false,
11+
inputs: [
12+
{
13+
indexed: true,
14+
internalType: "address",
15+
name: "sender",
16+
type: "address",
17+
},
18+
{
19+
indexed: true,
20+
internalType: "address",
21+
name: "target",
22+
type: "address",
23+
},
24+
{
25+
indexed: false,
26+
internalType: "uint256",
27+
name: "value",
28+
type: "uint256",
29+
},
30+
{
31+
indexed: false,
32+
internalType: "uint256",
33+
name: "messageNonce",
34+
type: "uint256",
35+
},
36+
{
37+
indexed: false,
38+
internalType: "uint256",
39+
name: "gasLimit",
40+
type: "uint256",
41+
},
42+
{
43+
indexed: false,
44+
internalType: "bytes",
45+
name: "message",
46+
type: "bytes",
47+
},
48+
],
49+
name: "SentMessage",
50+
type: "event",
51+
},
52+
{
53+
inputs: [
54+
{
55+
internalType: "address",
56+
name: "_from",
57+
type: "address",
58+
},
59+
{
60+
internalType: "address",
61+
name: "_to",
62+
type: "address",
63+
},
64+
{
65+
internalType: "uint256",
66+
name: "_value",
67+
type: "uint256",
68+
},
69+
{
70+
internalType: "uint256",
71+
name: "_messageNonce",
72+
type: "uint256",
73+
},
74+
{
75+
internalType: "bytes",
76+
name: "_message",
77+
type: "bytes",
78+
},
79+
{
80+
internalType: "uint32",
81+
name: "_newGasLimit",
82+
type: "uint32",
83+
},
84+
{
85+
internalType: "address",
86+
name: "_refundAddress",
87+
type: "address",
88+
},
89+
],
90+
name: "replayMessage",
91+
outputs: [],
92+
stateMutability: "payable",
93+
type: "function",
94+
},
95+
];
96+
97+
task("rescue-stuck-scroll-txn", "Rescue a failed Scroll transaction")
98+
.addParam("l1Hash", "Txn of the L1 message to rescue")
99+
.addParam("gasLimit", "Gas limit to use for the rescue transaction")
100+
.setAction(async function (taskArguments, hre_: any) {
101+
const chainId = await hre_.getChainId();
102+
if (!["1", "11155111"].includes(String(chainId))) {
103+
throw new Error("This script can only be run on Sepolia or Ethereum mainnet");
104+
}
105+
const signer = (await hre_.ethers.getSigners())[0] as unknown as Signer;
106+
const messengerContract = new Contract(L1_ADDRESS_MAP[chainId].scrollMessengerRelay, relayMessengerAbi, signer);
107+
108+
const txn = await signer.provider?.getTransactionReceipt(taskArguments.l1Hash);
109+
const relevantEvent = txn?.logs?.find(
110+
(log) => log.topics[0] === messengerContract.interface.getEventTopic("SentMessage")
111+
);
112+
if (!relevantEvent) {
113+
throw new Error("No relevant event found. Is this a Scroll bridge transaction?");
114+
}
115+
const decodedEvent = messengerContract.interface.parseLog(relevantEvent);
116+
const { sender, target, value, messageNonce, message } = decodedEvent.args;
117+
const refundAddress = await signer.getAddress();
118+
119+
console.debug("Log found. Event Decoded.");
120+
console.debug("Will replay with these parameters:", {
121+
_from: sender,
122+
_to: target,
123+
_value: value.toString(),
124+
_messageNonce: messageNonce.toString(),
125+
_message: message.toString(),
126+
_newGasLimit: taskArguments.gasLimit,
127+
_refundAddress: refundAddress,
128+
});
129+
console.debug("Replaying message (sending with 0.001ETH )...");
130+
const resultingTxn = await messengerContract.replayMessage(
131+
sender, // _from
132+
target, // _to
133+
value, // _value
134+
messageNonce, // _messageNonce
135+
message, // _message
136+
ethers.BigNumber.from(taskArguments.gasLimit), // _newGasLimit
137+
refundAddress, // _refundAddress
138+
{
139+
// 0.001 ETH to be sent to the Scroll relayer (to cover L1 gas costs)
140+
// Using recommended value default as described here: https://docs.scroll.io/en/developers/l1-and-l2-bridging/eth-and-erc20-token-bridge/
141+
// *Any* leftover ETH will be immediately refunded to the signer - this is just the L1 gas cost for submitting the transaction
142+
value: ethers.utils.parseEther("0.001"),
143+
}
144+
);
145+
console.log("Replay transaction hash:", resultingTxn.hash);
146+
});

0 commit comments

Comments
 (0)