diff --git a/wormchain/contracts/tools/accountant_register_mainnet.ts b/wormchain/contracts/tools/accountant_register_mainnet.ts new file mode 100644 index 0000000000..5100c88f64 --- /dev/null +++ b/wormchain/contracts/tools/accountant_register_mainnet.ts @@ -0,0 +1,63 @@ +import { toUtf8 } from "@cosmjs/encoding"; +import { + getWallet, + getWormchainSigningClient, +} from "@wormhole-foundation/wormchain-sdk"; +import { ZERO_FEE } from "@wormhole-foundation/wormchain-sdk/lib/core/consts"; +import "dotenv/config"; + +async function main() { + /* Set up cosmos client & wallet */ + + const wallet = await getWallet(process.env.MNEMONIC); + const client = await getWormchainSigningClient( + process.env.WORMCHAIN_HOST, + wallet + ); + + // there are several Cosmos chains in devnet, so check the config is as expected + let id = await client.getChainId(); + if (id !== "wormchain") { + throw new Error( + `Wormchain CosmWasmClient connection produced an unexpected chainID: ${id}` + ); + } + + const signers = await wallet.getAccounts(); + const signer = signers[0].address; + console.log("wormchain contract deployer is: ", signer); + + const msg = client.wasm.msgExecuteContract({ + sender: signer, + contract: + "wormhole14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9srrg465", + msg: toUtf8( + JSON.stringify({ + submit_vaas: { + vaas: [ + Buffer.from( + // This is the mainnet governance VAA for registering the Sui Token Bridge signed by the Guardians https://api.wormscan.io/api/v1/vaas/1/0000000000000000000000000000000000000000000000000000000000000004/8526464727442833848 + "01000000030d00a42d36048a27a763413bfa1d261daeb86f7feed7a9850d9915d8c44acee97dd4327c0351648ac4d496050e6af6534badd2b86416a5675f373e5a5ebf876505700002349ec8ae9aeef147e87b2b06a475d6ec760c20680285ecdd6b7b400cbd67504232e0cae965d27dc9f0a83fbed40ec0a759bd7ad9a014a69c90364a1df88048dc000323d01a78c01887d981e22ffb0f91d02c1d7f015393513db3f215af64bd9cb74829eb08fc9895f8b1247933dd23c2b71ac1bf5bbefb8bea829ab2f6b94317eaa901040305b91b17227313395a861365c8b110414e961ccf25a2b645226e6a307e488f6dace0896c354425f5dfb6b0cb968ef1f752653cd85179b9344f215b555601dd01060dcfd80b54b6f43e502b9628a5a2b92b453fa96718397ecbe0e279495ff37c0835fff5ee7f3b749de287a0c3440105ddd705d06975d56384792373645b7787df000715985ff2cf28e3a8062d9ffc7ef69fbd7c56082f0938cc564586d0dbcacf14986579e7d7a8629dc8a9cfcbf0c97c46ae6492d05c5fba193400746a43b6f1123d000a5db8adfca6d43dd345e130fa0cae250ac7cfa364a29d47cf219ce2b50d6f930a0e1bc399b5b92cdec3d00fcf6f2c7f0732996344812dae85afcfa077c67d94b8010bf68fe7c2ed3aaa180b01ba28052fa63d72509e642bf45f8b5c14c582d8e6eb99514d41ab7fd3f6451470e02a054a3630e347020b6330a8ec23efdc3e4da4550b010d7f3ec58dbb8ae21a2fb71941ed80d646469f1992e7fdc32706c327bfbe01b98011e8b377fce487237f9238fe9af09991f5da11d85aba5a4a81e99df8d066aead010e18e7de979a55bd568b26754fdd7d9e7b03572d742e5657f944ab35b44398a40e07a0c2399e13a244277138375e7e980bf6b666f39bc2f86afd2605f0249a5a53000f77d089279a354b7faa1f3fdc084f6e0ef684d9bcce8d9fb11b5568c0d0b215f15d54cc4383e1b7112fadc238f750b885f5f81a21f84e00ec4487a8064386cc2e01106d9d3067e19413e985f76852eb0cdd071fef659540ddf3a9d5610d492a68a13c61cd109f64c977c1274f9782dcbddfa46ee94331e02f98ba8fb37e22300bd63e0111819a499e30feb82190736054d2993918aeb591e3098b4df77630e93512fec4c122f2cccfe88f2b735f42a06571944d800f3dfcb07de7956330515ddb3c9a41360000000000764b7752000100000000000000000000000000000000000000000000000000000000000000047654167e9520c1b820000000000000000000000000000000000000000000546f6b656e4272696467650100000015ccceeb29348f71bdd22ffef43a2a19c1f5b5e17c5cca5411529120182672ade5", + "hex" + ).toString("base64"), + ], + }, + }) + ), + funds: [], + }); + const res = await client.signAndBroadcast(signer, [msg], { + ...ZERO_FEE, + gas: "10000000", + }); + console.log(res); +} + +try { + main(); +} catch (e: any) { + if (e?.message) { + console.error(e.message); + } + throw e; +} diff --git a/wormchain/contracts/tools/deploy_wormchain_testnet.ts b/wormchain/contracts/tools/deploy_wormchain_testnet.ts new file mode 100644 index 0000000000..c8b5a00e2b --- /dev/null +++ b/wormchain/contracts/tools/deploy_wormchain_testnet.ts @@ -0,0 +1,246 @@ +import { + CHAIN_ID_WORMCHAIN, + hexToUint8Array, + Other, + Payload, + serialiseVAA, + sign, + VAA, +} from "@certusone/wormhole-sdk"; +import { toBinary } from "@cosmjs/cosmwasm-stargate"; +import { fromBase64 } from "@cosmjs/encoding"; +import { + getWallet, + getWormchainSigningClient, +} from "@wormhole-foundation/wormchain-sdk"; +import { ZERO_FEE } from "@wormhole-foundation/wormchain-sdk/lib/core/consts"; +import "dotenv/config"; +import * as fs from "fs"; +import { readdirSync } from "fs"; +import { keccak256 } from "js-sha3"; +import * as util from "util"; + +if (process.env.INIT_SIGNERS_KEYS_CSV === "undefined") { + let msg = `.env is missing. run "make contracts-tools-deps" to fetch.`; + console.error(msg); + throw msg; +} +const VAA_SIGNERS = process.env.INIT_SIGNERS_KEYS_CSV.split(","); +const WORMCHAIN_HOST = process.env.WORMCHAIN_HOST; +if (!WORMCHAIN_HOST) { + throw "WORMCHAIN_HOST unset"; +} +let MNEMONIC = process.env.MNEMONIC; +if (!MNEMONIC) { + throw "MNEMONIC unset"; +} + +const GOVERNANCE_CHAIN = 1; +const GOVERNANCE_EMITTER = + "0000000000000000000000000000000000000000000000000000000000000004"; + +const readFileAsync = util.promisify(fs.readFile); + +/* + NOTE: Only append to this array: keeping the ordering is crucial, as the + contracts must be imported in a deterministic order so their addresses remain + deterministic. +*/ +type ContractName = string; +const artifacts: ContractName[] = ["wormchain_ibc_receiver.wasm"]; + +const ARTIFACTS_PATH = "../artifacts/"; +/* Check that the artifact folder contains all the wasm files we expect and nothing else */ + +try { + const actual_artifacts = readdirSync(ARTIFACTS_PATH).filter((a) => + a.endsWith(".wasm") + ); + + const missing_artifacts = artifacts.filter( + (a) => !actual_artifacts.includes(a) + ); + if (missing_artifacts.length) { + console.log( + "Error during wormchain deployment. The following files are expected to be in the artifacts folder:" + ); + missing_artifacts.forEach((file) => console.log(` - ${file}`)); + console.log( + "Hint: the deploy script needs to run after the contracts have been built." + ); + console.log( + "External binary blobs need to be manually added in tools/Dockerfile." + ); + process.exit(1); + } +} catch (err) { + console.error( + `${ARTIFACTS_PATH} cannot be read. Do you need to run "make contracts-deploy-setup"?` + ); + process.exit(1); +} + +async function main() { + /* Set up cosmos client & wallet */ + + const wallet = await getWallet(MNEMONIC); + const client = await getWormchainSigningClient(WORMCHAIN_HOST, wallet); + + // there are several Cosmos chains in devnet, so check the config is as expected + let id = await client.getChainId(); + if (id !== "wormchain-testnet-0") { + throw new Error( + `Wormchain CosmWasmClient connection produced an unexpected chainID: ${id}` + ); + } + + const signers = await wallet.getAccounts(); + const signer = signers[0].address; + console.log("wormchain contract deployer is: ", signer); + + /* Deploy artifacts */ + + const codeIds: { [name: ContractName]: number } = await artifacts.reduce( + async (prev, file) => { + // wait for the previous to finish, to avoid the race condition of wallet sequence mismatch. + const accum = await prev; + + const contract_bytes = await readFileAsync(`${ARTIFACTS_PATH}${file}`); + + const payload = keccak256(contract_bytes); + let vaa: VAA = { + version: 1, + guardianSetIndex: 0, + signatures: [], + timestamp: 0, + nonce: 0, + emitterChain: GOVERNANCE_CHAIN, + emitterAddress: GOVERNANCE_EMITTER, + sequence: BigInt(Math.floor(Math.random() * 100000000)), + consistencyLevel: 0, + payload: { + type: "Other", + // 32-byte id `WasmdModule`, action = 1 (ActionStoreCode), chain = Wormchain, payload = wasm hash + // https://github.com/wormhole-foundation/wormhole/blob/main/sdk/vaa/payloads.go#L55 + hex: `0000000000000000000000000000000000000000005761736D644D6F64756C65010${CHAIN_ID_WORMCHAIN.toString( + 16 + )}${payload}`, + }, + }; + vaa.signatures = sign(VAA_SIGNERS, vaa as unknown as VAA); + console.log("uploading", file); + const msg = client.core.msgStoreCode({ + signer, + wasm_byte_code: new Uint8Array(contract_bytes), + vaa: hexToUint8Array(serialiseVAA(vaa as unknown as VAA)), + }); + const result = await client.signAndBroadcast(signer, [msg], { + ...ZERO_FEE, + gas: "10000000", + }); + const codeId = Number( + JSON.parse(result.rawLog)[0] + .events.find(({ type }) => type === "store_code") + .attributes.find(({ key }) => key === "code_id").value + ); + console.log( + `uploaded ${file}, codeID: ${codeId}, tx: ${result.transactionHash}` + ); + + accum[file] = codeId; + return accum; + }, + Object() + ); + + // Instantiate contracts. + + async function instantiate(code_id: number, inst_msg: any, label: string) { + const instMsgBinary = toBinary(inst_msg); + const instMsgBytes = fromBase64(instMsgBinary); + + // see /sdk/vaa/governance.go + const codeIdBuf = Buffer.alloc(8); + codeIdBuf.writeBigInt64BE(BigInt(code_id)); + const codeIdHash = keccak256(codeIdBuf); + const codeIdLabelHash = keccak256( + Buffer.concat([ + Buffer.from(codeIdHash, "hex"), + Buffer.from(label, "utf8"), + ]) + ); + const fullHash = keccak256( + Buffer.concat([Buffer.from(codeIdLabelHash, "hex"), instMsgBytes]) + ); + + console.log(fullHash); + + let vaa: VAA = { + version: 1, + guardianSetIndex: 0, + signatures: [], + timestamp: 0, + nonce: 0, + emitterChain: GOVERNANCE_CHAIN, + emitterAddress: GOVERNANCE_EMITTER, + sequence: BigInt(Math.floor(Math.random() * 100000000)), + consistencyLevel: 0, + payload: { + type: "Other", + hex: `0000000000000000000000000000000000000000005761736D644D6F64756C65020${CHAIN_ID_WORMCHAIN.toString( + 16 + )}${fullHash}`, + }, + }; + // TODO: check for number of guardians in set and use the corresponding keys + vaa.signatures = sign(VAA_SIGNERS, vaa as unknown as VAA); + const msg = client.core.msgInstantiateContract({ + signer, + code_id, + label, + msg: instMsgBytes, + vaa: hexToUint8Array(serialiseVAA(vaa as unknown as VAA)), + }); + const result = await client.signAndBroadcast(signer, [msg], { + ...ZERO_FEE, + gas: "10000000", + }); + console.log("contract instantiation msg: ", msg); + console.log("contract instantiation result: ", result); + const addr = JSON.parse(result.rawLog)[0] + .events.find(({ type }) => type === "instantiate") + .attributes.find(({ key }) => key === "_contract_address").value; + console.log( + `deployed contract ${label}, codeID: ${code_id}, address: ${addr}, txHash: ${result.transactionHash}` + ); + + return addr; + } + + // Instantiate contracts. + // NOTE: Only append at the end, the ordering must be deterministic. + + const addresses: { + [contractName: string]: string; + } = {}; + + const wormchainIbcReceiverInstantiateMsg = {}; + addresses["wormchain_ibc_receiver.wasm"] = await instantiate( + codeIds["wormchain_ibc_receiver.wasm"], + wormchainIbcReceiverInstantiateMsg, + "wormchainIbcReceiver" + ); + console.log( + "instantiated wormchain ibc receiver contract: ", + addresses["wormchain_ibc_receiver.wasm"] + ); +} + +try { + main(); +} catch (e: any) { + if (e?.message) { + console.error(e.message); + } + throw e; +} diff --git a/wormchain/contracts/tools/upgrade_wormchain_testnet.ts b/wormchain/contracts/tools/upgrade_wormchain_testnet.ts new file mode 100644 index 0000000000..15e681fedd --- /dev/null +++ b/wormchain/contracts/tools/upgrade_wormchain_testnet.ts @@ -0,0 +1,230 @@ +import { + CHAIN_ID_WORMCHAIN, + hexToUint8Array, + Other, + Payload, + serialiseVAA, + sign, + VAA, +} from "@certusone/wormhole-sdk"; +import { toBinary } from "@cosmjs/cosmwasm-stargate"; +import { fromBase64 } from "@cosmjs/encoding"; +import { + getWallet, + getWormchainSigningClient, +} from "@wormhole-foundation/wormchain-sdk"; +import { ZERO_FEE } from "@wormhole-foundation/wormchain-sdk/lib/core/consts"; +import "dotenv/config"; +import * as fs from "fs"; +import { readdirSync } from "fs"; +import { keccak256 } from "js-sha3"; +import * as util from "util"; + +if (process.env.INIT_SIGNERS_KEYS_CSV === "undefined") { + let msg = `.env is missing. run "make contracts-tools-deps" to fetch.`; + console.error(msg); + throw msg; +} +const VAA_SIGNERS = process.env.INIT_SIGNERS_KEYS_CSV.split(","); +const WORMCHAIN_HOST = process.env.WORMCHAIN_HOST; +if (!WORMCHAIN_HOST) { + throw "WORMCHAIN_HOST unset"; +} +let MNEMONIC = process.env.MNEMONIC; +if (!MNEMONIC) { + throw "MNEMONIC unset"; +} + +const GOVERNANCE_CHAIN = 1; +const GOVERNANCE_EMITTER = + "0000000000000000000000000000000000000000000000000000000000000004"; + +const readFileAsync = util.promisify(fs.readFile); + +/* + NOTE: Only append to this array: keeping the ordering is crucial, as the + contracts must be imported in a deterministic order so their addresses remain + deterministic. +*/ +type ContractName = string; +const artifacts: ContractName[] = ["wormchain_ibc_receiver.wasm"]; + +const ARTIFACTS_PATH = "../artifacts/"; +/* Check that the artifact folder contains all the wasm files we expect and nothing else */ + +try { + const actual_artifacts = readdirSync(ARTIFACTS_PATH).filter((a) => + a.endsWith(".wasm") + ); + + const missing_artifacts = artifacts.filter( + (a) => !actual_artifacts.includes(a) + ); + if (missing_artifacts.length) { + console.log( + "Error during wormchain deployment. The following files are expected to be in the artifacts folder:" + ); + missing_artifacts.forEach((file) => console.log(` - ${file}`)); + console.log( + "Hint: the deploy script needs to run after the contracts have been built." + ); + console.log( + "External binary blobs need to be manually added in tools/Dockerfile." + ); + process.exit(1); + } +} catch (err) { + console.error( + `${ARTIFACTS_PATH} cannot be read. Do you need to run "make contracts-deploy-setup"?` + ); + process.exit(1); +} + +async function main() { + /* Set up cosmos client & wallet */ + + const wallet = await getWallet(MNEMONIC); + const client = await getWormchainSigningClient(WORMCHAIN_HOST, wallet); + + // there are several Cosmos chains in devnet, so check the config is as expected + let id = await client.getChainId(); + if (id !== "wormchain-testnet-0") { + throw new Error( + `Wormchain CosmWasmClient connection produced an unexpected chainID: ${id}` + ); + } + + const signers = await wallet.getAccounts(); + const signer = signers[0].address; + console.log("wormchain contract deployer is: ", signer); + + /* Deploy artifacts */ + + const codeIds: { [name: ContractName]: number } = await artifacts.reduce( + async (prev, file) => { + // wait for the previous to finish, to avoid the race condition of wallet sequence mismatch. + const accum = await prev; + + const contract_bytes = await readFileAsync(`${ARTIFACTS_PATH}${file}`); + + const payload = keccak256(contract_bytes); + let vaa: VAA = { + version: 1, + guardianSetIndex: 0, + signatures: [], + timestamp: 0, + nonce: 0, + emitterChain: GOVERNANCE_CHAIN, + emitterAddress: GOVERNANCE_EMITTER, + sequence: BigInt(Math.floor(Math.random() * 100000000)), + consistencyLevel: 0, + payload: { + type: "Other", + hex: `0000000000000000000000000000000000000000005761736D644D6F64756C65010${CHAIN_ID_WORMCHAIN.toString( + 16 + )}${payload}`, + }, + }; + vaa.signatures = sign(VAA_SIGNERS, vaa as unknown as VAA); + console.log("uploading", file); + const msg = client.core.msgStoreCode({ + signer, + wasm_byte_code: new Uint8Array(contract_bytes), + vaa: hexToUint8Array(serialiseVAA(vaa as unknown as VAA)), + }); + const result = await client.signAndBroadcast(signer, [msg], { + ...ZERO_FEE, + gas: "10000000", + }); + const codeId = Number( + JSON.parse(result.rawLog)[0] + .events.find(({ type }) => type === "store_code") + .attributes.find(({ key }) => key === "code_id").value + ); + console.log( + `uploaded ${file}, codeID: ${codeId}, tx: ${result.transactionHash}` + ); + + accum[file] = codeId; + return accum; + }, + Object() + ); + + // Migrate contracts. + + async function migrate(code_id: number, migrate_msg: any, contract: string) { + const migrateMsgBinary = toBinary(migrate_msg); + const migrateMsgBytes = fromBase64(migrateMsgBinary); + + // see /sdk/vaa/governance.go + const codeIdBuf = Buffer.alloc(8); + codeIdBuf.writeBigInt64BE(BigInt(code_id)); + const codeIdHash = keccak256(codeIdBuf); + const codeIdContractHash = keccak256( + Buffer.concat([ + Buffer.from(codeIdHash, "hex"), + Buffer.from(contract, "utf8"), + ]) + ); + const fullHash = keccak256( + Buffer.concat([Buffer.from(codeIdContractHash, "hex"), migrateMsgBytes]) + ); + + console.log(fullHash); + + let vaa: VAA = { + version: 1, + guardianSetIndex: 0, + signatures: [], + timestamp: 0, + nonce: 0, + emitterChain: GOVERNANCE_CHAIN, + emitterAddress: GOVERNANCE_EMITTER, + sequence: BigInt(Math.floor(Math.random() * 100000000)), + consistencyLevel: 0, + payload: { + type: "Other", + hex: `0000000000000000000000000000000000000000005761736D644D6F64756C65030${CHAIN_ID_WORMCHAIN.toString( + 16 + )}${fullHash}`, + }, + }; + // TODO: check for number of guardians in set and use the corresponding keys + vaa.signatures = sign(VAA_SIGNERS, vaa as unknown as VAA); + const msg = client.core.msgMigrateContract({ + signer, + code_id, + contract, + msg: migrateMsgBytes, + vaa: hexToUint8Array(serialiseVAA(vaa as unknown as VAA)), + }); + const result = await client.signAndBroadcast(signer, [msg], { + ...ZERO_FEE, + gas: "10000000", + }); + console.log("contract migration msg: ", msg); + console.log("contract migration result: ", result); + console.log( + `migrated contract ${contract}, codeID: ${code_id}, txHash: ${result.transactionHash}` + ); + } + + // Migrate contracts. + + const wormchainIbcReceiverMigrateMsg = {}; + await migrate( + codeIds["wormchain_ibc_receiver.wasm"], + wormchainIbcReceiverMigrateMsg, + "wormhole1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq0kdhcj" + ); +} + +try { + main(); +} catch (e: any) { + if (e?.message) { + console.error(e.message); + } + throw e; +}