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:
Csongor Kiss 2022-06-25 15:04:31 +01:00 committed by Csongor Kiss
parent 22dda6ba84
commit 3c6e1b07d6
3 changed files with 549 additions and 31 deletions

View File

@ -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
}

View File

@ -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")
}

View File

@ -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")
}