604 lines
20 KiB
Solidity
604 lines
20 KiB
Solidity
// SPDX-License-Identifier: Apache 2
|
|
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
|
import "forge-std/Test.sol";
|
|
|
|
import "../contracts/libraries/external/UnsafeBytesLib.sol";
|
|
import "@pythnetwork/pyth-sdk-solidity/AbstractPyth.sol";
|
|
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
|
|
import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
|
|
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
|
|
import "../contracts/pyth/PythInternalStructs.sol";
|
|
import "./utils/WormholeTestUtils.t.sol";
|
|
import "./utils/PythTestUtils.t.sol";
|
|
import "./utils/RandTestUtils.t.sol";
|
|
|
|
// Experiments to measure the gas usage of different ways of verifying prices in the EVM contract.
|
|
contract VerificationExperiments is
|
|
Test,
|
|
WormholeTestUtils,
|
|
PythTestUtils,
|
|
RandTestUtils
|
|
{
|
|
// 19, current mainnet number of guardians, is used to have gas estimates
|
|
// close to our mainnet transactions.
|
|
uint8 constant NUM_GUARDIANS = 19;
|
|
// 2/3 of the guardians should sign a message for a VAA which is 13 out of 19 guardians.
|
|
// It is possible to have more signers but the median seems to be 13.
|
|
uint8 constant NUM_GUARDIAN_SIGNERS = 13;
|
|
|
|
// We use 5 prices to form a batch of 5 prices, close to our mainnet transactions.
|
|
uint8 constant NUM_PRICES = 5;
|
|
|
|
// Private key for the threshold signature
|
|
uint256 THRESHOLD_KEY = 1234;
|
|
|
|
PythExperimental public pyth;
|
|
|
|
bytes32[] priceIds;
|
|
|
|
// Cached prices are populated in the setUp
|
|
PythStructs.Price[] cachedPrices;
|
|
bytes[] cachedPricesUpdateData;
|
|
uint cachedPricesUpdateFee;
|
|
uint64[] cachedPricesPublishTimes;
|
|
|
|
// Fresh prices are different prices that can be used
|
|
// as a fresh price to update the prices
|
|
PythStructs.Price[] freshPrices;
|
|
bytes[] freshPricesUpdateData;
|
|
uint freshPricesUpdateFee;
|
|
uint64[] freshPricesPublishTimes;
|
|
|
|
WormholeMerkleUpdate whMerkleUpdateDepth0;
|
|
WormholeMerkleUpdate whMerkleUpdateDepth1;
|
|
WormholeMerkleUpdate whMerkleUpdateDepth8;
|
|
|
|
ThresholdMerkleUpdate thresholdMerkleUpdateDepth0;
|
|
ThresholdMerkleUpdate thresholdMerkleUpdateDepth1;
|
|
ThresholdMerkleUpdate thresholdMerkleUpdateDepth8;
|
|
|
|
ThresholdUpdate thresholdUpdate;
|
|
|
|
bytes nativeUpdate;
|
|
|
|
uint64 sequence;
|
|
|
|
function setUp() public {
|
|
address payable wormhole = payable(
|
|
setUpWormholeReceiver(NUM_GUARDIANS)
|
|
);
|
|
|
|
// Deploy experimental contract
|
|
PythExperimental implementation = new PythExperimental();
|
|
ERC1967Proxy proxy = new ERC1967Proxy(
|
|
address(implementation),
|
|
new bytes(0)
|
|
);
|
|
pyth = PythExperimental(address(proxy));
|
|
|
|
uint16[] memory emitterChainIds = new uint16[](1);
|
|
emitterChainIds[0] = PythTestUtils.SOURCE_EMITTER_CHAIN_ID;
|
|
|
|
bytes32[] memory emitterAddresses = new bytes32[](1);
|
|
emitterAddresses[0] = PythTestUtils.SOURCE_EMITTER_ADDRESS;
|
|
pyth.initialize(
|
|
wormhole,
|
|
vm.addr(THRESHOLD_KEY),
|
|
emitterChainIds,
|
|
emitterAddresses,
|
|
PythTestUtils.GOVERNANCE_EMITTER_CHAIN_ID,
|
|
PythTestUtils.GOVERNANCE_EMITTER_ADDRESS,
|
|
0, // Initial governance sequence
|
|
60, // Valid time period in seconds
|
|
1 // single update fee in wei
|
|
);
|
|
|
|
priceIds = new bytes32[](NUM_PRICES);
|
|
priceIds[0] = bytes32(
|
|
0x1000000000000000000000000000000000000000000000000000000000000f00
|
|
);
|
|
for (uint i = 1; i < NUM_PRICES; ++i) {
|
|
priceIds[i] = bytes32(uint256(priceIds[i - 1]) + 1);
|
|
}
|
|
|
|
setRandSeed(12345);
|
|
for (uint i = 0; i < NUM_PRICES; ++i) {
|
|
uint64 publishTime = uint64(getRandUint() % 10);
|
|
|
|
cachedPrices.push(
|
|
PythStructs.Price(
|
|
int64(uint64(getRandUint() % 1000)), // Price
|
|
uint64(getRandUint() % 100), // Confidence
|
|
-5, // Expo
|
|
publishTime
|
|
)
|
|
);
|
|
cachedPricesPublishTimes.push(publishTime);
|
|
|
|
publishTime += uint64(getRandUint() % 10);
|
|
freshPrices.push(
|
|
PythStructs.Price(
|
|
int64(uint64(getRandUint() % 1000)), // Price
|
|
uint64(getRandUint() % 100), // Confidence
|
|
-5, // Expo
|
|
publishTime
|
|
)
|
|
);
|
|
freshPricesPublishTimes.push(publishTime);
|
|
}
|
|
|
|
// Populate the contract with the initial prices
|
|
(
|
|
cachedPricesUpdateData,
|
|
cachedPricesUpdateFee
|
|
) = generateWormholeUpdateDataAndFee(cachedPrices);
|
|
|
|
pyth.updatePriceFeeds{value: cachedPricesUpdateFee}(
|
|
cachedPricesUpdateData
|
|
);
|
|
|
|
(
|
|
freshPricesUpdateData,
|
|
freshPricesUpdateFee
|
|
) = generateWormholeUpdateDataAndFee(freshPrices);
|
|
|
|
// Generate the update payloads for the various verification systems
|
|
|
|
whMerkleUpdateDepth0 = generateWhMerkleUpdate(
|
|
priceIds[0],
|
|
freshPrices[0],
|
|
0
|
|
);
|
|
whMerkleUpdateDepth1 = generateWhMerkleUpdate(
|
|
priceIds[0],
|
|
freshPrices[0],
|
|
1
|
|
);
|
|
whMerkleUpdateDepth8 = generateWhMerkleUpdate(
|
|
priceIds[0],
|
|
freshPrices[0],
|
|
8
|
|
);
|
|
|
|
thresholdMerkleUpdateDepth0 = generateThresholdMerkleUpdate(
|
|
priceIds[0],
|
|
freshPrices[0],
|
|
0
|
|
);
|
|
thresholdMerkleUpdateDepth1 = generateThresholdMerkleUpdate(
|
|
priceIds[0],
|
|
freshPrices[0],
|
|
1
|
|
);
|
|
thresholdMerkleUpdateDepth8 = generateThresholdMerkleUpdate(
|
|
priceIds[0],
|
|
freshPrices[0],
|
|
8
|
|
);
|
|
|
|
thresholdUpdate = generateThresholdUpdate(priceIds[0], freshPrices[0]);
|
|
|
|
nativeUpdate = generateAttestationPayload(priceIds[0], freshPrices[0]);
|
|
}
|
|
|
|
// Get the payload for a wormhole batch price update
|
|
function generateWormholeUpdateDataAndFee(
|
|
PythStructs.Price[] memory prices
|
|
) internal returns (bytes[] memory updateData, uint updateFee) {
|
|
bytes memory vaa = generateWhBatchUpdate(
|
|
pricesToPriceAttestations(priceIds, prices),
|
|
sequence,
|
|
NUM_GUARDIAN_SIGNERS
|
|
);
|
|
|
|
++sequence;
|
|
|
|
updateData = new bytes[](1);
|
|
updateData[0] = vaa;
|
|
|
|
updateFee = pyth.getUpdateFee(updateData);
|
|
}
|
|
|
|
// Helper function to serialize a single price update to bytes.
|
|
// Returns a serialized PriceAttestation.
|
|
function generateAttestationPayload(
|
|
bytes32 priceId,
|
|
PythStructs.Price memory price
|
|
) internal returns (bytes memory data) {
|
|
bytes32[] memory attestationPriceIds = new bytes32[](1);
|
|
attestationPriceIds[0] = priceId;
|
|
PythStructs.Price[] memory prices = new PythStructs.Price[](1);
|
|
prices[0] = price;
|
|
PriceAttestation[] memory attestation = pricesToPriceAttestations(
|
|
attestationPriceIds,
|
|
prices
|
|
);
|
|
data = generatePriceFeedUpdatePayload(attestation);
|
|
|
|
return data;
|
|
}
|
|
|
|
// Helper function to generate a merkle proof of the given depth for the given price.
|
|
// Note: this function assumes that the data is on the leftmost branch of the tree and
|
|
// mocks the rest of the nodes (as the hash of the depth).
|
|
function generateMerkleProof(
|
|
bytes32 priceId,
|
|
PythStructs.Price memory price,
|
|
uint depth
|
|
)
|
|
internal
|
|
returns (bytes32 root, bytes memory data, bytes32[] memory proof)
|
|
{
|
|
data = generateAttestationPayload(priceId, price);
|
|
|
|
bytes32 curNodeHash = keccak256(data);
|
|
proof = new bytes32[](depth);
|
|
for (uint i = 0; i < depth; ) {
|
|
// pretend the ith sibling is just i
|
|
proof[i] = keccak256(abi.encode(i));
|
|
curNodeHash = keccak256(abi.encode(curNodeHash, proof[i]));
|
|
|
|
unchecked {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
root = curNodeHash;
|
|
|
|
return (root, data, proof);
|
|
}
|
|
|
|
// Generate a wormhole-attested merkle proof with the given depth.
|
|
function generateWhMerkleUpdate(
|
|
bytes32 priceId,
|
|
PythStructs.Price memory price,
|
|
uint depth
|
|
) internal returns (WormholeMerkleUpdate memory update) {
|
|
(
|
|
bytes32 root,
|
|
bytes memory data,
|
|
bytes32[] memory proof
|
|
) = generateMerkleProof(priceId, price, depth);
|
|
|
|
bytes memory rootVaa = generateVaa(
|
|
uint32(block.timestamp),
|
|
PythTestUtils.SOURCE_EMITTER_CHAIN_ID,
|
|
PythTestUtils.SOURCE_EMITTER_ADDRESS,
|
|
sequence,
|
|
bytes.concat(root),
|
|
NUM_GUARDIAN_SIGNERS
|
|
);
|
|
|
|
++sequence;
|
|
|
|
return WormholeMerkleUpdate(rootVaa, data, proof);
|
|
}
|
|
|
|
// Generate a threshold-signed merkle proof with the given depth.
|
|
function generateThresholdMerkleUpdate(
|
|
bytes32 priceId,
|
|
PythStructs.Price memory price,
|
|
uint depth
|
|
) internal returns (ThresholdMerkleUpdate memory update) {
|
|
(
|
|
bytes32 root,
|
|
bytes memory data,
|
|
bytes32[] memory proof
|
|
) = generateMerkleProof(priceId, price, depth);
|
|
|
|
data = generateAttestationPayload(priceId, price);
|
|
bytes32 hash = keccak256(data);
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(THRESHOLD_KEY, root);
|
|
bytes memory signature = abi.encodePacked(r, s, v - 27);
|
|
|
|
return ThresholdMerkleUpdate(signature, root, data, proof);
|
|
}
|
|
|
|
// Generate a threshold-signed price attestation.
|
|
function generateThresholdUpdate(
|
|
bytes32 priceId,
|
|
PythStructs.Price memory price
|
|
) internal returns (ThresholdUpdate memory update) {
|
|
bytes memory data = generateAttestationPayload(priceId, price);
|
|
bytes32 hash = keccak256(data);
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(THRESHOLD_KEY, hash);
|
|
bytes memory signature = abi.encodePacked(r, s, v - 27);
|
|
|
|
return ThresholdUpdate(signature, data);
|
|
}
|
|
|
|
function testWhBatchUpdate() public {
|
|
pyth.updatePriceFeeds{value: freshPricesUpdateFee}(
|
|
freshPricesUpdateData
|
|
);
|
|
}
|
|
|
|
function testUpdateWhMerkleProofDepth0() public {
|
|
pyth.updatePriceFeedsWhMerkle(
|
|
whMerkleUpdateDepth0.rootVaa,
|
|
whMerkleUpdateDepth0.data,
|
|
whMerkleUpdateDepth0.proof
|
|
);
|
|
}
|
|
|
|
function testUpdateWhMerkleProofDepth1() public {
|
|
pyth.updatePriceFeedsWhMerkle(
|
|
whMerkleUpdateDepth1.rootVaa,
|
|
whMerkleUpdateDepth1.data,
|
|
whMerkleUpdateDepth1.proof
|
|
);
|
|
}
|
|
|
|
function testUpdateWhMerkleProofDepth8() public {
|
|
pyth.updatePriceFeedsWhMerkle(
|
|
whMerkleUpdateDepth8.rootVaa,
|
|
whMerkleUpdateDepth8.data,
|
|
whMerkleUpdateDepth8.proof
|
|
);
|
|
}
|
|
|
|
function testUpdateThresholdMerkleProofDepth0() public {
|
|
pyth.updatePriceFeedsThresholdMerkle(
|
|
thresholdMerkleUpdateDepth0.rootSignature,
|
|
thresholdMerkleUpdateDepth0.rootHash,
|
|
thresholdMerkleUpdateDepth0.data,
|
|
thresholdMerkleUpdateDepth0.proof
|
|
);
|
|
}
|
|
|
|
function testUpdateThresholdMerkleProofDepth1() public {
|
|
pyth.updatePriceFeedsThresholdMerkle(
|
|
thresholdMerkleUpdateDepth1.rootSignature,
|
|
thresholdMerkleUpdateDepth1.rootHash,
|
|
thresholdMerkleUpdateDepth1.data,
|
|
thresholdMerkleUpdateDepth1.proof
|
|
);
|
|
}
|
|
|
|
function testUpdateThresholdMerkleProofDepth8() public {
|
|
pyth.updatePriceFeedsThresholdMerkle(
|
|
thresholdMerkleUpdateDepth8.rootSignature,
|
|
thresholdMerkleUpdateDepth8.rootHash,
|
|
thresholdMerkleUpdateDepth8.data,
|
|
thresholdMerkleUpdateDepth8.proof
|
|
);
|
|
}
|
|
|
|
function testUpdateThreshold() public {
|
|
pyth.updatePriceFeedsThreshold(
|
|
thresholdUpdate.signature,
|
|
thresholdUpdate.data
|
|
);
|
|
}
|
|
|
|
function testUpdateNative() public {
|
|
pyth.updatePriceFeedsNative(nativeUpdate);
|
|
}
|
|
}
|
|
|
|
// Pyth contract extended with methods for other verification systems (merkle proofs / threshold signatures)
|
|
contract PythExperimental is Pyth {
|
|
address thresholdPublicKey;
|
|
|
|
function initialize(
|
|
address wormhole,
|
|
address thresholdPublicKeyArg,
|
|
uint16[] calldata dataSourceEmitterChainIds,
|
|
bytes32[] calldata dataSourceEmitterAddresses,
|
|
uint16 governanceEmitterChainId,
|
|
bytes32 governanceEmitterAddress,
|
|
uint64 governanceInitialSequence,
|
|
uint validTimePeriodSeconds,
|
|
uint singleUpdateFeeInWei
|
|
) public {
|
|
thresholdPublicKey = thresholdPublicKeyArg;
|
|
|
|
Pyth._initialize(
|
|
wormhole,
|
|
dataSourceEmitterChainIds,
|
|
dataSourceEmitterAddresses,
|
|
governanceEmitterChainId,
|
|
governanceEmitterAddress,
|
|
governanceInitialSequence,
|
|
validTimePeriodSeconds,
|
|
singleUpdateFeeInWei
|
|
);
|
|
}
|
|
|
|
// Update a single price feed via a wormhole-attested merkle proof.
|
|
// data is expected to be a serialized PriceAttestation
|
|
function updatePriceFeedsWhMerkle(
|
|
bytes calldata rootVaa,
|
|
bytes memory data,
|
|
bytes32[] memory proof
|
|
) public payable {
|
|
IWormhole.VM memory vm = parseAndVerifyBatchAttestationVM(rootVaa);
|
|
assert(vm.payload.length == 32);
|
|
|
|
bytes32 expectedRoot = UnsafeBytesLib.toBytes32(vm.payload, 0);
|
|
bool validProof = isValidMerkleProof(expectedRoot, data, proof);
|
|
if (!validProof) revert PythErrors.InvalidArgument();
|
|
|
|
(
|
|
PythInternalStructs.PriceInfo memory info,
|
|
bytes32 priceId
|
|
) = parseSingleAttestationFromBatch(data, 0, data.length);
|
|
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
|
|
|
|
if (info.publishTime > latestPublishTime) {
|
|
setLatestPriceInfo(priceId, info);
|
|
emit PriceFeedUpdate(
|
|
priceId,
|
|
info.publishTime,
|
|
info.price,
|
|
info.conf
|
|
);
|
|
}
|
|
}
|
|
|
|
// Update a single price feed via a threshold-signed merkle proof.
|
|
// data is expected to be a serialized PriceAttestation
|
|
function updatePriceFeedsThresholdMerkle(
|
|
bytes memory rootSignature,
|
|
bytes32 rootHash,
|
|
bytes memory data,
|
|
bytes32[] memory proof
|
|
) public payable {
|
|
if (!verifySignature(rootHash, rootSignature, thresholdPublicKey))
|
|
revert PythErrors.InvalidArgument();
|
|
|
|
bool validProof = isValidMerkleProof(rootHash, data, proof);
|
|
if (!validProof) revert PythErrors.InvalidArgument();
|
|
|
|
(
|
|
PythInternalStructs.PriceInfo memory info,
|
|
bytes32 priceId
|
|
) = parseSingleAttestationFromBatch(data, 0, data.length);
|
|
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
|
|
|
|
if (info.publishTime > latestPublishTime) {
|
|
setLatestPriceInfo(priceId, info);
|
|
emit PriceFeedUpdate(
|
|
priceId,
|
|
info.publishTime,
|
|
info.price,
|
|
info.conf
|
|
);
|
|
}
|
|
}
|
|
|
|
// Update a single price feed via a threshold-signed price update.
|
|
// data is expected to be a serialized PriceAttestation.
|
|
function updatePriceFeedsThreshold(
|
|
bytes memory signature,
|
|
bytes memory data
|
|
) public payable {
|
|
bytes32 hash = keccak256(data);
|
|
if (!verifySignature(hash, signature, thresholdPublicKey))
|
|
revert PythErrors.InvalidArgument();
|
|
|
|
(
|
|
PythInternalStructs.PriceInfo memory info,
|
|
bytes32 priceId
|
|
) = parseSingleAttestationFromBatch(data, 0, data.length);
|
|
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
|
|
|
|
if (info.publishTime > latestPublishTime) {
|
|
setLatestPriceInfo(priceId, info);
|
|
emit PriceFeedUpdate(
|
|
priceId,
|
|
info.publishTime,
|
|
info.price,
|
|
info.conf
|
|
);
|
|
}
|
|
}
|
|
|
|
// Update a single price feed via a "native" price update (i.e., using the default ethereum tx signature for authentication).
|
|
// data is expected to be a serialized PriceAttestation.
|
|
// This function represents the lower bound on how much gas we can use.
|
|
function updatePriceFeedsNative(bytes memory data) public payable {
|
|
// TODO: this function should have a check on the sender.
|
|
// I'm assuming that check is not very expensive here.
|
|
|
|
(
|
|
PythInternalStructs.PriceInfo memory info,
|
|
bytes32 priceId
|
|
) = parseSingleAttestationFromBatch(data, 0, data.length);
|
|
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
|
|
|
|
if (info.publishTime > latestPublishTime) {
|
|
setLatestPriceInfo(priceId, info);
|
|
emit PriceFeedUpdate(
|
|
priceId,
|
|
info.publishTime,
|
|
info.price,
|
|
info.conf
|
|
);
|
|
}
|
|
}
|
|
|
|
// Verify that signature is a valid ECDSA signature of messageHash by signer.
|
|
function verifySignature(
|
|
bytes32 messageHash,
|
|
bytes memory signature,
|
|
address signer
|
|
) public pure returns (bool) {
|
|
bytes32 r;
|
|
bytes32 s;
|
|
uint8 v;
|
|
assembly {
|
|
r := mload(add(signature, 32))
|
|
s := mload(add(signature, 64))
|
|
v := byte(0, mload(add(signature, 96)))
|
|
}
|
|
if (v < 27) {
|
|
v += 27;
|
|
}
|
|
if (v != 27 && v != 28) {
|
|
return false;
|
|
}
|
|
address recoveredAddress = ecrecover(messageHash, v, r, s);
|
|
return (recoveredAddress == signer);
|
|
}
|
|
|
|
// Check that proof is a valid merkle proof of data. This function assumes that
|
|
// data is the leftmost node of the tree.
|
|
// TODO: need to encode left/right structure for proof nodes
|
|
function isValidMerkleProof(
|
|
bytes32 expectedRoot,
|
|
bytes memory data,
|
|
bytes32[] memory proof
|
|
) public returns (bool) {
|
|
bytes32 curNodeHash = keccak256(data);
|
|
for (uint i = 0; i < proof.length; ) {
|
|
curNodeHash = keccak256(abi.encode(curNodeHash, proof[i]));
|
|
unchecked {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return (expectedRoot == curNodeHash);
|
|
}
|
|
}
|
|
|
|
// A merkle tree price update delivered via wormhole.
|
|
// The update is valid if the data above hashed with the proof nodes sequentially
|
|
// equals the root hash in the wormhole VAA.
|
|
struct WormholeMerkleUpdate {
|
|
// The serialized bytes of a wormhole VAA
|
|
// The payload of this VAA is a single 32-byte root hash for the merkle tree.
|
|
bytes rootVaa;
|
|
// The serialized bytes of a PriceAttestation
|
|
bytes data;
|
|
// The chain of proof nodes.
|
|
bytes32[] proof;
|
|
}
|
|
|
|
// A merkle tree price update delivered via threshold signature.
|
|
// The update is valid if the data above hashed with the proof nodes sequentially
|
|
// equals the root hash, and the signature is valid for rootHash.
|
|
struct ThresholdMerkleUpdate {
|
|
bytes rootSignature;
|
|
bytes32 rootHash;
|
|
// The serialized bytes of a PriceAttestation
|
|
bytes data;
|
|
// The chain of proof nodes.
|
|
bytes32[] proof;
|
|
}
|
|
|
|
// A merkle tree price update delivered via threshold signature.
|
|
// The update is valid if the data above hashed with the proof nodes sequentially
|
|
// equals the root hash, and the signature is valid for rootHash.
|
|
struct ThresholdUpdate {
|
|
// Signature of the hash of the data.
|
|
bytes signature;
|
|
// The serialized bytes of a PriceAttestation
|
|
bytes data;
|
|
}
|