Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ abstract contract PythGovernance is
setTransactionFee(parseSetTransactionFeePayload(gi.payload));
} else if (gi.action == GovernanceAction.WithdrawFee) {
withdrawFee(parseWithdrawFeePayload(gi.payload));
} else if (
gi.action == GovernanceAction.SetWormholeAddressAndDataSources
) {
if (gi.targetChainId == 0)
revert PythErrors.InvalidGovernanceTarget();
setWormholeAddressAndDataSources(
parseSetWormholeAddressAndDataSourcesPayload(gi.payload)
);
} else {
revert PythErrors.InvalidGovernanceMessage();
}
Expand Down Expand Up @@ -252,6 +260,31 @@ abstract contract PythGovernance is
emit WormholeAddressSet(oldWormholeAddress, address(wormhole()));
}

/// @dev Sets wormhole address, data sources, and fee without dual-VAA re-verify.
/// Used for legacy → pro-compatible migration where guardian sets do not overlap.
function setWormholeAddressAndDataSources(
SetWormholeAddressAndDataSourcesPayload memory payload
) internal {
if (payload.newWormholeAddress == address(0))
revert PythErrors.InvalidWormholeAddressToSet();

uint256 codeSize;
address newWormholeAddress = payload.newWormholeAddress;
assembly {
codeSize := extcodesize(newWormholeAddress)
}
if (codeSize == 0) revert PythErrors.InvalidWormholeAddressToSet();

address oldWormholeAddress = address(wormhole());
setWormhole(payload.newWormholeAddress);
emit WormholeAddressSet(oldWormholeAddress, address(wormhole()));

setDataSources(
SetDataSourcesPayload({dataSources: payload.dataSources})
);
setFee(SetFeePayload({newFee: payload.newFee}));
}

function setTransactionFee(
SetTransactionFeePayload memory payload
) internal {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ contract PythGovernanceInstructions {
SetWormholeAddress, // 6
SetFeeInToken, // 7 - No-op for EVM chains
SetTransactionFee, // 8
WithdrawFee // 9
WithdrawFee, // 9
SetWormholeAddressAndDataSources // 10
}

struct GovernanceInstruction {
Expand Down Expand Up @@ -94,6 +95,12 @@ contract PythGovernanceInstructions {
uint256 fee;
}

struct SetWormholeAddressAndDataSourcesPayload {
address newWormholeAddress;
PythInternalStructs.DataSource[] dataSources;
uint newFee;
}

/// @dev Parse a GovernanceInstruction
function parseGovernanceInstruction(
bytes memory encodedInstruction
Expand Down Expand Up @@ -276,4 +283,46 @@ contract PythGovernanceInstructions {
if (encodedPayload.length != index)
revert PythErrors.InvalidGovernanceMessage();
}

/// @dev Parse a SetWormholeAddressAndDataSourcesPayload (action 10) with minimal validation
function parseSetWormholeAddressAndDataSourcesPayload(
bytes memory encodedPayload
)
public
pure
returns (SetWormholeAddressAndDataSourcesPayload memory payload)
{
uint index = 0;

payload.newWormholeAddress = address(encodedPayload.toAddress(index));
index += 20;

uint8 dataSourcesLength = encodedPayload.toUint8(index);
index += 1;

payload.dataSources = new PythInternalStructs.DataSource[](
dataSourcesLength
);

for (uint i = 0; i < dataSourcesLength; i++) {
payload.dataSources[i].chainId = encodedPayload.toUint16(index);
index += 2;

payload.dataSources[i].emitterAddress = encodedPayload.toBytes32(
index
);
index += 32;
}

uint64 val = encodedPayload.toUint64(index);
index += 8;

uint64 expo = encodedPayload.toUint64(index);
index += 8;

payload.newFee = uint256(val) * uint256(10) ** uint256(expo);

if (encodedPayload.length != index)
revert PythErrors.InvalidGovernanceMessage();
}
}
237 changes: 237 additions & 0 deletions target_chains/ethereum/contracts/test/PythGovernance.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,243 @@ contract PythGovernanceTest is
PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
}

function testSetWormholeAddressAndDataSources() public {
address newWormhole = address(setUpWormholeReceiver(1));
bytes32 newEmitter = bytes32(
0x0000000000000000000000000000000000000000000000000000000000002222
);

bytes memory data = abi.encodePacked(
MAGIC,
uint8(GovernanceModule.Target),
uint8(GovernanceAction.SetWormholeAddressAndDataSources),
TARGET_CHAIN_ID,
newWormhole,
uint8(1), // Number of data sources
uint16(26), // Chain ID
newEmitter,
uint64(0), // fee value
uint64(0) // fee expo
);

bytes memory vaa = encodeAndSignMessage(
data,
TEST_GOVERNANCE_CHAIN_ID,
TEST_GOVERNANCE_EMITTER,
1
);

address oldWormhole = address(PythGetters(address(pyth)).wormhole());
PythInternalStructs.DataSource[] memory oldDataSources = PythGetters(
address(pyth)
).validDataSources();
uint oldFee = PythGetters(address(pyth)).singleUpdateFeeInWei();

PythInternalStructs.DataSource[]
memory newDataSources = new PythInternalStructs.DataSource[](1);
newDataSources[0] = PythInternalStructs.DataSource(26, newEmitter);

vm.expectEmit(true, true, true, true);
emit WormholeAddressSet(oldWormhole, newWormhole);
vm.expectEmit(true, true, true, true);
emit DataSourcesSet(oldDataSources, newDataSources);
vm.expectEmit(true, true, true, true);
emit FeeSet(oldFee, 0);

PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);

assertEq(address(PythGetters(address(pyth)).wormhole()), newWormhole);
assertFalse(
PythGetters(address(pyth)).isValidDataSource(
TEST_PYTH2_WORMHOLE_CHAIN_ID,
TEST_PYTH2_WORMHOLE_EMITTER
)
);
assertTrue(
PythGetters(address(pyth)).isValidDataSource(26, newEmitter)
);
assertEq(PythGetters(address(pyth)).singleUpdateFeeInWei(), 0);
}

