// 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 "@pythnetwork/pyth-sdk-solidity/PythUtils.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 feed messages and VAAs for them 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 ); } 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 PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { function testConvertToUnit() public { // Price can't be negative vm.expectRevert(); PythUtils.convertToUint(-100, -5, 18); // Exponent can't be positive vm.expectRevert(); PythUtils.convertToUint(100, 5, 18); // Price with 18 decimals and exponent -5 assertEq( PythUtils.convertToUint(100, -5, 18), 1000000000000000 // 100 * 10^13 ); // Price with 9 decimals and exponent -2 assertEq( PythUtils.convertToUint(100, -2, 9), 1000000000 // 100 * 10^7 ); // Price with 4 decimals and exponent -5 assertEq(PythUtils.convertToUint(100, -5, 4), 10); // Price with 5 decimals and exponent -2 // @note: We will lose precision here as price is // 0.00001 and we are targetDecimals is 2. assertEq(PythUtils.convertToUint(100, -5, 2), 0); } }