From 9c4dea8e7bd4b5f9b85270dc7a5be2a90451d56f Mon Sep 17 00:00:00 2001 From: Rakshith R <58843287+rakshithvk19@users.noreply.github.com> Date: Sun, 30 Jun 2024 12:52:56 -0400 Subject: [PATCH 1/3] Create CombinePythPrice.sol --- .../sdk/solidity/CombinePythPrice.sol | 303 ++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 target_chains/ethereum/sdk/solidity/CombinePythPrice.sol diff --git a/target_chains/ethereum/sdk/solidity/CombinePythPrice.sol b/target_chains/ethereum/sdk/solidity/CombinePythPrice.sol new file mode 100644 index 0000000000..ba000f7e76 --- /dev/null +++ b/target_chains/ethereum/sdk/solidity/CombinePythPrice.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {PythStructs} from "./PythStructs.sol"; + +/** + * @title PythPriceCombinationLibrary + * @dev A library for combining Pyth prices using mathematical operations. + */ + +library CombinePythPrice { + ///////////////////// + // Errors // + ///////////////////// + + error DivisionByZero(); + error ExceedingComputationalRange(); + error AddingPricesOfDifferentExponents(int32 expo1, int32 expo2); + error MathErrorWhileScaling(); + + // Constants for working with Pyth's number representation + uint64 private constant MAX_PD_V_U64 = (1 << 28) - 1; + uint64 private constant PD_SCALE = 1_000_000_000; + int32 private constant PD_EXPO = -9; + + /** + * + * @param p1 Price 1 + * @param p2 Price 2 + * @return result of the addition of the prices + * @dev Function to add two pyth price when the exponents are same. + */ + function addPrices( + PythStructs.Price memory p1, + PythStructs.Price memory p2 + ) internal pure returns (PythStructs.Price memory result) { + int64 _price; + uint64 _conf; + + if (p1.expo != p2.expo) { + revert AddingPricesOfDifferentExponents(p1.expo, p2.expo); + } else { + _price = p1.price + p2.price; + _conf = p1.conf + p2.conf; + + return + PythStructs.Price({ + price: _price, + conf: _conf, + expo: p1.expo, + publishTime: min(p1.publishTime, p2.publishTime) + }); + } + } + + /** + * + * @param p1 Price 1 + * @param p2 Price 2 + * @param expo Exponent value to which the price should be transformed to. + * @return result of the addition of the prices + * @dev Function to convert the price and confindence interval to the required exponent and then add the two pyth prices. + */ + function addPriceToScaled( + PythStructs.Price memory p1, + PythStructs.Price memory p2, + int32 expo + ) internal pure returns (PythStructs.Price memory result) { + PythStructs.Price memory _p1 = p1.expo != expo + ? scaleToExponent(p1, expo) + : p1; + PythStructs.Price memory _p2 = p2.expo != expo + ? scaleToExponent(p2, expo) + : p2; + + return addPrices(_p1, _p2); + } + + /** + * + * @param self Price 1 + * @param other Price 2 + * @return result of the addition of the prices + * @dev Convert the price and confidence interval of Price 2 to the exponent of Price 1 and adds the two prices. + */ + function convertAndAddPrices( + PythStructs.Price memory self, + PythStructs.Price memory other + ) internal pure returns (PythStructs.Price memory result) { + if (self.expo != other.expo) { + PythStructs.Price memory scaledP2 = scaleToExponent( + other, + self.expo + ); + return addPrices(self, scaledP2); + } else { + return addPrices(self, other); + } + } + + /** + * + * @param self Price + * @param quote Price + * @param targetExpo Exponent of the result. + * @return result Returns the current price in a different quote currency. + * @dev Get the current price of this account in a different quote currency. + * If this account represents the price of the product X/Z, and `quote` represents the price of the product Y/Z, this method returns the price of X/Y. Use this method to get the price of e.g., mSOL/SOL from the mSOL/USD and SOL/USD accounts + */ + function getPriceInQuote( + PythStructs.Price memory self, + PythStructs.Price memory quote, + int32 targetExpo + ) public pure returns (PythStructs.Price memory result) { + PythStructs.Price memory divPrice = div(self, quote); + + if (divPrice.price != 0) { + return scaleToExponent(divPrice, targetExpo); + } + } + + /** + * @param self The price to be divided + * @param other The price to divide by + * @return result The resulting price after division + * @dev Divide `se;f` price by `other` while propagating the uncertainty in both prices into the result. + * This method will automatically select a reasonable exponent for the result. If both `self` and `other` are normalized, the + * exponent is `self.expo + PD_EXPO - other.expo` (i.e., the fraction has `PD_EXPO` digits of additional precision). If they are not + * normalized, this method will normalize them, resulting in an unpredictable result exponent. If the result is used in a context + * that requires a specific exponent, please call `scale_to_exponent` on it. + */ + function div( + PythStructs.Price memory self, + PythStructs.Price memory other + ) public pure returns (PythStructs.Price memory result) { + //Return zero price struct on denominator being 0 + if (other.price == 0) { + revert DivisionByZero(); + } + + /// Price is not guaranteed to store its price/confidence in normalized form. + /// Normalize them here to bound the range of price/conf, which is required to perform arithmetic operations. + PythStructs.Price memory base = normalize(self); + PythStructs.Price memory _other = normalize(other); + + //Converting to unsigned. + (uint64 basePrice, int64 baseSign) = toUnsigned(base.price); + (uint64 _otherPrice, int64 _otherSign) = toUnsigned(_other.price); + + // Compute the midprice, base in terms of other. + uint64 midPrice = ((basePrice * PD_SCALE) / _otherPrice); + int32 midPriceExpo = ((base.expo - _other.expo) + PD_EXPO); + + /// Compute the confidence interval. + /// + /// This code uses the 1-norm instead of the 2-norm for computational reasons. + /// Let p +- a and q +- b be the two arguments to this method. + /// The correct formula is p/q * sqrt( (a/p)^2 + (b/q)^2 ). + /// This quantity is difficult to compute due to the sqrt and overflow/underflow considerations. + /// + /// This code instead computes p/q * (a/p + b/q) = a/q + pb/q^2 . + /// This quantity is at most a factor of sqrt(2) greater than the correct result, which shouldn't matter considering that confidence intervals are typically ~0.1% of the price. + + /// This uses 57 bits and has an exponent of PD_EXPO. + uint64 otherConfPct = ((_other.conf * PD_SCALE) / _otherPrice); + + /// first term is 57 bits, second term is 57 + 58 - 29 = 86 bits. Same exponent as the midprice. + uint128 conf = uint128((base.conf * PD_SCALE) / _otherPrice) + + (uint128(otherConfPct) * uint128(midPrice)) / + uint128(PD_SCALE); + + if (conf < type(uint64).max) { + return + PythStructs.Price({ + price: int64(midPrice) * baseSign * _otherSign, + conf: uint64(conf), + expo: midPriceExpo, + publishTime: min(self.publishTime, _other.publishTime) + }); + } else { + revert ExceedingComputationalRange(); + } + } + + /** + * + * @param a Unixtimestamp A + * @param b Unixtimestamp B + * @dev Helper function to find the minimum of two Unix timestamps + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * + * @param price Input the price value that needs to be Normalized + * @dev Return the price struct after normalizing the price and confidence interval between the values of MAX_PD_V_I64 and MIN_PD_V_I64. + * MAX_PD_V_I64 = int64(MAX_PD_V_U64) i.e int64((1 << 28) - 1) + * MIN_PD_V_I64 = -MAX_PD_V_I64 + */ + function normalize( + PythStructs.Price memory price + ) internal pure returns (PythStructs.Price memory result) { + (uint64 p, int64 _s) = toUnsigned(price.price); + uint64 c = price.conf; + int32 e = price.expo; + + ///Revert transaction incase `p` or `c` is zero leading to division by zero error. + if (p == 0 || c == 0) { + revert DivisionByZero(); + } + + // Scaling down if `p` and `c` are above MAX_PD_U64 + while (p > MAX_PD_V_U64 || c > MAX_PD_V_U64) { + p /= 10; + c /= 10; + e += 1; + ///Returns 0 incase of `p` or `c` becomes 0 during normalization. + if (p == 0 || c == 0) { + revert DivisionByZero(); + } + } + + int64 signedPrice = int64(p) * _s; + return + PythStructs.Price({ + price: signedPrice, + conf: c, + expo: e, + publishTime: price.publishTime + }); + } + + /** + * + * @param x Price value that needs to convert to be unsigned integer + * @return abs value of x + * @return sign of x + * @dev returns the unsigned value and the sign of the signed integer provided in the argument. + */ + function toUnsigned(int64 x) internal pure returns (uint64, int64) { + if (x == type(int64).min) { + //Edge case + return (uint64(type(int64).max) + 1, -1); + } else if (x < 0) { + return (uint64(-x), -1); + } else { + return (uint64(x), 1); + } + } + + /** + * + * @param self The price struct that needs to be scaled to the required exponent + * @param targetExpo The target exponent that the price needs to be scaled to + * @dev returns the price after scaling the it to the target exponent. + */ + function scaleToExponent( + PythStructs.Price memory self, + int32 targetExpo + ) internal pure returns (PythStructs.Price memory result) { + int256 delta = targetExpo - self.expo; + int64 p = self.price; + uint64 c = self.conf; + + if (delta >= 0) { + while (delta > 0 && (p != 0 || c != 0)) { + p = p / 10; + c = c / 10; + delta--; + } + } else { + while (delta < 0) { + /// Following checks for p: + ///1. Overflow + ///2. Underflow + ///3. Division by 0 + ///4. min of int64 when multiplied by 10 causes underflow + if ( + p > type(int64).max / 10 || + p < type(int64).min / 10 || + (p == type(int64).min && p % 10 != 0) + ) { + revert MathErrorWhileScaling(); + } + + /// Following checks for c: + ///1. Overflow + ///2. Division by 0 + if (c > type(uint64).max / 10 || c % 10 != 0) { + revert MathErrorWhileScaling(); + } + + p = p * 10; + c = c * 10; + delta++; + } + } + + return PythStructs.Price(p, c, targetExpo, self.publishTime); + } +} From f4f1479a7588dd34123ec58e79bea5d2c26232c4 Mon Sep 17 00:00:00 2001 From: Rakshith R <58843287+rakshithvk19@users.noreply.github.com> Date: Sun, 30 Jun 2024 12:54:02 -0400 Subject: [PATCH 2/3] Create CombinePythPrice.abi --- .../sdk/solidity/abis/CombinePythPrice.abi | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 target_chains/ethereum/sdk/solidity/abis/CombinePythPrice.abi diff --git a/target_chains/ethereum/sdk/solidity/abis/CombinePythPrice.abi b/target_chains/ethereum/sdk/solidity/abis/CombinePythPrice.abi new file mode 100644 index 0000000000..fd36337618 --- /dev/null +++ b/target_chains/ethereum/sdk/solidity/abis/CombinePythPrice.abi @@ -0,0 +1,218 @@ +[ + { + "type": "function", + "name": "div", + "inputs": [ + { + "name": "self", + "type": "tuple", + "internalType": "struct PythStructs.Price", + "components": [ + { + "name": "price", + "type": "int64", + "internalType": "int64" + }, + { + "name": "conf", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "expo", + "type": "int32", + "internalType": "int32" + }, + { + "name": "publishTime", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "other", + "type": "tuple", + "internalType": "struct PythStructs.Price", + "components": [ + { + "name": "price", + "type": "int64", + "internalType": "int64" + }, + { + "name": "conf", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "expo", + "type": "int32", + "internalType": "int32" + }, + { + "name": "publishTime", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "outputs": [ + { + "name": "result", + "type": "tuple", + "internalType": "struct PythStructs.Price", + "components": [ + { + "name": "price", + "type": "int64", + "internalType": "int64" + }, + { + "name": "conf", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "expo", + "type": "int32", + "internalType": "int32" + }, + { + "name": "publishTime", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "getPriceInQuote", + "inputs": [ + { + "name": "self", + "type": "tuple", + "internalType": "struct PythStructs.Price", + "components": [ + { + "name": "price", + "type": "int64", + "internalType": "int64" + }, + { + "name": "conf", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "expo", + "type": "int32", + "internalType": "int32" + }, + { + "name": "publishTime", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "quote", + "type": "tuple", + "internalType": "struct PythStructs.Price", + "components": [ + { + "name": "price", + "type": "int64", + "internalType": "int64" + }, + { + "name": "conf", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "expo", + "type": "int32", + "internalType": "int32" + }, + { + "name": "publishTime", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "targetExpo", + "type": "int32", + "internalType": "int32" + } + ], + "outputs": [ + { + "name": "result", + "type": "tuple", + "internalType": "struct PythStructs.Price", + "components": [ + { + "name": "price", + "type": "int64", + "internalType": "int64" + }, + { + "name": "conf", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "expo", + "type": "int32", + "internalType": "int32" + }, + { + "name": "publishTime", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "error", + "name": "AddingPricesOfDifferentExponents", + "inputs": [ + { + "name": "expo1", + "type": "int32", + "internalType": "int32" + }, + { + "name": "expo2", + "type": "int32", + "internalType": "int32" + } + ] + }, + { + "type": "error", + "name": "DivisionByZero", + "inputs": [] + }, + { + "type": "error", + "name": "ExceedingComputationalRange", + "inputs": [] + }, + { + "type": "error", + "name": "MathErrorWhileScaling", + "inputs": [] + } +] From ee1b077ef1b6cbdd4f84944a9171aced41f93b89 Mon Sep 17 00:00:00 2001 From: Rakshith R <58843287+rakshithvk19@users.noreply.github.com> Date: Sun, 30 Jun 2024 12:57:04 -0400 Subject: [PATCH 3/3] Added test --- .../sdk/solidity/Test/CombinePythPrice.t.sol | 690 ++++++++++++++++++ 1 file changed, 690 insertions(+) create mode 100644 target_chains/ethereum/sdk/solidity/Test/CombinePythPrice.t.sol diff --git a/target_chains/ethereum/sdk/solidity/Test/CombinePythPrice.t.sol b/target_chains/ethereum/sdk/solidity/Test/CombinePythPrice.t.sol new file mode 100644 index 0000000000..ab8580a089 --- /dev/null +++ b/target_chains/ethereum/sdk/solidity/Test/CombinePythPrice.t.sol @@ -0,0 +1,690 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity ^0.8.25; + +import {Test, console2} from "forge-std/Test.sol"; +import {CombinePythPrice} from "src/CombinePythPrice.sol"; +import {PythStructs} from "src/PythStructs.sol"; + +contract CombinePythPriceTest is Test { + PythStructs.Price public Price; + uint64 private constant PD_SCALE = 1_000_000_000; + + function setUp() public view {} + + //////////////////////////////// + // toUnsigned() // + //////////////////////////////// + + function testToUnsignedPositive() public pure { + (uint64 value, int64 sign) = CombinePythPrice.toUnsigned(100); + assertEq(value, 100); + assertEq(sign, 1); + } + + function testToUnsignedNegative() public pure { + (uint64 value, int64 sign) = CombinePythPrice.toUnsigned(-100); + assertEq(value, 100); + assertEq(sign, -1); + } + + function testToUnsignedZero() public pure { + (uint64 value, int64 sign) = CombinePythPrice.toUnsigned(0); + assertEq(value, 0); + assertEq(sign, 1); + } + + function testToUnsignedMinInt64() public pure { + (uint64 value, int64 sign) = CombinePythPrice.toUnsigned( + type(int64).min + ); + assertEq(value, uint64(type(int64).max) + 1); + assertEq(sign, -1); + } + + function testToUnsignedMaxInt64() public pure { + (uint64 value, int64 sign) = CombinePythPrice.toUnsigned( + type(int64).max + ); + assertEq(value, uint64(type(int64).max)); + assertEq(sign, 1); + } + + function testToUnsignedFuzzPositive(int64 input) public pure { + vm.assume(input > 0); + (uint64 value, int64 sign) = CombinePythPrice.toUnsigned(input); + assertEq(value, uint64(input)); + assertEq(sign, 1); + } + + function testToUnsignedFuzzNegative(int64 input) public pure { + vm.assume(input < 0 && input != type(int64).min); + (uint64 value, int64 sign) = CombinePythPrice.toUnsigned(input); + assertEq(value, uint64(-input)); + assertEq(sign, -1); + } + + //////////////////////////////// + // min() // + //////////////////////////////// + + function testMinBasicCase() public pure { + uint256 a = 100; + uint256 b = 200; + uint256 result = CombinePythPrice.min(a, b); + assert(result == a); + } + + function testMinReversedOrder() public pure { + uint256 a = 200; + uint256 b = 100; + uint256 result = CombinePythPrice.min(a, b); + assert(result == b); + } + + function testMinEqualValues() public pure { + uint256 a = 100; + uint256 b = 100; + uint256 result = CombinePythPrice.min(a, b); + assert(result == a); + assert(result == b); + } + + function testMinZeroAndPositive() public pure { + uint256 a = 0; + uint256 b = 100; + uint256 result = CombinePythPrice.min(a, b); + assert(result == a); + } + + function testMinPositiveAndZero() public pure { + uint256 a = 100; + uint256 b = 0; + uint256 result = CombinePythPrice.min(a, b); + assert(result == b); + } + + function testMinLargeNumbers() public pure { + uint256 a = type(uint256).max; + uint256 b = type(uint256).max - 1; + uint256 result = CombinePythPrice.min(a, b); + assert(result == b); + } + + function testMinFuzz(uint256 a, uint256 b) public pure { + uint256 result = CombinePythPrice.min(a, b); + assert(result <= a && result <= b); + assert(result == a || result == b); + } + + function testMinWithTypicalTimestamps() public view { + uint256 currentTimestamp = block.timestamp; + uint256 futureTimestamp = currentTimestamp + 3600; // 1 hour in the future + uint256 result = CombinePythPrice.min( + currentTimestamp, + futureTimestamp + ); + assert(result == currentTimestamp); + } + + function testMinWithPastAndFutureTimestamps() public pure { + uint256 pastTimestamp = 1609459200; // 2021-01-01 00:00:00 UTC + uint256 futureTimestamp = 1735689600; // 2025-01-01 00:00:00 UTC + uint256 result = CombinePythPrice.min(pastTimestamp, futureTimestamp); + assert(result == pastTimestamp); + } + + //////////////////////////////// + // scaleToExponent() // + //////////////////////////////// + + function testScaleToExponentNoChange() public pure { + PythStructs.Price memory price = PythStructs.Price({ + price: 1000, + conf: 10, + expo: -2, + publishTime: 1625097600 + }); + PythStructs.Price memory result = CombinePythPrice.scaleToExponent( + price, + -2 + ); + assert(result.price == 1000); + assert(result.conf == 10); + assert(result.expo == -2); + assert(result.publishTime == 1625097600); + } + + function testScaleToExponentScaleUp() public pure { + PythStructs.Price memory price = PythStructs.Price({ + price: 1000, + conf: 10, + expo: -2, + publishTime: 1625097600 + }); + PythStructs.Price memory result = CombinePythPrice.scaleToExponent( + price, + 0 + ); + assert(result.price == 10); + assert(result.conf == 0); + assert(result.expo == 0); + assert(result.publishTime == 1625097600); + } + + function testScaleToExponentScaleDown() public pure { + PythStructs.Price memory price = PythStructs.Price({ + price: 1000, + conf: 10, + expo: -2, + publishTime: 1625097600 + }); + PythStructs.Price memory result = CombinePythPrice.scaleToExponent( + price, + -4 + ); + assert(result.price == 100000); + assert(result.conf == 1000); + assert(result.expo == -4); + assert(result.publishTime == 1625097600); + } + + function testScaleToExponentZeroPrice() public pure { + PythStructs.Price memory price = PythStructs.Price({ + price: 0, + conf: 10, + expo: -2, + publishTime: 1625097600 + }); + PythStructs.Price memory result = CombinePythPrice.scaleToExponent( + price, + 0 + ); + assert(result.price == 0); + assert(result.conf == 0); + assert(result.expo == 0); + assert(result.publishTime == 1625097600); + } + + function testScaleToExponentZeroConf() public pure { + PythStructs.Price memory price = PythStructs.Price({ + price: 1000, + conf: 0, + expo: -2, + publishTime: 1625097600 + }); + PythStructs.Price memory result = CombinePythPrice.scaleToExponent( + price, + 0 + ); + assert(result.price == 10); + assert(result.conf == 0); + assert(result.expo == 0); + assert(result.publishTime == 1625097600); + } + + function testScaleToExponentLargeScaleUp() public pure { + PythStructs.Price memory price = PythStructs.Price({ + price: 1000000000, + conf: 10000000, + expo: -8, + publishTime: 1625097600 + }); + PythStructs.Price memory result = CombinePythPrice.scaleToExponent( + price, + 0 + ); + assert(result.price == 10); + assert(result.conf == 0); + assert(result.expo == 0); + assert(result.publishTime == 1625097600); + } + + function testScaleToExponentLargeScaleDown() public { + PythStructs.Price memory price = PythStructs.Price({ + price: 10, + conf: 1, + expo: 0, + publishTime: 1625097600 + }); + + vm.expectRevert(CombinePythPrice.MathErrorWhileScaling.selector); + CombinePythPrice.scaleToExponent(price, -8); + } + + function testScaleToExponentNegativePrice() public pure { + PythStructs.Price memory price = PythStructs.Price({ + price: -1000, + conf: 10, + expo: -2, + publishTime: 1625097600 + }); + PythStructs.Price memory result = CombinePythPrice.scaleToExponent( + price, + 0 + ); + assert(result.price == -10); + assert(result.conf == 0); + assert(result.expo == 0); + assert(result.publishTime == 1625097600); + } + + /////////////////////////// + // Normalise() // + /////////////////////////// + + function testNormalizeNormalCase() public view { + PythStructs.Price memory price = PythStructs.Price({ + price: 1000000, + conf: 1000, + expo: -6, + publishTime: block.timestamp + }); + + PythStructs.Price memory result = CombinePythPrice.normalize(price); + + assertEq(result.price, 1000000); + assertEq(result.conf, 1000); + assertEq(result.expo, -6); + assertEq(result.publishTime, price.publishTime); + } + + function testNormalizeNegativePrice() public view { + PythStructs.Price memory price = PythStructs.Price({ + price: -1000000, + conf: 1000, + expo: -6, + publishTime: block.timestamp + }); + + PythStructs.Price memory result = CombinePythPrice.normalize(price); + + assertEq(result.price, -1000000); + assertEq(result.conf, 1000); + assertEq(result.expo, -6); + assertEq(result.publishTime, price.publishTime); + } + + function testNormalizeLargePrice() public { + PythStructs.Price memory price = PythStructs.Price({ + price: 1_000_000_000_000_000_000, + conf: 1_000_000_000, + expo: -6, + publishTime: block.timestamp + }); + + vm.expectRevert(CombinePythPrice.DivisionByZero.selector); + CombinePythPrice.normalize(price); + } + + function testNormalizeMinInt64() public { + PythStructs.Price memory price = PythStructs.Price({ + price: type(int64).min, + conf: 1000, + expo: -6, + publishTime: block.timestamp + }); + + vm.expectRevert(CombinePythPrice.DivisionByZero.selector); + CombinePythPrice.normalize(price); + } + + function testNormalizeMaxUint64() public { + PythStructs.Price memory price = PythStructs.Price({ + price: type(int64).max, + conf: 1, + expo: 0, + publishTime: block.timestamp + }); + + vm.expectRevert(CombinePythPrice.DivisionByZero.selector); + CombinePythPrice.normalize(price); + } + + function testNormalizeDivisionByZero() public { + PythStructs.Price memory price = PythStructs.Price({ + price: 0, + conf: 0, + expo: -6, + publishTime: block.timestamp + }); + + vm.expectRevert(CombinePythPrice.DivisionByZero.selector); + CombinePythPrice.normalize(price); + } + + //////////////////////// + // div // + //////////////////////// + + function testDivNormalCase() public view { + PythStructs.Price memory price1 = PythStructs.Price({ + price: 1000000000, // $1.00 + conf: 10000000, // $0.01 + expo: -9, + publishTime: block.timestamp + }); + + PythStructs.Price memory price2 = PythStructs.Price({ + price: 2000000000, // $2.00 + conf: 20000000, // $0.02 + expo: -9, + publishTime: block.timestamp + }); + + PythStructs.Price memory result = CombinePythPrice.div(price1, price2); + + assertEq(result.price, 500000000); // 0.5 + assertEq(result.conf, 10000000); // 0.0075 + assertEq(result.expo, -9); + } + + function testDivDifferentExponents() public view { + PythStructs.Price memory price1 = PythStructs.Price({ + price: 1_000_000, // $1.00 + conf: 10_000, // $0.01 + expo: -6, + publishTime: block.timestamp + }); + + PythStructs.Price memory price2 = PythStructs.Price({ + price: 2_000_000_000, // $2.00 + conf: 20_000_000, // $0.02 + expo: -9, + publishTime: block.timestamp + }); + + PythStructs.Price memory result = CombinePythPrice.div(price1, price2); + + assertEq(result.price, 5000000); // 0.5 + assertEq(result.conf, 100000); // 0.0075 + assertEq(result.expo, -7); + } + + function testDivZeroDenominator() public { + PythStructs.Price memory price1 = PythStructs.Price({ + price: 1000000000, + conf: 10000000, + expo: -9, + publishTime: block.timestamp + }); + + PythStructs.Price memory price2 = PythStructs.Price({ + price: 0, + conf: 20000000, + expo: -9, + publishTime: block.timestamp + }); + + vm.expectRevert(CombinePythPrice.DivisionByZero.selector); + CombinePythPrice.div(price1, price2); + } + + function testDivLargeNumbers() public view { + PythStructs.Price memory price1 = PythStructs.Price({ + price: 1000000000000000000, // 1e18 + conf: 10000000000000000, // 1e16 + expo: -9, + publishTime: block.timestamp + }); + + PythStructs.Price memory price2 = PythStructs.Price({ + price: 1000000000, // 1e9 + conf: 10000000, // 1e7 + expo: -9, + publishTime: block.timestamp + }); + + PythStructs.Price memory result = CombinePythPrice.div(price1, price2); + + assertEq(result.price, 1000000000); // 1e9 + assertEq(result.conf, 20000000); // 2e7 + assertEq(result.expo, 0); + } + + function testDivNegativeNumbers() public view { + PythStructs.Price memory price1 = PythStructs.Price({ + price: -1000000000, // -$1.00 + conf: 10000000, // $0.01 + expo: -9, + publishTime: block.timestamp + }); + + PythStructs.Price memory price2 = PythStructs.Price({ + price: 2000000000, // $2.00 + conf: 20000000, // $0.02 + expo: -9, + publishTime: block.timestamp + }); + + PythStructs.Price memory result = CombinePythPrice.div(price1, price2); + + assertEq(result.price, -500000000); // -0.5 + assertEq(result.conf, 10000000); // 0.0075 + assertEq(result.expo, -9); + } + + //////////////////////////////// + // getPriceInQuotes // + //////////////////////////////// + + function testGetPriceInQuote_SameExponent() public view { + PythStructs.Price memory basePrice = PythStructs.Price({ + price: 100_000_000, + conf: 1_000_000, + expo: -8, + publishTime: block.timestamp + }); + PythStructs.Price memory quotePrice = PythStructs.Price({ + price: 50_000_000, + conf: 500_000, + expo: -8, + publishTime: block.timestamp + }); + + PythStructs.Price memory result = CombinePythPrice.getPriceInQuote( + basePrice, + quotePrice, + -9 + ); + + PythStructs.Price memory nRes = CombinePythPrice.normalize(quotePrice); + console2.log("price", nRes.price); + console2.log("conf", nRes.conf); + + assertEq(result.price, 2_000_000_000); + assertEq(result.conf, 40000000); + assertEq(result.expo, -9); + } + + function testGetPriceInQuote_DifferentExponents() public view { + PythStructs.Price memory basePrice = PythStructs.Price({ + price: 100000000, + conf: 1000000, + expo: -8, + publishTime: block.timestamp + }); + PythStructs.Price memory quotePrice = PythStructs.Price({ + price: 5000000000, + conf: 50000000, + expo: -10, + publishTime: block.timestamp + }); + + PythStructs.Price memory result = CombinePythPrice.getPriceInQuote( + basePrice, + quotePrice, + -9 + ); + + assertEq(result.price, 2000000000); + assertEq(result.conf, 40000000); + assertEq(result.expo, -9); + } + + function testGetPriceInQuote_ZeroQuotePrice() public { + PythStructs.Price memory basePrice = PythStructs.Price({ + price: 100000000, + conf: 1000000, + expo: -8, + publishTime: block.timestamp + }); + PythStructs.Price memory quotePrice = PythStructs.Price({ + price: 0, + conf: 0, + expo: -8, + publishTime: block.timestamp + }); + + vm.expectRevert(CombinePythPrice.DivisionByZero.selector); + + CombinePythPrice.getPriceInQuote(basePrice, quotePrice, -9); + } + + function testGetPriceInQuote_NegativePrices() public view { + PythStructs.Price memory basePrice = PythStructs.Price({ + price: -100000000, + conf: 1000000, + expo: -8, + publishTime: block.timestamp + }); + PythStructs.Price memory quotePrice = PythStructs.Price({ + price: -50000000, + conf: 500000, + expo: -8, + publishTime: block.timestamp + }); + + PythStructs.Price memory result = CombinePythPrice.getPriceInQuote( + basePrice, + quotePrice, + -9 + ); + + assertEq(result.price, 2000000000); + assertEq(result.conf, 40000000); + assertEq(result.expo, -9); + } + + function testGetPriceInQuote_LargeExponentDifference() public view { + PythStructs.Price memory basePrice = PythStructs.Price({ + price: 100000000000000, + conf: 1000000000000, + expo: -14, + publishTime: block.timestamp + }); + PythStructs.Price memory quotePrice = PythStructs.Price({ + price: 5, + conf: 1, + expo: -1, + publishTime: block.timestamp + }); + + PythStructs.Price memory result = CombinePythPrice.getPriceInQuote( + basePrice, + quotePrice, + -9 + ); + + assertEq(result.price, 2000000000); + assertEq(result.conf, 420000000); + assertEq(result.expo, -9); + } + + ////////////////////// + // addPrice // + ////////////////////// + + function testAddPricesWithSameExponent() public pure { + PythStructs.Price memory p1 = PythStructs.Price({ + price: 100000000, + conf: 1000000, + expo: -8, + publishTime: 1625097600 + }); + + PythStructs.Price memory p2 = PythStructs.Price({ + price: 200000000, + conf: 2000000, + expo: -8, + publishTime: 1625097700 + }); + + PythStructs.Price memory result = CombinePythPrice.addPrices(p1, p2); + + assertEq(result.price, 300000000); + assertEq(result.conf, 3000000); + assertEq(result.expo, -8); + assertEq(result.publishTime, 1625097600); + } + + function testAddPricesWithNegativeValues() public pure { + PythStructs.Price memory p1 = PythStructs.Price({ + price: -100000000, + conf: 1000000, + expo: -8, + publishTime: 1625097600 + }); + + PythStructs.Price memory p2 = PythStructs.Price({ + price: 200000000, + conf: 2000000, + expo: -8, + publishTime: 1625097700 + }); + + PythStructs.Price memory result = CombinePythPrice.addPrices(p1, p2); + + assertEq(result.price, 100000000); + assertEq(result.conf, 3000000); + assertEq(result.expo, -8); + assertEq(result.publishTime, 1625097600); + } + + function testAddPricesWithZeroValues() public pure { + PythStructs.Price memory p1 = PythStructs.Price({ + price: 0, + conf: 0, + expo: -8, + publishTime: 1625097600 + }); + + PythStructs.Price memory p2 = PythStructs.Price({ + price: 200000000, + conf: 2000000, + expo: -8, + publishTime: 1625097700 + }); + + PythStructs.Price memory result = CombinePythPrice.addPrices(p1, p2); + + assertEq(result.price, 200000000); + assertEq(result.conf, 2000000); + assertEq(result.expo, -8); + assertEq(result.publishTime, 1625097600); + } + + function testAddPricesWithDifferentExponents() public { + PythStructs.Price memory p1 = PythStructs.Price({ + price: 100000000, + conf: 1000000, + expo: -8, + publishTime: 1625097600 + }); + + PythStructs.Price memory p2 = PythStructs.Price({ + price: 200000000, + conf: 2000000, + expo: -9, + publishTime: 1625097700 + }); + + vm.expectRevert( + abi.encodeWithSelector( + CombinePythPrice.AddingPricesOfDifferentExponents.selector, + -8, + -9 + ) + ); + CombinePythPrice.addPrices(p1, p2); + } +}