[eth] Add benchmark tests (#368)
* Add remappings This helps vs code solidity LSP work * Remove unused wormhole contract * Format foundry config file * Fix install foundry script * Add benchmark tests and its utils
This commit is contained in:
parent
a19cd93cd3
commit
0df243ba9e
|
@ -34,3 +34,42 @@ npm run install-forge-deps
|
||||||
|
|
||||||
After installing the dependencies. Run `forge build` to build the contracts and `forge test` to
|
After installing the dependencies. Run `forge build` to build the contracts and `forge test` to
|
||||||
test the contracts using tests in `forge-test` directory.
|
test the contracts using tests in `forge-test` directory.
|
||||||
|
|
||||||
|
### Gas Benchmark
|
||||||
|
|
||||||
|
You can use foundry to run benchmark tests written in [`forge-test/GasBenchmark.t.sol`](./forge-test/GasBenchmark.t.sol). To run the tests with gas report
|
||||||
|
you can run `forge test --gas-report --match-contract GasBenchmark`. However, as there are multiple benchmarks, this might not be useful. You can run a
|
||||||
|
specific benchmark test by passing the test name using `--match-test`. A full command to run `testBenchmarkUpdatePriceFeedsFresh` benchmark test is like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
forge test --gas-report --match-contract GasBenchmark --match-test testBenchmarkUpdatePriceFeedsFresh
|
||||||
|
```
|
||||||
|
|
||||||
|
A gas report should have a couple of tables like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
╭───────────────────────────────────────────────────────────────────────────────────────────┬─────────────────┬────────┬────────┬─────────┬─────────╮
|
||||||
|
│ node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy contract ┆ ┆ ┆ ┆ ┆ │
|
||||||
|
╞═══════════════════════════════════════════════════════════════════════════════════════════╪═════════════════╪════════╪════════╪═════════╪═════════╡
|
||||||
|
│ Deployment Cost ┆ Deployment Size ┆ ┆ ┆ ┆ │
|
||||||
|
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
|
||||||
|
│ 164236 ┆ 2050 ┆ ┆ ┆ ┆ │
|
||||||
|
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
|
||||||
|
│ Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls │
|
||||||
|
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
|
||||||
|
│ ............. ┆ ..... ┆ ..... ┆ ..... ┆ ..... ┆ .. │
|
||||||
|
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
|
||||||
|
│ parseAndVerifyVM ┆ 90292 ┆ 91262 ┆ 90292 ┆ 138792 ┆ 50 │
|
||||||
|
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
|
||||||
|
│ updatePriceFeeds ┆ 187385 ┆ 206005 ┆ 187385 ┆ 1118385 ┆ 50 │
|
||||||
|
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
|
||||||
|
│ ............. ┆ ..... ┆ ..... ┆ ..... ┆ ..... ┆ ... │
|
||||||
|
╰───────────────────────────────────────────────────────────────────────────────────────────┴─────────────────┴────────┴────────┴─────────┴─────────╯
|
||||||
|
```
|
||||||
|
|
||||||
|
For most of the methods, the median gas usage is an indication of our desired gas usage. Because the calls that store something in the storage
|
||||||
|
for the first time use significantly more gas.
|
||||||
|
|
||||||
|
If you like to optimize the contract and measure the gas optimization you can get gas snapshots using `forge snapshot` and evaluate your
|
||||||
|
optimization with it. For more information, please refer to [Gas Snapshots documentation](https://book.getfoundry.sh/forge/gas-snapshots).
|
||||||
|
Once you optimized the code, please share the snapshot difference (generated using `forge snapshot --diff <old-snapshot>`) in the PR too.
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
// contracts/Implementation.sol
|
|
||||||
// SPDX-License-Identifier: Apache 2
|
|
||||||
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
import "../Implementation.sol";
|
|
||||||
|
|
||||||
contract MockImplementation is Implementation {
|
|
||||||
function initialize() initializer public {
|
|
||||||
// this function needs to be exposed for an upgrade to pass
|
|
||||||
}
|
|
||||||
|
|
||||||
function testNewImplementationActive() external pure returns (bool) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
// SPDX-License-Identifier: Apache 2
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
|
||||||
|
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
|
||||||
|
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
|
||||||
|
import "./utils/WormholeTestUtils.t.sol";
|
||||||
|
import "./utils/PythTestUtils.t.sol";
|
||||||
|
|
||||||
|
contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
uint constant BENCHMARK_ITERATIONS = 1000;
|
||||||
|
|
||||||
|
IPyth public pyth;
|
||||||
|
|
||||||
|
bytes32[] priceIds;
|
||||||
|
PythStructs.Price[] prices;
|
||||||
|
uint64 sequence;
|
||||||
|
uint randSeed;
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
pyth = IPyth(setUpPyth(setUpWormhole(NUM_GUARDIANS)));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint i = 0; i < NUM_PRICES; ++i) {
|
||||||
|
prices.push(PythStructs.Price(
|
||||||
|
int64(uint64(getRand() % 1000)), // Price
|
||||||
|
uint64(getRand() % 100), // Confidence
|
||||||
|
-5, // Expo
|
||||||
|
getRand() % 10 // publishTime
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRand() internal returns (uint val) {
|
||||||
|
++randSeed;
|
||||||
|
val = uint(keccak256(abi.encode(randSeed)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function advancePrices() internal {
|
||||||
|
for (uint i = 0; i < NUM_PRICES; ++i) {
|
||||||
|
prices[i].price = int64(uint64(getRand() % 1000));
|
||||||
|
prices[i].conf = uint64(getRand() % 100);
|
||||||
|
prices[i].publishTime += getRand() % 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUpdateDataAndFee() internal returns (bytes[] memory updateData, uint updateFee) {
|
||||||
|
bytes memory vaa = generatePriceFeedUpdateVAA(
|
||||||
|
priceIds,
|
||||||
|
prices,
|
||||||
|
sequence,
|
||||||
|
NUM_GUARDIAN_SIGNERS
|
||||||
|
);
|
||||||
|
|
||||||
|
++sequence;
|
||||||
|
|
||||||
|
updateData = new bytes[](1);
|
||||||
|
updateData[0] = vaa;
|
||||||
|
|
||||||
|
updateFee = pyth.getUpdateFee(updateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBenchmarkUpdatePriceFeedsFresh() public {
|
||||||
|
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
|
||||||
|
advancePrices();
|
||||||
|
|
||||||
|
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
|
||||||
|
pyth.updatePriceFeeds{value: updateFee}(updateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBenchmarkUpdatePriceFeedsNotFresh() public {
|
||||||
|
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
|
||||||
|
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
|
||||||
|
pyth.updatePriceFeeds{value: updateFee}(updateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBenchmarkUpdatePriceFeedsIfNecessaryFresh() public {
|
||||||
|
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
|
||||||
|
advancePrices();
|
||||||
|
|
||||||
|
uint64[] memory publishTimes = new uint64[](NUM_PRICES);
|
||||||
|
|
||||||
|
for (uint j = 0; j < NUM_PRICES; ++j) {
|
||||||
|
publishTimes[j] = uint64(prices[j].publishTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
|
||||||
|
|
||||||
|
// Since the prices have advanced, the publishTimes are newer than one in
|
||||||
|
// the contract and hence, the call should succeed.
|
||||||
|
pyth.updatePriceFeedsIfNecessary{value: updateFee}(updateData, priceIds, publishTimes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBenchmarkUpdatePriceFeedsIfNecessaryNotFresh() public {
|
||||||
|
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
|
||||||
|
uint64[] memory publishTimes = new uint64[](NUM_PRICES);
|
||||||
|
|
||||||
|
for (uint j = 0; j < NUM_PRICES; ++j) {
|
||||||
|
publishTimes[j] = uint64(prices[j].publishTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
|
||||||
|
|
||||||
|
// Since the price is not advanced, the publishTimes are the same as the
|
||||||
|
// ones in the contract except the first update.
|
||||||
|
if (i > 0) {
|
||||||
|
vm.expectRevert(bytes("no prices in the submitted batch have fresh prices, so this update will have no effect"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pyth.updatePriceFeedsIfNecessary{value: updateFee}(updateData, priceIds, publishTimes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBenchmarkGetPrice() public {
|
||||||
|
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
|
||||||
|
pyth.updatePriceFeeds{value: updateFee}(updateData);
|
||||||
|
|
||||||
|
// Set the block timestamp to the publish time, so getPrice work as expected.
|
||||||
|
vm.warp(prices[0].publishTime);
|
||||||
|
|
||||||
|
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
|
||||||
|
pyth.getPrice(priceIds[getRand() % NUM_PRICES]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
// SPDX-License-Identifier: Apache 2
|
|
||||||
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
import "../contracts/pyth/PythUpgradable.sol";
|
|
||||||
import "forge-std/Test.sol";
|
|
||||||
|
|
||||||
contract TestPythUpgradable is Test {
|
|
||||||
PythUpgradable public pyth;
|
|
||||||
|
|
||||||
function setUp() public {
|
|
||||||
pyth = new PythUpgradable();
|
|
||||||
// The values below are just dummy values and this test does nothing.
|
|
||||||
pyth.initialize(
|
|
||||||
address(0x0000000000000000000000000000000000000000000000000000000000000000),
|
|
||||||
0,
|
|
||||||
0x0000000000000000000000000000000000000000000000000000000000000000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
// SPDX-License-Identifier: Apache 2
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "../../contracts/pyth/PythUpgradable.sol";
|
||||||
|
import "../../contracts/pyth/PythInternalStructs.sol";
|
||||||
|
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
||||||
|
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
|
||||||
|
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
|
||||||
|
|
||||||
|
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
import "./WormholeTestUtils.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;
|
||||||
|
|
||||||
|
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));
|
||||||
|
pyth.initialize(
|
||||||
|
wormhole,
|
||||||
|
SOURCE_EMITTER_CHAIN_ID,
|
||||||
|
SOURCE_EMITTER_ADDRESS
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: All the logic below should be moved to the initializer
|
||||||
|
pyth.addDataSource(
|
||||||
|
SOURCE_EMITTER_CHAIN_ID,
|
||||||
|
SOURCE_EMITTER_ADDRESS
|
||||||
|
);
|
||||||
|
|
||||||
|
pyth.updateSingleUpdateFeeInWei(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
pyth.updateValidTimePeriodSeconds(
|
||||||
|
60
|
||||||
|
);
|
||||||
|
|
||||||
|
pyth.updateGovernanceDataSource(
|
||||||
|
GOVERNANCE_EMITTER_CHAIN_ID,
|
||||||
|
GOVERNANCE_EMITTER_ADDRESS,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return address(pyth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates byte-encoded payload for the given prices. It sets the emaPrice the same
|
||||||
|
// as the given price. You can use this to mock wormhole call using `vm.mockCall` and
|
||||||
|
// return a VM struct with this payload.
|
||||||
|
// You can use generatePriceFeedUpdateVAA to generate a VAA for a price update.
|
||||||
|
function generatePriceFeedUpdatePayload(
|
||||||
|
bytes32[] memory priceIds,
|
||||||
|
PythStructs.Price[] memory prices
|
||||||
|
) public returns (bytes memory payload) {
|
||||||
|
assertEq(priceIds.length, prices.length);
|
||||||
|
|
||||||
|
bytes memory attestations = new bytes(0);
|
||||||
|
|
||||||
|
for (uint i = 0; i < prices.length; ++i) {
|
||||||
|
// encodePacked uses padding for arrays and we don't want it, so we manually concat them.
|
||||||
|
attestations = abi.encodePacked(
|
||||||
|
attestations,
|
||||||
|
priceIds[i], // Product ID, we use the same price Id. This field is not used.
|
||||||
|
priceIds[i], // Price ID,
|
||||||
|
prices[i].price, // Price
|
||||||
|
prices[i].conf, // Confidence
|
||||||
|
prices[i].expo, // Exponent
|
||||||
|
prices[i].price, // EMA price
|
||||||
|
prices[i].conf // EMA confidence
|
||||||
|
);
|
||||||
|
|
||||||
|
// Breaking this in two encodePackes because of the limited EVM stack.
|
||||||
|
attestations = abi.encodePacked(
|
||||||
|
attestations,
|
||||||
|
uint8(PythInternalStructs.PriceAttestationStatus.TRADING),
|
||||||
|
uint32(5), // Number of publishers. This field is not used.
|
||||||
|
uint32(10), // Maximum number of publishers. This field is not used.
|
||||||
|
uint64(prices[i].publishTime), // Attestation time. This field is not used.
|
||||||
|
uint64(prices[i].publishTime), // Publish time.
|
||||||
|
// Previous values are unused as status is trading. We use the same value
|
||||||
|
// to make sure the test is irrelevant of the logic of which price is chosen.
|
||||||
|
uint64(prices[i].publishTime), // Previous publish time.
|
||||||
|
prices[i].price, // Previous price
|
||||||
|
prices[i].conf // Previous confidence
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(prices.length), // Number of attestations
|
||||||
|
uint16(attestations.length / prices.length), // Size of a single price attestation.
|
||||||
|
attestations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a VAA for the given prices.
|
||||||
|
// This method calls generatePriceFeedUpdatePayload and then creates a VAA with it.
|
||||||
|
// The VAAs generated from this method use block timestamp as their timestamp.
|
||||||
|
function generatePriceFeedUpdateVAA(
|
||||||
|
bytes32[] memory priceIds,
|
||||||
|
PythStructs.Price[] memory prices,
|
||||||
|
uint64 sequence,
|
||||||
|
uint8 numSigners
|
||||||
|
) public returns (bytes memory vaa) {
|
||||||
|
bytes memory payload = generatePriceFeedUpdatePayload(
|
||||||
|
priceIds,
|
||||||
|
prices
|
||||||
|
);
|
||||||
|
|
||||||
|
vaa = generateVaa(
|
||||||
|
uint32(block.timestamp),
|
||||||
|
SOURCE_EMITTER_CHAIN_ID,
|
||||||
|
SOURCE_EMITTER_ADDRESS,
|
||||||
|
sequence,
|
||||||
|
payload,
|
||||||
|
numSigners
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contract PythTestUtilsTest is Test, WormholeTestUtils, PythTestUtils {
|
||||||
|
// TODO: It is better to have a PythEvents contract that be extendable.
|
||||||
|
event PriceFeedUpdate(bytes32 indexed id, bool indexed fresh, uint16 chainId, uint64 sequenceNumber, uint lastPublishTime, uint publishTime, int64 price, uint64 conf);
|
||||||
|
|
||||||
|
function testGeneratePriceFeedUpdateVAAWorks() public {
|
||||||
|
IPyth pyth = IPyth(setUpPyth(setUpWormhole(
|
||||||
|
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 = generatePriceFeedUpdateVAA(
|
||||||
|
priceIds,
|
||||||
|
prices,
|
||||||
|
1, // Sequence
|
||||||
|
1 // No. Signers
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes[] memory updateData = new bytes[](1);
|
||||||
|
updateData[0] = vaa;
|
||||||
|
|
||||||
|
uint updateFee = pyth.getUpdateFee(updateData);
|
||||||
|
|
||||||
|
vm.expectEmit(true, true, false, true);
|
||||||
|
emit PriceFeedUpdate(priceIds[0], true, SOURCE_EMITTER_CHAIN_ID, 1, 0, 1, 100, 10);
|
||||||
|
|
||||||
|
pyth.updatePriceFeeds{value: updateFee}(updateData);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
// SPDX-License-Identifier: Apache 2
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "../../contracts/wormhole/Implementation.sol";
|
||||||
|
import "../../contracts/wormhole/Setup.sol";
|
||||||
|
import "../../contracts/wormhole/Wormhole.sol";
|
||||||
|
import "../../contracts/wormhole/interfaces/IWormhole.sol";
|
||||||
|
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
|
||||||
|
abstract contract WormholeTestUtils is Test {
|
||||||
|
function setUpWormhole(uint8 numGuardians) public returns (address) {
|
||||||
|
Implementation wormholeImpl = new Implementation();
|
||||||
|
Setup wormholeSetup = new Setup();
|
||||||
|
|
||||||
|
Wormhole wormhole = new Wormhole(address(wormholeSetup), new bytes(0));
|
||||||
|
|
||||||
|
address[] memory initSigners = new address[](numGuardians);
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < numGuardians; ++i) {
|
||||||
|
initSigners[i] = vm.addr(i + 1); // i+1 is the private key for the i-th signer.
|
||||||
|
}
|
||||||
|
|
||||||
|
// These values are the default values used in our tilt test environment
|
||||||
|
// and are not important.
|
||||||
|
Setup(address(wormhole)).setup(
|
||||||
|
address(wormholeImpl),
|
||||||
|
initSigners,
|
||||||
|
2, // Ethereum chain ID
|
||||||
|
1, // Governance source chain ID (1 = solana)
|
||||||
|
0x0000000000000000000000000000000000000000000000000000000000000004 // Governance source address
|
||||||
|
);
|
||||||
|
|
||||||
|
return address(wormhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateVaa(
|
||||||
|
uint32 timestamp,
|
||||||
|
uint16 emitterChainId,
|
||||||
|
bytes32 emitterAddress,
|
||||||
|
uint64 sequence,
|
||||||
|
bytes memory payload,
|
||||||
|
uint8 numSigners
|
||||||
|
) public returns (bytes memory vaa) {
|
||||||
|
bytes memory body = abi.encodePacked(
|
||||||
|
timestamp,
|
||||||
|
uint32(0), // Nonce. It is zero for single VAAs.
|
||||||
|
emitterChainId,
|
||||||
|
emitterAddress,
|
||||||
|
sequence,
|
||||||
|
uint8(0), // Consistency level (sometimes no. confirmation block). Not important here.
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes32 hash = keccak256(abi.encodePacked(keccak256(body)));
|
||||||
|
|
||||||
|
bytes memory signatures = new bytes(0);
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < numSigners; ++i) {
|
||||||
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(i + 1, hash);
|
||||||
|
// encodePacked uses padding for arrays and we don't want it, so we manually concat them.
|
||||||
|
signatures = abi.encodePacked(
|
||||||
|
signatures,
|
||||||
|
uint8(i), // Guardian index of the signature
|
||||||
|
r,
|
||||||
|
s,
|
||||||
|
v - 27 // v is either 27 or 28. 27 is added to v in Eth (following BTC) but Wormhole doesn't use it.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
vaa = abi.encodePacked(
|
||||||
|
uint8(1), // Version
|
||||||
|
uint32(0), // Guardian set index. it is initialized by 0
|
||||||
|
numSigners,
|
||||||
|
signatures,
|
||||||
|
body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contract WormholeTestUtilsTest is Test, WormholeTestUtils {
|
||||||
|
function testGenerateVaaWorks() public {
|
||||||
|
IWormhole wormhole = IWormhole(setUpWormhole(5));
|
||||||
|
|
||||||
|
bytes memory vaa = generateVaa(
|
||||||
|
112,
|
||||||
|
7,
|
||||||
|
0x0000000000000000000000000000000000000000000000000000000000000bad,
|
||||||
|
10,
|
||||||
|
hex"deadbeaf",
|
||||||
|
4
|
||||||
|
);
|
||||||
|
|
||||||
|
(Structs.VM memory vm, bool valid, ) = wormhole.parseAndVerifyVM(vaa);
|
||||||
|
assertTrue(valid);
|
||||||
|
|
||||||
|
assertEq(vm.timestamp, 112);
|
||||||
|
assertEq(vm.emitterChainId, 7);
|
||||||
|
assertEq(vm.emitterAddress, 0x0000000000000000000000000000000000000000000000000000000000000bad);
|
||||||
|
assertEq(vm.payload, hex"deadbeaf");
|
||||||
|
assertEq(vm.signatures.length, 4);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
[profile.default]
|
[profile.default]
|
||||||
solc_version = "0.8.4"
|
solc_version = '0.8.4'
|
||||||
optimizer = true
|
optimizer = true
|
||||||
optimizer_runs = 200
|
optimizer_runs = 200
|
||||||
src="contracts"
|
src = 'contracts'
|
||||||
# We put the tests into the forge-test directory (instead of test) so that
|
# We put the tests into the forge-test directory (instead of test) so that
|
||||||
# truffle doesn't try to build them
|
# truffle doesn't try to build them
|
||||||
test="forge-test"
|
test = 'forge-test'
|
||||||
|
|
||||||
libs = [
|
libs = [
|
||||||
'lib',
|
'lib',
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
@ensdomains/=node_modules/@ensdomains/
|
||||||
|
@openzeppelin/=node_modules/@openzeppelin/
|
||||||
|
@pythnetwork/=node_modules/@pythnetwork/
|
||||||
|
ds-test/=lib/forge-std/lib/ds-test/src/
|
||||||
|
forge-std/=lib/forge-std/src/
|
||||||
|
truffle/=node_modules/truffle/
|
|
@ -13,7 +13,7 @@ if [ ! -f foundry.toml ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Read compiler version from foundry.toml
|
# Read compiler version from foundry.toml
|
||||||
SOLC_VERSION=$(grep solc_version foundry.toml | cut -d'=' -f2 | tr -d '" ') || true
|
SOLC_VERSION=$(grep solc_version foundry.toml | cut -d'=' -f2 | tr -d "' ") || true
|
||||||
|
|
||||||
if [ -z "$SOLC_VERSION" ]; then
|
if [ -z "$SOLC_VERSION" ]; then
|
||||||
echo "solc_version not found in foundry.toml." >& 2
|
echo "solc_version not found in foundry.toml." >& 2
|
||||||
|
|
Loading…
Reference in New Issue