[eth] Make governance transfer less error-prone (#355)

This commit is contained in:
Ali Behjati 2022-10-19 19:46:26 +02:00 committed by GitHub
parent 808e9392e3
commit 398b18743d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 178 additions and 64 deletions

View File

@ -63,4 +63,8 @@ contract PythGetters is PythState {
function validTimePeriodSeconds() public view returns (uint) { function validTimePeriodSeconds() public view returns (uint) {
return _state.validTimePeriodSeconds; return _state.validTimePeriodSeconds;
} }
function governanceDataSourceIndex() public view returns (uint32) {
return _state.governanceDataSourceIndex;
}
} }

View File

@ -45,14 +45,16 @@ abstract contract PythGovernance is PythGetters, PythSetters, PythGovernanceInst
if (gi.action == GovernanceAction.UpgradeContract) { if (gi.action == GovernanceAction.UpgradeContract) {
require(gi.targetChainId != 0, "upgrade with chain id 0 is not possible"); require(gi.targetChainId != 0, "upgrade with chain id 0 is not possible");
upgradeContract(gi.payload); upgradeContract(gi.payload);
} else if (gi.action == GovernanceAction.SetGovernanceDataSource) { } else if (gi.action == GovernanceAction.AuthorizeGovernanceDataSourceTransfer) {
setGovernanceDataSource(gi.payload); AuthorizeGovernanceDataSourceTransfer(gi.payload);
} else if (gi.action == GovernanceAction.SetDataSources) { } else if (gi.action == GovernanceAction.SetDataSources) {
setDataSources(gi.payload); setDataSources(gi.payload);
} else if (gi.action == GovernanceAction.SetFee) { } else if (gi.action == GovernanceAction.SetFee) {
setFee(gi.payload); setFee(gi.payload);
} else if (gi.action == GovernanceAction.SetValidPeriod) { } else if (gi.action == GovernanceAction.SetValidPeriod) {
setValidPeriod(gi.payload); setValidPeriod(gi.payload);
} else if (gi.action == GovernanceAction.RequestGovernanceDataSourceTransfer) {
revert("RequestGovernanceDataSourceTransfer can be only part of AuthorizeGovernanceDataSourceTransfer message");
} else { } else {
revert("invalid governance action"); revert("invalid governance action");
} }
@ -67,13 +69,39 @@ abstract contract PythGovernance is PythGetters, PythSetters, PythGovernanceInst
function upgradeUpgradableContract(UpgradeContractPayload memory payload) virtual internal; function upgradeUpgradableContract(UpgradeContractPayload memory payload) virtual internal;
function setGovernanceDataSource(bytes memory encodedPayload) internal { // Transfer the governance data source to a new value with sanity checks
SetGovernanceDataSourcePayload memory payload = parseSetGovernanceDataSourcePayload(encodedPayload); // to ensure the new governance data source can manage the contract.
function AuthorizeGovernanceDataSourceTransfer(bytes memory encodedPayload) internal {
PythInternalStructs.DataSource memory oldGovernanceDatSource = governanceDataSource(); PythInternalStructs.DataSource memory oldGovernanceDatSource = governanceDataSource();
setGovernanceDataSource(payload.newGovernanceDataSource); AuthorizeGovernanceDataSourceTransferPayload memory payload = parseAuthorizeGovernanceDataSourceTransferPayload(encodedPayload);
setLastExecutedGovernanceSequence(payload.initialSequence);
// Make sure the claimVaa is a valid VAA with RequestGovernanceDataSourceTransfer governance message
// If it's valid then its emitter can take over the governance from the current emitter.
// The VAA is checked here to ensure that the new governance data source is valid and can send message
// through wormhole.
(IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(payload.claimVaa);
require(valid, reason);
GovernanceInstruction memory gi = parseGovernanceInstruction(vm.payload);
require(gi.targetChainId == chainId() || gi.targetChainId == 0, "invalid target chain for this governance instruction");
require(gi.action == GovernanceAction.RequestGovernanceDataSourceTransfer,
"governance data source change inner vaa is not of claim action type");
RequestGovernanceDataSourceTransferPayload memory claimPayload = parseRequestGovernanceDataSourceTransferPayload(gi.payload);
// Governance data source index is used to prevent replay attacks, so a claimVaa cannot be used twice.
require(governanceDataSourceIndex() < claimPayload.governanceDataSourceIndex,
"cannot upgrade to an older governance data source");
setGovernanceDataSourceIndex(claimPayload.governanceDataSourceIndex);
PythInternalStructs.DataSource memory newGovernanceDS = PythInternalStructs.DataSource(vm.emitterChainId, vm.emitterAddress);
setGovernanceDataSource(newGovernanceDS);
// Setting the last executed governance to the claimVaa sequence to avoid using older sequences.
setLastExecutedGovernanceSequence(vm.sequence);
emit GovernanceDataSourceSet(oldGovernanceDatSource, governanceDataSource(), lastExecutedGovernanceSequence()); emit GovernanceDataSourceSet(oldGovernanceDatSource, governanceDataSource(), lastExecutedGovernanceSequence());
} }

View File

@ -26,10 +26,11 @@ contract PythGovernanceInstructions {
enum GovernanceAction { enum GovernanceAction {
UpgradeContract, // 0 UpgradeContract, // 0
SetGovernanceDataSource, // 1 AuthorizeGovernanceDataSourceTransfer, // 1
SetDataSources, // 2 SetDataSources, // 2
SetFee, // 3 SetFee, // 3
SetValidPeriod // 4 SetValidPeriod, // 4
RequestGovernanceDataSourceTransfer // 5
} }
struct GovernanceInstruction { struct GovernanceInstruction {
@ -43,9 +44,17 @@ contract PythGovernanceInstructions {
address newImplementation; address newImplementation;
} }
struct SetGovernanceDataSourcePayload { struct AuthorizeGovernanceDataSourceTransferPayload {
PythInternalStructs.DataSource newGovernanceDataSource; // Transfer governance control over this contract to another data source.
uint64 initialSequence; // The claimVaa field is a VAA created by the new data source; using a VAA prevents mistakes
// in the handoff by ensuring that the new data source can send VAAs (i.e., is not an invalid address).
bytes claimVaa;
}
struct RequestGovernanceDataSourceTransferPayload {
// Governance data source index is used to prevent replay attacks
// So a claimVaa cannot be used twice.
uint32 governanceDataSourceIndex;
} }
struct SetDataSourcesPayload { struct SetDataSourcesPayload {
@ -94,20 +103,21 @@ contract PythGovernanceInstructions {
require(encodedPayload.length == index, "invalid length for UpgradeContractPayload"); require(encodedPayload.length == index, "invalid length for UpgradeContractPayload");
} }
/// @dev Parse a SetGovernanceDataSourcePayload (action 2) with minimal validation /// @dev Parse a AuthorizeGovernanceDataSourceTransferPayload (action 2) with minimal validation
function parseSetGovernanceDataSourcePayload(bytes memory encodedPayload) public pure returns (SetGovernanceDataSourcePayload memory sgds) { function parseAuthorizeGovernanceDataSourceTransferPayload(bytes memory encodedPayload) public pure returns (AuthorizeGovernanceDataSourceTransferPayload memory sgds) {
sgds.claimVaa = encodedPayload;
}
/// @dev Parse a AuthorizeGovernanceDataSourceTransferPayload (action 2) with minimal validation
function parseRequestGovernanceDataSourceTransferPayload(bytes memory encodedPayload) public pure
returns (RequestGovernanceDataSourceTransferPayload memory sgdsClaim) {
uint index = 0; uint index = 0;
sgds.newGovernanceDataSource.chainId = encodedPayload.toUint16(index); sgdsClaim.governanceDataSourceIndex = encodedPayload.toUint32(index);
index += 2; index += 4;
sgds.newGovernanceDataSource.emitterAddress = encodedPayload.toBytes32(index); require(encodedPayload.length == index, "invalid length for RequestGovernanceDataSourceTransferPayload");
index += 32;
sgds.initialSequence = encodedPayload.toUint64(index);
index += 8;
require(encodedPayload.length == index, "invalid length for SetGovernanceDataSourcePayload");
} }
/// @dev Parse a SetDataSourcesPayload (action 3) with minimal validation /// @dev Parse a SetDataSourcesPayload (action 3) with minimal validation

View File

@ -37,4 +37,8 @@ contract PythSetters is PythState {
function setLastExecutedGovernanceSequence(uint64 sequence) internal { function setLastExecutedGovernanceSequence(uint64 sequence) internal {
_state.lastExecutedGovernanceSequence = sequence; _state.lastExecutedGovernanceSequence = sequence;
} }
function setGovernanceDataSourceIndex(uint32 newIndex) internal {
_state.governanceDataSourceIndex = newIndex;
}
} }

View File

@ -39,6 +39,10 @@ contract PythStorage {
// Mapping of cached price information // Mapping of cached price information
// priceId => PriceInfo // priceId => PriceInfo
mapping(bytes32 => PythInternalStructs.PriceInfo) latestPriceInfo; mapping(bytes32 => PythInternalStructs.PriceInfo) latestPriceInfo;
// Index of the governance data source, increased each time the governance data source
// changes.
uint32 governanceDataSourceIndex;
} }
} }

View File

@ -1004,16 +1004,32 @@ contract("Pyth", function () {
); );
}); });
it("Setting governance data source should work", async function () { it("Transferring governance data source should work", async function () {
const data = new governance.SetGovernanceDataSourceInstruction( const newEmitterAddress = "0x0000000000000000000000000000000000000000000000000000000000001111";
governance.CHAINS.ethereum, const newEmitterChain = governance.CHAINS.acala;
new governance.DataSource(
governance.CHAINS.acala, const claimInstructionData = new governance.RequestGovernanceDataSourceTransferInstruction(
new governance.HexString32Bytes( governance.CHAINS.unset,
"0x0000000000000000000000000000000000000000000000000000000000001111", 1
) ).serialize();
),
BigInt(10) const claimVaaHexString = await createVAAFromUint8Array(
claimInstructionData,
newEmitterChain,
newEmitterAddress,
1
);
await expectRevert(
this.pythProxy.executeGovernanceInstruction(claimVaaHexString),
"VAA is not coming from the governance data source"
);
const claimVaa = Buffer.from(claimVaaHexString.substring(2), 'hex');
const data = new governance.AuthorizeGovernanceDataSourceTransferInstruction(
governance.CHAINS.unset,
claimVaa
).serialize(); ).serialize();
const vaa = await createVAAFromUint8Array(data, const vaa = await createVAAFromUint8Array(data,
@ -1025,40 +1041,75 @@ contract("Pyth", function () {
const oldGovernanceDataSource = await this.pythProxy.governanceDataSource(); const oldGovernanceDataSource = await this.pythProxy.governanceDataSource();
const receipt = await this.pythProxy.executeGovernanceInstruction(vaa); const receipt = await this.pythProxy.executeGovernanceInstruction(vaa);
const newGovernanceDataSource = await this.pythProxy.governanceDataSource();
expectEvent(receipt, 'GovernanceDataSourceSet', { expectEvent(receipt, 'GovernanceDataSourceSet', {
oldDataSource: oldGovernanceDataSource, oldDataSource: oldGovernanceDataSource,
newDataSource: await this.pythProxy.governanceDataSource(), newDataSource: newGovernanceDataSource,
}); });
const newVaaFromOldGovernanceSource = await createVAAFromUint8Array(data, expect(newGovernanceDataSource.chainId).equal(newEmitterChain.toString());
expect(newGovernanceDataSource.emitterAddress).equal(newEmitterAddress);
// Verifies the data source has changed.
await expectRevert(
this.pythProxy.executeGovernanceInstruction(vaa),
"VAA is not coming from the governance data source"
);
// Make sure a claim vaa does not get executed
const claimLonely = new governance.RequestGovernanceDataSourceTransferInstruction(
governance.CHAINS.unset,
2
).serialize();
const claimLonelyVaa = await createVAAFromUint8Array(
claimLonely,
newEmitterChain,
newEmitterAddress,
2
);
await expectRevert(
this.pythProxy.executeGovernanceInstruction(claimLonelyVaa),
"RequestGovernanceDataSourceTransfer can be only part of AuthorizeGovernanceDataSourceTransfer message"
);
// Transfer back the ownership to the old governance data source without increasing
// the governance index should not work
// A wrong vaa that does not move the governance index
const transferBackClaimInstructionDataWrong = new governance.RequestGovernanceDataSourceTransferInstruction(
governance.CHAINS.unset,
1 // The same governance data source index => Should fail
).serialize();
const transferBackClaimVaaHexStringWrong = await createVAAFromUint8Array(
transferBackClaimInstructionDataWrong,
testGovernanceChainId, testGovernanceChainId,
testGovernanceEmitter, testGovernanceEmitter,
2 2
); );
await expectRevert( const transferBackClaimVaaWrong = Buffer.from(transferBackClaimVaaHexStringWrong.substring(2), 'hex');
this.pythProxy.executeGovernanceInstruction(newVaaFromOldGovernanceSource),
"VAA is not coming from the governance data source"
);
const newVaaFromNewGovernanceOldSequence = await createVAAFromUint8Array(data, const transferBackDataWrong = new governance.AuthorizeGovernanceDataSourceTransferInstruction(
governance.CHAINS.acala, governance.CHAINS.unset,
"0x0000000000000000000000000000000000000000000000000000000000001111", transferBackClaimVaaWrong
).serialize();
const transferBackVaaWrong = await createVAAFromUint8Array(transferBackDataWrong,
newEmitterChain,
newEmitterAddress,
2 2
); );
await expectRevert( await expectRevert(
this.pythProxy.executeGovernanceInstruction(newVaaFromNewGovernanceOldSequence), this.pythProxy.executeGovernanceInstruction(transferBackVaaWrong),
"VAA is older than the last executed governance VAA" "cannot upgrade to an older governance data source"
); );
const newVaaFromNewGovernanceGood = await createVAAFromUint8Array(data,
governance.CHAINS.acala,
"0x0000000000000000000000000000000000000000000000000000000000001111",
20
);
await this.pythProxy.executeGovernanceInstruction(newVaaFromNewGovernanceGood);
}); });
it("Setting data sources should work", async function () { it("Setting data sources should work", async function () {

View File

@ -6,8 +6,9 @@ export {
HexString32Bytes, HexString32Bytes,
SetDataSourcesInstruction, SetDataSourcesInstruction,
SetFeeInstruction, SetFeeInstruction,
SetGovernanceDataSourceInstruction, SetValidPeriodInstruction,
SetValidPeriodInstruction RequestGovernanceDataSourceTransferInstruction,
AuthorizeGovernanceDataSourceTransferInstruction
} from "./instructions" } from "./instructions"
export { export {

View File

@ -1,4 +1,4 @@
import { ChainId } from "@certusone/wormhole-sdk"; import { ChainId, CHAINS } from "@certusone/wormhole-sdk";
import { Serializable, BufferBuilder } from "./serialize"; import { Serializable, BufferBuilder } from "./serialize";
@ -9,10 +9,11 @@ enum Module {
enum TargetAction { enum TargetAction {
UpgradeContract = 0, UpgradeContract = 0,
SetGovernanceDataSource, AuthorizeGovernanceDataSourceTransfer,
SetDataSources, SetDataSources,
SetFee, SetFee,
SetValidPeriod, SetValidPeriod,
RequestGovernanceDataSourceTransfer,
} }
abstract class HexString implements Serializable { abstract class HexString implements Serializable {
@ -126,20 +127,16 @@ export class EthereumUpgradeContractInstruction extends TargetInstruction {
} }
} }
export class SetGovernanceDataSourceInstruction extends TargetInstruction { export class AuthorizeGovernanceDataSourceTransferInstruction extends TargetInstruction {
constructor( constructor(
targetChainId: ChainId, targetChainId: ChainId,
private governanceDataSource: DataSource, private claimVaa: Buffer,
private initialSequence: bigint,
) { ) {
super(TargetAction.SetGovernanceDataSource, targetChainId); super(TargetAction.AuthorizeGovernanceDataSourceTransfer, targetChainId);
} }
protected serializePayload(): Buffer { protected serializePayload(): Buffer {
return new BufferBuilder() return this.claimVaa;
.addObject(this.governanceDataSource)
.addBigUint64(this.initialSequence)
.build();
} }
} }
@ -190,3 +187,18 @@ export class SetValidPeriodInstruction extends TargetInstruction {
.build(); .build();
} }
} }
export class RequestGovernanceDataSourceTransferInstruction extends TargetInstruction {
constructor(
targetChainId: ChainId,
private governanceDataSourceIndex: number,
) {
super(TargetAction.RequestGovernanceDataSourceTransfer, targetChainId);
}
protected serializePayload(): Buffer {
return new BufferBuilder()
.addUint32(this.governanceDataSourceIndex)
.build();
}
}