| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579 |
- import { ethers } from "ethers"
- import { NETWORKS } from "./networks"
- import { encode, Encoding, impossible, Payload, typeWidth } from "./vaa"
- import axios from "axios";
- import * as celo from "@celo-tools/celo-ethers-wrapper";
- import { solidityKeccak256 } from "ethers/lib/utils"
- import { CHAINS, CONTRACTS, Contracts, EVMChainName } from "@certusone/wormhole-sdk/lib/cjs/utils/consts";
- import { BridgeImplementation__factory, Implementation__factory, NFTBridgeImplementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts";
- 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()
- let guardianSetsPromise = Promise.all([...Array(result.currentGuardianSetIndex + 1).keys()].map((i) => core.getGuardianSet(i)))
- let [
- guardianSetExpiry,
- chainId,
- evmChainId,
- isFork,
- governanceChainId,
- governanceContract,
- messageFee,
- implementationSlot,
- guardianSets
- ] = await Promise.all([
- core.getGuardianSetExpiry(),
- core.chainId(),
- maybeUnsupported(core.evmChainId()),
- maybeUnsupported(core.isFork()),
- core.governanceChainId(),
- core.governanceContract(),
- core.messageFee(),
- getStorageAt(rpc, contract_address, _IMPLEMENTATION_SLOT, ["address"]),
- guardianSetsPromise
- ])
- result.guardianSetExpiry = guardianSetExpiry
- result.chainId = chainId
- result.evmChainId = evmChainId.toString()
- result.isFork = isFork
- result.governanceChainId = governanceChainId
- result.governanceContract = governanceContract
- result.messageFee = messageFee
- result.implementation = implementationSlot[0]
- result.isInitialized = await core.isInitialized(result.implementation)
- result.guardianSet = {}
- for (let [i, guardianSet] of guardianSets.entries()) {
- result.guardianSet[i] = { keys: guardianSet[0], expiry: guardianSet[1] }
- }
- 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
- const registrationsPromise = Promise.all(
- Object.entries(CHAINS)
- .filter(([c_name, _]) => c_name !== chain && c_name !== "unset")
- .map(async ([c_name, c_id]) => [c_name, await tb.bridgeContracts(c_id)])
- )
- let [
- wormhole,
- implementationSlotTb,
- tokenImplementation,
- chainIdTb,
- finality,
- evmChainIdTb,
- isForkTb,
- governanceChainIdTb,
- governanceContractTb,
- WETH,
- registrations
- ] = await Promise.all([
- tb.wormhole(),
- getStorageAt(rpc, contract_address, _IMPLEMENTATION_SLOT, ["address"]),
- tb.tokenImplementation(),
- tb.chainId(),
- tb.finality(),
- maybeUnsupported(tb.evmChainId()),
- maybeUnsupported(tb.isFork()),
- tb.governanceChainId(),
- tb.governanceContract(),
- tb.WETH(),
- registrationsPromise
- ])
- result.wormhole = wormhole
- result.implementation = implementationSlotTb[0]
- result.isInitialized = await tb.isInitialized(result.implementation)
- result.tokenImplementation = tokenImplementation
- result.chainId = chainIdTb
- result.finality = finality
- result.evmChainId = evmChainIdTb.toString()
- result.isFork = isForkTb
- result.governanceChainId = governanceChainIdTb
- result.governanceContract = governanceContractTb
- result.WETH = WETH
- result.registrations = {}
- for (let [c_name, c] of registrations) {
- result.registrations[c_name] = c
- }
- 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
- const registrationsPromiseNb = Promise.all(
- Object.entries(CHAINS)
- .filter(([c_name, _]) => c_name !== chain && c_name !== "unset")
- .map(async ([c_name, c_id]) => [c_name, await nb.bridgeContracts(c_id)])
- )
- let [
- wormholeNb,
- implementationSlotNb,
- tokenImplementationNb,
- chainIdNb,
- finalityNb,
- evmChainIdNb,
- isForkNb,
- governanceChainIdNb,
- governanceContractNb,
- registrationsNb
- ] = await Promise.all([
- nb.wormhole(),
- getStorageAt(rpc, contract_address, _IMPLEMENTATION_SLOT, ["address"]),
- nb.tokenImplementation(),
- nb.chainId(),
- nb.finality(),
- maybeUnsupported(nb.evmChainId()),
- maybeUnsupported(nb.isFork()),
- nb.governanceChainId(),
- nb.governanceContract(),
- registrationsPromiseNb
- ])
- result.wormhole = wormholeNb
- result.implementation = implementationSlotNb[0]
- result.isInitialized = await nb.isInitialized(result.implementation)
- result.tokenImplementation = tokenImplementationNb
- result.chainId = chainIdNb
- result.finality = finalityNb
- result.evmChainId = evmChainIdNb.toString()
- result.isFork = isForkNb
- result.governanceChainId = governanceChainIdNb
- result.governanceContract = governanceContractNb
- result.registrations = {}
- for (let [c_name, c] of registrationsNb) {
- result.registrations[c_name] = c
- }
- break
- default:
- impossible(module)
- }
- return result
- }
- export async function getImplementation(
- network: "MAINNET" | "TESTNET" | "DEVNET",
- chain: EVMChainName,
- module: "Core" | "NFTBridge" | "TokenBridge",
- contract_address: string | undefined,
- _rpc: string | undefined
- ): Promise<ethers.BigNumber> {
- 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]
- switch (module) {
- case "Core":
- contract_address = contract_address ? contract_address : contracts.core;
- break
- case "TokenBridge":
- contract_address = contract_address ? contract_address : contracts.token_bridge;
- break
- case "NFTBridge":
- contract_address = contract_address ? contract_address : contracts.nft_bridge;
- break
- default:
- impossible(module)
- }
- return (await getStorageAt(rpc, contract_address, _IMPLEMENTATION_SLOT, ["address"]))[0]
- }
- export async function execute_evm(
- payload: Payload,
- vaa: Buffer,
- network: "MAINNET" | "TESTNET" | "DEVNET",
- chain: EVMChainName,
- contract_address: string | undefined,
- _rpc: string | undefined
- ) {
- 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)`)
- }
- if (!n.key) {
- throw Error(`No ${network} key defined for ${chain} (see networks.ts)`)
- }
- let key: string = n.key
- let contracts: Contracts = CONTRACTS[network][chain]
- let provider: ethers.providers.JsonRpcProvider;
- let signer: ethers.Wallet;
- if (chain === "celo") {
- provider = new celo.CeloProvider(rpc)
- await provider.ready
- signer = new celo.CeloWallet(key, provider)
- } 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
- // testnet (or devnet), they might require additional guards
- let overrides: ethers.Overrides = {}
- if (chain === "karura" || chain == "acala") {
- overrides = await getKaruraGasParams(n.rpc)
- } else if (chain === "polygon") {
- let feeData = await provider.getFeeData();
- overrides = {
- maxFeePerGas: feeData.maxFeePerGas?.mul(50) || undefined,
- maxPriorityFeePerGas: feeData.maxPriorityFeePerGas?.mul(50) || undefined,
- };
- } else if (chain === "klaytn" || chain === "fantom") {
- overrides = { gasPrice: (await signer.getGasPrice()).toString() }
- }
- switch (payload.module) {
- case "Core":
- 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(contract_address)
- switch (payload.type) {
- case "GuardianSetUpgrade":
- console.log("Submitting new guardian set")
- console.log("Hash: " + (await cb.submitNewGuardianSet(vaa, overrides)).hash)
- break
- case "ContractUpgrade":
- console.log("Upgrading core contract")
- console.log("Hash: " + (await cb.submitContractUpgrade(vaa, overrides)).hash)
- break
- default:
- impossible(payload)
- }
- 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}`)
- }
- let n = new NFTBridgeImplementation__factory(signer)
- let nb = n.attach(contract_address)
- switch (payload.type) {
- case "ContractUpgrade":
- console.log("Upgrading contract")
- console.log("Hash: " + (await nb.upgrade(vaa, overrides)).hash)
- console.log("Don't forget to verify the new implementation! See ethereum/VERIFY.md for instructions")
- break
- case "RegisterChain":
- console.log("Registering chain")
- console.log("Hash: " + (await nb.registerChain(vaa, overrides)).hash)
- break
- case "Transfer":
- console.log("Completing transfer")
- console.log("Hash: " + (await nb.completeTransfer(vaa, overrides)).hash)
- break
- default:
- impossible(payload)
- }
- 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}`)
- }
- let t = new BridgeImplementation__factory(signer)
- let tb = t.attach(contract_address)
- switch (payload.type) {
- case "ContractUpgrade":
- console.log("Upgrading contract")
- console.log("Hash: " + (await tb.upgrade(vaa, overrides)).hash)
- console.log("Don't forget to verify the new implementation! See ethereum/VERIFY.md for instructions")
- break
- case "RegisterChain":
- console.log("Registering chain")
- console.log("Hash: " + (await tb.registerChain(vaa, overrides)).hash)
- break
- case "Transfer":
- console.log("Completing transfer")
- console.log("Hash: " + (await tb.completeTransfer(vaa, overrides)).hash)
- break
- case "AttestMeta":
- console.log("Creating wrapped token")
- console.log("Hash: " + (await tb.createWrapped(vaa, overrides)).hash)
- break
- case "TransferWithPayload":
- console.log("Completing transfer with payload")
- console.log("Hash: " + (await tb.completeTransferWithPayload(vaa, overrides)).hash)
- break
- default:
- impossible(payload)
- break
- }
- break
- default:
- impossible(payload)
- }
- }
- /**
- *
- * 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;
- }> {
- const gasLimit = 21000000;
- const storageLimit = 64001;
- const res = (
- await axios.post(rpc, {
- id: 0,
- jsonrpc: "2.0",
- method: "eth_getEthGas",
- params: [
- {
- gasLimit,
- storageLimit,
- },
- ],
- })
- ).data.result;
- return {
- gasLimit: parseInt(res.gasLimit, 16),
- 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
- }
- async function maybeUnsupported<T>(query: Promise<T>): Promise<T> {
- try {
- return await query
- } catch (e) {
- if (e.reason === "unsupported") {
- return e.reason
- }
- throw e
- }
- }
|