pyth-crosschain/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol

378 lines
14 KiB
Solidity

// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "../../contracts/pyth/PythUpgradable.sol";
import "../../contracts/pyth/PythInternalStructs.sol";
import "../../contracts/pyth/PythAccumulator.sol";
import "../../contracts/libraries/MerkleTree.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
import "@pythnetwork/pyth-sdk-solidity/IPythEvents.sol";
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
import "forge-std/Test.sol";
import "./WormholeTestUtils.t.sol";
import "./RandTestUtils.t.sol";
abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils {
uint16 constant SOURCE_EMITTER_CHAIN_ID = 0x1;
bytes32 constant SOURCE_EMITTER_ADDRESS =
0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b;
uint16 constant GOVERNANCE_EMITTER_CHAIN_ID = 0x1;
bytes32 constant GOVERNANCE_EMITTER_ADDRESS =
0x0000000000000000000000000000000000000000000000000000000000000011;
uint constant SINGLE_UPDATE_FEE_IN_WEI = 1;
function setUpPyth(address wormhole) public returns (address) {
PythUpgradable implementation = new PythUpgradable();
ERC1967Proxy proxy = new ERC1967Proxy(
address(implementation),
new bytes(0)
);
PythUpgradable pyth = PythUpgradable(address(proxy));
uint16[] memory emitterChainIds = new uint16[](1);
emitterChainIds[0] = SOURCE_EMITTER_CHAIN_ID;
bytes32[] memory emitterAddresses = new bytes32[](1);
emitterAddresses[0] = SOURCE_EMITTER_ADDRESS;
pyth.initialize(
wormhole,
emitterChainIds,
emitterAddresses,
GOVERNANCE_EMITTER_CHAIN_ID,
GOVERNANCE_EMITTER_ADDRESS,
0, // Initial governance sequence
60, // Valid time period in seconds
SINGLE_UPDATE_FEE_IN_WEI // single update fee in wei
);
return address(pyth);
}
function singleUpdateFeeInWei() public view returns (uint) {
return SINGLE_UPDATE_FEE_IN_WEI;
}
/// Utilities to help generating price attestations and VAAs for them
enum PriceAttestationStatus {
Unknown,
Trading
}
struct PriceAttestation {
bytes32 productId;
bytes32 priceId;
int64 price;
uint64 conf;
int32 expo;
int64 emaPrice;
uint64 emaConf;
PriceAttestationStatus status;
uint32 numPublishers;
uint32 maxNumPublishers;
uint64 attestationTime;
uint64 publishTime;
uint64 prevPublishTime;
int64 prevPrice;
uint64 prevConf;
}
struct PriceFeedMessage {
bytes32 priceId;
int64 price;
uint64 conf;
int32 expo;
uint64 publishTime;
uint64 prevPublishTime;
int64 emaPrice;
uint64 emaConf;
}
struct MerkleUpdateConfig {
uint8 depth;
uint8 numSigners;
uint16 source_chain_id;
bytes32 source_emitter_address;
bool brokenVaa;
}
function encodePriceFeedMessages(
PriceFeedMessage[] memory priceFeedMessages
) internal pure returns (bytes[] memory encodedPriceFeedMessages) {
encodedPriceFeedMessages = new bytes[](priceFeedMessages.length);
for (uint i = 0; i < priceFeedMessages.length; i++) {
encodedPriceFeedMessages[i] = abi.encodePacked(
uint8(PythAccumulator.MessageType.PriceFeed),
priceFeedMessages[i].priceId,
priceFeedMessages[i].price,
priceFeedMessages[i].conf,
priceFeedMessages[i].expo,
priceFeedMessages[i].publishTime,
priceFeedMessages[i].prevPublishTime,
priceFeedMessages[i].emaPrice,
priceFeedMessages[i].emaConf
);
}
}
function generateWhMerkleUpdateWithSource(
PriceFeedMessage[] memory priceFeedMessages,
MerkleUpdateConfig memory config
) internal returns (bytes memory whMerkleUpdateData) {
bytes[] memory encodedPriceFeedMessages = encodePriceFeedMessages(
priceFeedMessages
);
(bytes20 rootDigest, bytes[] memory proofs) = MerkleTree
.constructProofs(encodedPriceFeedMessages, config.depth);
bytes memory wormholePayload = abi.encodePacked(
uint32(0x41555756), // PythAccumulator.ACCUMULATOR_WORMHOLE_MAGIC
uint8(PythAccumulator.UpdateType.WormholeMerkle),
uint64(0), // Slot, not used in target networks
uint32(0), // Ring size, not used in target networks
rootDigest
);
bytes memory wormholeMerkleVaa = generateVaa(
0,
config.source_chain_id,
config.source_emitter_address,
0,
wormholePayload,
config.numSigners
);
if (config.brokenVaa) {
uint mutPos = getRandUint() % wormholeMerkleVaa.length;
// mutate the random position by 1 bit
wormholeMerkleVaa[mutPos] = bytes1(
uint8(wormholeMerkleVaa[mutPos]) ^ 1
);
}
whMerkleUpdateData = abi.encodePacked(
uint32(0x504e4155), // PythAccumulator.ACCUMULATOR_MAGIC
uint8(1), // major version
uint8(0), // minor version
uint8(0), // trailing header size
uint8(PythAccumulator.UpdateType.WormholeMerkle),
uint16(wormholeMerkleVaa.length),
wormholeMerkleVaa,
uint8(priceFeedMessages.length)
);
for (uint i = 0; i < priceFeedMessages.length; i++) {
whMerkleUpdateData = abi.encodePacked(
whMerkleUpdateData,
uint16(encodedPriceFeedMessages[i].length),
encodedPriceFeedMessages[i],
proofs[i]
);
}
}
function generateWhMerkleUpdate(
PriceFeedMessage[] memory priceFeedMessages,
uint8 depth,
uint8 numSigners
) internal returns (bytes memory whMerkleUpdateData) {
whMerkleUpdateData = generateWhMerkleUpdateWithSource(
priceFeedMessages,
MerkleUpdateConfig(
depth,
numSigners,
SOURCE_EMITTER_CHAIN_ID,
SOURCE_EMITTER_ADDRESS,
false
)
);
}
function generateForwardCompatibleWhMerkleUpdate(
PriceFeedMessage[] memory priceFeedMessages,
uint8 depth,
uint8 numSigners,
uint8 minorVersion,
bytes memory trailingHeaderData
) internal returns (bytes memory whMerkleUpdateData) {
bytes[] memory encodedPriceFeedMessages = encodePriceFeedMessages(
priceFeedMessages
);
(bytes20 rootDigest, bytes[] memory proofs) = MerkleTree
.constructProofs(encodedPriceFeedMessages, depth);
// refactoring some of these generateWormhole functions was necessary
// to workaround the stack too deep limit.
bytes
memory wormholeMerkleVaa = generateForwardCompatibleWormholeMerkleVaa(
rootDigest,
trailingHeaderData,
numSigners
);
{
whMerkleUpdateData = abi.encodePacked(
generateForwardCompatibleWormholeMerkleUpdateHeader(
minorVersion,
trailingHeaderData
),
uint16(wormholeMerkleVaa.length),
wormholeMerkleVaa,
uint8(priceFeedMessages.length)
);
}
for (uint i = 0; i < priceFeedMessages.length; i++) {
whMerkleUpdateData = abi.encodePacked(
whMerkleUpdateData,
uint16(encodedPriceFeedMessages[i].length),
encodedPriceFeedMessages[i],
proofs[i]
);
}
}
function generateForwardCompatibleWormholeMerkleUpdateHeader(
uint8 minorVersion,
bytes memory trailingHeaderData
) private returns (bytes memory whMerkleUpdateHeader) {
whMerkleUpdateHeader = abi.encodePacked(
uint32(0x504e4155), // PythAccumulator.ACCUMULATOR_MAGIC
uint8(1), // major version
minorVersion,
uint8(trailingHeaderData.length), // trailing header size
trailingHeaderData,
uint8(PythAccumulator.UpdateType.WormholeMerkle)
);
}
function generateForwardCompatibleWormholeMerkleVaa(
bytes20 rootDigest,
bytes memory futureData,
uint8 numSigners
) internal returns (bytes memory wormholeMerkleVaa) {
wormholeMerkleVaa = generateVaa(
0,
SOURCE_EMITTER_CHAIN_ID,
SOURCE_EMITTER_ADDRESS,
0,
abi.encodePacked(
uint32(0x41555756), // PythAccumulator.ACCUMULATOR_WORMHOLE_MAGIC
uint8(PythAccumulator.UpdateType.WormholeMerkle),
uint64(0), // Slot, not used in target networks
uint32(0), // Ring size, not used in target networks
rootDigest, // this can have bytes past this for future versions
futureData
),
numSigners
);
}
// Generates byte-encoded payload for the given price attestations. You can use this to mock wormhole
// call using `vm.mockCall` and return a VM struct with this payload.
// You can use generatePriceFeedUpdate to generate a VAA for a price update.
function generatePriceFeedUpdatePayload(
PriceAttestation[] memory attestations
) public pure returns (bytes memory payload) {
bytes memory encodedAttestations = new bytes(0);
for (uint i = 0; i < attestations.length; ++i) {
// encodePacked uses padding for arrays and we don't want it, so we manually concat them.
encodedAttestations = abi.encodePacked(
encodedAttestations,
attestations[i].productId,
attestations[i].priceId,
attestations[i].price,
attestations[i].conf,
attestations[i].expo,
attestations[i].emaPrice,
attestations[i].emaConf
);
// Breaking this in two encodePackes because of the limited EVM stack.
encodedAttestations = abi.encodePacked(
encodedAttestations,
uint8(attestations[i].status),
attestations[i].numPublishers,
attestations[i].maxNumPublishers,
attestations[i].attestationTime,
attestations[i].publishTime,
attestations[i].prevPublishTime,
attestations[i].prevPrice,
attestations[i].prevConf
);
}
payload = abi.encodePacked(
uint32(0x50325748), // Magic
uint16(3), // Major version
uint16(0), // Minor version
uint16(1), // Header size of 1 byte as it only contains payloadId
uint8(2), // Payload ID 2 means it's a batch price attestation
uint16(attestations.length), // Number of attestations
uint16(encodedAttestations.length / attestations.length), // Size of a single price attestation.
encodedAttestations
);
}
function pricesToPriceAttestations(
bytes32[] memory priceIds,
PythStructs.Price[] memory prices
) public returns (PriceAttestation[] memory attestations) {
assertEq(priceIds.length, prices.length);
attestations = new PriceAttestation[](prices.length);
for (uint i = 0; i < prices.length; ++i) {
// Product ID, we use the same price Id. This field is not used.
attestations[i].productId = priceIds[i];
attestations[i].priceId = priceIds[i];
attestations[i].price = prices[i].price;
attestations[i].conf = prices[i].conf;
attestations[i].expo = prices[i].expo;
// Same price and conf is used for emaPrice and emaConf
attestations[i].emaPrice = prices[i].price;
attestations[i].emaConf = prices[i].conf;
attestations[i].status = PriceAttestationStatus.Trading;
attestations[i].numPublishers = 5; // This field is not used
attestations[i].maxNumPublishers = 10; // This field is not used
attestations[i].attestationTime = uint64(prices[i].publishTime); // This field is not used
attestations[i].publishTime = uint64(prices[i].publishTime);
// Fields below are not used when status is Trading. just setting them to
// the same value as the prices.
attestations[i].prevPublishTime = uint64(prices[i].publishTime);
attestations[i].prevPrice = prices[i].price;
attestations[i].prevConf = prices[i].conf;
}
}
function pricesToPriceFeedMessages(
bytes32[] memory priceIds,
PythStructs.Price[] memory prices
) public returns (PriceFeedMessage[] memory priceFeedMessages) {
assertGe(priceIds.length, prices.length);
priceFeedMessages = new PriceFeedMessage[](prices.length);
for (uint i = 0; i < prices.length; ++i) {
priceFeedMessages[i].priceId = priceIds[i];
priceFeedMessages[i].price = prices[i].price;
priceFeedMessages[i].conf = prices[i].conf;
priceFeedMessages[i].expo = prices[i].expo;
priceFeedMessages[i].publishTime = uint64(prices[i].publishTime);
priceFeedMessages[i].prevPublishTime =
uint64(prices[i].publishTime) -
1;
priceFeedMessages[i].emaPrice = prices[i].price;
priceFeedMessages[i].emaConf = prices[i].conf;
}
}
}