Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into james/acx-1507-add-…
Browse files Browse the repository at this point in the history
…ability-to-mock-block-ranges-in-disputer-local-runs
  • Loading branch information
pxrl committed Sep 11, 2023
2 parents 4884031 + 279fad9 commit b9ce17a
Show file tree
Hide file tree
Showing 60 changed files with 1,282 additions and 342 deletions.
9 changes: 6 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,12 @@ NODE_MAX_CONCURRENCY=25
SEND_RELAYS=false


# List of destination chains to be supported by the relayer. If set to a
# non-empty list, only transfers going to these chains will be filled. For
# example, if set to [1], only transfers destined for Ethereum will be filled.
# List of origin and destination chains to be supported by the relayer. If set
# to a non-empty list, only transfers complying with the specified origin and
# destination chains will be filled. For example:
# RELAYER_ORIGIN_CHAINS=[1] # Only fill deposits that were placed on Optimism.
# RELAYER_DESTINATION_CHAINS=[10] # Only fill deposits destined for Ethereum.
RELAYER_ORIGIN_CHAINS=[1,10,137,324,42161]
RELAYER_DESTINATION_CHAINS=[1,10,137,42161]


Expand Down
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
project: "./tsconfig.eslint.json",
},
rules: {
"prettier/prettier": ["warn"],
Expand All @@ -35,6 +36,8 @@ module.exports = {
"@typescript-eslint/no-unused-vars": ["error", { ignoreRestSiblings: true }],
"chai-expect/missing-assertion": 2,
"no-duplicate-imports": "error",
// "require-await": "error",
"@typescript-eslint/no-floating-promises": ["error"],
},
settings: {
node: {
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"dependencies": {
"@across-protocol/contracts-v2": "2.4.3",
"@across-protocol/sdk-v2": "0.15.20",
"@across-protocol/sdk-v2": "0.15.21",
"@arbitrum/sdk": "^3.1.3",
"@defi-wonderland/smock": "^2.3.5",
"@eth-optimism/sdk": "^3.1.0",
Expand Down Expand Up @@ -54,10 +54,13 @@
"test": "hardhat test",
"build": "tsc --build",
"watch": "tsc --build --incremental --watch",
"build:test": "tsc --project tsconfig.test.json",
"clean": "dir=\"./node_modules\"; mv \"${dir}\" \"${dir}_\" 2>/dev/null && rm -r \"${dir}_\" &",
"reinstall": "yarn clean && yarn install && yarn build",
"update": "git pull && yarn reinstall && yarn version --non-interactive && git show --quiet",
"relay": "HARDHAT_CONFIG=./dist/hardhat.config.js node ./dist/index.js --relayer"
"relay": "HARDHAT_CONFIG=./dist/hardhat.config.js node ./dist/index.js --relayer",
"deposit": "yarn ts-node ./scripts/spokepool.ts deposit",
"dispute": "yarn ts-node ./scripts/hubpool.ts dispute"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.2.3",
Expand All @@ -71,6 +74,7 @@
"@types/minimist": "^1.2.2",
"@types/mocha": "^10.0.1",
"@types/node": "^20.3.1",
"@types/sinon": "^10.0.16",
"@typescript-eslint/eslint-plugin": "^4.29.1",
"@typescript-eslint/parser": "^4.29.1",
"chai": "^4.3.7",
Expand Down
274 changes: 274 additions & 0 deletions scripts/hubpool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import minimist from "minimist";
import { WETH9__factory as WETH9 } from "@across-protocol/contracts-v2";
import { BigNumber, ethers, Wallet } from "ethers";
import { config } from "dotenv";
import { getNetworkName, getSigner } from "../src/utils";
import * as utils from "./utils";

const { MaxUint256, One: bnOne } = ethers.constants;
const { formatEther, formatUnits } = ethers.utils;

// https://nodejs.org/api/process.html#exit-codes
const NODE_SUCCESS = 0;
const NODE_INPUT_ERR = 9;
const NODE_APP_ERR = 127; // user-defined

function bnMax(a: BigNumber, b: BigNumber): BigNumber {
const result = a.sub(b);
return result.isZero() || result.gt(0) ? a : b;
}

async function dispute(args: Record<string, number | string>, signer: Wallet): Promise<boolean> {
const ethBuffer = "0.1"; // Spare ether required to pay for gas.

const chainId = Number(args.chainId);
const { force, txnHash } = args;

const network = getNetworkName(chainId);
const hubPool = await utils.getContract(chainId, "HubPool");
signer = signer.connect(hubPool.provider);
const [bondTokenAddress, bondAmount, proposal, liveness, latestBlock] = await Promise.all([
hubPool.bondToken(),
hubPool.bondAmount(),
hubPool.rootBundleProposal(),
hubPool.liveness(),
hubPool.provider.getBlock("latest"),
]);

const filter = hubPool.filters.ProposeRootBundle();
const avgBlockTime = 12.5; // @todo import
const fromBlock = Math.floor(latestBlock.number - (liveness - avgBlockTime));
const bondToken = WETH9.connect(bondTokenAddress, hubPool.provider);
const [bondBalance, decimals, symbol, allowance, proposals] = await Promise.all([
bondToken.balanceOf(signer.address),
bondToken.decimals(),
bondToken.symbol(),
bondToken.allowance(signer.address, hubPool.address),
hubPool.queryFilter(filter, fromBlock, latestBlock.number),
]);

/* Resolve the existing proposal to dump its information. */
const { poolRebalanceRoot, relayerRefundRoot, slowRelayRoot, challengePeriodEndTimestamp } = proposal;
const rootBundleProposal = proposals.find(({ args }) => {
return (
args.poolRebalanceRoot === poolRebalanceRoot &&
args.relayerRefundRoot === relayerRefundRoot &&
args.slowRelayRoot === slowRelayRoot
);
});
const fields = {
address: bondToken.address,
symbol,
amount: formatUnits(bondAmount, decimals),
balance: formatUnits(bondBalance, decimals),
};

// @dev This works fine but is hackish. Might be nice to refactor later.
const proposalKeys = Object.keys(proposal).filter((key) => isNaN(Number(key)));
const _proposal = {
blockNumber: rootBundleProposal?.blockNumber,
transactionHash: rootBundleProposal?.transactionHash,
...Object.fromEntries(proposalKeys.map((k) => [k, proposal[k]])),
};

const padLeft = [...Object.keys(fields), ...Object.keys(_proposal)].reduce(
(acc, cur) => (cur.length > acc ? cur.length : acc),
0
);
console.log(
`${network} HubPool Dispute Bond:\n` +
Object.entries(fields)
.map(([k, v]) => `\t${k.padEnd(padLeft)} : ${v}`)
.join("\n") +
"\n"
);

if (rootBundleProposal === undefined) {
console.log(
`Warning: No matching root bundle proposal found between ${network} blocks ${fromBlock}, ${latestBlock.number}.`
);
} else {
console.log(
`${network} Root Bundle Proposal:\n` +
Object.entries(_proposal)
.map(([k, v]) => `\t${k.padEnd(padLeft)} : ${v}`)
.join("\n") +
"\n"
);
}

if (allowance.lt(bondAmount)) {
console.log(`Approving ${network} HubPool @ ${hubPool.address} to transfer ${symbol}.`);
const approval = await bondToken.connect(signer).approve(hubPool.address, MaxUint256);
console.log(`Approval: ${approval.hash}...`);
await approval.wait();
}

if (bondBalance.lt(bondAmount)) {
const buffer = ethers.utils.parseEther(ethBuffer);
const ethBalance = await signer.getBalance();
if (ethBalance.lt(bondAmount.add(buffer))) {
const minDeposit = bondAmount.add(buffer).sub(ethBalance).sub(bondBalance);
console.log(
`Cannot dispute - insufficient ${symbol} balance.` + ` Deposit at least ${formatUnits(minDeposit, 18)} ETH.`
);
return false;
}
const depositAmount = bnMax(bondAmount.sub(bondBalance), bnOne); // Enforce minimum 1 Wei for test.
console.log(`Depositing ${formatEther(depositAmount)} @ ${bondToken.address}.`);
const deposit = await bondToken.connect(signer).deposit({ value: depositAmount });
console.log(`Deposit: ${deposit.hash}...`);
await deposit.wait();
}
if (latestBlock.timestamp >= challengePeriodEndTimestamp && !force) {
console.log("Nothing to dispute: no active propopsal.");
return txnHash === undefined;
}

// The txn hash of the proposal must be supplied in order to dispute.
// If no hash was supplied, request the user to re-run with the applicable hash.
if (txnHash !== rootBundleProposal.transactionHash && !force) {
if (txnHash !== undefined) {
console.log(`Invalid proposal transaction hash supplied: ${txnHash}.`);
}
console.log(
"To dispute, re-run with the following transaction hash (WARNING: THIS *WILL* SUBMIT A DISPUTE):\n" +
`\n\t--txnHash ${rootBundleProposal.transactionHash}\n` +
"\nFor example:\n" +
`\n\tyarn dispute --txnHash ${rootBundleProposal.transactionHash}\n`
);
return txnHash === undefined;
}

const dispute = await hubPool.connect(signer).disputeRootBundle();
console.log(`Disputing ${network} HubPool proposal: ${dispute.hash}.`);
await dispute.wait();
console.log("Disputed HubPool proposal.");

return true;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function search(args: Record<string, number | string>, _signer: Wallet): Promise<boolean> {
const eventName = args.event as string;
const fromBlock = Number(args.fromBlock) || undefined;
const toBlock = Number(args.toBlock) || undefined;
const chainId = Number(args.chainId);

if (!isNaN(fromBlock) && !isNaN(toBlock) && toBlock < fromBlock) {
throw new Error(`Invalid block range: ${fromBlock}, ${toBlock}`);
}

const [configStore, hubPool] = await Promise.all([
utils.getContract(chainId, "ConfigStore"),
utils.getContract(chainId, "HubPool"),
]);

const filter = hubPool.filters[eventName]?.();
if (filter === undefined) {
throw new Error(`Unrecognised HubPool event (${eventName})`);
}

const events = await hubPool.queryFilter(filter, fromBlock, toBlock);
const CHAIN_ID_INDICES = ethers.utils.formatBytes32String("CHAIN_ID_INDICES");
for (const { transactionHash, blockNumber, data, topics } of events) {
const [block, liveness, _chainIds] = await Promise.all([
hubPool.provider.getBlock(blockNumber),
hubPool.liveness({ blockTag: blockNumber }),
configStore.globalConfig(CHAIN_ID_INDICES, { blockTag: blockNumber }),
]);

const DEFAULT_CHAIN_IDS = chainId === 1 ? utils.chains : utils.testChains;
const chainIds = _chainIds.length > 0 ? JSON.parse(_chainIds.replaceAll('"', "")) : DEFAULT_CHAIN_IDS;

const args = hubPool.interface.parseLog({ data, topics }).args;
const eventArgs = Object.keys(args).filter((key) => isNaN(Number(key)));
const dateStr = new Date(Number(block.timestamp * 1000)).toUTCString();

const fields = {
blockNumber,
timestamp: `${block.timestamp} (${dateStr})`,
transactionHash,
liveness,
chainIds: chainIds.join(","),
...Object.fromEntries(eventArgs.map((arg) => [arg, args[arg]])),
};
const padLeft = Object.keys(fields).reduce((acc, cur) => (cur.length > acc ? cur.length : acc), 0);
console.log(
Object.entries(fields)
.map(([k, v]) => `${k.padEnd(padLeft)} : ${v}`)
.join("\n") + "\n"
);
}

return true;
}

function usage(badInput?: string): boolean {
let usageStr = badInput ? `\nUnrecognized input: "${badInput}".\n\n` : "";
const walletOpts = "mnemonic|privateKey";
const runtimeArgs = {
dispute: ["--chainId", "[--txnHash <proposalHash>]"],
search: ["--chainId", "--event <eventName>", "[--fromBlock <fromBlock>]", "[--toBlock <toBlock>]"],
};

usageStr += "Usage:\n";
usageStr += Object.entries(runtimeArgs)
.map(([k, v]) => `\tyarn hubpool --wallet <${walletOpts}> ${k} ${v.join(" ")}`)
.join("\n");

console.log(usageStr);
return badInput === undefined ? false : true;
}

async function run(argv: string[]): Promise<number> {
const opts = {
string: ["chainId", "transactionHash", "event", "fromBlock", "toBlock", "wallet"],
boolean: ["force"],
default: {
chainId: 1,
event: "ProposeRootBundle",
wallet: "mnemonic",
force: false,
},
alias: {
transactionHash: "txnHash",
},
unknown: usage,
};
const args = minimist(argv.slice(1), opts);

config();

let signer: Wallet;
try {
signer = await getSigner({ keyType: args.wallet, cleanEnv: true });
} catch (err) {
return usage(args.wallet) ? NODE_SUCCESS : NODE_INPUT_ERR;
}

let result: boolean;
switch (argv[0]) {
case "dispute":
result = await dispute(args, signer);
break;
case "search":
result = await search(args, signer);
break;
default:
return usage() ? NODE_SUCCESS : NODE_INPUT_ERR;
}

return result ? NODE_SUCCESS : NODE_APP_ERR;
}

if (require.main === module) {
run(process.argv.slice(2))
.then(async (result) => {
process.exitCode = result;
})
.catch(async (error) => {
console.error("Process exited with", error);
process.exitCode = NODE_APP_ERR;
});
}
Loading

0 comments on commit b9ce17a

Please sign in to comment.