[eth] Add setWormholeAddress governance message (#917)

* [eth] Add setWormholeAddress governance message

* Address review feedbacks

* Add signatures to errors

This will help with debugging

* Update abi
This commit is contained in:
Ali Behjati 2023-06-26 11:31:19 +02:00 committed by GitHub
parent e4d494db9f
commit e39277dd9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 247 additions and 3 deletions

View File

@ -2,6 +2,7 @@ export {
DataSource,
AptosAuthorizeUpgradeContractInstruction,
EthereumUpgradeContractInstruction,
EthereumSetWormholeAddress,
HexString20Bytes,
HexString32Bytes,
SetDataSourcesInstruction,

View File

@ -14,6 +14,7 @@ enum TargetAction {
SetFee,
SetValidPeriod,
RequestGovernanceDataSourceTransfer,
SetWormholeAddress,
}
abstract class HexString implements Serializable {
@ -194,3 +195,13 @@ export class RequestGovernanceDataSourceTransferInstruction extends TargetInstru
.build();
}
}
export class EthereumSetWormholeAddress extends TargetInstruction {
constructor(targetChainId: ChainId, private address: HexString20Bytes) {
super(TargetAction.SetWormholeAddress, targetChainId);
}
protected serializePayload(): Buffer {
return this.address.serialize();
}
}

View File

@ -34,6 +34,10 @@ abstract contract PythGovernance is
);
event FeeSet(uint oldFee, uint newFee);
event ValidPeriodSet(uint oldValidPeriod, uint newValidPeriod);
event WormholeAddressSet(
address oldWormholeAddress,
address newWormholeAddress
);
function verifyGovernanceVM(
bytes memory encodedVM
@ -86,6 +90,13 @@ abstract contract PythGovernance is
) {
// RequestGovernanceDataSourceTransfer can be only part of AuthorizeGovernanceDataSourceTransfer message
revert PythErrors.InvalidGovernanceMessage();
} else if (gi.action == GovernanceAction.SetWormholeAddress) {
if (gi.targetChainId == 0)
revert PythErrors.InvalidGovernanceTarget();
setWormholeAddress(
parseSetWormholeAddressPayload(gi.payload),
encodedVM
);
} else {
revert PythErrors.InvalidGovernanceMessage();
}
@ -190,4 +201,46 @@ abstract contract PythGovernance is
emit ValidPeriodSet(oldValidPeriod, validTimePeriodSeconds());
}
function setWormholeAddress(
SetWormholeAddressPayload memory payload,
bytes memory encodedVM
) internal {
address oldWormholeAddress = address(wormhole());
setWormhole(payload.newWormholeAddress);
// We want to verify that the new wormhole address is valid, so we make sure that it can
// parse and verify the same governance VAA that is used to set it.
(IWormhole.VM memory vm, bool valid, ) = wormhole().parseAndVerifyVM(
encodedVM
);
if (!valid) revert PythErrors.InvalidGovernanceMessage();
if (!isValidGovernanceDataSource(vm.emitterChainId, vm.emitterAddress))
revert PythErrors.InvalidGovernanceMessage();
if (vm.sequence != lastExecutedGovernanceSequence())
revert PythErrors.InvalidWormholeAddressToSet();
GovernanceInstruction memory gi = parseGovernanceInstruction(
vm.payload
);
if (gi.action != GovernanceAction.SetWormholeAddress)
revert PythErrors.InvalidWormholeAddressToSet();
// Purposefully, we don't check whether the chainId is the same as the current chainId because
// we might want to change the chain id of the wormhole contract.
// The following check is not necessary for security, but is a sanity check that the new wormhole
// contract parses the payload correctly.
SetWormholeAddressPayload
memory newPayload = parseSetWormholeAddressPayload(gi.payload);
if (newPayload.newWormholeAddress != payload.newWormholeAddress)
revert PythErrors.InvalidWormholeAddressToSet();
emit WormholeAddressSet(oldWormholeAddress, address(wormhole()));
}
}

View File

