// 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 { 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; } 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 generateWhMerkleUpdate( PriceFeedMessage[] memory priceFeedMessages, uint8 depth, uint8 numSigners ) internal returns (bytes memory whMerkleUpdateData) { bytes[] memory encodedPriceFeedMessages = encodePriceFeedMessages( priceFeedMessages ); (bytes20 rootDigest, bytes[] memory proofs) = MerkleTree .constructProofs(encodedPriceFeedMessages, 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, SOURCE_EMITTER_CHAIN_ID, SOURCE_EMITTER_ADDRESS, 0, wormholePayload, numSigners ); 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 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 ); } // Generates a VAA for the given attestations. // This method calls generatePriceFeedUpdatePayload and then creates a VAA with it. // The VAAs generated from this method use block timestamp as their timestamp. function generateWhBatchUpdate( PriceAttestation[] memory attestations, uint64 sequence, uint8 numSigners ) public returns (bytes memory vaa) { bytes memory payload = generatePriceFeedUpdatePayload(attestations); vaa = generateVaa( uint32(block.timestamp), SOURCE_EMITTER_CHAIN_ID, SOURCE_EMITTER_ADDRESS, sequence, payload, numSigners ); } 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; } } } contract PythTestUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { function testGenerateWhBatchUpdateWorks() public { IPyth pyth = IPyth( setUpPyth( setUpWormholeReceiver( 1 // Number of guardians ) ) ); bytes32[] memory priceIds = new bytes32[](1); priceIds[ 0 ] = 0x0000000000000000000000000000000000000000000000000000000000000222; PythStructs.Price[] memory prices = new PythStructs.Price[](1); prices[0] = PythStructs.Price( 100, // Price 10, // Confidence -5, // Exponent 1 // Publish time ); bytes memory vaa = generateWhBatchUpdate( pricesToPriceAttestations(priceIds, prices), 1, // Sequence 1 // No. Signers ); bytes[] memory updateData = new bytes[](1); updateData[0] = vaa; uint updateFee = pyth.getUpdateFee(updateData); vm.expectEmit(true, false, false, true); emit PriceFeedUpdate(priceIds[0], 1, 100, 10); pyth.updatePriceFeeds{value: updateFee}(updateData); } }