function testSetWormholeAddressAndDataSourcesRejectsZeroAddress() public {
bytes memory data = abi.encodePacked(
MAGIC,
uint8(GovernanceModule.Target),
uint8(GovernanceAction.SetWormholeAddressAndDataSources),
TARGET_CHAIN_ID,
address(0),
uint8(1),
uint16(26),
bytes32(
0x0000000000000000000000000000000000000000000000000000000000002222
),
uint64(0),
uint64(0)
);

bytes memory vaa = encodeAndSignMessage(
data,
TEST_GOVERNANCE_CHAIN_ID,
TEST_GOVERNANCE_EMITTER,
1
);

vm.expectRevert(PythErrors.InvalidWormholeAddressToSet.selector);
PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
}

function testSetWormholeAddressAndDataSourcesRejectsNoCode() public {
address noCode = makeAddr("noCodeWormhole");

bytes memory data = abi.encodePacked(
MAGIC,
uint8(GovernanceModule.Target),
uint8(GovernanceAction.SetWormholeAddressAndDataSources),
TARGET_CHAIN_ID,
noCode,
uint8(1),
uint16(26),
bytes32(
0x0000000000000000000000000000000000000000000000000000000000002222
),
uint64(0),
uint64(0)
);

bytes memory vaa = encodeAndSignMessage(
data,
TEST_GOVERNANCE_CHAIN_ID,
TEST_GOVERNANCE_EMITTER,
1
);

vm.expectRevert(PythErrors.InvalidWormholeAddressToSet.selector);
PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
}

function testSetWormholeAddressAndDataSourcesRejectsTrailingBytes()
public
{
address newWormhole = address(setUpWormholeReceiver(1));

bytes memory data = abi.encodePacked(
MAGIC,
uint8(GovernanceModule.Target),
uint8(GovernanceAction.SetWormholeAddressAndDataSources),
TARGET_CHAIN_ID,
newWormhole,
uint8(1),
uint16(26),
bytes32(
0x0000000000000000000000000000000000000000000000000000000000002222
),
uint64(0),
uint64(0),
uint8(0xff) // Trailing byte
);

bytes memory vaa = encodeAndSignMessage(
data,
TEST_GOVERNANCE_CHAIN_ID,
TEST_GOVERNANCE_EMITTER,
1
);

vm.expectRevert(PythErrors.InvalidGovernanceMessage.selector);
PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
}

function testSetWormholeAddressAndDataSourcesRejectsTruncatedPayload()
public
{
address newWormhole = address(setUpWormholeReceiver(1));

// Missing fee expo (truncated after fee value)
bytes memory data = abi.encodePacked(
MAGIC,
uint8(GovernanceModule.Target),
uint8(GovernanceAction.SetWormholeAddressAndDataSources),
TARGET_CHAIN_ID,
newWormhole,
uint8(1),
uint16(26),
bytes32(
0x0000000000000000000000000000000000000000000000000000000000002222
),
uint64(0) // fee value only — missing expo
);

bytes memory vaa = encodeAndSignMessage(
data,
TEST_GOVERNANCE_CHAIN_ID,
TEST_GOVERNANCE_EMITTER,
1
);

vm.expectRevert();
PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
}

function testSetWormholeAddressAndDataSourcesRejectsChainIdZero() public {
address newWormhole = address(setUpWormholeReceiver(1));

bytes memory data = abi.encodePacked(
MAGIC,
uint8(GovernanceModule.Target),
uint8(GovernanceAction.SetWormholeAddressAndDataSources),
uint16(0), // Chain ID 0 (unset)
newWormhole,
uint8(1),
uint16(26),
bytes32(
0x0000000000000000000000000000000000000000000000000000000000002222
),
uint64(0),
uint64(0)
);

bytes memory vaa = encodeAndSignMessage(
data,
TEST_GOVERNANCE_CHAIN_ID,
TEST_GOVERNANCE_EMITTER,
1
);

vm.expectRevert(PythErrors.InvalidGovernanceTarget.selector);
PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
}

function testSetWormholeAddressAndDataSourcesSetsNonZeroFee() public {
address newWormhole = address(setUpWormholeReceiver(1));
bytes32 newEmitter = bytes32(
0x0000000000000000000000000000000000000000000000000000000000003333
);

// fee = 5 * 10^3 = 5000
bytes memory data = abi.encodePacked(
MAGIC,
uint8(GovernanceModule.Target),
uint8(GovernanceAction.SetWormholeAddressAndDataSources),
TARGET_CHAIN_ID,
newWormhole,
uint8(1),
uint16(26),
newEmitter,
uint64(5),
uint64(3)
);

bytes memory vaa = encodeAndSignMessage(
data,
TEST_GOVERNANCE_CHAIN_ID,
TEST_GOVERNANCE_EMITTER,
1
);

PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
assertEq(PythGetters(address(pyth)).singleUpdateFeeInWei(), 5000);
}

function encodeAndSignWormholeMessage(
bytes memory data,
uint16 emitterChainId,
Expand Down
Loading