From 7e2cf1f9818099c63c21d101afbfedb1903ee9ba Mon Sep 17 00:00:00 2001 From: valentin Date: Tue, 3 Aug 2021 16:53:47 +0200 Subject: [PATCH] pyth data bridge target chain module for EVM Change-Id: Ieaeed6374e72a5348e05c40bd25598b74061a9a0 --- ethereum/.env.template | 7 + ethereum/.env.test | 11 +- ethereum/contracts/pyth/Pyth.sol | 98 +++++++ ethereum/contracts/pyth/PythDataBridge.sol | 15 ++ ethereum/contracts/pyth/PythGetters.sol | 46 ++++ ethereum/contracts/pyth/PythGovernance.sol | 89 +++++++ .../contracts/pyth/PythImplementation.sol | 25 ++ ethereum/contracts/pyth/PythSetters.sol | 44 ++++ ethereum/contracts/pyth/PythSetup.sol | 36 +++ ethereum/contracts/pyth/PythState.sol | 38 +++ ethereum/contracts/pyth/PythStructs.sol | 50 ++++ .../pyth/mock/MockBridgeImplementation.sol | 16 ++ ethereum/migrations/4_deploy_pyth.js | 37 +++ ethereum/test/pyth.js | 240 ++++++++++++++++++ 14 files changed, 750 insertions(+), 2 deletions(-) create mode 100644 ethereum/contracts/pyth/Pyth.sol create mode 100644 ethereum/contracts/pyth/PythDataBridge.sol create mode 100644 ethereum/contracts/pyth/PythGetters.sol create mode 100644 ethereum/contracts/pyth/PythGovernance.sol create mode 100644 ethereum/contracts/pyth/PythImplementation.sol create mode 100644 ethereum/contracts/pyth/PythSetters.sol create mode 100644 ethereum/contracts/pyth/PythSetup.sol create mode 100644 ethereum/contracts/pyth/PythState.sol create mode 100644 ethereum/contracts/pyth/PythStructs.sol create mode 100644 ethereum/contracts/pyth/mock/MockBridgeImplementation.sol create mode 100644 ethereum/migrations/4_deploy_pyth.js create mode 100644 ethereum/test/pyth.js diff --git a/ethereum/.env.template b/ethereum/.env.template index d08011a11..a13353ce8 100644 --- a/ethereum/.env.template +++ b/ethereum/.env.template @@ -16,3 +16,10 @@ BRIDGE_INIT_CHAIN_ID= # 0x02 BRIDGE_INIT_GOV_CHAIN_ID= # 0x3 BRIDGE_INIT_GOV_CONTRACT= # 0x0000000000000000000000000000000000000000000000000000000000000004 BRIDGE_INIT_WETH= # 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + +# Pyth Migrations # Example Format +PYTH_INIT_CHAIN_ID= # 0x2 +PYTH_INIT_GOV_CHAIN_ID= # 0x3 +PYTH_INIT_GOV_CONTRACT= # 0x0000000000000000000000000000000000000000000000000000000000000004 +PYTH_TO_WORMHOLE_CHAIN_ID= # 0x5 +PYTH_TO_WORMHOLE_CONTRACT= # 0x0000000000000000000000000000000000000000000000000000000000000006 \ No newline at end of file diff --git a/ethereum/.env.test b/ethereum/.env.test index 1aeafa4cc..9ab31bd4c 100644 --- a/ethereum/.env.test +++ b/ethereum/.env.test @@ -1,4 +1,4 @@ -# Migrations +# Wormhole Core Migrations INIT_SIGNERS=["0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"] INIT_CHAIN_ID=0x2 INIT_GOV_CHAIN_ID=0x1 @@ -8,4 +8,11 @@ INIT_GOV_CONTRACT=0x000000000000000000000000000000000000000000000000000000000000 BRIDGE_INIT_CHAIN_ID=0x02 BRIDGE_INIT_GOV_CHAIN_ID=0x1 BRIDGE_INIT_GOV_CONTRACT=0x0000000000000000000000000000000000000000000000000000000000000004 -BRIDGE_INIT_WETH=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 \ No newline at end of file +BRIDGE_INIT_WETH=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + +#Pyth Migrations +PYTH_INIT_CHAIN_ID=0x2 +PYTH_INIT_GOV_CHAIN_ID=0x3 +PYTH_INIT_GOV_CONTRACT=0x0000000000000000000000000000000000000000000000000000000000000004 +PYTH_TO_WORMHOLE_CHAIN_ID=0x5 +PYTH_TO_WORMHOLE_CONTRACT=0x0000000000000000000000000000000000000000000000000000000000000006 \ No newline at end of file diff --git a/ethereum/contracts/pyth/Pyth.sol b/ethereum/contracts/pyth/Pyth.sol new file mode 100644 index 000000000..4c3fdd027 --- /dev/null +++ b/ethereum/contracts/pyth/Pyth.sol @@ -0,0 +1,98 @@ +// contracts/Bridge.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../libraries/external/BytesLib.sol"; + +import "./PythGetters.sol"; +import "./PythSetters.sol"; +import "./PythStructs.sol"; +import "./PythGovernance.sol"; + +contract Pyth is PythGovernance { + using BytesLib for bytes; + + function attestPrice(bytes memory encodedVm) public returns (PythStructs.PriceAttestation memory pa) { + (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm); + + require(valid, reason); + require(verifyPythVM(vm), "invalid emitter"); + + PythStructs.PriceAttestation memory price = parsePriceAttestation(vm.payload); + + PythStructs.PriceAttestation memory latestPrice = latestAttestation(price.productId, price.priceType); + + if(price.timestamp > latestPrice.timestamp) { + setLatestAttestation(price.productId, price.priceType, price); + } + + return price; + } + + function verifyPythVM(IWormhole.VM memory vm) public view returns (bool valid) { + if (vm.emitterChainId != pyth2WormholeChainId()) { + return false; + } + if (vm.emitterAddress != pyth2WormholeContract()) { + return false; + } + return true; + } + + function parsePriceAttestation(bytes memory encodedPriceAttestation) public pure returns (PythStructs.PriceAttestation memory pa) { + uint index = 0; + + pa.magic = encodedPriceAttestation.toUint32(index); + index += 4; + require(pa.magic == 0x50325748, "invalid protocol"); + + pa.version = encodedPriceAttestation.toUint16(index); + index += 2; + require(pa.version == 1, "invalid protocol"); + + pa.payloadId = encodedPriceAttestation.toUint8(index); + index += 1; + require(pa.payloadId == 1, "invalid PriceAttestation"); + + pa.productId = encodedPriceAttestation.toBytes32(index); + index += 32; + pa.priceId = encodedPriceAttestation.toBytes32(index); + index += 32; + + pa.priceType = encodedPriceAttestation.toUint8(index); + index += 1; + + pa.price = int64(encodedPriceAttestation.toUint64(index)); + index += 8; + pa.exponent = int32(encodedPriceAttestation.toUint32(index)); + index += 4; + + pa.twap.value = int64(encodedPriceAttestation.toUint64(index)); + index += 8; + pa.twap.numerator = int64(encodedPriceAttestation.toUint64(index)); + index += 8; + pa.twap.denominator = int64(encodedPriceAttestation.toUint64(index)); + index += 8; + + pa.twac.value = int64(encodedPriceAttestation.toUint64(index)); + index += 8; + pa.twac.numerator = int64(encodedPriceAttestation.toUint64(index)); + index += 8; + pa.twac.denominator = int64(encodedPriceAttestation.toUint64(index)); + index += 8; + + pa.confidenceInterval = encodedPriceAttestation.toUint64(index); + index += 8; + + pa.status = encodedPriceAttestation.toUint8(index); + index += 1; + pa.corpAct = encodedPriceAttestation.toUint8(index); + index += 1; + + pa.timestamp = encodedPriceAttestation.toUint64(index); + index += 8; + + require(encodedPriceAttestation.length == index, "invalid PriceAttestation"); + } +} diff --git a/ethereum/contracts/pyth/PythDataBridge.sol b/ethereum/contracts/pyth/PythDataBridge.sol new file mode 100644 index 000000000..59044df13 --- /dev/null +++ b/ethereum/contracts/pyth/PythDataBridge.sol @@ -0,0 +1,15 @@ +// contracts/Wormhole.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract PythDataBridge is ERC1967Proxy { + constructor (address implementation, bytes memory initData) + ERC1967Proxy( + implementation, + initData + ) + {} +} \ No newline at end of file diff --git a/ethereum/contracts/pyth/PythGetters.sol b/ethereum/contracts/pyth/PythGetters.sol new file mode 100644 index 000000000..dacb409b0 --- /dev/null +++ b/ethereum/contracts/pyth/PythGetters.sol @@ -0,0 +1,46 @@ +// contracts/Getters.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../interfaces/IWormhole.sol"; + +import "./PythState.sol"; + +contract PythGetters is PythState { + function governanceActionIsConsumed(bytes32 hash) public view returns (bool) { + return _state.consumedGovernanceActions[hash]; + } + + function isInitialized(address impl) public view returns (bool) { + return _state.initializedImplementations[impl]; + } + + function wormhole() public view returns (IWormhole) { + return IWormhole(_state.wormhole); + } + + function chainId() public view returns (uint16){ + return _state.provider.chainId; + } + + function governanceChainId() public view returns (uint16){ + return _state.provider.governanceChainId; + } + + function governanceContract() public view returns (bytes32){ + return _state.provider.governanceContract; + } + + function pyth2WormholeChainId() public view returns (uint16){ + return _state.provider.pyth2WormholeChainId; + } + + function pyth2WormholeContract() public view returns (bytes32){ + return _state.provider.pyth2WormholeContract; + } + + function latestAttestation(bytes32 product, uint8 priceType) public view returns (PythStructs.PriceAttestation memory attestation){ + return _state.latestAttestations[product][priceType]; + } +} \ No newline at end of file diff --git a/ethereum/contracts/pyth/PythGovernance.sol b/ethereum/contracts/pyth/PythGovernance.sol new file mode 100644 index 000000000..b9818d643 --- /dev/null +++ b/ethereum/contracts/pyth/PythGovernance.sol @@ -0,0 +1,89 @@ +// contracts/Bridge.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +import "../libraries/external/BytesLib.sol"; + +import "./PythGetters.sol"; +import "./PythSetters.sol"; +import "./PythStructs.sol"; + +import "../interfaces/IWormhole.sol"; + +contract PythGovernance is PythGetters, PythSetters, ERC1967Upgrade { + using BytesLib for bytes; + + bytes32 constant module = 0x0000000000000000000000000000000000000000000000000000000050797468; + + // Execute a UpgradeContract governance message + function upgrade(bytes memory encodedVM) public { + (IWormhole.VM memory vm, bool valid, string memory reason) = verifyGovernanceVM(encodedVM); + require(valid, reason); + + setGovernanceActionConsumed(vm.hash); + + PythStructs.UpgradeContract memory implementation = parseContractUpgrade(vm.payload); + + require(implementation.module == module, "wrong module"); + require(implementation.chain == chainId(), "wrong chain id"); + + upgradeImplementation(implementation.newContract); + } + + function verifyGovernanceVM(bytes memory encodedVM) internal view returns (IWormhole.VM memory parsedVM, bool isValid, string memory invalidReason){ + (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVM); + + if(!valid){ + return (vm, valid, reason); + } + + if (vm.emitterChainId != governanceChainId()) { + return (vm, false, "wrong governance chain"); + } + if (vm.emitterAddress != governanceContract()) { + return (vm, false, "wrong governance contract"); + } + + if(governanceActionIsConsumed(vm.hash)){ + return (vm, false, "governance action already consumed"); + } + + return (vm, true, ""); + } + + event ContractUpgraded(address indexed oldContract, address indexed newContract); + function upgradeImplementation(address newImplementation) internal { + address currentImplementation = _getImplementation(); + + _upgradeTo(newImplementation); + + // Call initialize function of the new implementation + (bool success, bytes memory reason) = newImplementation.delegatecall(abi.encodeWithSignature("initialize()")); + + require(success, string(reason)); + + emit ContractUpgraded(currentImplementation, newImplementation); + } + + function parseContractUpgrade(bytes memory encodedUpgrade) public pure returns (PythStructs.UpgradeContract memory cu) { + uint index = 0; + + cu.module = encodedUpgrade.toBytes32(index); + index += 32; + + cu.action = encodedUpgrade.toUint8(index); + index += 1; + + require(cu.action == 1, "invalid ContractUpgrade 1"); + + cu.chain = encodedUpgrade.toUint16(index); + index += 2; + + cu.newContract = address(uint160(uint256(encodedUpgrade.toBytes32(index)))); + index += 32; + + require(encodedUpgrade.length == index, "invalid ContractUpgrade 2"); + } +} diff --git a/ethereum/contracts/pyth/PythImplementation.sol b/ethereum/contracts/pyth/PythImplementation.sol new file mode 100644 index 000000000..e665ce7a9 --- /dev/null +++ b/ethereum/contracts/pyth/PythImplementation.sol @@ -0,0 +1,25 @@ +// contracts/Implementation.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +import "./Pyth.sol"; + + +contract PythImplementation is Pyth { + modifier initializer() { + address impl = ERC1967Upgrade._getImplementation(); + + require( + !isInitialized(impl), + "already initialized" + ); + + setInitialized(impl); + + _; + } +} diff --git a/ethereum/contracts/pyth/PythSetters.sol b/ethereum/contracts/pyth/PythSetters.sol new file mode 100644 index 000000000..42ad85fba --- /dev/null +++ b/ethereum/contracts/pyth/PythSetters.sol @@ -0,0 +1,44 @@ +// contracts/Setters.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./PythState.sol"; + +contract PythSetters is PythState { + function setInitialized(address implementatiom) internal { + _state.initializedImplementations[implementatiom] = true; + } + + function setGovernanceActionConsumed(bytes32 hash) internal { + _state.consumedGovernanceActions[hash] = true; + } + + function setChainId(uint16 chainId) internal { + _state.provider.chainId = chainId; + } + + function setGovernanceChainId(uint16 chainId) internal { + _state.provider.governanceChainId = chainId; + } + + function setGovernanceContract(bytes32 governanceContract) internal { + _state.provider.governanceContract = governanceContract; + } + + function setPyth2WormholeChainId(uint16 chainId) internal { + _state.provider.pyth2WormholeChainId = chainId; + } + + function setPyth2WormholeContract(bytes32 contractAddr) internal { + _state.provider.pyth2WormholeContract = contractAddr; + } + + function setWormhole(address wh) internal { + _state.wormhole = payable(wh); + } + + function setLatestAttestation(bytes32 product, uint8 priceType, PythStructs.PriceAttestation memory attestation) internal { + _state.latestAttestations[product][priceType] = attestation; + } +} \ No newline at end of file diff --git a/ethereum/contracts/pyth/PythSetup.sol b/ethereum/contracts/pyth/PythSetup.sol new file mode 100644 index 000000000..a47f066c8 --- /dev/null +++ b/ethereum/contracts/pyth/PythSetup.sol @@ -0,0 +1,36 @@ +// contracts/PythSetup.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "./PythSetters.sol"; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +contract PythSetup is PythSetters, ERC1967Upgrade { + function setup( + address implementation, + + uint16 chainId, + address wormhole, + + uint16 governanceChainId, + bytes32 governanceContract, + + uint16 pyth2WormholeChainId, + bytes32 pyth2WormholeContract + ) public { + setChainId(chainId); + + setWormhole(wormhole); + + setGovernanceChainId(governanceChainId); + setGovernanceContract(governanceContract); + + setPyth2WormholeChainId(pyth2WormholeChainId); + setPyth2WormholeContract(pyth2WormholeContract); + + _upgradeTo(implementation); + } +} diff --git a/ethereum/contracts/pyth/PythState.sol b/ethereum/contracts/pyth/PythState.sol new file mode 100644 index 000000000..c8aefe965 --- /dev/null +++ b/ethereum/contracts/pyth/PythState.sol @@ -0,0 +1,38 @@ +// contracts/State.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./PythStructs.sol"; + +contract PythStorage { + struct Provider { + uint16 chainId; + + uint16 governanceChainId; + bytes32 governanceContract; + + uint16 pyth2WormholeChainId; + bytes32 pyth2WormholeContract; + } + + struct State { + address payable wormhole; + + Provider provider; + + // Mapping of consumed governance actions + mapping(bytes32 => bool) consumedGovernanceActions; + + // Mapping of initialized implementations + mapping(address => bool) initializedImplementations; + + // Mapping of cached price attestations + // productId => priceType => PriceAttestation + mapping(bytes32 => mapping(uint8 => PythStructs.PriceAttestation)) latestAttestations; + } +} + +contract PythState { + PythStorage.State _state; +} \ No newline at end of file diff --git a/ethereum/contracts/pyth/PythStructs.sol b/ethereum/contracts/pyth/PythStructs.sol new file mode 100644 index 000000000..c61b7e51f --- /dev/null +++ b/ethereum/contracts/pyth/PythStructs.sol @@ -0,0 +1,50 @@ +// contracts/Structs.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../libraries/external/BytesLib.sol"; + +contract PythStructs { + using BytesLib for bytes; + + struct Ema { + int64 value; + int64 numerator; + int64 denominator; + } + + struct PriceAttestation { + uint32 magic; // constant "P2WH" + uint16 version; + + // PayloadID uint8 = 1 + uint8 payloadId; + + bytes32 productId; + bytes32 priceId; + + uint8 priceType; + + int64 price; + int32 exponent; + + Ema twap; + Ema twac; + + uint64 confidenceInterval; + + uint8 status; + uint8 corpAct; + + uint64 timestamp; + } + + struct UpgradeContract { + bytes32 module; + uint8 action; + uint16 chain; + + address newContract; + } +} \ No newline at end of file diff --git a/ethereum/contracts/pyth/mock/MockBridgeImplementation.sol b/ethereum/contracts/pyth/mock/MockBridgeImplementation.sol new file mode 100644 index 000000000..3306616ec --- /dev/null +++ b/ethereum/contracts/pyth/mock/MockBridgeImplementation.sol @@ -0,0 +1,16 @@ +// contracts/Implementation.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../PythImplementation.sol"; + +contract MockPythImplementation is PythImplementation { + function initialize() initializer public { + // this function needs to be exposed for an upgrade to pass + } + + function testNewImplementationActive() external pure returns (bool) { + return true; + } +} diff --git a/ethereum/migrations/4_deploy_pyth.js b/ethereum/migrations/4_deploy_pyth.js new file mode 100644 index 000000000..a6831c4d3 --- /dev/null +++ b/ethereum/migrations/4_deploy_pyth.js @@ -0,0 +1,37 @@ +require('dotenv').config({ path: "../.env" }); + +const PythDataBridge = artifacts.require("PythDataBridge"); +const PythImplementation = artifacts.require("PythImplementation"); +const PythSetup = artifacts.require("PythSetup"); +const Wormhole = artifacts.require("Wormhole"); + +const chainId = process.env.PYTH_INIT_CHAIN_ID; +const governanceChainId = process.env.PYTH_INIT_GOV_CHAIN_ID; +const governanceContract = process.env.PYTH_INIT_GOV_CONTRACT; // bytes32 +const pyth2WormholeChainId = process.env.PYTH_TO_WORMHOLE_CHAIN_ID; +const pyth2WormholeContract = process.env.PYTH_TO_WORMHOLE_CONTRACT; // bytes32 + +module.exports = async function (deployer) { + // deploy implementation + await deployer.deploy(PythImplementation); + // deploy implementation + await deployer.deploy(PythSetup); + + // encode initialisation data + const setup = new web3.eth.Contract(PythSetup.abi, PythSetup.address); + const initData = setup.methods.setup( + PythImplementation.address, + + chainId, + (await Wormhole.deployed()).address, + + governanceChainId, + governanceContract, + + pyth2WormholeChainId, + pyth2WormholeContract, + ).encodeABI(); + + // deploy proxy + await deployer.deploy(PythDataBridge, PythSetup.address, initData); +}; diff --git a/ethereum/test/pyth.js b/ethereum/test/pyth.js new file mode 100644 index 000000000..02be232ab --- /dev/null +++ b/ethereum/test/pyth.js @@ -0,0 +1,240 @@ +const jsonfile = require('jsonfile'); +const elliptic = require('elliptic'); +const BigNumber = require('bignumber.js'); + +const Wormhole = artifacts.require("Wormhole"); +const PythDataBridge = artifacts.require("PythDataBridge"); +const PythImplementation = artifacts.require("PythImplementation"); +const MockPythImplementation = artifacts.require("MockPythImplementation"); + +const testSigner1PK = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; +const testSigner2PK = "892330666a850761e7370376430bb8c2aa1494072d3bfeaed0c4fa3d5a9135fe"; + +const WormholeImplementationFullABI = jsonfile.readFileSync("build/contracts/Implementation.json").abi +const P2WImplementationFullABI = jsonfile.readFileSync("build/contracts/PythImplementation.json").abi + +contract("Pyth", function () { + const testSigner1 = web3.eth.accounts.privateKeyToAccount(testSigner1PK); + const testSigner2 = web3.eth.accounts.privateKeyToAccount(testSigner2PK); + const testChainId = "2"; + const testGovernanceChainId = "3"; + const testGovernanceContract = "0x0000000000000000000000000000000000000000000000000000000000000004"; + const testPyth2WormholeChainId = "5"; + const testPyth2WormholeContract = "0x0000000000000000000000000000000000000000000000000000000000000006"; + + + it("should be initialized with the correct signers and values", async function(){ + const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address); + + // chain id + const chainId = await initialized.methods.chainId().call(); + assert.equal(chainId, testChainId); + + // governance + const governanceChainId = await initialized.methods.governanceChainId().call(); + assert.equal(governanceChainId, testGovernanceChainId); + const governanceContract = await initialized.methods.governanceContract().call(); + assert.equal(governanceContract, testGovernanceContract); + + // pyth2wormhole + const pyth2wormChain = await initialized.methods.pyth2WormholeChainId().call(); + assert.equal(pyth2wormChain, testPyth2WormholeChainId); + const pyth2wormContract = await initialized.methods.pyth2WormholeContract().call(); + assert.equal(pyth2wormContract, testPyth2WormholeContract); + }) + + it("should accept a valid upgrade", async function() { + const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address); + const accounts = await web3.eth.getAccounts(); + + const mock = await MockPythImplementation.new(); + + let data = [ + "0x0000000000000000000000000000000000000000000000000000000050797468", + "01", + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + web3.eth.abi.encodeParameter("address", mock.address).substring(2), + ].join('') + + const vm = await signAndEncodeVM( + 1, + 1, + testGovernanceChainId, + testGovernanceContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + let before = await web3.eth.getStorageAt(PythDataBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(before.toLowerCase(), PythImplementation.address.toLowerCase()); + + await initialized.methods.upgrade("0x" + vm).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + let after = await web3.eth.getStorageAt(PythDataBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(after.toLowerCase(), mock.address.toLowerCase()); + + const mockImpl = new web3.eth.Contract(MockPythImplementation.abi, PythDataBridge.address); + + let isUpgraded = await mockImpl.methods.testNewImplementationActive().call(); + + assert.ok(isUpgraded); + }) + + let testUpdate = "0x"+ + "503257480001011515151515151515151515151515151515151515151515151515151515151515DEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDE01DEADBEEFDEADBABEFFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE0000000000000065010000000000075BCD15"; + + it("should parse price update correctly", async function() { + const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address); + + let parsed = await initialized.methods.parsePriceAttestation(testUpdate).call(); + + assert.equal(parsed.magic, 1345476424); + assert.equal(parsed.version, 1); + assert.equal(parsed.payloadId, 1); + assert.equal(parsed.productId, "0x1515151515151515151515151515151515151515151515151515151515151515"); + assert.equal(parsed.priceId, "0xdededededededededededededededededededededededededededededededede"); + assert.equal(parsed.priceType, 1); + assert.equal(parsed.price, -2401053088876217666); + assert.equal(parsed.exponent, -3); + + assert.equal(parsed.twap.value, -42); + assert.equal(parsed.twap.numerator, 15); + assert.equal(parsed.twap.denominator, 37); + + assert.equal(parsed.twac.value, 42); + assert.equal(parsed.twac.numerator, 1111); + assert.equal(parsed.twac.denominator, 2222); + + assert.equal(parsed.confidenceInterval, 101); + + assert.equal(parsed.status, 1); + assert.equal(parsed.corpAct, 0); + + assert.equal(parsed.timestamp, 123456789); + }) + + it("should attest price updates over wormhole", async function() { + const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address); + const accounts = await web3.eth.getAccounts(); + + const vm = await signAndEncodeVM( + 1, + 1, + testPyth2WormholeChainId, + testPyth2WormholeContract, + 0, + testUpdate, + [ + testSigner1PK + ], + 0, + 0 + ); + + let result = await initialized.methods.attestPrice("0x"+vm).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + }) + + it("should cache price updates", async function() { + const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address); + + let cached = await initialized.methods.latestAttestation("0x1515151515151515151515151515151515151515151515151515151515151515", 1).call(); + + assert.equal(cached.magic, 1345476424); + assert.equal(cached.version, 1); + assert.equal(cached.payloadId, 1); + assert.equal(cached.productId, "0x1515151515151515151515151515151515151515151515151515151515151515"); + assert.equal(cached.priceId, "0xdededededededededededededededededededededededededededededededede"); + assert.equal(cached.priceType, 1); + assert.equal(cached.price, -2401053088876217666); + assert.equal(cached.exponent, -3); + + assert.equal(cached.twap.value, -42); + assert.equal(cached.twap.numerator, 15); + assert.equal(cached.twap.denominator, 37); + + assert.equal(cached.twac.value, 42); + assert.equal(cached.twac.numerator, 1111); + assert.equal(cached.twac.denominator, 2222); + + assert.equal(cached.confidenceInterval, 101); + + assert.equal(cached.status, 1); + assert.equal(cached.corpAct, 0); + + assert.equal(cached.timestamp, 123456789); + }) +}); + +const signAndEncodeVM = async function ( + timestamp, + nonce, + emitterChainId, + emitterAddress, + sequence, + data, + signers, + guardianSetIndex, + consistencyLevel +) { + const body = [ + web3.eth.abi.encodeParameter("uint32", timestamp).substring(2 + (64 - 8)), + web3.eth.abi.encodeParameter("uint32", nonce).substring(2 + (64 - 8)), + web3.eth.abi.encodeParameter("uint16", emitterChainId).substring(2 + (64 - 4)), + web3.eth.abi.encodeParameter("bytes32", emitterAddress).substring(2), + web3.eth.abi.encodeParameter("uint64", sequence).substring(2 + (64 - 16)), + web3.eth.abi.encodeParameter("uint8", consistencyLevel).substring(2 + (64 - 2)), + data.substr(2) + ] + + const hash = web3.utils.soliditySha3(web3.utils.soliditySha3("0x" + body.join(""))) + + let signatures = ""; + + for (let i in signers) { + const ec = new elliptic.ec("secp256k1"); + const key = ec.keyFromPrivate(signers[i]); + const signature = key.sign(hash.substr(2), {canonical: true}); + + const packSig = [ + web3.eth.abi.encodeParameter("uint8", i).substring(2 + (64 - 2)), + zeroPadBytes(signature.r.toString(16), 32), + zeroPadBytes(signature.s.toString(16), 32), + web3.eth.abi.encodeParameter("uint8", signature.recoveryParam).substr(2 + (64 - 2)), + ] + + signatures += packSig.join("") + } + + const vm = [ + web3.eth.abi.encodeParameter("uint8", 1).substring(2 + (64 - 2)), + web3.eth.abi.encodeParameter("uint32", guardianSetIndex).substring(2 + (64 - 8)), + web3.eth.abi.encodeParameter("uint8", signers.length).substring(2 + (64 - 2)), + + signatures, + body.join("") + ].join(""); + + return vm +} + +function zeroPadBytes(value, length) { + while (value.length < 2 * length) { + value = "0" + value; + } + return value; +} \ No newline at end of file