Browse Source

Merge pull request #86 from pyth-network/tompntn-evm-parse-batch

Add ability to parse batch price attestations to EVM contract
Tom Pointon 3 years ago
parent
commit
b77e31adea
3 changed files with 241 additions and 44 deletions
  1. 99 12
      ethereum/contracts/pyth/Pyth.sol
  2. 21 12
      ethereum/contracts/pyth/PythStructs.sol
  3. 121 20
      ethereum/test/pyth.js

+ 99 - 12
ethereum/contracts/pyth/Pyth.sol

@@ -40,20 +40,107 @@ contract Pyth is PythGovernance {
         return true;
     }
 
+    
+    function parseBatchPriceAttestation(bytes memory encoded) public pure returns (PythStructs.BatchPriceAttestation memory bpa) {
+        uint index = 0;
+
+        // Check header
+        bpa.header.magic = encoded.toUint32(index);
+        index += 4;
+        require(bpa.header.magic == 0x50325748, "invalid magic value");
+
+        bpa.header.version = encoded.toUint16(index);
+        index += 2;
+        require(bpa.header.version == 2, "invalid version");
+
+        bpa.header.payloadId = encoded.toUint8(index);
+        index += 1;
+        // Payload ID of 2 required for batch header
+        require(bpa.header.payloadId == 2, "invalid payload ID");
+
+        // Parse the number of attestations
+        bpa.nAttestations = encoded.toUint16(index);
+        index += 2;
+
+        // Parse the attestation size
+        bpa.attestationSize = encoded.toUint16(index);
+        index += 2;
+        require(encoded.length == (index + (bpa.attestationSize * bpa.nAttestations)), "invalid BatchPriceAttestation size");
+
+        bpa.attestations = new PythStructs.PriceAttestation[](bpa.nAttestations);
+
+        // Deserialize each attestation
+        for (uint j=0; j < bpa.nAttestations; j++) {
+            // Header
+            bpa.attestations[j].header.magic = encoded.toUint32(index);
+            index += 4;
+            require(bpa.attestations[j].header.magic == 0x50325748, "invalid magic value");
+
+            bpa.attestations[j].header.version = encoded.toUint16(index);
+            index += 2;
+            require(bpa.attestations[j].header.version == 2, "invalid version");
+
+            bpa.attestations[j].header.payloadId = encoded.toUint8(index);
+            index += 1;
+            // Payload ID of 1 required for individual attestation
+            require(bpa.attestations[j].header.payloadId == 1, "invalid payload ID");
+
+            // Attestation
+            bpa.attestations[j].productId = encoded.toBytes32(index);
+            index += 32;
+
+            bpa.attestations[j].priceId = encoded.toBytes32(index);
+            index += 32;
+            bpa.attestations[j].priceType = encoded.toUint8(index);
+            index += 1;
+
+            bpa.attestations[j].price = int64(encoded.toUint64(index));
+            index += 8;
+
+            bpa.attestations[j].exponent = int32(encoded.toUint32(index));
+            index += 4;
+
+            bpa.attestations[j].emaPrice.value = int64(encoded.toUint64(index));
+            index += 8;
+            bpa.attestations[j].emaPrice.numerator = int64(encoded.toUint64(index));
+            index += 8;
+            bpa.attestations[j].emaPrice.denominator = int64(encoded.toUint64(index));
+            index += 8;
+
+            bpa.attestations[j].emaConf.value = int64(encoded.toUint64(index));
+            index += 8;
+            bpa.attestations[j].emaConf.numerator = int64(encoded.toUint64(index));
+            index += 8;
+            bpa.attestations[j].emaConf.denominator = int64(encoded.toUint64(index));
+            index += 8;
+
+            bpa.attestations[j].confidenceInterval = encoded.toUint64(index);
+            index += 8;
+
+            bpa.attestations[j].status = encoded.toUint8(index);
+            index += 1;
+            bpa.attestations[j].corpAct = encoded.toUint8(index);
+            index += 1;
+
+            bpa.attestations[j].timestamp = encoded.toUint64(index);
+            index += 8;
+        }
+    }
+
     function parsePriceAttestation(bytes memory encodedPriceAttestation) public pure returns (PythStructs.PriceAttestation memory pa) {
         uint index = 0;
 
-        pa.magic = encodedPriceAttestation.toUint32(index);
+        pa.header.magic = encodedPriceAttestation.toUint32(index);
         index += 4;
-        require(pa.magic == 0x50325748, "invalid protocol");
+        require(pa.header.magic == 0x50325748, "invalid protocol");
 
-        pa.version = encodedPriceAttestation.toUint16(index);
+        pa.header.version = encodedPriceAttestation.toUint16(index);
         index += 2;
-        require(pa.version == 1, "invalid protocol");
+        require(pa.header.version == 1, "invalid protocol");
 
-        pa.payloadId = encodedPriceAttestation.toUint8(index);
+        pa.header.payloadId = encodedPriceAttestation.toUint8(index);
         index += 1;
-        require(pa.payloadId == 1, "invalid PriceAttestation");
+        require(pa.header.payloadId == 1, "invalid PriceAttestation");
 
         pa.productId = encodedPriceAttestation.toBytes32(index);
         index += 32;
@@ -68,18 +155,18 @@ contract Pyth is PythGovernance {
         pa.exponent = int32(encodedPriceAttestation.toUint32(index));
         index += 4;
 
-        pa.twap.value = int64(encodedPriceAttestation.toUint64(index));
+        pa.emaPrice.value = int64(encodedPriceAttestation.toUint64(index));
         index += 8;
-        pa.twap.numerator = int64(encodedPriceAttestation.toUint64(index));
+        pa.emaPrice.numerator = int64(encodedPriceAttestation.toUint64(index));
         index += 8;
-        pa.twap.denominator = int64(encodedPriceAttestation.toUint64(index));
+        pa.emaPrice.denominator = int64(encodedPriceAttestation.toUint64(index));
         index += 8;
 
-        pa.twac.value = int64(encodedPriceAttestation.toUint64(index));
+        pa.emaConf.value = int64(encodedPriceAttestation.toUint64(index));
         index += 8;
-        pa.twac.numerator = int64(encodedPriceAttestation.toUint64(index));
+        pa.emaConf.numerator = int64(encodedPriceAttestation.toUint64(index));
         index += 8;
-        pa.twac.denominator = int64(encodedPriceAttestation.toUint64(index));
+        pa.emaConf.denominator = int64(encodedPriceAttestation.toUint64(index));
         index += 8;
 
         pa.confidenceInterval = encodedPriceAttestation.toUint64(index);

+ 21 - 12
ethereum/contracts/pyth/PythStructs.sol

@@ -8,29 +8,32 @@ import "../libraries/external/BytesLib.sol";
 contract PythStructs {
     using BytesLib for bytes;
 
-    struct Ema {
-        int64 value;
-        int64 numerator;
-        int64 denominator;
+    struct BatchPriceAttestation {
+        Header header;
+
+        uint16 nAttestations;
+        uint16 attestationSize;
+        PriceAttestation[] attestations;
     }
 
-    struct PriceAttestation {
-        uint32 magic; // constant "P2WH"
+    struct Header {
+        uint32 magic;
         uint16 version;
-
-        // PayloadID uint8 = 1
         uint8 payloadId;
+    }
+
+    struct PriceAttestation {
+        Header header;
 
         bytes32 productId;
         bytes32 priceId;
-
         uint8 priceType;
 
         int64 price;
         int32 exponent;
 
-        Ema twap;
-        Ema twac;
+        Rational emaPrice;
+        Rational emaConf;
 
         uint64 confidenceInterval;
 
@@ -40,6 +43,12 @@ contract PythStructs {
         uint64 timestamp;
     }
 
+    struct Rational {
+        int64 value;
+        int64 numerator;
+        int64 denominator;
+    }
+
     struct UpgradeContract {
         bytes32 module;
         uint8 action;
@@ -47,4 +56,4 @@ contract PythStructs {
 
         address newContract;
     }
-}
+}

+ 121 - 20
ethereum/test/pyth.js

@@ -19,8 +19,8 @@ contract("Pyth", function () {
     const testChainId = "2";
     const testGovernanceChainId = "3";
     const testGovernanceContract = "0x0000000000000000000000000000000000000000000000000000000000000004";
-    const testPyth2WormholeChainId = "5";
-    const testPyth2WormholeEmitter = "0x0000000000000000000000000000000000000000000000000000000000000006";
+    const testPyth2WormholeChainId = "1";
+    const testPyth2WormholeEmitter = "0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b";
 
 
     it("should be initialized with the correct signers and values", async function(){
@@ -99,22 +99,22 @@ contract("Pyth", function () {
 
         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.header.magic, 1345476424);
+        assert.equal(parsed.header.version, 1);
+        assert.equal(parsed.header.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.emaPrice.value, -42);
+        assert.equal(parsed.emaPrice.numerator, 15);
+        assert.equal(parsed.emaPrice.denominator, 37);
 
-        assert.equal(parsed.twac.value, 42);
-        assert.equal(parsed.twac.numerator, 1111);
-        assert.equal(parsed.twac.denominator, 2222);
+        assert.equal(parsed.emaConf.value, 42);
+        assert.equal(parsed.emaConf.numerator, 1111);
+        assert.equal(parsed.emaConf.denominator, 2222);
 
         assert.equal(parsed.confidenceInterval, 101);
 
@@ -124,6 +124,107 @@ contract("Pyth", function () {
         assert.equal(parsed.timestamp, 123456789);
     })
 
+    it("should parse batch price attestation correctly", async function() {
+        let rawBatchPriceAttestation = "0x"+"503257480002020004009650325748000201c0e11df4c58a4e53f2bc059ba57a7c8f30ddada70b5bdc3753f90b824b64dd73c1902e05cdf03bc089a943d921f87ccd0e3e1b774b5660d037b9f428c0d3305e01000000000000071dfffffffb00000000000005f70000000132959bbd00000000c8bfed5f00000000000000030000000041c7b65b00000000c8bfed5f0000000000000003010000000000622f65f4503257480002017090c4ecf0309718d04c5a162c08aa4b78f533f688fa2f3ccd7be74c2a253a54fd4caca566fc44a9d6585420959d13897877c606477b3f0e7f247295b7275620010000000000000440fffffffb00000000000005fb000000015cfe8c9d00000000e3dbaa7f00000000000000020000000041c7c5bb00000000e3dbaa7f0000000000000007010000000000622f65f4503257480002012f064374f55cb2efbbef29329de3b652013a76261876c55a1caf3a489c721ccd8c5dd422900917e8e26316fe598e8f062058d390644e0e36d42c187298420ccd010000000000000609fffffffb00000000000005cd00000001492c19bd00000000dd92071f00000000000000020000000041c7d3fb00000000dd92071f0000000000000001010000000000622f65f45032574800020171ddabd1a2c1fb6d6c4707b245b7c0ab6af0ae7b96b2ff866954a0b71124aee517fbe895e5416ddb4d5af9d83c599ee2c4f94cb25e8597f9e5978bd63a7cdcb70100000000000007bcfffffffb00000000000005e2000000014db2995d00000000dd8f775f00000000000000020000000041c7df9b00000000dd8f775f0000000000000003010000000000622f65f4";
+
+        const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address);
+
+        const magic = 1345476424;
+        const version = 2;
+
+        let parsed = await initialized.methods.parseBatchPriceAttestation(rawBatchPriceAttestation).call();
+
+        // Check the header
+        assert.equal(parsed.header.magic, magic);
+        assert.equal(parsed.header.version, version);
+        assert.equal(parsed.header.payloadId, 2);
+
+        assert.equal(parsed.nAttestations, 4);
+        assert.equal(parsed.attestationSize, 150);
+
+        assert.equal(parsed.attestations.length, 4);
+
+        // Attestation #1
+        assert.equal(parsed.attestations[0].header.magic, magic);
+        assert.equal(parsed.attestations[0].header.version, version);
+        assert.equal(parsed.attestations[0].header.payloadId, 1);
+        assert.equal(parsed.attestations[0].productId, "0xc0e11df4c58a4e53f2bc059ba57a7c8f30ddada70b5bdc3753f90b824b64dd73");
+        assert.equal(parsed.attestations[0].priceId, "0xc1902e05cdf03bc089a943d921f87ccd0e3e1b774b5660d037b9f428c0d3305e");
+        assert.equal(parsed.attestations[0].priceType, 1);
+        assert.equal(parsed.attestations[0].price, 1821);
+        assert.equal(parsed.attestations[0].exponent, -5);
+        assert.equal(parsed.attestations[0].emaPrice.value, 1527);
+        assert.equal(parsed.attestations[0].emaPrice.numerator, 5143632829);
+        assert.equal(parsed.attestations[0].emaPrice.denominator, 3368021343);
+        assert.equal(parsed.attestations[0].emaConf.value, 3);
+        assert.equal(parsed.attestations[0].emaConf.numerator, 1103607387);
+        assert.equal(parsed.attestations[0].emaConf.denominator, 3368021343);
+        assert.equal(parsed.attestations[0].confidenceInterval, 3);
+        assert.equal(parsed.attestations[0].status, 1);
+        assert.equal(parsed.attestations[0].corpAct, 0);
+        assert.equal(parsed.attestations[0].timestamp, 1647273460);
+
+        // Attestation #2
+        assert.equal(parsed.attestations[1].header.magic, magic);
+        assert.equal(parsed.attestations[1].header.version, version);
+        assert.equal(parsed.attestations[1].header.payloadId, 1);
+        assert.equal(parsed.attestations[1].productId, "0x7090c4ecf0309718d04c5a162c08aa4b78f533f688fa2f3ccd7be74c2a253a54");
+        assert.equal(parsed.attestations[1].priceId, "0xfd4caca566fc44a9d6585420959d13897877c606477b3f0e7f247295b7275620");
+        assert.equal(parsed.attestations[1].priceType, 1);
+        assert.equal(parsed.attestations[1].price, 1088);
+        assert.equal(parsed.attestations[1].exponent, -5);
+        assert.equal(parsed.attestations[1].emaPrice.value, 1531);
+        assert.equal(parsed.attestations[1].emaPrice.numerator, 5855153309);
+        assert.equal(parsed.attestations[1].emaPrice.denominator, 3822824063);
+        assert.equal(parsed.attestations[1].emaConf.value, 2);
+        assert.equal(parsed.attestations[1].emaConf.numerator, 1103611323);
+        assert.equal(parsed.attestations[1].emaConf.denominator, 3822824063);
+        assert.equal(parsed.attestations[1].confidenceInterval, 7);
+        assert.equal(parsed.attestations[1].status, 1);
+        assert.equal(parsed.attestations[1].corpAct, 0);
+        assert.equal(parsed.attestations[1].timestamp, 1647273460);
+
+        // Attestation #3
+        assert.equal(parsed.attestations[2].header.magic, magic);
+        assert.equal(parsed.attestations[2].header.version, version);
+        assert.equal(parsed.attestations[2].header.payloadId, 1);
+        assert.equal(parsed.attestations[2].productId, "0x2f064374f55cb2efbbef29329de3b652013a76261876c55a1caf3a489c721ccd");
+        assert.equal(parsed.attestations[2].priceId, "0x8c5dd422900917e8e26316fe598e8f062058d390644e0e36d42c187298420ccd");
+        assert.equal(parsed.attestations[2].priceType, 1);
+        assert.equal(parsed.attestations[2].price, 1545);
+        assert.equal(parsed.attestations[2].exponent, -5);
+        assert.equal(parsed.attestations[2].emaPrice.value, 1485);
+        assert.equal(parsed.attestations[2].emaPrice.numerator, 5522594237);
+        assert.equal(parsed.attestations[2].emaPrice.denominator, 3717334815);
+        assert.equal(parsed.attestations[2].emaConf.value, 2);
+        assert.equal(parsed.attestations[2].emaConf.numerator, 1103614971);
+        assert.equal(parsed.attestations[2].emaConf.denominator, 3717334815);
+        assert.equal(parsed.attestations[2].confidenceInterval, 1);
+        assert.equal(parsed.attestations[2].status, 1);
+        assert.equal(parsed.attestations[2].corpAct, 0);
+        assert.equal(parsed.attestations[2].timestamp, 1647273460);
+
+        // Attestation #4
+        assert.equal(parsed.attestations[3].header.magic, magic);
+        assert.equal(parsed.attestations[3].header.version, version);
+        assert.equal(parsed.attestations[3].header.payloadId, 1);
+        assert.equal(parsed.attestations[3].productId, "0x71ddabd1a2c1fb6d6c4707b245b7c0ab6af0ae7b96b2ff866954a0b71124aee5");
+        assert.equal(parsed.attestations[3].priceId, "0x17fbe895e5416ddb4d5af9d83c599ee2c4f94cb25e8597f9e5978bd63a7cdcb7");
+        assert.equal(parsed.attestations[3].priceType, 1);
+        assert.equal(parsed.attestations[3].price, 1980);
+        assert.equal(parsed.attestations[3].exponent, -5);
+        assert.equal(parsed.attestations[3].emaPrice.value, 1506);
+        assert.equal(parsed.attestations[3].emaPrice.numerator, 5598517597);
+        assert.equal(parsed.attestations[3].emaPrice.denominator, 3717166943);
+        assert.equal(parsed.attestations[3].emaConf.value, 2);
+        assert.equal(parsed.attestations[3].emaConf.numerator, 1103617947);
+        assert.equal(parsed.attestations[3].emaConf.denominator, 3717166943);
+        assert.equal(parsed.attestations[3].confidenceInterval, 3);
+        assert.equal(parsed.attestations[3].status, 1);
+        assert.equal(parsed.attestations[3].corpAct, 0);
+        assert.equal(parsed.attestations[3].timestamp, 1647273460);
+    })
+
     it("should attest price updates over wormhole", async function() {
         const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address);
         const accounts = await web3.eth.getAccounts();
@@ -154,22 +255,22 @@ contract("Pyth", function () {
 
         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.header.magic, 1345476424);
+        assert.equal(cached.header.version, 1);
+        assert.equal(cached.header.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.emaPrice.value, -42);
+        assert.equal(cached.emaPrice.numerator, 15);
+        assert.equal(cached.emaPrice.denominator, 37);
 
-        assert.equal(cached.twac.value, 42);
-        assert.equal(cached.twac.numerator, 1111);
-        assert.equal(cached.twac.denominator, 2222);
+        assert.equal(cached.emaConf.value, 42);
+        assert.equal(cached.emaConf.numerator, 1111);
+        assert.equal(cached.emaConf.denominator, 2222);
 
         assert.equal(cached.confidenceInterval, 101);