check_proposal.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. /* eslint-disable @typescript-eslint/no-floating-promises */
  2. /* eslint-disable unicorn/prefer-top-level-await */
  3. /* eslint-disable @typescript-eslint/restrict-template-expressions */
  4. /* eslint-disable no-console */
  5. import { createHash } from "node:crypto";
  6. import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
  7. import type { PythCluster } from "@pythnetwork/client/lib/cluster";
  8. import { getPythClusterApiUrl } from "@pythnetwork/client/lib/cluster";
  9. import {
  10. CosmosUpgradeContract,
  11. EvmExecute,
  12. EvmSetWormholeAddress,
  13. EvmUpgradeContract,
  14. getProposalInstructions,
  15. MultisigParser,
  16. WormholeMultisigInstruction,
  17. } from "@pythnetwork/xc-admin-common";
  18. import type { AccountMeta } from "@solana/web3.js";
  19. import { Keypair, PublicKey } from "@solana/web3.js";
  20. import SquadsMesh from "@sqds/mesh";
  21. import Web3 from "web3";
  22. import yargs from "yargs";
  23. import { hideBin } from "yargs/helpers";
  24. import { CosmWasmChain, EvmChain } from "../src/core/chains";
  25. import {
  26. EvmEntropyContract,
  27. EvmPriceFeedContract,
  28. getCodeDigestWithoutAddress,
  29. EvmWormholeContract,
  30. } from "../src/core/contracts/evm";
  31. import { DefaultStore } from "../src/node/utils/store";
  32. const parser = yargs(hideBin(process.argv))
  33. .usage("Usage: $0 --cluster <cluster_id> --proposal <proposal_address>")
  34. .options({
  35. cluster: {
  36. type: "string",
  37. demandOption: true,
  38. desc: "Multsig Cluster name to check proposal on can be one of [devnet, testnet, mainnet-beta]",
  39. },
  40. proposal: {
  41. type: "string",
  42. demandOption: true,
  43. desc: "The proposal address to check",
  44. },
  45. });
  46. async function main() {
  47. const argv = await parser.argv;
  48. const cluster = argv.cluster as PythCluster;
  49. const squad = SquadsMesh.endpoint(
  50. getPythClusterApiUrl(cluster),
  51. new NodeWallet(Keypair.generate()), // dummy wallet
  52. );
  53. const transaction = await squad.getTransaction(new PublicKey(argv.proposal));
  54. const instructions = await getProposalInstructions(squad, transaction);
  55. const multisigParser = MultisigParser.fromCluster(cluster);
  56. const parsedInstructions = instructions.map((instruction) => {
  57. return multisigParser.parseInstruction({
  58. programId: instruction.programId,
  59. data: instruction.data as Buffer,
  60. keys: instruction.keys as AccountMeta[],
  61. });
  62. });
  63. for (const instruction of parsedInstructions) {
  64. if (instruction instanceof WormholeMultisigInstruction) {
  65. if (instruction.governanceAction instanceof EvmSetWormholeAddress) {
  66. console.log(
  67. `Verifying EVM set wormhole address on ${instruction.governanceAction.targetChainId}`,
  68. );
  69. for (const chain of Object.values(DefaultStore.chains)) {
  70. if (
  71. chain instanceof EvmChain &&
  72. chain.wormholeChainName ===
  73. instruction.governanceAction.targetChainId
  74. ) {
  75. const address = instruction.governanceAction.address;
  76. const contract = new EvmWormholeContract(chain, address);
  77. const currentIndex = await contract.getCurrentGuardianSetIndex();
  78. const guardianSet = await contract.getGuardianSet();
  79. const proxyContract = new EvmPriceFeedContract(chain, address);
  80. const proxyCode = await proxyContract.getCode();
  81. const receiverImplementation =
  82. await proxyContract.getImplementationAddress();
  83. const implementationCode = await new EvmPriceFeedContract(
  84. chain,
  85. receiverImplementation,
  86. ).getCode();
  87. const proxyDigest = Web3.utils.keccak256(proxyCode);
  88. const implementationDigest =
  89. Web3.utils.keccak256(implementationCode);
  90. const guardianSetDigest = Web3.utils.keccak256(
  91. JSON.stringify(guardianSet),
  92. );
  93. console.log(
  94. `${chain.getId()} Address:\t\t${address}\nproxy digest:\t\t${proxyDigest}\nimplementation digest:\t${implementationDigest} \nguardian set index:\t${currentIndex} \nguardian set:\t\t${guardianSetDigest}`,
  95. );
  96. }
  97. }
  98. }
  99. if (instruction.governanceAction instanceof EvmUpgradeContract) {
  100. console.log(
  101. `Verifying EVM Upgrade Contract on ${instruction.governanceAction.targetChainId}`,
  102. );
  103. for (const chain of Object.values(DefaultStore.chains)) {
  104. if (
  105. chain instanceof EvmChain &&
  106. chain.isMainnet() === (cluster === "mainnet-beta") &&
  107. chain.wormholeChainName ===
  108. instruction.governanceAction.targetChainId
  109. ) {
  110. const address = instruction.governanceAction.address;
  111. const contract = new EvmPriceFeedContract(chain, address);
  112. const code = await contract.getCodeDigestWithoutAddress();
  113. // this should be the same keccak256 of the deployedCode property generated by truffle
  114. console.log(`${chain.getId()} Address:${address} digest:${code}`);
  115. }
  116. }
  117. }
  118. if (instruction.governanceAction instanceof CosmosUpgradeContract) {
  119. console.log(
  120. `Verifying Cosmos Upgrade Contract on ${instruction.governanceAction.targetChainId}`,
  121. );
  122. for (const chain of Object.values(DefaultStore.chains)) {
  123. if (
  124. chain instanceof CosmWasmChain &&
  125. chain.wormholeChainName ===
  126. instruction.governanceAction.targetChainId
  127. ) {
  128. const codeId = instruction.governanceAction.codeId;
  129. const code = await chain.getCode(Number(codeId));
  130. // this should be the same checksums.txt in our release file
  131. console.log(
  132. `${chain.getId()} Code Id:${codeId} digest:${createHash("sha256")
  133. .update(code)
  134. .digest("hex")}`,
  135. );
  136. }
  137. }
  138. }
  139. if (instruction.governanceAction instanceof EvmExecute) {
  140. // Note: it only checks for upgrade entropy contracts right now
  141. console.log(
  142. `Verifying EVMExecute on ${instruction.governanceAction.targetChainId}`,
  143. );
  144. for (const chain of Object.values(DefaultStore.chains)) {
  145. if (
  146. chain instanceof EvmChain &&
  147. chain.wormholeChainName ===
  148. instruction.governanceAction.targetChainId
  149. ) {
  150. const executorAddress =
  151. instruction.governanceAction.executorAddress;
  152. const callAddress = instruction.governanceAction.callAddress;
  153. const calldata = instruction.governanceAction.calldata;
  154. // TODO: If we add additional EVM contracts using the executor, we need to
  155. // add some logic here to identify what kind of contract is at the call address.
  156. const contract = new EvmEntropyContract(chain, callAddress);
  157. const owner = await contract.getOwner();
  158. if (
  159. executorAddress.toUpperCase() !==
  160. owner.replace("0x", "").toUpperCase()
  161. ) {
  162. console.log(
  163. `Executor Address: ${executorAddress.toUpperCase()} is not equal to Owner Address: ${owner
  164. .replace("0x", "")
  165. .toUpperCase()}`,
  166. );
  167. continue;
  168. }
  169. // TODO: This logic assumes we are calling upgradeTo on the contract at callAddress.
  170. // In the future, this logic may need to be generalized to support calling other functions.
  171. const invokedMethod = "upgradeTo(address)";
  172. const calldataHex = calldata.toString("hex");
  173. const web3 = new Web3();
  174. const methodSignature = web3.eth.abi
  175. .encodeFunctionSignature(invokedMethod)
  176. .replace("0x", "");
  177. let newImplementationAddress: string | undefined;
  178. if (calldataHex.startsWith(methodSignature)) {
  179. newImplementationAddress = web3.eth.abi.decodeParameter(
  180. "address",
  181. calldataHex.replace(methodSignature, ""),
  182. ) as unknown as string;
  183. }
  184. if (newImplementationAddress === undefined) {
  185. console.log(
  186. `We couldn't parse the instruction for ${chain.getId()}`,
  187. );
  188. continue;
  189. }
  190. const newImplementationCode = await getCodeDigestWithoutAddress(
  191. chain.getWeb3(),
  192. newImplementationAddress,
  193. );
  194. // this should be the same keccak256 of the deployedCode property generated by truffle
  195. console.log(
  196. `${chain.getId()} call ${invokedMethod} with arguments (${newImplementationAddress}) on ${contract.getType()} at address:${callAddress} from executor:${executorAddress}.`,
  197. );
  198. console.log(
  199. `${chain.getId()} new implementation address:${newImplementationAddress} has code digest:${newImplementationCode}`,
  200. );
  201. }
  202. }
  203. }
  204. }
  205. }
  206. }
  207. main();