pyth-crosschain/target_chains/ethereum/contracts/test/pyth.js

896 lines
25 KiB
JavaScript

const elliptic = require("elliptic");
const governance = require("xc_admin_common");
const { deployProxy, upgradeProxy } = require("@openzeppelin/truffle-upgrades");
const {
expectRevert,
expectEvent,
time,
} = require("@openzeppelin/test-helpers");
const { assert, expect } = require("chai");
const { EvmSetWormholeAddress } = require("xc_admin_common");
// 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");
const testSigner1PK =
"cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
const testSigner2PK =
"892330666a850761e7370376430bb8c2aa1494072d3bfeaed0c4fa3d5a9135fe";
contract("Pyth", function () {
const testSigner1 = web3.eth.accounts.privateKeyToAccount(testSigner1PK);
const testSigner2 = web3.eth.accounts.privateKeyToAccount(testSigner2PK);
const testGovernanceChainId = "1";
const testGovernanceEmitter =
"0x0000000000000000000000000000000000000000000000000000000000001234";
const testPyth2WormholeChainId = "1";
const testPyth2WormholeEmitter =
"0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b";
// Place all atomic operations that are done within migrations here.
beforeEach(async function () {
this.pythProxy = await deployProxy(PythUpgradable, [
(await Wormhole.deployed()).address,
[testPyth2WormholeChainId],
[testPyth2WormholeEmitter],
testGovernanceChainId,
testGovernanceEmitter,
0, // Initial governance sequence
60, // Validity time in seconds
0, // single update fee in wei
]);
});
it("should be initialized with the correct signers and values", async function () {
await this.pythProxy.isValidDataSource(
testPyth2WormholeChainId,
testPyth2WormholeEmitter
);
});
it("there should be no owner", async function () {
// Check that the ownership is renounced.
const owner = await this.pythProxy.owner();
assert.equal(owner, "0x0000000000000000000000000000000000000000");
});
it("deployer cannot upgrade the contract", async function () {
// upgrade proxy should fail
await expectRevert(
upgradeProxy(this.pythProxy.address, MockPythUpgrade),
"Ownable: caller is not the owner."
);
});
async function updatePriceFeeds(
contract,
batches,
valueInWei,
chainId,
emitter
) {
let updateData = [];
for (let data of batches) {
const vm = await signAndEncodeVM(
1,
1,
chainId || testPyth2WormholeChainId,
emitter || testPyth2WormholeEmitter,
0,
data,
[testSigner1PK],
0,
0
);
updateData.push("0x" + vm);
}
return await contract.updatePriceFeeds(updateData, { value: valueInWei });
}
/**
* Create a governance instruction VAA from the Instruction object. Then
* Submit and execute it on the contract.
* @param contract Pyth contract
* @param {governance.PythGovernanceAction} governanceInstruction
* @param {number} sequence
*/
async function createAndThenSubmitGovernanceInstructionVaa(
contract,
governanceInstruction,
sequence
) {
await contract.executeGovernanceInstruction(
await createVAAFromUint8Array(
governanceInstruction.encode(),
testGovernanceChainId,
testGovernanceEmitter,
sequence
)
);
}
it("should attest price updates empty", async function () {
const receipt = await updatePriceFeeds(this.pythProxy, []);
expectEvent.notEmitted(receipt, "PriceFeedUpdate");
});
/**
* Set fee to `newFee` by creating and submitting a governance instruction for it.
* @param contarct Pyth contract
* @param {number} newFee
* @param {number=} governanceSequence Sequence number of the governance instruction. Defaults to 1.
*/
async function setFeeTo(contract, newFee, governanceSequence) {
await createAndThenSubmitGovernanceInstructionVaa(
contract,
new governance.SetFee("ethereum", BigInt(newFee), BigInt(0)),
governanceSequence ?? 1
);
}
it("should fail transaction if a price is not found", async function () {
await expectRevertCustomError(
this.pythProxy.queryPriceFeed(
"0xdeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeed"
),
"PriceFeedNotFound"
);
});
/**
* Set valid time period to `newValidPeriod` by creating and submitting a
* governance instruction for it.
* @param contract Pyth contract
* @param {number} newValidPeriod
* @param {number=} governanceSequence Sequence number of the governance instruction. Defaults to 1.
*/
async function setValidPeriodTo(
contract,
newValidPeriod,
governanceSequence
) {
await createAndThenSubmitGovernanceInstructionVaa(
contract,
new governance.SetValidPeriod("ethereum", BigInt(newValidPeriod)),
governanceSequence ?? 1
);
}
// Governance
// Logics that apply to all governance messages
it("Make sure invalid magic and module won't work", async function () {
// First 4 bytes of data are magic and the second byte after that is module
const data = new governance.SetValidPeriod("ethereum", BigInt(10)).encode();
const wrongMagic = Buffer.from(data);
wrongMagic[1] = 0;
const vaaWrongMagic = await createVAAFromUint8Array(
wrongMagic,
testGovernanceChainId,
testGovernanceEmitter,
1
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(vaaWrongMagic),
"InvalidGovernanceMessage"
);
const wrongModule = Buffer.from(data);
wrongModule[4] = 0;
const vaaWrongModule = await createVAAFromUint8Array(
wrongModule,
testGovernanceChainId,
testGovernanceEmitter,
1
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(vaaWrongModule),
"InvalidGovernanceTarget"
);
const outOfBoundModule = Buffer.from(data);
outOfBoundModule[4] = 20;
const vaaOutOfBoundModule = await createVAAFromUint8Array(
outOfBoundModule,
testGovernanceChainId,
testGovernanceEmitter,
1
);
await expectRevert(
this.pythProxy.executeGovernanceInstruction(vaaOutOfBoundModule),
"Panic: Enum value out of bounds."
);
});
it("Make sure governance with wrong sender won't work", async function () {
const data = new governance.SetValidPeriod("ethereum", BigInt(10)).encode();
const vaaWrongEmitter = await createVAAFromUint8Array(
data,
testGovernanceChainId,
"0x0000000000000000000000000000000000000000000000000000000000001111",
1
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(vaaWrongEmitter),
"InvalidGovernanceDataSource"
);
const vaaWrongChain = await createVAAFromUint8Array(
data,
governance.CHAINS.karura,
testGovernanceEmitter,
1
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(vaaWrongChain),
"InvalidGovernanceDataSource"
);
});
it("Make sure governance with only target chain id and 0 work", async function () {
const wrongChainData = new governance.SetValidPeriod(
"solana",
BigInt(10)
).encode();
const wrongChainVaa = await createVAAFromUint8Array(
wrongChainData,
testGovernanceChainId,
testGovernanceEmitter,
1
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(wrongChainVaa),
"InvalidGovernanceTarget"
);
const dataForAllChains = new governance.SetValidPeriod(
"unset",
BigInt(10)
).encode();
const vaaForAllChains = await createVAAFromUint8Array(
dataForAllChains,
testGovernanceChainId,
testGovernanceEmitter,
1
);
await this.pythProxy.executeGovernanceInstruction(vaaForAllChains);
const dataForEth = new governance.SetValidPeriod(
"ethereum",
BigInt(10)
).encode();
const vaaForEth = await createVAAFromUint8Array(
dataForEth,
testGovernanceChainId,
testGovernanceEmitter,
2
);
await this.pythProxy.executeGovernanceInstruction(vaaForEth);
});
it("Make sure that governance messages are executed in order and cannot be reused", async function () {
const data = new governance.SetValidPeriod("ethereum", BigInt(10)).encode();
const vaaSeq1 = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
await this.pythProxy.executeGovernanceInstruction(vaaSeq1),
// Replaying shouldn't work
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(vaaSeq1),
"OldGovernanceMessage"
);
const vaaSeq2 = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
2
);
await this.pythProxy.executeGovernanceInstruction(vaaSeq2),
// Replaying shouldn't work
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(vaaSeq1),
"OldGovernanceMessage"
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(vaaSeq2),
"OldGovernanceMessage"
);
});
// Per governance type logic
it("Upgrading the contract with chain id 0 is invalid", async function () {
const newImplementation = await PythUpgradable.new();
const data = new governance.EvmUpgradeContract(
"unset", // 0
newImplementation.address.replace("0x", "")
).encode();
const vaa = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(vaa),
"InvalidGovernanceTarget"
);
});
it("Upgrading the contract should work", async function () {
const newImplementation = await PythUpgradable.new();
const data = new governance.EvmUpgradeContract(
"ethereum",
newImplementation.address.replace("0x", "")
).encode();
const vaa = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
const receipt = await this.pythProxy.executeGovernanceInstruction(vaa);
// Couldn't get the oldImplementation address.
expectEvent(receipt, "ContractUpgraded", {
newImplementation: newImplementation.address,
});
expectEvent(receipt, "Upgraded", {
implementation: newImplementation.address,
});
});
it("Upgrading the contract to a non-pyth contract won't work", async function () {
const newImplementation = await MockUpgradeableProxy.new();
const data = new governance.EvmUpgradeContract(
"ethereum",
newImplementation.address.replace("0x", "")
).encode();
const vaa = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
// Calling a non-existing method will cause a revert with no explanation.
await expectRevert(
this.pythProxy.executeGovernanceInstruction(vaa),
"revert"
);
});
it("Transferring governance data source should work", async function () {
const newEmitterAddress =
"0x0000000000000000000000000000000000000000000000000000000000001111";
const newEmitterChain = governance.CHAINS.acala;
const claimInstructionData =
new governance.RequestGovernanceDataSourceTransfer("unset", 1).encode();
const claimVaaHexString = await createVAAFromUint8Array(
claimInstructionData,
newEmitterChain,
newEmitterAddress,
1
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(claimVaaHexString),
"InvalidGovernanceDataSource"
);
const claimVaa = Buffer.from(claimVaaHexString.substring(2), "hex");
const data = new governance.AuthorizeGovernanceDataSourceTransfer(
"unset",
claimVaa
).encode();
const vaa = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
const oldGovernanceDataSource = await this.pythProxy.governanceDataSource();
const receipt = await this.pythProxy.executeGovernanceInstruction(vaa);
const newGovernanceDataSource = await this.pythProxy.governanceDataSource();
expectEvent(receipt, "GovernanceDataSourceSet", {
oldDataSource: oldGovernanceDataSource,
newDataSource: newGovernanceDataSource,
});
expect(newGovernanceDataSource.chainId).equal(newEmitterChain.toString());
expect(newGovernanceDataSource.emitterAddress).equal(newEmitterAddress);
// Verifies the data source has changed.
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(vaa),
"InvalidGovernanceDataSource"
);
// Make sure a claim vaa does not get executed
const claimLonely = new governance.RequestGovernanceDataSourceTransfer(
"unset",
2
).encode();
const claimLonelyVaa = await createVAAFromUint8Array(
claimLonely,
newEmitterChain,
newEmitterAddress,
2
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(claimLonelyVaa),
"InvalidGovernanceMessage"
);
// 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.RequestGovernanceDataSourceTransfer(
"unset",
1 // The same governance data source index => Should fail
).encode();
const transferBackClaimVaaHexStringWrong = await createVAAFromUint8Array(
transferBackClaimInstructionDataWrong,
testGovernanceChainId,
testGovernanceEmitter,
2
);
const transferBackClaimVaaWrong = Buffer.from(
transferBackClaimVaaHexStringWrong.substring(2),
"hex"
);
const transferBackDataWrong =
new governance.AuthorizeGovernanceDataSourceTransfer(
"unset",
transferBackClaimVaaWrong
).encode();
const transferBackVaaWrong = await createVAAFromUint8Array(
transferBackDataWrong,
newEmitterChain,
newEmitterAddress,
2
);
await expectRevertCustomError(
this.pythProxy.executeGovernanceInstruction(transferBackVaaWrong),
"OldGovernanceMessage"
);
});
it("Setting data sources should work", async function () {
const data = new governance.SetDataSources("ethereum", [
{
emitterChain: governance.CHAINS.acala,
emitterAddress:
"0000000000000000000000000000000000000000000000000000000000001111",
},
]).encode();
const vaa = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
const oldDataSources = await this.pythProxy.validDataSources();
const receipt = await this.pythProxy.executeGovernanceInstruction(vaa);
expectEvent(receipt, "DataSourcesSet", {
oldDataSources: oldDataSources,
newDataSources: await this.pythProxy.validDataSources(),
});
assert.isTrue(
await this.pythProxy.isValidDataSource(
governance.CHAINS.acala,
"0x0000000000000000000000000000000000000000000000000000000000001111"
)
);
assert.isFalse(
await this.pythProxy.isValidDataSource(
testPyth2WormholeChainId,
testPyth2WormholeEmitter
)
);
// TODO: try to publish prices
});
it("Setting fee should work", async function () {
const data = new governance.SetFee(
"ethereum",
BigInt(5),
BigInt(3) // 5*10**3 = 5000
).encode();
const vaa = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
const oldFee = await this.pythProxy.singleUpdateFeeInWei();
const receipt = await this.pythProxy.executeGovernanceInstruction(vaa);
expectEvent(receipt, "FeeSet", {
oldFee: oldFee,
newFee: await this.pythProxy.singleUpdateFeeInWei(),
});
assert.equal(await this.pythProxy.singleUpdateFeeInWei(), "5000");
// TODO: check that fee is applied
});
it("Setting valid period should work", async function () {
const data = new governance.SetValidPeriod("ethereum", BigInt(0)).encode();
const vaa = await createVAAFromUint8Array(
data,
testGovernanceChainId,
testGovernanceEmitter,
1
);
const oldValidPeriod = await this.pythProxy.validTimePeriodSeconds();
const receipt = await this.pythProxy.executeGovernanceInstruction(vaa);
expectEvent(receipt, "ValidPeriodSet", {
oldValidPeriod: oldValidPeriod,
newValidPeriod: await this.pythProxy.validTimePeriodSeconds(),
});
assert.equal(await this.pythProxy.validTimePeriodSeconds(), "0");
// The behaviour of valid time period is extensively tested before,
// 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.EvmSetWormholeAddress(
"ethereum",
newWormhole.address.replace("0x", "")
).encode();
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.EvmSetWormholeAddress(
"ethereum",
newWormholeReceiver.address.replace("0x", "")
).encode();
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.EvmSetWormholeAddress(
"ethereum",
newWormhole.address.replace("0x", "")
).encode();
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 () {
const contractVersion = await this.pythProxy.version();
const { version } = require("../package.json");
expect(contractVersion).equal(version);
});
});
const signAndEncodeVM = async function (
timestamp,
nonce,
emitterChainId,
emitterAddress,
sequence,
data,
signers,
guardianSetIndex,
consistencyLevel
) {
const body = [
web3.eth.abi.encodeParameter("uint32", timestamp).substring(2 + (64 - 8)),
web3.eth.abi.encodeParameter("uint32", nonce).substring(2 + (64 - 8)),
web3.eth.abi
.encodeParameter("uint16", emitterChainId)
.substring(2 + (64 - 4)),
web3.eth.abi.encodeParameter("bytes32", emitterAddress).substring(2),
web3.eth.abi.encodeParameter("uint64", sequence).substring(2 + (64 - 16)),
web3.eth.abi
.encodeParameter("uint8", consistencyLevel)
.substring(2 + (64 - 2)),
data.substr(2),
];
const hash = web3.utils.soliditySha3(
web3.utils.soliditySha3("0x" + body.join(""))
);
let signatures = "";
for (let i in signers) {
const ec = new elliptic.ec("secp256k1");
const key = ec.keyFromPrivate(signers[i]);
const signature = key.sign(hash.substr(2), { canonical: true });
const packSig = [
web3.eth.abi.encodeParameter("uint8", i).substring(2 + (64 - 2)),
zeroPadBytes(signature.r.toString(16), 32),
zeroPadBytes(signature.s.toString(16), 32),
web3.eth.abi
.encodeParameter("uint8", signature.recoveryParam)
.substr(2 + (64 - 2)),
];
signatures += packSig.join("");
}
const vm = [
web3.eth.abi.encodeParameter("uint8", 1).substring(2 + (64 - 2)),
web3.eth.abi
.encodeParameter("uint32", guardianSetIndex)
.substring(2 + (64 - 8)),
web3.eth.abi
.encodeParameter("uint8", signers.length)
.substring(2 + (64 - 2)),
signatures,
body.join(""),
].join("");
return vm;
};
function zeroPadBytes(value, length) {
while (value.length < 2 * length) {
value = "0" + value;
}
return value;
}
async function createVAAFromUint8Array(
dataBuffer,
emitterChainId,
emitterAddress,
sequence
) {
const dataHex = "0x" + dataBuffer.toString("hex");
return (
"0x" +
(await signAndEncodeVM(
0,
0,
emitterChainId.toString(),
emitterAddress,
sequence,
dataHex,
[testSigner1PK],
0,
0
))
);
}
// There is no way to check event with given args has not emitted with expectEvent
// or how many times an event was emitted. This function is implemented to count
// the matching events and is used for the mentioned purposes.
function getNumMatchingEvents(receipt, eventName, args) {
let matchCnt = 0;
for (let log of receipt.logs) {
if (log.event === eventName) {
let match = true;
for (let argKey in args) {
if (log.args[argKey].toString() !== args[argKey].toString()) {
match = false;
break;
}
}
if (match) {
matchCnt++;
}
}
}
return matchCnt;
}
function expectEventNotEmittedWithArgs(receipt, eventName, args) {
const matches = getNumMatchingEvents(receipt, eventName, args);
assert(
matches === 0,
`Expected no matching emitted event. But found ${matches}.`
);
}
function expectEventMultipleTimes(receipt, eventName, args, cnt) {
const matches = getNumMatchingEvents(receipt, eventName, args);
assert(matches === cnt, `Expected ${cnt} event matches, found ${matches}.`);
}
async function expectRevertCustomError(promise, reason) {
try {
await promise;
expect.fail("Expected promise to throw but it didn't");
} catch (revert) {
if (reason) {
const reasonId = web3.utils.keccak256(reason + "()").substr(0, 10);
expect(
JSON.stringify(revert),
`Expected custom error ${reason} (${reasonId})`
).to.include(reasonId);
}
}
}