310 lines
10 KiB
Solidity
310 lines
10 KiB
Solidity
// SPDX-License-Identifier: Apache 2
|
|
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
|
import "forge-std/Test.sol";
|
|
|
|
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
|
|
import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
|
|
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
|
|
import "./utils/WormholeTestUtils.t.sol";
|
|
import "./utils/PythTestUtils.t.sol";
|
|
import "./utils/RandTestUtils.t.sol";
|
|
|
|
contract PythTest is Test, WormholeTestUtils, PythTestUtils {
|
|
IPyth public pyth;
|
|
|
|
// -1 is equal to 0xffffff which is the biggest uint if converted back
|
|
uint64 constant MAX_UINT64 = uint64(int64(-1));
|
|
|
|
// 2/3 of the guardians should sign a message for a VAA which is 13 out of 19 guardians.
|
|
// It is possible to have more signers but the median seems to be 13.
|
|
uint8 constant NUM_GUARDIAN_SIGNERS = 13;
|
|
|
|
// We will have less than 512 price for a foreseeable future.
|
|
uint8 constant MERKLE_TREE_DEPTH = 9;
|
|
|
|
function setUp() public {
|
|
pyth = IPyth(setUpPyth(setUpWormholeReceiver(NUM_GUARDIAN_SIGNERS)));
|
|
}
|
|
|
|
function generateRandomPriceMessages(
|
|
uint length
|
|
)
|
|
internal
|
|
returns (bytes32[] memory priceIds, PriceFeedMessage[] memory messages)
|
|
{
|
|
messages = new PriceFeedMessage[](length);
|
|
priceIds = new bytes32[](length);
|
|
|
|
for (uint i = 0; i < length; i++) {
|
|
messages[i].priceId = bytes32(i + 1); // price ids should be non-zero and unique
|
|
messages[i].price = getRandInt64();
|
|
messages[i].conf = getRandUint64();
|
|
messages[i].expo = getRandInt32();
|
|
messages[i].emaPrice = getRandInt64();
|
|
messages[i].emaConf = getRandUint64();
|
|
messages[i].publishTime = getRandUint64();
|
|
messages[i].prevPublishTime = getRandUint64();
|
|
|
|
priceIds[i] = messages[i].priceId;
|
|
}
|
|
}
|
|
|
|
// This method divides messages into a couple of batches and creates
|
|
// updateData for them. It returns the updateData and the updateFee
|
|
function createBatchedUpdateDataFromMessagesWithConfig(
|
|
PriceFeedMessage[] memory messages,
|
|
MerkleUpdateConfig memory config
|
|
) internal returns (bytes[] memory updateData, uint updateFee) {
|
|
uint batchSize = 1 + (getRandUint() % messages.length);
|
|
uint numBatches = (messages.length + batchSize - 1) / batchSize;
|
|
|
|
updateData = new bytes[](numBatches);
|
|
|
|
for (uint i = 0; i < messages.length; i += batchSize) {
|
|
uint len = batchSize;
|
|
if (messages.length - i < len) {
|
|
len = messages.length - i;
|
|
}
|
|
|
|
PriceFeedMessage[] memory batchMessages = new PriceFeedMessage[](
|
|
len
|
|
);
|
|
for (uint j = i; j < i + len; j++) {
|
|
batchMessages[j - i] = messages[j];
|
|
}
|
|
|
|
updateData[i / batchSize] = generateWhMerkleUpdateWithSource(
|
|
batchMessages,
|
|
config
|
|
);
|
|
}
|
|
|
|
updateFee = pyth.getUpdateFee(updateData);
|
|
}
|
|
|
|
function createBatchedUpdateDataFromMessages(
|
|
PriceFeedMessage[] memory messages
|
|
) internal returns (bytes[] memory updateData, uint updateFee) {
|
|
(updateData, updateFee) = createBatchedUpdateDataFromMessagesWithConfig(
|
|
messages,
|
|
MerkleUpdateConfig(
|
|
MERKLE_TREE_DEPTH,
|
|
NUM_GUARDIAN_SIGNERS,
|
|
SOURCE_EMITTER_CHAIN_ID,
|
|
SOURCE_EMITTER_ADDRESS,
|
|
false
|
|
)
|
|
);
|
|
}
|
|
|
|
/// Testing parsePriceFeedUpdates method.
|
|
function testParsePriceFeedUpdatesWorks(uint seed) public {
|
|
setRandSeed(seed);
|
|
uint numMessages = 1 + (getRandUint() % 10);
|
|
(
|
|
bytes32[] memory priceIds,
|
|
PriceFeedMessage[] memory messages
|
|
) = generateRandomPriceMessages(numMessages);
|
|
|
|
(
|
|
bytes[] memory updateData,
|
|
uint updateFee
|
|
) = createBatchedUpdateDataFromMessages(messages);
|
|
PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
|
|
value: updateFee
|
|
}(updateData, priceIds, 0, MAX_UINT64);
|
|
|
|
for (uint i = 0; i < numMessages; i++) {
|
|
assertEq(priceFeeds[i].id, priceIds[i]);
|
|
assertEq(priceFeeds[i].price.price, messages[i].price);
|
|
assertEq(priceFeeds[i].price.conf, messages[i].conf);
|
|
assertEq(priceFeeds[i].price.expo, messages[i].expo);
|
|
assertEq(priceFeeds[i].price.publishTime, messages[i].publishTime);
|
|
assertEq(priceFeeds[i].emaPrice.price, messages[i].emaPrice);
|
|
assertEq(priceFeeds[i].emaPrice.conf, messages[i].emaConf);
|
|
assertEq(priceFeeds[i].emaPrice.expo, messages[i].expo);
|
|
assertEq(
|
|
priceFeeds[i].emaPrice.publishTime,
|
|
messages[i].publishTime
|
|
);
|
|
}
|
|
}
|
|
|
|
function testParsePriceFeedUpdatesWorksWithOverlappingWithinTimeRangeUpdates()
|
|
public
|
|
{
|
|
PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
|
|
|
|
messages[0].priceId = bytes32(uint(1));
|
|
messages[0].price = 1000;
|
|
messages[0].publishTime = 10;
|
|
|
|
messages[1].priceId = bytes32(uint(1));
|
|
messages[1].price = 2000;
|
|
messages[1].publishTime = 20;
|
|
|
|
(
|
|
bytes[] memory updateData,
|
|
uint updateFee
|
|
) = createBatchedUpdateDataFromMessages(messages);
|
|
|
|
bytes32[] memory priceIds = new bytes32[](1);
|
|
priceIds[0] = bytes32(uint(1));
|
|
|
|
PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
|
|
value: updateFee
|
|
}(updateData, priceIds, 0, 20);
|
|
|
|
assertEq(priceFeeds.length, 1);
|
|
assertEq(priceFeeds[0].id, bytes32(uint(1)));
|
|
|
|
assertTrue(
|
|
(priceFeeds[0].price.price == 1000 &&
|
|
priceFeeds[0].price.publishTime == 10) ||
|
|
(priceFeeds[0].price.price == 2000 &&
|
|
priceFeeds[0].price.publishTime == 20)
|
|
);
|
|
}
|
|
|
|
function testParsePriceFeedUpdatesWorksWithOverlappingMixedTimeRangeUpdates()
|
|
public
|
|
{
|
|
PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
|
|
|
|
messages[0].priceId = bytes32(uint(1));
|
|
messages[0].price = 1000;
|
|
messages[0].publishTime = 10;
|
|
|
|
messages[1].priceId = bytes32(uint(1));
|
|
messages[1].price = 2000;
|
|
messages[1].publishTime = 20;
|
|
|
|
(
|
|
bytes[] memory updateData,
|
|
uint updateFee
|
|
) = createBatchedUpdateDataFromMessages(messages);
|
|
|
|
bytes32[] memory priceIds = new bytes32[](1);
|
|
priceIds[0] = bytes32(uint(1));
|
|
|
|
PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
|
|
value: updateFee
|
|
}(updateData, priceIds, 5, 15);
|
|
|
|
assertEq(priceFeeds.length, 1);
|
|
assertEq(priceFeeds[0].id, bytes32(uint(1)));
|
|
assertEq(priceFeeds[0].price.price, 1000);
|
|
assertEq(priceFeeds[0].price.publishTime, 10);
|
|
|
|
priceFeeds = pyth.parsePriceFeedUpdates{value: updateFee}(
|
|
updateData,
|
|
priceIds,
|
|
15,
|
|
25
|
|
);
|
|
|
|
assertEq(priceFeeds.length, 1);
|
|
assertEq(priceFeeds[0].id, bytes32(uint(1)));
|
|
assertEq(priceFeeds[0].price.price, 2000);
|
|
assertEq(priceFeeds[0].price.publishTime, 20);
|
|
}
|
|
|
|
function testParsePriceFeedUpdatesRevertsIfUpdateVAAIsInvalid(
|
|
uint seed
|
|
) public {
|
|
setRandSeed(seed);
|
|
uint numMessages = 1 + (getRandUint() % 10);
|
|
(
|
|
bytes32[] memory priceIds,
|
|
PriceFeedMessage[] memory messages
|
|
) = generateRandomPriceMessages(numMessages);
|
|
|
|
(
|
|
bytes[] memory updateData,
|
|
uint updateFee
|
|
) = createBatchedUpdateDataFromMessagesWithConfig(
|
|
messages,
|
|
MerkleUpdateConfig(
|
|
MERKLE_TREE_DEPTH,
|
|
NUM_GUARDIAN_SIGNERS,
|
|
SOURCE_EMITTER_CHAIN_ID,
|
|
SOURCE_EMITTER_ADDRESS,
|
|
true
|
|
)
|
|
);
|
|
|
|
// It might revert due to different wormhole errors
|
|
vm.expectRevert();
|
|
pyth.parsePriceFeedUpdates{value: updateFee}(
|
|
updateData,
|
|
priceIds,
|
|
0,
|
|
MAX_UINT64
|
|
);
|
|
}
|
|
|
|
function testParsePriceFeedUpdatesRevertsIfUpdateSourceChainIsInvalid()
|
|
public
|
|
{
|
|
uint numMessages = 10;
|
|
(
|
|
bytes32[] memory priceIds,
|
|
PriceFeedMessage[] memory messages
|
|
) = generateRandomPriceMessages(numMessages);
|
|
|
|
(
|
|
bytes[] memory updateData,
|
|
uint updateFee
|
|
) = createBatchedUpdateDataFromMessagesWithConfig(
|
|
messages,
|
|
MerkleUpdateConfig(
|
|
MERKLE_TREE_DEPTH,
|
|
NUM_GUARDIAN_SIGNERS,
|
|
SOURCE_EMITTER_CHAIN_ID + 1,
|
|
SOURCE_EMITTER_ADDRESS,
|
|
false
|
|
)
|
|
);
|
|
|
|
vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
|
|
pyth.parsePriceFeedUpdates{value: updateFee}(
|
|
updateData,
|
|
priceIds,
|
|
0,
|
|
MAX_UINT64
|
|
);
|
|
}
|
|
|
|
function testParsePriceFeedUpdatesRevertsIfUpdateSourceAddressIsInvalid()
|
|
public
|
|
{
|
|
uint numMessages = 10;
|
|
(
|
|
bytes32[] memory priceIds,
|
|
PriceFeedMessage[] memory messages
|
|
) = generateRandomPriceMessages(numMessages);
|
|
|
|
(bytes[] memory updateData, uint updateFee) = createBatchedUpdateDataFromMessagesWithConfig(
|
|
messages,
|
|
MerkleUpdateConfig(
|
|
MERKLE_TREE_DEPTH,
|
|
NUM_GUARDIAN_SIGNERS,
|
|
SOURCE_EMITTER_CHAIN_ID,
|
|
0x00000000000000000000000000000000000000000000000000000000000000aa, // Random wrong source address
|
|
false
|
|
)
|
|
);
|
|
|
|
vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
|
|
pyth.parsePriceFeedUpdates{value: updateFee}(
|
|
updateData,
|
|
priceIds,
|
|
0,
|
|
MAX_UINT64
|
|
);
|
|
}
|
|
}
|