clients/js: Add command to hijack a mainnet fork + misc utilities
* Add command to update evm storage slot * Add command to ecrecover * Add command to print contract addresses * Add command to print RPC urls * Add command to query on-chain state of EVM contracts * Print digest in parse VAA command
This commit is contained in:
parent
22dda6ba84
commit
3c6e1b07d6
|
@ -1,31 +1,129 @@
|
|||
import { BridgeImplementation__factory, Implementation__factory, NFTBridgeImplementation__factory } from "@certusone/wormhole-sdk"
|
||||
import { BridgeImplementation__factory, CHAINS, Implementation__factory, NFTBridgeImplementation__factory } from "@certusone/wormhole-sdk"
|
||||
import { ethers } from "ethers"
|
||||
import { NETWORKS } from "./networks"
|
||||
import { impossible, Payload } from "./vaa"
|
||||
import { encode, Encoding, impossible, Payload, typeWidth } from "./vaa"
|
||||
import { Contracts, CONTRACTS, EVMChainName } from "@certusone/wormhole-sdk"
|
||||
import axios from "axios";
|
||||
import * as celo from "@celo-tools/celo-ethers-wrapper";
|
||||
import { solidityKeccak256 } from "ethers/lib/utils"
|
||||
|
||||
const _IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
|
||||
|
||||
export async function query_contract_evm(
|
||||
network: "MAINNET" | "TESTNET" | "DEVNET",
|
||||
chain: EVMChainName,
|
||||
module: "Core" | "NFTBridge" | "TokenBridge",
|
||||
contract_address: string | undefined,
|
||||
_rpc: string | undefined
|
||||
): Promise<object> {
|
||||
let n = NETWORKS[network][chain]
|
||||
let rpc: string | undefined = _rpc ?? n.rpc;
|
||||
if (rpc === undefined) {
|
||||
throw Error(`No ${network} rpc defined for ${chain} (see networks.ts)`)
|
||||
}
|
||||
|
||||
let contracts: Contracts = CONTRACTS[network][chain]
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(rpc)
|
||||
|
||||
let result: any = {}
|
||||
|
||||
switch (module) {
|
||||
case "Core":
|
||||
contract_address = contract_address ? contract_address : contracts.core;
|
||||
if (contract_address === undefined) {
|
||||
throw Error(`Unknown core contract on ${network} for ${chain}`)
|
||||
}
|
||||
const core = Implementation__factory.connect(contract_address, provider)
|
||||
result.address = contract_address
|
||||
result.currentGuardianSetIndex = await core.getCurrentGuardianSetIndex()
|
||||
result.guardianSet = {}
|
||||
for (let i of Array(result.currentGuardianSetIndex + 1).keys()) {
|
||||
let guardian_set = await core.getGuardianSet(i)
|
||||
result.guardianSet[i] = { keys: guardian_set[0], expiry: guardian_set[1] }
|
||||
}
|
||||
result.guardianSetExpiry = await core.getGuardianSetExpiry()
|
||||
result.chainId = await core.chainId()
|
||||
result.governanceChainId = await core.governanceChainId()
|
||||
result.governanceContract = await core.governanceContract()
|
||||
result.messageFee = await core.messageFee()
|
||||
result.implementation = (await getStorageAt(rpc, contract_address, _IMPLEMENTATION_SLOT, ["address"]))[0]
|
||||
result.isInitialized = await core.isInitialized(result.implementation)
|
||||
break
|
||||
case "TokenBridge":
|
||||
contract_address = contract_address ? contract_address : contracts.token_bridge;
|
||||
if (contract_address === undefined) {
|
||||
throw Error(`Unknown token bridge contract on ${network} for ${chain}`)
|
||||
}
|
||||
const tb = BridgeImplementation__factory.connect(contract_address, provider)
|
||||
result.address = contract_address
|
||||
result.wormhole = await tb.wormhole()
|
||||
result.implementation = (await getStorageAt(rpc, contract_address, _IMPLEMENTATION_SLOT, ["address"]))[0]
|
||||
result.isInitialized = await tb.isInitialized(result.implementation)
|
||||
result.tokenImplementation = await tb.tokenImplementation()
|
||||
result.chainId = await tb.chainId()
|
||||
result.governanceChainId = await tb.governanceChainId()
|
||||
result.governanceContract = await tb.governanceContract()
|
||||
result.WETH = await tb.WETH()
|
||||
result.registrations = {}
|
||||
for (let [c_name, c_id] of Object.entries(CHAINS)) {
|
||||
if (c_name === chain || c_name === "unset") {
|
||||
continue
|
||||
}
|
||||
result.registrations[c_name] = await tb.bridgeContracts(c_id)
|
||||
}
|
||||
break
|
||||
case "NFTBridge":
|
||||
contract_address = contract_address ? contract_address : contracts.nft_bridge;
|
||||
if (contract_address === undefined) {
|
||||
throw Error(`Unknown nft bridge contract on ${network} for ${chain}`)
|
||||
}
|
||||
const nb = NFTBridgeImplementation__factory.connect(contract_address, provider)
|
||||
result.address = contract_address
|
||||
result.wormhole = await nb.wormhole()
|
||||
result.implementation = (await getStorageAt(rpc, contract_address, _IMPLEMENTATION_SLOT, ["address"]))[0]
|
||||
result.isInitialized = await nb.isInitialized(result.implementation)
|
||||
result.tokenImplementation = await nb.tokenImplementation()
|
||||
result.chainId = await nb.chainId()
|
||||
result.governanceChainId = await nb.governanceChainId()
|
||||
result.governanceContract = await nb.governanceContract()
|
||||
result.registrations = {}
|
||||
for (let [c_name, c_id] of Object.entries(CHAINS)) {
|
||||
if (c_name === chain || c_name === "unset") {
|
||||
continue
|
||||
}
|
||||
result.registrations[c_name] = await nb.bridgeContracts(c_id)
|
||||
}
|
||||
break
|
||||
default:
|
||||
impossible(module)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export async function execute_governance_evm(
|
||||
payload: Payload,
|
||||
vaa: Buffer,
|
||||
network: "MAINNET" | "TESTNET" | "DEVNET",
|
||||
chain: EVMChainName
|
||||
chain: EVMChainName,
|
||||
contract_address: string | undefined,
|
||||
_rpc: string | undefined
|
||||
) {
|
||||
let n = NETWORKS[network][chain]
|
||||
if (!n.rpc) {
|
||||
let rpc: string | undefined = _rpc ?? n.rpc;
|
||||
if (rpc === undefined) {
|
||||
throw Error(`No ${network} rpc defined for ${chain} (see networks.ts)`)
|
||||
}
|
||||
if (!n.key) {
|
||||
throw Error(`No ${network} key defined for ${chain} (see networks.ts)`)
|
||||
}
|
||||
let rpc: string = n.rpc
|
||||
let key: string = n.key
|
||||
|
||||
let contracts: Contracts = CONTRACTS[network][chain]
|
||||
|
||||
let provider = undefined
|
||||
let signer = undefined
|
||||
let provider: ethers.providers.JsonRpcProvider;
|
||||
let signer: ethers.Wallet;
|
||||
if (chain === "celo") {
|
||||
provider = new celo.CeloProvider(rpc)
|
||||
await provider.ready
|
||||
|
@ -33,7 +131,7 @@ export async function execute_governance_evm(
|
|||
} else {
|
||||
provider = new ethers.providers.JsonRpcProvider(rpc)
|
||||
signer = new ethers.Wallet(key, provider)
|
||||
}
|
||||
}
|
||||
|
||||
// Here we apply a set of chain-specific overrides.
|
||||
// NOTE: some of these might have only been tested on mainnet. If it fails in
|
||||
|
@ -53,11 +151,12 @@ export async function execute_governance_evm(
|
|||
|
||||
switch (payload.module) {
|
||||
case "Core":
|
||||
if (contracts.core === undefined) {
|
||||
contract_address = contract_address ? contract_address : contracts.core;
|
||||
if (contract_address === undefined) {
|
||||
throw Error(`Unknown core contract on ${network} for ${chain}`)
|
||||
}
|
||||
let c = new Implementation__factory(signer)
|
||||
let cb = c.attach(contracts.core)
|
||||
let cb = c.attach(contract_address)
|
||||
switch (payload.type) {
|
||||
case "GuardianSetUpgrade":
|
||||
console.log("Submitting new guardian set")
|
||||
|
@ -72,11 +171,12 @@ export async function execute_governance_evm(
|
|||
}
|
||||
break
|
||||
case "NFTBridge":
|
||||
if (contracts.nft_bridge === undefined) {
|
||||
contract_address = contract_address ? contract_address : contracts.nft_bridge;
|
||||
if (contract_address === undefined) {
|
||||
throw Error(`Unknown nft bridge contract on ${network} for ${chain}`)
|
||||
}
|
||||
let n = new NFTBridgeImplementation__factory(signer)
|
||||
let nb = n.attach(contracts.nft_bridge)
|
||||
let nb = n.attach(contract_address)
|
||||
switch (payload.type) {
|
||||
case "ContractUpgrade":
|
||||
console.log("Upgrading contract")
|
||||
|
@ -93,11 +193,12 @@ export async function execute_governance_evm(
|
|||
}
|
||||
break
|
||||
case "TokenBridge":
|
||||
if (contracts.token_bridge === undefined) {
|
||||
contract_address = contract_address ? contract_address : contracts.token_bridge;
|
||||
if (contract_address === undefined) {
|
||||
throw Error(`Unknown token bridge contract on ${network} for ${chain}`)
|
||||
}
|
||||
let t = new BridgeImplementation__factory(signer)
|
||||
let tb = t.attach(contracts.token_bridge)
|
||||
let tb = t.attach(contract_address)
|
||||
switch (payload.type) {
|
||||
case "ContractUpgrade":
|
||||
console.log("Upgrading contract")
|
||||
|
@ -118,7 +219,69 @@ export async function execute_governance_evm(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getKaruraGasParams(rpc: string): Promise<{
|
||||
/**
|
||||
*
|
||||
* Hijack a core contract. This function is useful when working with a mainnet
|
||||
* fork (hardhat or anvil). A fork of the mainnet contract will naturally store
|
||||
* the mainnet guardian set, so we can't readily interact with these contracts,
|
||||
* because we can't forge signed VAAs for those guardians. This function uses
|
||||
* [[setStorageAt]] to override the guardian set to something we have the
|
||||
* private keys for (typically the devnet guardian used for testing).
|
||||
* This way we can test contract upgrades before rolling them out on mainnet.
|
||||
*
|
||||
* @param rpc the JSON RPC endpoint (needs to be hardhat of anvil)
|
||||
* @param contract_address address of the core bridge contract
|
||||
* @param guardian_addresses addresses of the desired guardian set to upgrade to
|
||||
* @param new_guardian_set_index if specified, the new guardian set will be
|
||||
* written into this guardian set index, and the guardian set index of the
|
||||
* contract changed to it.
|
||||
* If unspecified, then the current guardian set index will be overridden.
|
||||
* In particular, it's possible to both upgrade or downgrade the guardian set
|
||||
* this way. The latter is useful for testing locally if you already have some
|
||||
* VAAs handy that are signed by guardian set 0.
|
||||
*/
|
||||
export async function hijack_evm(
|
||||
rpc: string,
|
||||
contract_address: string,
|
||||
guardian_addresses: string[],
|
||||
new_guardian_set_index: number | undefined
|
||||
): Promise<void> {
|
||||
const GUARDIAN_SETS_SLOT = 0x02
|
||||
const GUARDIAN_SET_INDEX_SLOT = 0x3
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(rpc)
|
||||
const core = Implementation__factory.connect(contract_address, provider)
|
||||
let guardianSetIndex: number
|
||||
let guardianSetExpiry: number
|
||||
[guardianSetIndex, guardianSetExpiry] = await getStorageAt(rpc, contract_address, GUARDIAN_SET_INDEX_SLOT, ["uint32", "uint32"])
|
||||
console.log("Attempting to hijack core bridge guardian set.")
|
||||
const current_set = await core.getGuardianSet(guardianSetIndex)
|
||||
console.log(`Current guardian set (index ${guardianSetIndex}):`)
|
||||
console.log(current_set[0])
|
||||
|
||||
if (new_guardian_set_index !== undefined) {
|
||||
await setStorageAt(rpc, contract_address, GUARDIAN_SET_INDEX_SLOT, ["uint32", "uint32"], [new_guardian_set_index, guardianSetExpiry])
|
||||
guardianSetIndex = await core.getCurrentGuardianSetIndex()
|
||||
if (new_guardian_set_index !== guardianSetIndex) {
|
||||
throw Error("Failed to update guardian set index.")
|
||||
} else {
|
||||
console.log(`Guardian set index updated to ${new_guardian_set_index}`)
|
||||
}
|
||||
}
|
||||
const addresses_slot = computeMappingElemSlot(GUARDIAN_SETS_SLOT, guardianSetIndex)
|
||||
console.log(`Writing new set of guardians into set ${guardianSetIndex}...`)
|
||||
guardian_addresses.forEach(async (address, i) => {
|
||||
await setStorageAt(rpc, contract_address, computeArrayElemSlot(addresses_slot, i), ["address"], [address])
|
||||
})
|
||||
await setStorageAt(rpc, contract_address, addresses_slot, ["uint256"], [guardian_addresses.length])
|
||||
const after_guardian_set_index = await core.getCurrentGuardianSetIndex()
|
||||
const new_set = await core.getGuardianSet(after_guardian_set_index)
|
||||
console.log(`Current guardian set (index ${after_guardian_set_index}):`)
|
||||
console.log(new_set[0])
|
||||
console.log("Success.")
|
||||
}
|
||||
|
||||
async function getKaruraGasParams(rpc: string): Promise<{
|
||||
gasPrice: number;
|
||||
gasLimit: number;
|
||||
}> {
|
||||
|
@ -143,3 +306,129 @@ export async function getKaruraGasParams(rpc: string): Promise<{
|
|||
gasPrice: parseInt(res.gasPrice, 16),
|
||||
};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Storage manipulation
|
||||
//
|
||||
// Below we define a set of utilities for working with the EVM storage. For
|
||||
// reference on storage layout, see [1].
|
||||
//
|
||||
// [1]: https://docs.soliditylang.org/en/v0.8.14/internals/layout_in_storage.html
|
||||
|
||||
export type StorageSlot = ethers.BigNumber
|
||||
// we're a little more permissive in contravariant positions...
|
||||
export type StorageSlotish = ethers.BigNumberish
|
||||
|
||||
/**
|
||||
*
|
||||
* Compute the storage slot of an array element.
|
||||
*
|
||||
* @param array_slot the storage slot of the array variable
|
||||
* @param offset the index of the element to compute the storage slot for
|
||||
*/
|
||||
export function computeArrayElemSlot(array_slot: StorageSlotish, offset: number): StorageSlot {
|
||||
return ethers.BigNumber.from(solidityKeccak256(["bytes"], [array_slot])).add(offset)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Compute the storage slot of a mapping key.
|
||||
*
|
||||
* @param map_slot the storage slot of the mapping variable
|
||||
* @param key the key to compute the storage slot for
|
||||
*/
|
||||
export function computeMappingElemSlot(map_slot: StorageSlotish, key: any): StorageSlot {
|
||||
const slot_preimage = ethers.utils.defaultAbiCoder.encode(["uint256", "uint256"], [key, map_slot])
|
||||
return ethers.BigNumber.from(solidityKeccak256(["bytes"], [slot_preimage]))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the values stored in a storage slot. [[ethers.Provider.getStorageAt]]
|
||||
* returns the whole slot as one 32 byte value, but if there are multiple values
|
||||
* stored in the slot (which solidity does to save gas), it is useful to parse
|
||||
* the output accordingly. This function is a wrapper around the storage query
|
||||
* provided by [[ethers]] that does the additional parsing.
|
||||
*
|
||||
* @param rpc the JSON RPC endpoint
|
||||
* @param contract_address address of the contract to be queried
|
||||
* @param storage_slot the storage slot to query
|
||||
* @param types The types of values stored in the storage slot. It's a list,
|
||||
* because solidity packs multiple values into a single storage slot to save gas
|
||||
* when the elements fit.
|
||||
*
|
||||
* @returns _values the values to write into the slot (packed)
|
||||
*/
|
||||
async function getStorageAt(rpc: string, contract_address: string, storage_slot: StorageSlotish, types: Encoding[]): Promise<any[]> {
|
||||
const total = types.map((typ) => typeWidth(typ)).reduce((x, y) => (x + y))
|
||||
if (total > 32) {
|
||||
throw new Error(`Storage slots can contain a maximum of 32 bytes. Total size of ${types} is ${total} bytes.`)
|
||||
}
|
||||
|
||||
const string_val: string =
|
||||
await (new ethers.providers.JsonRpcProvider(rpc).getStorageAt(contract_address, storage_slot))
|
||||
let val = ethers.BigNumber.from(string_val)
|
||||
let ret: any[] = []
|
||||
// we decode the elements one by one, by shifting down the stuff we've parsed already
|
||||
types.forEach((typ) => {
|
||||
const padded = ethers.utils.defaultAbiCoder.encode(["uint256"], [val])
|
||||
ret.push(ethers.utils.defaultAbiCoder.decode([typ], padded)[0])
|
||||
val = val.shr(typeWidth(typ) * 8)
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Use the 'hardhat_setStorageAt' rpc method to override a storage slot of a
|
||||
* contract. This method is understood by both hardhat and anvil (from foundry).
|
||||
* Useful for manipulating the storage of a forked mainnet contract (such as for
|
||||
* changing the guardian set to allow submitting VAAs to).
|
||||
*
|
||||
* @param rpc the JSON RPC endpoint (needs to be hardhat of anvil)
|
||||
* @param contract_address address of the contract to be queried
|
||||
* @param storage_slot the storage slot to query
|
||||
* @param types The types of values stored in the storage slot. It's a list,
|
||||
* because solidity packs multiple values into a single storage slot to save gas
|
||||
* when the elements fit. This means that when writing into the slot, all values
|
||||
* must be accounted for, otherwise we end up zeroing out some fields.
|
||||
* @param values the values to write into the slot (packed)
|
||||
*
|
||||
* @returns the `data` property of the JSON response
|
||||
*/
|
||||
export async function setStorageAt(rpc: string, contract_address: string, storage_slot: StorageSlotish, types: Encoding[], values: any[]): Promise<any> {
|
||||
// we need to reverse the values and types arrays, because the first element
|
||||
// is stored at the rightmost bytes.
|
||||
//
|
||||
// for example:
|
||||
// uint32 a
|
||||
// uint32 b
|
||||
// will be stored as 0x...b...a
|
||||
const _values = values.reverse()
|
||||
const _types = types.reverse()
|
||||
const total = _types.map((typ) => typeWidth(typ)).reduce((x, y) => (x + y))
|
||||
// ensure that the types fit into a slot
|
||||
if (total > 32) {
|
||||
throw new Error(`Storage slots can contain a maximum of 32 bytes. Total size of ${_types} is ${total} bytes.`)
|
||||
}
|
||||
if (_types.length !== _values.length) {
|
||||
throw new Error(`Expected ${_types.length} value(s), but got ${_values.length}.`)
|
||||
}
|
||||
// as far as I could tell, `ethers` doesn't provide a way to pack multiple
|
||||
// values into a single slot (the abi coder pads everything to 32 bytes), so we do it ourselves
|
||||
const val = "0x" + _types.map((typ, i) => encode(typ, _values[i])).reduce((x, y) => x + y).padStart(64, "0")
|
||||
// format the storage slot
|
||||
const slot = ethers.utils.defaultAbiCoder.encode(["uint256"], [storage_slot])
|
||||
console.log(`slot ${slot} := ${val}`)
|
||||
|
||||
return (await axios.post(rpc, {
|
||||
id: 0,
|
||||
jsonrpc: "2.0",
|
||||
method: "hardhat_setStorageAt",
|
||||
params: [
|
||||
contract_address,
|
||||
slot,
|
||||
val,
|
||||
],
|
||||
})).data
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ import yargs from "yargs";
|
|||
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
import { isTerraChain, setDefaultWasm } from "@certusone/wormhole-sdk";
|
||||
import { isTerraChain, assertEVMChain, CONTRACTS, setDefaultWasm } from "@certusone/wormhole-sdk";
|
||||
import { execute_governance_solana } from "./solana";
|
||||
import { execute_governance_evm } from "./evm";
|
||||
import { execute_governance_evm, hijack_evm, query_contract_evm, setStorageAt } from "./evm";
|
||||
import { execute_governance_terra } from "./terra";
|
||||
import * as vaa from "./vaa";
|
||||
import { impossible, Payload, serialiseVAA, VAA } from "./vaa";
|
||||
|
@ -17,6 +17,8 @@ import {
|
|||
isEVMChain,
|
||||
toChainId,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { ethers } from "ethers";
|
||||
import { NETWORKS } from "./networks";
|
||||
|
||||
setDefaultWasm("node");
|
||||
|
||||
|
@ -58,7 +60,7 @@ yargs(hideBin(process.argv))
|
|||
.option("guardian-secret", {
|
||||
alias: "g",
|
||||
required: true,
|
||||
describe: "Guardians' secret keys",
|
||||
describe: "Guardians' secret keys (CSV)",
|
||||
type: "string",
|
||||
})
|
||||
// Registration
|
||||
|
@ -148,7 +150,7 @@ yargs(hideBin(process.argv))
|
|||
type: "ContractUpgrade",
|
||||
chain: toChainId(argv["chain"]),
|
||||
address: Buffer.from(
|
||||
argv["contract-address"].padStart(64, "0"),
|
||||
evm_address(argv["contract-address"]),
|
||||
"hex"
|
||||
),
|
||||
};
|
||||
|
@ -168,7 +170,7 @@ yargs(hideBin(process.argv))
|
|||
}
|
||||
)
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Parse
|
||||
// Misc
|
||||
.command(
|
||||
"parse <vaa>",
|
||||
"Parse a VAA",
|
||||
|
@ -182,6 +184,206 @@ yargs(hideBin(process.argv))
|
|||
const buf = Buffer.from(String(argv.vaa), "hex");
|
||||
const parsed_vaa = vaa.parse(buf);
|
||||
console.log(parsed_vaa);
|
||||
console.log("Digest:", vaa.vaaDigest(parsed_vaa))
|
||||
})
|
||||
.command("recover <digest> <signature>", "Recover an address from a signature", (yargs) => {
|
||||
return yargs
|
||||
.positional("digest", {
|
||||
describe: "digest",
|
||||
type: "string"
|
||||
})
|
||||
.positional("signature", {
|
||||
describe: "signature",
|
||||
type: "string"
|
||||
});
|
||||
}, async (argv) => {
|
||||
console.log(ethers.utils.recoverAddress(hex(argv["digest"]), hex(argv["signature"])))
|
||||
})
|
||||
.command("contract <network> <chain> <module>", "Print contract address", (yargs) => {
|
||||
return yargs
|
||||
.positional("network", {
|
||||
describe: "network",
|
||||
type: "string",
|
||||
choices: ["mainnet", "testnet", "devnet"],
|
||||
})
|
||||
.positional("chain", {
|
||||
describe: "Chain to query",
|
||||
type: "string",
|
||||
choices: Object.keys(CHAINS),
|
||||
})
|
||||
.positional("module", {
|
||||
describe: "Module to query",
|
||||
type: "string",
|
||||
choices: ["Core", "NFTBridge", "TokenBridge"],
|
||||
})
|
||||
}, async (argv) => {
|
||||
assertChain(argv["chain"])
|
||||
assertEVMChain(argv["chain"])
|
||||
const network = argv.network.toUpperCase();
|
||||
if (
|
||||
network !== "MAINNET" &&
|
||||
network !== "TESTNET" &&
|
||||
network !== "DEVNET"
|
||||
) {
|
||||
throw Error(`Unknown network: ${network}`);
|
||||
}
|
||||
let module = argv["module"] as
|
||||
| "Core"
|
||||
| "NFTBridge"
|
||||
| "TokenBridge";
|
||||
switch (module) {
|
||||
case "Core":
|
||||
console.log(CONTRACTS[network][argv["chain"]]["core"])
|
||||
break;
|
||||
case "NFTBridge":
|
||||
console.log(CONTRACTS[network][argv["chain"]]["nft_bridge"])
|
||||
break;
|
||||
case "TokenBridge":
|
||||
console.log(CONTRACTS[network][argv["chain"]]["token_bridge"])
|
||||
break;
|
||||
default:
|
||||
impossible(module)
|
||||
}
|
||||
})
|
||||
.command("rpc <network> <chain>", "Print RPC address", (yargs) => {
|
||||
return yargs
|
||||
.positional("network", {
|
||||
describe: "network",
|
||||
type: "string",
|
||||
choices: ["mainnet", "testnet", "devnet"],
|
||||
})
|
||||
.positional("chain", {
|
||||
describe: "Chain to query",
|
||||
type: "string",
|
||||
choices: Object.keys(CHAINS),
|
||||
})
|
||||
}, async (argv) => {
|
||||
assertChain(argv["chain"])
|
||||
assertEVMChain(argv["chain"])
|
||||
const network = argv.network.toUpperCase();
|
||||
if (
|
||||
network !== "MAINNET" &&
|
||||
network !== "TESTNET" &&
|
||||
network !== "DEVNET"
|
||||
) {
|
||||
throw Error(`Unknown network: ${network}`);
|
||||
}
|
||||
console.log(NETWORKS[network][argv["chain"]].rpc)
|
||||
})
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Evm utilities
|
||||
.command("evm", "EVM utilites", (yargs) => {
|
||||
return yargs
|
||||
.option("rpc", {
|
||||
describe: "RPC endpoint",
|
||||
type: "string",
|
||||
required: false
|
||||
})
|
||||
.command("address-from-secret <secret>", "Compute a 20 byte eth address from a 32 byte private key", (yargs) => {
|
||||
return yargs
|
||||
.positional("secret", { type: "string", describe: "Secret key (32 bytes)" })
|
||||
}, (argv) => {
|
||||
console.log(ethers.utils.computeAddress(argv["secret"]))
|
||||
})
|
||||
.command("storage-update", "Update a storage slot on an EVM fork during testing (anvil or hardhat)", (yargs) => {
|
||||
return yargs
|
||||
.option("contract-address", {
|
||||
alias: "a",
|
||||
describe: "Contract address",
|
||||
type: "string",
|
||||
required: true,
|
||||
})
|
||||
.option("storage-slot", {
|
||||
alias: "k",
|
||||
describe: "Storage slot to modify",
|
||||
type: "string",
|
||||
required: true,
|
||||
})
|
||||
.option("value", {
|
||||
alias: "v",
|
||||
describe: "Value to write into the slot (32 bytes)",
|
||||
type: "string",
|
||||
required: true,
|
||||
});
|
||||
}, async (argv) => {
|
||||
const result = await setStorageAt(argv["rpc"], evm_address(argv["contract-address"]), argv["storage-slot"], ["uint256"], [argv["value"]]);
|
||||
console.log(result);
|
||||
})
|
||||
.command("info", "Query info about the on-chain state of the contract", (yargs) => {
|
||||
return yargs
|
||||
.option("chain", {
|
||||
alias: "c",
|
||||
describe: "Chain to query",
|
||||
type: "string",
|
||||
choices: Object.keys(CHAINS),
|
||||
required: true,
|
||||
})
|
||||
.option("module", {
|
||||
alias: "m",
|
||||
describe: "Module to query",
|
||||
type: "string",
|
||||
choices: ["Core", "NFTBridge", "TokenBridge"],
|
||||
required: true,
|
||||
})
|
||||
.option("network", {
|
||||
alias: "n",
|
||||
describe: "network",
|
||||
type: "string",
|
||||
choices: ["mainnet", "testnet", "devnet"],
|
||||
required: true,
|
||||
})
|
||||
.option("contract-address", {
|
||||
alias: "a",
|
||||
describe: "Contract to query (override config)",
|
||||
type: "string",
|
||||
required: false,
|
||||
});
|
||||
}, async (argv) => {
|
||||
assertChain(argv["chain"])
|
||||
assertEVMChain(argv["chain"])
|
||||
const network = argv.network.toUpperCase();
|
||||
if (
|
||||
network !== "MAINNET" &&
|
||||
network !== "TESTNET" &&
|
||||
network !== "DEVNET"
|
||||
) {
|
||||
throw Error(`Unknown network: ${network}`);
|
||||
}
|
||||
let module = argv["module"] as
|
||||
| "Core"
|
||||
| "NFTBridge"
|
||||
| "TokenBridge";
|
||||
let rpc = argv["rpc"] ?? NETWORKS[network][argv["chain"]].rpc
|
||||
console.log(JSON.stringify(await query_contract_evm(network, argv["chain"], module, argv["contract-address"], rpc), null, 2))
|
||||
})
|
||||
.command("hijack", "Override the guardian set of the core bridge contract during testing (anvil or hardhat)", (yargs) => {
|
||||
return yargs
|
||||
.option("core-contract-address", {
|
||||
alias: "a",
|
||||
describe: "Core contract address",
|
||||
type: "string",
|
||||
default: CONTRACTS.MAINNET.ethereum.core,
|
||||
})
|
||||
.option("guardian-address", {
|
||||
alias: "g",
|
||||
required: true,
|
||||
describe: "Guardians' public addresses (CSV)",
|
||||
type: "string",
|
||||
})
|
||||
.option("guardian-set-index", {
|
||||
alias: "i",
|
||||
required: false,
|
||||
describe: "New guardian set index (if unspecified, default to overriding the current index)",
|
||||
type: "number"
|
||||
});
|
||||
}, async (argv) => {
|
||||
const guardian_addresses = argv["guardian-address"].split(",")
|
||||
let rpc = argv["rpc"] ?? NETWORKS.DEVNET.ethereum.rpc
|
||||
await hijack_evm(rpc, argv["core-contract-address"], guardian_addresses, argv["guardian-set-index"])
|
||||
})
|
||||
},
|
||||
(_) => {
|
||||
yargs.showHelp();
|
||||
}
|
||||
)
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -209,9 +411,19 @@ yargs(hideBin(process.argv))
|
|||
type: "string",
|
||||
choices: ["mainnet", "testnet", "devnet"],
|
||||
required: true,
|
||||
});
|
||||
},
|
||||
async (argv) => {
|
||||
})
|
||||
.option("contract-address", {
|
||||
alias: "a",
|
||||
describe: "Contract to submit VAA to (override config)",
|
||||
type: "string",
|
||||
required: false,
|
||||
})
|
||||
.option("rpc", {
|
||||
describe: "RPC endpoint",
|
||||
type: "string",
|
||||
required: false
|
||||
})
|
||||
}, async (argv) => {
|
||||
const vaa_hex = String(argv.vaa);
|
||||
const buf = Buffer.from(vaa_hex, "hex");
|
||||
const parsed_vaa = vaa.parse(buf);
|
||||
|
@ -270,7 +482,7 @@ yargs(hideBin(process.argv))
|
|||
"This VAA does not specify the target chain, please provide it by hand using the '--chain' flag."
|
||||
);
|
||||
} else if (isEVMChain(chain)) {
|
||||
await execute_governance_evm(parsed_vaa.payload, buf, network, chain);
|
||||
await execute_governance_evm(parsed_vaa.payload, buf, network, chain, argv["contract-address"], argv["rpc"]);
|
||||
} else if (isTerraChain(chain)) {
|
||||
await execute_governance_terra(parsed_vaa.payload, buf, network, chain);
|
||||
} else if (chain === "solana") {
|
||||
|
@ -286,3 +498,11 @@ yargs(hideBin(process.argv))
|
|||
}
|
||||
}
|
||||
).argv;
|
||||
|
||||
function hex(x: string): string {
|
||||
return ethers.utils.hexlify(x, { allowMissingPrefix: true })
|
||||
}
|
||||
|
||||
function evm_address(x: string): string {
|
||||
return hex(x).substring(2).padStart(64, "0")
|
||||
}
|
||||
|
|
|
@ -148,6 +148,10 @@ export function serialiseVAA(vaa: VAA<Payload>) {
|
|||
return body.join("")
|
||||
}
|
||||
|
||||
export function vaaDigest(vaa: VAA<Payload>) {
|
||||
return solidityKeccak256(["bytes"], [solidityKeccak256(["bytes"], ["0x" + vaaBody(vaa)])])
|
||||
}
|
||||
|
||||
function vaaBody(vaa: VAA<Payload>) {
|
||||
let payload = vaa.payload
|
||||
let payload_str: string
|
||||
|
@ -187,8 +191,7 @@ function vaaBody(vaa: VAA<Payload>) {
|
|||
}
|
||||
|
||||
export function sign(signers: string[], vaa: VAA<Payload>): Signature[] {
|
||||
const body = vaaBody(vaa)
|
||||
const hash = solidityKeccak256(["bytes"], [solidityKeccak256(["bytes"], ["0x" + body])])
|
||||
const hash = vaaDigest(vaa)
|
||||
const ec = new elliptic.ec("secp256k1")
|
||||
|
||||
return signers.map((signer, i) => {
|
||||
|
@ -283,7 +286,7 @@ const coreContractUpgradeParser: P<CoreContractUpgrade> =
|
|||
length: 32,
|
||||
encoding: "hex",
|
||||
assert: Buffer.from("Core").toString("hex").padStart(64, "0"),
|
||||
formatter: (_str) => module
|
||||
formatter: (_str) => "Core"
|
||||
})
|
||||
.uint8("type", {
|
||||
assert: 1,
|
||||
|
@ -416,26 +419,32 @@ export function impossible(a: never): any {
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Encoder utils
|
||||
|
||||
type Encoding
|
||||
export type Encoding
|
||||
= "uint8"
|
||||
| "uint16"
|
||||
| "uint32"
|
||||
| "uint64"
|
||||
| "uint128"
|
||||
| "uint256"
|
||||
| "bytes32"
|
||||
| "address"
|
||||
|
||||
function typeWidth(type: Encoding): number {
|
||||
export function typeWidth(type: Encoding): number {
|
||||
switch (type) {
|
||||
case "uint8": return 1
|
||||
case "uint16": return 2
|
||||
case "uint32": return 4
|
||||
case "uint64": return 8
|
||||
case "uint128": return 16
|
||||
case "uint256": return 32
|
||||
case "bytes32": return 32
|
||||
case "address": return 20
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't find a satisfactory binary serialisation solution, so we just use
|
||||
// the ethers library's encoding logic
|
||||
function encode(type: Encoding, val: any): string {
|
||||
export function encode(type: Encoding, val: any): string {
|
||||
// ethers operates on hex strings (sigh) and left pads everything to 32
|
||||
// bytes (64 characters). We take last 2*n characters where n is the width
|
||||
// of the type being serialised in bytes (since a byte is represented as 2
|
||||
|
@ -445,6 +454,6 @@ function encode(type: Encoding, val: any): string {
|
|||
|
||||
// Encode a string as binary left-padded to 32 bytes, represented as a hex
|
||||
// string (64 chars long)
|
||||
function encodeString(str: string): Buffer {
|
||||
export function encodeString(str: string): Buffer {
|
||||
return Buffer.from(Buffer.from(str).toString("hex").padStart(64, "0"), "hex")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue