[eth] Make governance transfer less error-prone (#355)
This commit is contained in:
parent
808e9392e3
commit
398b18743d
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -6,8 +6,9 @@ export {
|
||||||
HexString32Bytes,
|
HexString32Bytes,
|
||||||
SetDataSourcesInstruction,
|
SetDataSourcesInstruction,
|
||||||
SetFeeInstruction,
|
SetFeeInstruction,
|
||||||
SetGovernanceDataSourceInstruction,
|
SetValidPeriodInstruction,
|
||||||
SetValidPeriodInstruction
|
RequestGovernanceDataSourceTransferInstruction,
|
||||||
|
AuthorizeGovernanceDataSourceTransferInstruction
|
||||||
} from "./instructions"
|
} from "./instructions"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue