311 lines
11 KiB
Solidity
311 lines
11 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 "@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);
|
|
}
|
|
}
|