Prechádzať zdrojové kódy

[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
Ali Behjati 3 rokov pred
rodič
commit
0df243ba9e

+ 39 - 0
ethereum/README.md

@@ -34,3 +34,42 @@ npm run install-forge-deps
 
 After installing the dependencies. Run `forge build` to build the contracts and `forge test` to
 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.

+ 0 - 16
ethereum/contracts/wormhole/mock/MockImplementation.sol

@@ -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;
-    }
-}

+ 146 - 0
ethereum/forge-test/GasBenchmark.t.sol

@@ -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]);
+        }
+    }
+}

+ 0 - 20
ethereum/forge-test/PythUpgradable.t.sol

@@ -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
-        );
-    }
-}

+ 170 - 0
ethereum/forge-test/utils/PythTestUtils.t.sol

@@ -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);
+    }
+}

+ 104 - 0
ethereum/forge-test/utils/WormholeTestUtils.t.sol

@@ -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);
+    }
+}

+ 3 - 3
ethereum/foundry.toml

@@ -1,11 +1,11 @@
 [profile.default]
-solc_version = "0.8.4"
+solc_version = '0.8.4'
 optimizer = true
 optimizer_runs = 200
-src="contracts"
+src = 'contracts'
 # We put the tests into the forge-test directory (instead of test) so that
 # truffle doesn't try to build them
-test="forge-test"
+test = 'forge-test'
 
 libs = [
     'lib',

+ 6 - 0
ethereum/remappings.txt

@@ -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/

+ 1 - 1
scripts/install-foundry.sh

@@ -13,7 +13,7 @@ if [ ! -f foundry.toml ]; then
 fi
 
 # 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
   echo "solc_version not found in foundry.toml." >& 2