714 lines
23 KiB
TypeScript
714 lines
23 KiB
TypeScript
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/esm/utils/consts";
|
|
import {
|
|
BridgeImplementation__factory,
|
|
Implementation__factory,
|
|
NFTBridgeImplementation__factory,
|
|
} from "@certusone/wormhole-sdk/lib/esm/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;
|
|
case "RecoverChainId":
|
|
console.log("Recovering chain ID");
|
|
console.log(
|
|
"Hash: " + (await cb.submitRecoverChainId(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 "RecoverChainId":
|
|
console.log("Recovering chain ID");
|
|
console.log(
|
|
"Hash: " + (await nb.submitRecoverChainId(vaa, overrides)).hash
|
|
);
|
|
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 "RecoverChainId":
|
|
console.log("Recovering chain ID");
|
|
console.log(
|
|
"Hash: " + (await tb.submitRecoverChainId(vaa, overrides)).hash
|
|
);
|
|
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 | "unsupported"> {
|
|
try {
|
|
return await query;
|
|
} catch (e) {
|
|
if (e.reason === "unsupported") {
|
|
return e.reason;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|