|
| 1 | +// SPDX-License-Identifier: Apache 2 |
| 2 | + |
| 3 | +pragma solidity ^0.8.0; |
| 4 | + |
| 5 | +import "../../contracts/pyth/PythUpgradable.sol"; |
| 6 | +import "../../contracts/pyth/PythInternalStructs.sol"; |
| 7 | +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; |
| 8 | +import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; |
| 9 | +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; |
| 10 | + |
| 11 | + |
| 12 | +import "forge-std/Test.sol"; |
| 13 | +import "./WormholeTestUtils.t.sol"; |
| 14 | + |
| 15 | +abstract contract PythTestUtils is Test, WormholeTestUtils { |
| 16 | + uint16 constant SOURCE_EMITTER_CHAIN_ID = 0x1; |
| 17 | + bytes32 constant SOURCE_EMITTER_ADDRESS = 0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b; |
| 18 | + |
| 19 | + uint16 constant GOVERNANCE_EMITTER_CHAIN_ID = 0x1; |
| 20 | + bytes32 constant GOVERNANCE_EMITTER_ADDRESS = 0x0000000000000000000000000000000000000000000000000000000000000011; |
| 21 | + |
| 22 | + function setUpPyth(address wormhole) public returns (address) { |
| 23 | + PythUpgradable implementation = new PythUpgradable(); |
| 24 | + ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), new bytes(0)); |
| 25 | + PythUpgradable pyth = PythUpgradable(address(proxy)); |
| 26 | + pyth.initialize( |
| 27 | + wormhole, |
| 28 | + SOURCE_EMITTER_CHAIN_ID, |
| 29 | + SOURCE_EMITTER_ADDRESS |
| 30 | + ); |
| 31 | + |
| 32 | + // TODO: All the logic below should be moved to the initializer |
| 33 | + pyth.addDataSource( |
| 34 | + SOURCE_EMITTER_CHAIN_ID, |
| 35 | + SOURCE_EMITTER_ADDRESS |
| 36 | + ); |
| 37 | + |
| 38 | + pyth.updateSingleUpdateFeeInWei( |
| 39 | + 1 |
| 40 | + ); |
| 41 | + |
| 42 | + pyth.updateValidTimePeriodSeconds( |
| 43 | + 60 |
| 44 | + ); |
| 45 | + |
| 46 | + pyth.updateGovernanceDataSource( |
| 47 | + GOVERNANCE_EMITTER_CHAIN_ID, |
| 48 | + GOVERNANCE_EMITTER_ADDRESS, |
| 49 | + 0 |
| 50 | + ); |
| 51 | + |
| 52 | + return address(pyth); |
| 53 | + } |
| 54 | + |
| 55 | + // Generates byte-encoded payload for the given prices. It sets the emaPrice the same |
| 56 | + // as the given price. You can use this to mock wormhole call using `vm.mockCall` and |
| 57 | + // return a VM struct with this payload. |
| 58 | + // You can use generatePriceFeedUpdateVAA to generate a VAA for a price update. |
| 59 | + function generatePriceFeedUpdatePayload( |
| 60 | + bytes32[] memory priceIds, |
| 61 | + PythStructs.Price[] memory prices |
| 62 | + ) public returns (bytes memory payload) { |
| 63 | + assertEq(priceIds.length, prices.length); |
| 64 | + |
| 65 | + bytes memory attestations = new bytes(0); |
| 66 | + |
| 67 | + for (uint i = 0; i < prices.length; ++i) { |
| 68 | + // encodePacked uses padding for arrays and we don't want it, so we manually concat them. |
| 69 | + attestations = abi.encodePacked( |
| 70 | + attestations, |
| 71 | + priceIds[i], // Product ID, we use the same price Id. This field is not used. |
| 72 | + priceIds[i], // Price ID, |
| 73 | + prices[i].price, // Price |
| 74 | + prices[i].conf, // Confidence |
| 75 | + prices[i].expo, // Exponent |
| 76 | + prices[i].price, // EMA price |
| 77 | + prices[i].conf // EMA confidence |
| 78 | + ); |
| 79 | + |
| 80 | + // Breaking this in two encodePackes because of the limited EVM stack. |
| 81 | + attestations = abi.encodePacked( |
| 82 | + attestations, |
| 83 | + uint8(PythInternalStructs.PriceAttestationStatus.TRADING), |
| 84 | + uint32(5), // Number of publishers. This field is not used. |
| 85 | + uint32(10), // Maximum number of publishers. This field is not used. |
| 86 | + uint64(prices[i].publishTime), // Attestation time. This field is not used. |
| 87 | + uint64(prices[i].publishTime), // Publish time. |
| 88 | + // Previous values are unused as status is trading. We use the same value |
| 89 | + // to make sure the test is irrelevant of the logic of which price is chosen. |
| 90 | + uint64(prices[i].publishTime), // Previous publish time. |
| 91 | + prices[i].price, // Previous price |
| 92 | + prices[i].conf // Previous confidence |
| 93 | + ); |
| 94 | + } |
| 95 | + |
| 96 | + payload = abi.encodePacked( |
| 97 | + uint32(0x50325748), // Magic |
| 98 | + uint16(3), // Major version |
| 99 | + uint16(0), // Minor version |
| 100 | + uint16(1), // Header size of 1 byte as it only contains payloadId |
| 101 | + uint8(2), // Payload ID 2 means it's a batch price attestation |
| 102 | + uint16(prices.length), // Number of attestations |
| 103 | + uint16(attestations.length / prices.length), // Size of a single price attestation. |
| 104 | + attestations |
| 105 | + ); |
| 106 | + } |
| 107 | + |
| 108 | + // Generates a VAA for the given prices. |
| 109 | + // This method calls generatePriceFeedUpdatePayload and then creates a VAA with it. |
| 110 | + // The VAAs generated from this method use block timestamp as their timestamp. |
| 111 | + function generatePriceFeedUpdateVAA( |
| 112 | + bytes32[] memory priceIds, |
| 113 | + PythStructs.Price[] memory prices, |
| 114 | + uint64 sequence, |
| 115 | + uint8 numSigners |
| 116 | + ) public returns (bytes memory vaa) { |
| 117 | + bytes memory payload = generatePriceFeedUpdatePayload( |
| 118 | + priceIds, |
| 119 | + prices |
| 120 | + ); |
| 121 | + |
| 122 | + vaa = generateVaa( |
| 123 | + uint32(block.timestamp), |
| 124 | + SOURCE_EMITTER_CHAIN_ID, |
| 125 | + SOURCE_EMITTER_ADDRESS, |
| 126 | + sequence, |
| 127 | + payload, |
| 128 | + numSigners |
| 129 | + ); |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +contract PythTestUtilsTest is Test, WormholeTestUtils, PythTestUtils { |
| 134 | + // TODO: It is better to have a PythEvents contract that be extendable. |
| 135 | + event PriceFeedUpdate(bytes32 indexed id, bool indexed fresh, uint16 chainId, uint64 sequenceNumber, uint lastPublishTime, uint publishTime, int64 price, uint64 conf); |
| 136 | + |
| 137 | + function testGeneratePriceFeedUpdateVAAWorks() public { |
| 138 | + IPyth pyth = IPyth(setUpPyth(setUpWormhole( |
| 139 | + 1 // Number of guardians |
| 140 | + ))); |
| 141 | + |
| 142 | + bytes32[] memory priceIds = new bytes32[](1); |
| 143 | + priceIds[0] = 0x0000000000000000000000000000000000000000000000000000000000000222; |
| 144 | + |
| 145 | + PythStructs.Price[] memory prices = new PythStructs.Price[](1); |
| 146 | + prices[0] = PythStructs.Price( |
| 147 | + 100, // Price |
| 148 | + 10, // Confidence |
| 149 | + -5, // Exponent |
| 150 | + 1 // Publish time |
| 151 | + ); |
| 152 | + |
| 153 | + bytes memory vaa = generatePriceFeedUpdateVAA( |
| 154 | + priceIds, |
| 155 | + prices, |
| 156 | + 1, // Sequence |
| 157 | + 1 // No. Signers |
| 158 | + ); |
| 159 | + |
| 160 | + bytes[] memory updateData = new bytes[](1); |
| 161 | + updateData[0] = vaa; |
| 162 | + |
| 163 | + uint updateFee = pyth.getUpdateFee(updateData); |
| 164 | + |
| 165 | + vm.expectEmit(true, true, false, true); |
| 166 | + emit PriceFeedUpdate(priceIds[0], true, SOURCE_EMITTER_CHAIN_ID, 1, 0, 1, 100, 10); |
| 167 | + |
| 168 | + pyth.updatePriceFeeds{value: updateFee}(updateData); |
| 169 | + } |
| 170 | +} |
0 commit comments