diff --git a/governance/xc_admin/packages/xc_admin_common/package.json b/governance/xc_admin/packages/xc_admin_common/package.json index c23668d54a..3d1c60e858 100644 --- a/governance/xc_admin/packages/xc_admin_common/package.json +++ b/governance/xc_admin/packages/xc_admin_common/package.json @@ -229,6 +229,16 @@ "types": "./dist/cjs/governance_payload/SetWormholeAddress.d.ts" } }, + "./governance_payload/MigrateGovernanceAndWormhole": { + "import": { + "default": "./dist/esm/governance_payload/MigrateGovernanceAndWormhole.mjs", + "types": "./dist/esm/governance_payload/MigrateGovernanceAndWormhole.d.ts" + }, + "require": { + "default": "./dist/cjs/governance_payload/MigrateGovernanceAndWormhole.cjs", + "types": "./dist/cjs/governance_payload/MigrateGovernanceAndWormhole.d.ts" + } + }, "./governance_payload/StellarExecutorAction": { "import": { "default": "./dist/esm/governance_payload/StellarExecutorAction.mjs", diff --git a/governance/xc_admin/packages/xc_admin_common/src/__tests__/GovernancePayload.test.ts b/governance/xc_admin/packages/xc_admin_common/src/__tests__/GovernancePayload.test.ts index ccff6cef54..42d0a8b5ec 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/__tests__/GovernancePayload.test.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/__tests__/GovernancePayload.test.ts @@ -31,6 +31,7 @@ import { SetDataSources } from "../governance_payload/SetDataSources"; import { SetFee, SetFeeInToken } from "../governance_payload/SetFee"; import { SetTransactionFee } from "../governance_payload/SetTransactionFee"; import { SetValidPeriod } from "../governance_payload/SetValidPeriod"; +import { MigrateGovernanceAndWormhole } from "../governance_payload/MigrateGovernanceAndWormhole"; import { CosmosUpgradeContract, EvmUpgradeContract, @@ -271,6 +272,103 @@ test("GovernancePayload ser/de", () => { ), ).toBeTruthy(); + const migrateGovernanceAndWormhole = new MigrateGovernanceAndWormhole( + "ethereum", + "0102030405060708090a0b0c0d0e0f1011121314", + [ + { + emitterAddress: + "6bb14509a612f01fbbc4cffeebd4bbfb492a86df717ebe92eb6df432a3f00a25", + emitterChain: 1, + }, + { + emitterAddress: + "000000000000000000000000000000000000000000000000000000000000012d", + emitterChain: 3, + }, + ], + { + emitterAddress: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + emitterChain: 1, + }, + 2, + ); + const migrateGovernanceAndWormholeBuffer = + migrateGovernanceAndWormhole.encode(); + console.log(migrateGovernanceAndWormholeBuffer.toJSON()); + expect( + migrateGovernanceAndWormholeBuffer.equals( + Buffer.from([ + // header: PTGM | module=1 | action=10 | ethereum chain id + 80, 84, 71, 77, 1, 10, 0, 2, + // wormhole address (20 bytes) + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + // numSources = 2 + 2, + // data source 1 + 0, 1, 107, 177, 69, 9, 166, 18, 240, 31, 187, 196, 207, 254, 235, 212, + 187, 251, 73, 42, 134, 223, 113, 126, 190, 146, 235, 109, 244, 50, 163, + 240, 10, 37, + // data source 2 + 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 45, + // governance emitter + 0, 1, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, + 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, + 170, 170, 170, 170, 170, + // governanceDataSourceIndex = 2 + 0, 0, 0, 2, + ]), + ), + ).toBeTruthy(); + const decodedMigrateGovernanceAndWormhole = + MigrateGovernanceAndWormhole.decode(migrateGovernanceAndWormholeBuffer); + expect(decodedMigrateGovernanceAndWormhole?.targetChainId).toBe("ethereum"); + expect(decodedMigrateGovernanceAndWormhole?.address).toBe( + "0102030405060708090a0b0c0d0e0f1011121314", + ); + expect(decodedMigrateGovernanceAndWormhole?.dataSources).toEqual([ + { + emitterAddress: + "6bb14509a612f01fbbc4cffeebd4bbfb492a86df717ebe92eb6df432a3f00a25", + emitterChain: 1, + }, + { + emitterAddress: + "000000000000000000000000000000000000000000000000000000000000012d", + emitterChain: 3, + }, + ]); + expect(decodedMigrateGovernanceAndWormhole?.governanceDataSource).toEqual({ + emitterAddress: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + emitterChain: 1, + }); + expect(decodedMigrateGovernanceAndWormhole?.governanceDataSourceIndex).toBe( + 2, + ); + expect( + decodeGovernancePayload(migrateGovernanceAndWormholeBuffer), + ).toBeInstanceOf(MigrateGovernanceAndWormhole); + + // Empty data sources is a valid payload + // (header + address + numSources=0 + governance emitter + index). + const migratePayload = new MigrateGovernanceAndWormhole( + "ethereum", + "0102030405060708090a0b0c0d0e0f1011121314", + [], + { + emitterAddress: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + emitterChain: 1, + }, + 2, + ); + const migrateBuffer = migratePayload.encode(); + expect(migrateBuffer.length).toBe(8 + 20 + 1 + 34 + 4); + expect(migrateBuffer[5]).toBe(10); + const upgradeContract = new UpgradeContract256Bit( "starknet", "043d0ed8155263af0862372df3af9403c502358661f317f62fbdc026d03beaee", @@ -483,6 +581,30 @@ function governanceActionArb(): Arbitrary { }, ); return fc.oneof(evmArb, starknetArb); + } else if (header.action === "MigrateGovernanceAndWormhole") { + return fc + .record({ + address: hexBytesArb({ maxLength: 20, minLength: 20 }), + dataSources: fc.array(dataSourceArb()), + governanceDataSource: dataSourceArb(), + governanceDataSourceIndex: fc.nat({ max: 0xffff_ffff }), + }) + .map( + ({ + address, + dataSources, + governanceDataSource, + governanceDataSourceIndex, + }) => { + return new MigrateGovernanceAndWormhole( + header.targetChainId, + address, + dataSources, + governanceDataSource, + governanceDataSourceIndex, + ); + }, + ); } else if (header.action === "Execute") { return fc .record({ diff --git a/governance/xc_admin/packages/xc_admin_common/src/governance_payload/MigrateGovernanceAndWormhole.ts b/governance/xc_admin/packages/xc_admin_common/src/governance_payload/MigrateGovernanceAndWormhole.ts new file mode 100644 index 0000000000..81e3a73ba1 --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_common/src/governance_payload/MigrateGovernanceAndWormhole.ts @@ -0,0 +1,116 @@ +import * as BufferLayout from "@solana/buffer-layout"; +import type { ChainName } from "../chains"; +import { safeBufferConcat } from "../utils/buffer"; +import * as BufferLayoutExt from "./BufferLayoutExt"; +import type { ActionName, PythGovernanceAction } from "./PythGovernanceAction"; +import { PythGovernanceHeader } from "./PythGovernanceAction"; +import type { DataSource } from "./SetDataSources"; + +const DataSourceLayout: BufferLayout.Structure = + BufferLayout.struct([ + BufferLayout.u16be("emitterChain"), + BufferLayoutExt.hexBytes(32, "emitterAddress"), + ]); + +/** + * Migrate the wormhole address, price data sources, and governance emitter + * on the target chain. + * + * Wire format after the governance header: + * newWormholeAddress(20) | + * numSources(u8) | [emitterChain(u16be) | emitterAddress(32)]* | + * governanceEmitterChain(u16be) | governanceEmitterAddress(32) | + * governanceDataSourceIndex(u32be) + * + * Fee is not included; set fee separately via SetFee before migration. + */ +export class MigrateGovernanceAndWormhole implements PythGovernanceAction { + readonly actionName: ActionName; + + constructor( + readonly targetChainId: ChainName, + readonly address: string, + readonly dataSources: DataSource[], + readonly governanceDataSource: DataSource, + readonly governanceDataSourceIndex: number, + ) { + this.actionName = "MigrateGovernanceAndWormhole"; + } + + static decode(data: Buffer): MigrateGovernanceAndWormhole | undefined { + const header = PythGovernanceHeader.decode(data); + if (!header || header.action !== "MigrateGovernanceAndWormhole") { + return undefined; + } + + let index = PythGovernanceHeader.span; + const address = BufferLayoutExt.hexBytes(20).decode(data, index); + index += 20; + + const numSources = BufferLayout.u8().decode(data, index); + index += 1; + const dataSources = []; + for (let i = 0; i < numSources; i++) { + dataSources.push(DataSourceLayout.decode(data, index)); + index += DataSourceLayout.span; + } + + const governanceDataSource = DataSourceLayout.decode(data, index); + index += DataSourceLayout.span; + + const governanceDataSourceIndex = BufferLayout.u32be().decode(data, index); + index += 4; + + if (index !== data.length) { + return undefined; + } + + return new MigrateGovernanceAndWormhole( + header.targetChainId, + address, + dataSources, + governanceDataSource, + governanceDataSourceIndex, + ); + } + + encode(): Buffer { + const headerBuffer = new PythGovernanceHeader( + this.targetChainId, + "MigrateGovernanceAndWormhole", + ).encode(); + + const addressBuf = Buffer.alloc(20); + BufferLayoutExt.hexBytes(20).encode(this.address, addressBuf); + + const numSourcesBuf = Buffer.alloc(1); + BufferLayout.u8().encode(this.dataSources.length, numSourcesBuf); + + const dataSourceBufs = this.dataSources.map((source) => { + const buf = Buffer.alloc(DataSourceLayout.span); + DataSourceLayout.encode(source, buf); + return buf; + }); + + const governanceDataSourceBuf = Buffer.alloc(DataSourceLayout.span); + DataSourceLayout.encode( + this.governanceDataSource, + governanceDataSourceBuf, + ); + + const governanceDataSourceIndexBuf = Buffer.alloc(4); + BufferLayout.u32be().encode( + this.governanceDataSourceIndex, + governanceDataSourceIndexBuf, + ); + + return safeBufferConcat([ + headerBuffer, + addressBuf, + numSourcesBuf, + ...dataSourceBufs, + governanceDataSourceBuf, + governanceDataSourceIndexBuf, + ]); + } +} diff --git a/governance/xc_admin/packages/xc_admin_common/src/governance_payload/PythGovernanceAction.ts b/governance/xc_admin/packages/xc_admin_common/src/governance_payload/PythGovernanceAction.ts index df13519343..07e2448219 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/governance_payload/PythGovernanceAction.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/governance_payload/PythGovernanceAction.ts @@ -21,6 +21,7 @@ export const TargetAction = { SetFeeInToken: 7, SetTransactionFee: 8, WithdrawFee: 9, + MigrateGovernanceAndWormhole: 10, } as const; export const EvmExecutorAction = { @@ -74,6 +75,8 @@ export function toActionName( return "SetTransactionFee"; case 9: return "WithdrawFee"; + case 10: + return "MigrateGovernanceAndWormhole"; } } else if ( deserialized.moduleId == MODULE_EVM_EXECUTOR && diff --git a/governance/xc_admin/packages/xc_admin_common/src/governance_payload/index.ts b/governance/xc_admin/packages/xc_admin_common/src/governance_payload/index.ts index b0fa1f0949..915e0b2b09 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/governance_payload/index.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/governance_payload/index.ts @@ -14,6 +14,7 @@ import { EvmSetWormholeAddress, StarknetSetWormholeAddress, } from "./SetWormholeAddress"; +import { MigrateGovernanceAndWormhole } from "./MigrateGovernanceAndWormhole"; import { CallStellarExecutor, UpgradeStellarExecutor, @@ -84,6 +85,8 @@ export function decodeGovernancePayload( return undefined; } } + case "MigrateGovernanceAndWormhole": + return MigrateGovernanceAndWormhole.decode(data); case "Execute": return EvmExecute.decode(data); case "SetTransactionFee": @@ -125,6 +128,7 @@ export * from "./SetFee"; export * from "./SetTransactionFee"; export * from "./SetValidPeriod"; export * from "./SetWormholeAddress"; +export * from "./MigrateGovernanceAndWormhole"; export * from "./StellarExecutorAction"; export * from "./UpdateTrustedSigner"; export * from "./UpgradeContract"; diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts index 48127e0e31..d57e3b7e4c 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts +++ b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts @@ -6,6 +6,7 @@ import { MultisigParser, PythGovernanceActionImpl, SetDataSources, + MigrateGovernanceAndWormhole, WormholeMultisigInstruction, } from "@pythnetwork/xc-admin-common"; import type { PublicKey } from "@solana/web3.js"; @@ -88,7 +89,10 @@ const getInstructionSummary = ( ); } else if (governanceAction instanceof PythGovernanceActionImpl) { return [{ name: governanceAction.action } as const]; - } else if (governanceAction instanceof SetDataSources) { + } else if ( + governanceAction instanceof SetDataSources || + governanceAction instanceof MigrateGovernanceAndWormhole + ) { return [{ name: governanceAction.actionName } as const]; } else { return [{ name: "unknown" } as const];