diff --git a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol index cac977ce5c..f90762451f 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol @@ -370,33 +370,190 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { } contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { + function assertCrossRateEquals( + int64 price1, + int32 expo1, + int64 price2, + int32 expo2, + int32 targetExpo, + int256 expectedPrice + ) internal { + int256 price = PythUtils.deriveCrossRate(price1, expo1, price2, expo2, targetExpo); + assertEq(price, expectedPrice); + } + + function assertCrossRateReverts( + int64 price1, + int32 expo1, + int64 price2, + int32 expo2, + int32 targetExpo, + bytes4 expectedError + ) internal { + vm.expectRevert(expectedError); + PythUtils.deriveCrossRate(price1, expo1, price2, expo2, targetExpo); + } + function testConvertToUnit() public { - // Price can't be negative - vm.expectRevert(); + + // Test 1: Price can't be negative + vm.expectRevert(PythErrors.NegativeInputPrice.selector); PythUtils.convertToUint(-100, -5, 18); - // Exponent can't be positive - vm.expectRevert(); - PythUtils.convertToUint(100, 5, 18); + // Test 2: Exponent can't be less than -255 + vm.expectRevert(PythErrors.InvalidInputExpo.selector); + PythUtils.convertToUint(100, -256, 18); + // Test 3: This test will fail as the 10 ** 237 is too large for a uint256 + vm.expectRevert(PythErrors.ExponentOverflow.selector); + assertEq(PythUtils.convertToUint(100, -255, 18), 0); + + // Test 4: Combined Exponent can't be greater than 58 and less than -58 + // See the calculation how we came up with 58 in PythUtils.sol + vm.expectRevert(PythErrors.ExponentOverflow.selector); + assertEq(PythUtils.convertToUint(100, 50, 9), 0); // 50 + 9 = 59 > 58 + vm.expectRevert(PythErrors.ExponentOverflow.selector); + assertEq(PythUtils.convertToUint(100, -96, 37), 0); // -96 + 37 = -59 < -58 + + // Test 5: Negative Exponent Tests // Price with 18 decimals and exponent -5 assertEq( PythUtils.convertToUint(100, -5, 18), - 1000000000000000 // 100 * 10^13 + 100_0_000_000_000_000 // 100 * 10^13 ); - // Price with 9 decimals and exponent -2 assertEq( PythUtils.convertToUint(100, -2, 9), - 1000000000 // 100 * 10^7 + 100_0_000_000 // 100 * 10^7 ); - // Price with 4 decimals and exponent -5 + // Test 6: Price with 4 decimals and exponent -5 assertEq(PythUtils.convertToUint(100, -5, 4), 10); - // Price with 5 decimals and exponent -2 + // Test 7: Price with 5 decimals and exponent -2 // @note: We will lose precision here as price is // 0.00001 and we are targetDecimals is 2. assertEq(PythUtils.convertToUint(100, -5, 2), 0); + assertEq(PythUtils.convertToUint(123, -8, 5), 0); + + // Test 8: Positive Exponent Tests + // Price with 18 decimals and exponent 5 + assertEq(PythUtils.convertToUint(100, 5, 18), 100_00_000_000_000_000_000_000_000); // 100 with 23 zeros + // Test 9: Price with 9 decimals and exponent 2 + assertEq(PythUtils.convertToUint(100, 2, 9), 100_00_000_000_000); // 100 with 11 zeros + + // Test 10: Price with 2 decimals and exponent 1 + assertEq(PythUtils.convertToUint(100, 1, 2), 100_000); // 100 with 3 zeros + + + // Special Cases + // Test 11: price = 0, any expo/decimals returns 0 + assertEq(PythUtils.convertToUint(0, -58, 0), 0); + assertEq(PythUtils.convertToUint(0, 0, 0), 0); + assertEq(PythUtils.convertToUint(0, 58, 0), 0); + assertEq(PythUtils.convertToUint(0, -58, 58), 0); + + // Test 12: smallest positive price, maximum downward exponent (should round to zero) + assertEq(PythUtils.convertToUint(1, -58, 0), 0); + assertEq(PythUtils.convertToUint(1, -58, 58), 1); + + // Test 13: deltaExponent == 0 (should be identical to price) + assertEq(PythUtils.convertToUint(123456, 0, 0), 123456); + assertEq(PythUtils.convertToUint(123456, -5, 5), 123456); // -5 + 5 == 0 + + // Test 14: deltaExponent > 0 (should shift price up) + assertEq(PythUtils.convertToUint(123456, 5, 0), 12345600000); + assertEq(PythUtils.convertToUint(123456, 5, 2), 1234560000000); + + // Test 15: deltaExponent < 0 (should shift price down) + assertEq(PythUtils.convertToUint(123456, -5, 0), 1); + assertEq(PythUtils.convertToUint(123456, -5, 2), 123); + + // Test 16: division with truncation + assertEq(PythUtils.convertToUint(999, -2, 0), 9); // 999/100 = 9 (truncated) + assertEq(PythUtils.convertToUint(199, -2, 0), 1); // 199/100 = 1 (truncated) + assertEq(PythUtils.convertToUint(99, -2, 0), 0); // 99/100 = 0 (truncated) + + // Test 17: Big price and scaling, but outside of bounds + vm.expectRevert(PythErrors.ExponentOverflow.selector); + assertEq(PythUtils.convertToUint(100_000_000, 10, 50),0); + + // Test 18: Big price and scaling + assertEq(PythUtils.convertToUint(100_000_000, -50, 10),0); // -50 + 10 = -40 > -58 + vm.expectRevert(PythErrors.ExponentOverflow.selector); + assertEq(PythUtils.convertToUint(100_000_000, 10, 50), 0); // 10 + 50 = 60 > 58 + + // Test 19: Decimals just save from truncation + assertEq(PythUtils.convertToUint(5, -1, 1), 5); // 5/10*10 = 5 + assertEq(PythUtils.convertToUint(5, -1, 2), 50); // 5/10*100 = 50 + + // 10. Test: Big price and scaling, should be inside the bounds + // We have to convert int64 -> int256 -> uint256 before multiplying by 10 ** 58 + assertEq(PythUtils.convertToUint(type(int64).max, 50, 8), uint256(int256(type(int64).max)) * 10 ** 58); // 50 + 8 = 58 + assertEq(PythUtils.convertToUint(type(int64).max, -64, 8), 0); // -64 + 8 = -56 > -58 + assertEq(PythUtils.convertToUint(type(int64).max, -50, 1), 0); // -64 + 1 = -63 < -58 + + // 11. Test: Big price and scaling, should be inside the bounds + vm.expectRevert(PythErrors.ExponentOverflow.selector); + assertEq(PythUtils.convertToUint(type(int64).max, 50, 9), 0); // 50 + 9 = 59 > 58 + vm.expectRevert(PythErrors.ExponentOverflow.selector); + assertEq(PythUtils.convertToUint(type(int64).max, -60, 1), 0); // -60 + 1 = -59 < -58 + + } + + function testDeriveCrossRate() public { + + // Test 1: Prices can't be negative + assertCrossRateReverts(-100, -2, 100, -2, 5, PythErrors.NegativeInputPrice.selector); + assertCrossRateReverts(100, -2, -100, -2, 5, PythErrors.NegativeInputPrice.selector); + assertCrossRateReverts(-100, -2, -100, -2, 5, PythErrors.NegativeInputPrice.selector); + + // Test 2: Exponent can't be less than -255 + assertCrossRateReverts(100, -256, 100, -2, 5, PythErrors.InvalidInputExpo.selector); + assertCrossRateReverts(100, -2, 100, -256, 5, PythErrors.InvalidInputExpo.selector); + assertCrossRateReverts(100, -256, 100, -256, 5, PythErrors.InvalidInputExpo.selector); + + // Test 3: Basic Tests with negative exponents + assertCrossRateEquals(500, -8, 500, -8, -5, 100000); + assertCrossRateEquals(10_000, -8, 100, -2, -5, 10); + assertCrossRateEquals(10_000, -2, 100, -8, -5, 100_00_000_000_000); + + // Test 4: Basic Tests with positive exponents + assertCrossRateEquals(100, 2, 100, 2, -5, 100000); // 100 * 10^2 / 100 * 10^2 = 10000 / 10000 = 1 == 100000 * 10^-5 + // We will loose preistion as the the target exponent is 5 making the price 0.00001 + assertCrossRateEquals(100, 8, 100, 8, 5, 0); + assertCrossRateEquals(100, 2, 100, 2, -5, 100000); // 100 * 10^2 / 100 * 10^2 = 10000 / 10000 = 1 + // We will loose preistion as the the target exponent is 5 making the price 0.00001 + assertCrossRateEquals(100, 2, 100, 2, 5, 0); + + // Test 5: Different Exponent Tests + assertCrossRateEquals(10_000, -2, 100, -4, 0, 10_000); // 10_000 / 100 = 100 * 10(-2 - -4) = 10_000 with 0 decimals = 10_000 + assertCrossRateEquals(10_000, -2, 100, -4, 5, 0); // 10_000 / 100 = 100 * 10(-2 - -4) = 10_000 with 5 decimals = 0 + assertCrossRateEquals(10_000, -2, 10_000, -1, 5, 0); // It will truncate to 0 + assertCrossRateEquals(10_000, -10, 10_000, -2, 0, 0); // It will truncate to 0 + + assertCrossRateEquals(100_000_000, -2, 100, -8, -8, 100_000_000_000_000_000_000); // 100_000_000 / 100 = 1_000_000 * 10(-2 - -8) = 1000000 * 10^6 = 1000000000000 + + // Exponent Edge Tests + console.log("\nExponent Edge Tests"); + assertCrossRateEquals(10_000, 0, 100, 0, 0, 100); + assertCrossRateReverts(10_000, 0, 100, 0, -255, PythErrors.ExponentOverflow.selector); + assertCrossRateReverts(10_000, 0, 100, -255, -255, PythErrors.ExponentOverflow.selector); + assertCrossRateReverts(10_000, -255, 100, 0, 0, PythErrors.ExponentOverflow.selector); + assertCrossRateReverts(10_000, -255, 100, -178, -5, PythErrors.ExponentOverflow.selector); + + console.log("\nRealistic Tests"); + // Realistic Tests + // Test case 1: (StEth/Eth / Eth/USD = ETH/BTC) + int256 price = PythUtils.deriveCrossRate(206487956502, -8, 206741615681, -8, -8); + assertApproxEqRel(price, 100000000, 9e17); // $1 + + // Test case 2: + price = PythUtils.deriveCrossRate(520010, -8, 38591, -8, -8); + assertApproxEqRel(price, 1347490347, 9e17); // $13.47 + + // Test case 3: + price = PythUtils.deriveCrossRate(520010, -8, 38591, -8, -12); + assertApproxEqRel(price, 13474903475432, 9e17); // $13.47 } } diff --git a/target_chains/ethereum/sdk/solidity/Math.sol b/target_chains/ethereum/sdk/solidity/Math.sol new file mode 100644 index 0000000000..488f88f823 --- /dev/null +++ b/target_chains/ethereum/sdk/solidity/Math.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.3.0) (utils/math/Math.sol) +// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.3.0/contracts/utils/math/Math.sol + +// note: We only copy the subset of the library methods that we need. + +pragma solidity ^0.8.0; + +library Math { + + /// @dev division or modulo by zero + uint256 internal constant DIVISION_BY_ZERO = 0x12; + /// @dev arithmetic underflow or overflow + uint256 internal constant UNDER_OVERFLOW = 0x11; + + + /** + * @dev Return the 512-bit multiplication of two uint256. + * + * The result is stored in two 256 variables such that product = high * 2²⁵⁶ + low. + */ + function mul512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) { + // 512-bit multiply [high low] = x * y. Compute the product mod 2²⁵⁶ and mod 2²⁵⁶ - 1, then use + // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 + // variables such that product = high * 2²⁵⁶ + low. + /// @solidity memory-safe-assembly + assembly { + let mm := mulmod(a, b, not(0)) + low := mul(a, b) + high := sub(sub(mm, low), lt(mm, low)) + } + } + + /** + * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant. + * + * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone. + * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute + * one branch when needed, making this function more expensive. + */ + function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) { + unchecked { + // branchless ternary works because: + // b ^ (a ^ b) == a + // b ^ 0 == b + return b ^ ((a ^ b) * toUint(condition)); + } + } + + /** + * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump. + */ + function toUint(bool b) internal pure returns (uint256 u) { + /// @solidity memory-safe-assembly + assembly { + u := iszero(iszero(b)) + } + } + + /// @dev Reverts with a panic code. Recommended to use with + /// the internal constants with predefined codes. + function panic(uint256 code) internal pure { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, 0x4e487b71) + mstore(0x20, code) + revert(0x1c, 0x24) + } + } + + + /** + * @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or + * denominator == 0. + * + * Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by + * Uniswap Labs also under MIT license. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { + unchecked { + (uint256 high, uint256 low) = mul512(x, y); + + // Handle non-overflow cases, 256 by 256 division. + if (high == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. + return low / denominator; + } + + // Make sure the result is less than 2²⁵⁶. Also prevents denominator == 0. + if (denominator <= high) { + panic(ternary(denominator == 0, DIVISION_BY_ZERO, UNDER_OVERFLOW)); + } + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [high low]. + uint256 remainder; + /// @solidity memory-safe-assembly + assembly { + // Compute remainder using mulmod. + remainder := mulmod(x, y, denominator) + + // Subtract 256 bit number from 512 bit number. + high := sub(high, gt(remainder, low)) + low := sub(low, remainder) + } + + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. + // Always >= 1. See https://cs.stackexchange.com/q/138556/92363. + + uint256 twos = denominator & (0 - denominator); + /// @solidity memory-safe-assembly + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [high low] by twos. + low := div(low, twos) + + // Flip twos such that it is 2²⁵⁶ / twos. If twos is zero, then it becomes one. + twos := add(div(sub(0, twos), twos), 1) + } + + // Shift in bits from high into low. + low |= high * twos; + + // Invert denominator mod 2²⁵⁶. Now that denominator is an odd number, it has an inverse modulo 2²⁵⁶ such + // that denominator * inv ≡ 1 mod 2²⁵⁶. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv ≡ 1 mod 2⁴. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also + // works in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2⁸ + inverse *= 2 - denominator * inverse; // inverse mod 2¹⁶ + inverse *= 2 - denominator * inverse; // inverse mod 2³² + inverse *= 2 - denominator * inverse; // inverse mod 2⁶⁴ + inverse *= 2 - denominator * inverse; // inverse mod 2¹²⁸ + inverse *= 2 - denominator * inverse; // inverse mod 2²⁵⁶ + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2²⁵⁶. Since the preconditions guarantee that the outcome is + // less than 2²⁵⁶, this is the final result. We don't need to compute the high bits of the result and high + // is no longer required. + result = low * inverse; + return result; + } + } + + // /** + // * @dev Calculates x * y / denominator with full precision, following the selected rounding direction. + // */ + // function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { + // return mulDiv(x, y, denominator) + toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0); + // } + + /** + * @dev Returns the absolute unsigned value of a signed value. + */ + function abs(int256 n) internal pure returns (uint256) { + unchecked { + // Formula from the "Bit Twiddling Hacks" by Sean Eron Anderson. + // Since `n` is a signed integer, the generated bytecode will use the SAR opcode to perform the right shift, + // taking advantage of the most significant (or "sign" bit) in two's complement representation. + // This opcode adds new most significant bits set to the value of the previous most significant bit. As a result, + // the mask will either be `bytes32(0)` (if n is positive) or `~bytes32(0)` (if n is negative). + int256 mask = n >> 255; + + // A `bytes32(0)` mask leaves the input unchanged, while a `~bytes32(0)` mask complements it. + return uint256((n + mask) ^ mask); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with a success flag (no overflow). + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + uint256 c = a * b; + /// @solidity memory-safe-assembly + assembly { + // Only true when the multiplication doesn't overflow + // (c / a == b) || (a == 0) + success := or(eq(div(c, a), b), iszero(a)) + } + // equivalent to: success ? c : 0 + result = c * toUint(success); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a success flag (no division by zero). + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + success = b > 0; + /// @solidity memory-safe-assembly + assembly { + // The `DIV` opcode returns zero when the denominator is 0. + result := div(a, b) + } + } + } + +} \ No newline at end of file diff --git a/target_chains/ethereum/sdk/solidity/PythErrors.sol b/target_chains/ethereum/sdk/solidity/PythErrors.sol index ad98f11e30..aab4475e62 100644 --- a/target_chains/ethereum/sdk/solidity/PythErrors.sol +++ b/target_chains/ethereum/sdk/solidity/PythErrors.sol @@ -49,4 +49,12 @@ library PythErrors { error InvalidTwapUpdateData(); // The twap update data set is invalid. error InvalidTwapUpdateDataSet(); + // The Input Price is negative. + error NegativeInputPrice(); + // The Input Exponent is invalid. + error InvalidInputExpo(); + // The combined price is greater than int64.max. + error CombinedPriceOverflow(); + // The exponent is greater than 77 or less than -77. + error ExponentOverflow(); } diff --git a/target_chains/ethereum/sdk/solidity/PythUtils.sol b/target_chains/ethereum/sdk/solidity/PythUtils.sol index 96f08e8a8c..d098113447 100644 --- a/target_chains/ethereum/sdk/solidity/PythUtils.sol +++ b/target_chains/ethereum/sdk/solidity/PythUtils.sol @@ -2,8 +2,11 @@ pragma solidity ^0.8.0; import "./PythStructs.sol"; +import "./PythErrors.sol"; +import "./Math.sol"; library PythUtils { + uint8 public constant PRECISION = 36; /// @notice Converts a Pyth price to a uint256 with a target number of decimals /// @param price The Pyth price /// @param expo The Pyth price exponent @@ -12,25 +15,105 @@ library PythUtils { /// @dev Function will lose precision if targetDecimals is less than the Pyth price decimals. /// This method will truncate any digits that cannot be represented by the targetDecimals. /// e.g. If the price is 0.000123 and the targetDecimals is 2, the result will be 0 + /// This function will overflow if the combined exponent(targetDecimals + expo) is greater than 58 or less than -58. + /// This function will also revert if prices combined with the targetDecimals are greater than 10 ** 58 or less than 10 ** -58. function convertToUint( int64 price, int32 expo, uint8 targetDecimals ) public pure returns (uint256) { - if (price < 0 || expo > 0 || expo < -255) { - revert(); + if (price < 0) { + revert PythErrors.NegativeInputPrice(); } + if (expo < -255) { + revert PythErrors.InvalidInputExpo(); + } + + // If targetDecimals is 6, we want to multiply the final price by 10 ** -6 + // So the delta exponent is targetDecimals + currentExpo + int32 deltaExponent = int32(uint32(targetDecimals)) + expo; - uint8 priceDecimals = uint8(uint32(-1 * expo)); + // Bounds check: prevent overflow/underflow with base 10 exponentiation + // Calculation: 10 ** n <= (2 ** 256 - 63) - 1 + // n <= log10((2 ** 193) - 1) + // n <= 58.2 + if (deltaExponent > 58 || deltaExponent < -58) revert PythErrors.ExponentOverflow(); - if (targetDecimals >= priceDecimals) { - return - uint(uint64(price)) * - 10 ** uint32(targetDecimals - priceDecimals); + // We can safely cast the price to uint256 because the above condition will revert if the price is negative + uint256 unsignedPrice = uint256(uint64(price)); + + if (deltaExponent > 0) { + (bool success, uint256 result) = Math.tryMul(unsignedPrice, 10 ** uint32(deltaExponent)); + if (!success) { + revert PythErrors.CombinedPriceOverflow(); + } + return result; } else { - return - uint(uint64(price)) / - 10 ** uint32(priceDecimals - targetDecimals); + (bool success, uint256 result) = Math.tryDiv(unsignedPrice, 10 ** uint(Math.abs(deltaExponent))); + if (!success) { + revert PythErrors.CombinedPriceOverflow(); + } + return result; + } + } + + /// @notice Combines two prices to get a cross-rate + /// @param price1 The first price (a/b) + /// @param expo1 The exponent of the first price + /// @param price2 The second price (c/b) + /// @param expo2 The exponent of the second price + /// @param targetExpo The target number of decimals for the cross-rate + /// @return crossRate The cross-rate (a/c) + /// @dev This function will revert if either price is negative or if the exponents are invalid. + /// @dev This function will also revert if the cross-rate is greater than int64.max + /// @notice This function doesn't return the combined confidence interval. + function deriveCrossRate( + int64 price1, + int32 expo1, + int64 price2, + int32 expo2, + int32 targetExpo + ) public pure returns (int256 crossRate) { + // Check if the input prices are negative + if (price1 < 0 || price2 < 0) { + revert PythErrors.NegativeInputPrice(); + } + // Check if the input exponents are valid and not less than -255 + if (expo1 < -255 || expo2 < -255) { + revert PythErrors.InvalidInputExpo(); } + + // We can safely cast the prices to uint64 because we know they are positive + uint256 fixedPointPrice = Math.mulDiv(uint64(price1), 10 ** PRECISION, uint64(price2)); + int32 combinedExpo = expo1 - expo2 - int32(int8(PRECISION)); + + int32 factoredExpo = combinedExpo - targetExpo; + + if (factoredExpo > 77 || factoredExpo < -77) revert PythErrors.ExponentOverflow(); + // Convert the price to the target exponent + // We can't use the convertToUint function because it accepts int64 and we need to use uint256 + // It makes more sense to ask users for exponent and not decimals here. + if (factoredExpo > 0) { + // If combinedExpo is greater than targetExpo, we need to multiply + (bool success, uint256 result) = Math.tryMul(fixedPointPrice, 10 ** uint32(factoredExpo)); + if (!success) { + revert PythErrors.CombinedPriceOverflow(); + } + fixedPointPrice = result; + } else if (factoredExpo < 0) { + // If combinedExpo is less than targetExpo, we need to divide + (bool success, uint256 result) = Math.tryDiv(fixedPointPrice, 10 ** uint32(Math.abs(factoredExpo))); + if (!success) { + revert PythErrors.CombinedPriceOverflow(); + } + fixedPointPrice = result; + } + + // Check if the combined price fits in int256 + if (fixedPointPrice > uint256((type(int256).max))) { + revert PythErrors.CombinedPriceOverflow(); + } + + return int256(fixedPointPrice); } }