@ -30,7 +30,8 @@ contract PythGovernanceInstructions {
SetDataSources, // 2
SetFee, // 3
SetValidPeriod, // 4
RequestGovernanceDataSourceTransfer // 5
RequestGovernanceDataSourceTransfer, // 5
SetWormholeAddress // 6
}
struct GovernanceInstruction {
@ -69,6 +70,10 @@ contract PythGovernanceInstructions {
uint newValidPeriod;
}
struct SetWormholeAddressPayload {
address newWormholeAddress;
}
/// @dev Parse a GovernanceInstruction
function parseGovernanceInstruction(
bytes memory encodedInstruction
@ -199,4 +204,17 @@ contract PythGovernanceInstructions {
if (encodedPayload.length != index)
revert PythErrors.InvalidGovernanceMessage();
}
/// @dev Parse a UpdateWormholeAddressPayload (action 6) with minimal validation
function parseSetWormholeAddressPayload(
bytes memory encodedPayload
) public pure returns (SetWormholeAddressPayload memory sw) {
uint index = 0;
sw.newWormholeAddress = address(encodedPayload.toAddress(index));
index += 20;
if (encodedPayload.length != index)
revert PythErrors.InvalidGovernanceMessage();
}
}

View File

@ -80,7 +80,7 @@ module.exports = {
settings: {
optimizer: {
enabled: true,
runs: 5000,
runs: 2000,
},
},
},

View File

@ -10,8 +10,18 @@ const {
const { assert, expect } = require("chai");
// Use "WormholeReceiver" if you are testing with Wormhole Receiver
const Setup = artifacts.require("Setup");
const Implementation = artifacts.require("Implementation");
const Wormhole = artifacts.require("Wormhole");
const ReceiverSetup = artifacts.require("ReceiverSetup");
const ReceiverImplementation = artifacts.require("ReceiverImplementation");
const WormholeReceiver = artifacts.require("WormholeReceiver");
const wormholeGovernanceChainId = governance.CHAINS.solana;
const wormholeGovernanceContract =
"0x0000000000000000000000000000000000000000000000000000000000000004";
const PythUpgradable = artifacts.require("PythUpgradable");
const MockPythUpgrade = artifacts.require("MockPythUpgrade");
const MockUpgradeableProxy = artifacts.require("MockUpgradeableProxy");
@ -1069,6 +1079,136 @@ contract("Pyth", function () {
// and adding it here will cause more complexity (and is not so short).
});
it("Setting wormhole address should work", async function () {
// Deploy a new wormhole contract
const newSetup = await Setup.new();
const newImpl = await Implementation.new();
// encode initialisation data
const initData = newSetup.contract.methods
.setup(
newImpl.address,
[testSigner1.address],
governance.CHAINS.polygon, // changing the chain id to polygon
wormholeGovernanceChainId,
wormholeGovernanceContract
)
.encodeABI();
const newWormhole = await Wormhole.new(newSetup.address, initData);
// Creating the vaa to set the new wormhole address
const data = new governance.EthereumSetWormholeAddress(
governance.CHAINS.ethereum,
new governance.HexString20Bytes(newWormhole.address)
).serialize();
const vaa = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
assert.equal(await this.pythProxy.chainId(), governance.CHAINS.ethereum);
const oldWormholeAddress = await this.pythProxy.wormhole();
const receipt = await this.pythProxy.executeGovernanceInstruction(vaa);
expectEvent(receipt, "WormholeAddressSet", {
oldWormholeAddress: oldWormholeAddress,
newWormholeAddress: newWormhole.address,
});
assert.equal(await this.pythProxy.wormhole(), newWormhole.address);
assert.equal(await this.pythProxy.chainId(), governance.CHAINS.polygon);
});
it("Setting wormhole address to WormholeReceiver should work", async function () {
// Deploy a new wormhole receiver contract
const newReceiverSetup = await ReceiverSetup.new();
const newReceiverImpl = await ReceiverImplementation.new();
// encode initialisation data
const initData = newReceiverSetup.contract.methods
.setup(
newReceiverImpl.address,
[testSigner1.address],
governance.CHAINS.polygon, // changing the chain id to polygon
wormholeGovernanceChainId,
wormholeGovernanceContract
)
.encodeABI();
const newWormholeReceiver = await WormholeReceiver.new(
newReceiverSetup.address,
initData
);
// Creating the vaa to set the new wormhole address
const data = new governance.EthereumSetWormholeAddress(
governance.CHAINS.ethereum,
new governance.HexString20Bytes(newWormholeReceiver.address)
).serialize();
const vaa = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
assert.equal(await this.pythProxy.chainId(), governance.CHAINS.ethereum);
const oldWormholeAddress = await this.pythProxy.wormhole();
const receipt = await this.pythProxy.executeGovernanceInstruction(vaa);
expectEvent(receipt, "WormholeAddressSet", {
oldWormholeAddress: oldWormholeAddress,
newWormholeAddress: newWormholeReceiver.address,
});
assert.equal(await this.pythProxy.wormhole(), newWormholeReceiver.address);
assert.equal(await this.pythProxy.chainId(), governance.CHAINS.polygon);
});
it("Setting wormhole address to a wrong contract should reject", async function () {
// Deploy a new wormhole contract
const newSetup = await Setup.new();
const newImpl = await Implementation.new();
// encode initialisation data
const initData = newSetup.contract.methods
.setup(
newImpl.address,
[testSigner2.address], // A wrong signer
governance.CHAINS.ethereum,
wormholeGovernanceChainId,
wormholeGovernanceContract
)
.encodeABI();
const newWormhole = await Wormhole.new(newSetup.address, initData);
// Creating the vaa to set the new wormhole address
const data = new governance.EthereumSetWormholeAddress(
governance.CHAINS.ethereum,
new governance.HexString20Bytes(newWormhole.address)
).serialize();
const wrongVaa = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(wrongVaa),
"InvalidGovernanceMessage"
);
});
// Version
it("Make sure version is the npm package version", async function () {

View File

@ -288,7 +288,7 @@ module.exports = {
settings: {
optimizer: {
enabled: true,
runs: 5000,
runs: 2000,
},
},
},

View File

@ -4,29 +4,45 @@ pragma solidity ^0.8.0;
library PythErrors {
// Function arguments are invalid (e.g., the arguments lengths mismatch)
// Signature: 0xa9cb9e0d
error InvalidArgument();
// Update data is coming from an invalid data source.
// Signature: 0xe60dce71
error InvalidUpdateDataSource();
// Update data is invalid (e.g., deserialization error)
// Signature: 0xe69ffece
error InvalidUpdateData();
// Insufficient fee is paid to the method.
// Signature: 0x025dbdd4
error InsufficientFee();
// There is no fresh update, whereas expected fresh updates.
// Signature: 0xde2c57fa
error NoFreshUpdate();
// There is no price feed found within the given range or it does not exists.
// Signature: 0x45805f5d
error PriceFeedNotFoundWithinRange();
// Price feed not found or it is not pushed on-chain yet.
// Signature: 0x14aebe68
error PriceFeedNotFound();
// Requested price is stale.
// Signature: 0x19abf40e
error StalePrice();
// Given message is not a valid Wormhole VAA.
// Signature: 0x2acbe915
error InvalidWormholeVaa();
// Governance message is invalid (e.g., deserialization error).
// Signature: 0x97363b35
error InvalidGovernanceMessage();
// Governance message is not for this contract.
// Signature: 0x63daeb77
error InvalidGovernanceTarget();
// Governance message is coming from an invalid data source.
// Signature: 0x360f2d87
error InvalidGovernanceDataSource();
// Governance message is old.
// Signature: 0x88d1b847
error OldGovernanceMessage();
// The wormhole address to set in SetWormholeAddress governance is invalid.
// Signature: 0x13d3ed82
error InvalidWormholeAddressToSet();
}

View File

@ -34,6 +34,11 @@
"name": "InvalidUpdateDataSource",
"type": "error"
},
{
"inputs": [],
"name": "InvalidWormholeAddressToSet",
"type": "error"
},
{
"inputs": [],
"name": "InvalidWormholeVaa",