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 33ee71645b..d557ce1fb9 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol @@ -371,21 +371,27 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { function assertCrossRateEquals( - int64 price1, - int32 expo1, - int64 price2, + int64 price1, + int32 expo1, + int64 price2, int32 expo2, - int32 targetExpo, + int32 targetExpo, uint256 expectedPrice ) internal { - uint256 price = PythUtils.deriveCrossRate(price1, expo1, price2, expo2, targetExpo); + uint256 price = PythUtils.deriveCrossRate( + price1, + expo1, + price2, + expo2, + targetExpo + ); assertEq(price, expectedPrice); } function assertCrossRateReverts( - int64 price1, - int32 expo1, - int64 price2, + int64 price1, + int32 expo1, + int64 price2, int32 expo2, int32 targetExpo, bytes4 expectedError @@ -395,7 +401,6 @@ contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { } function testConvertToUnit() public { - // Test 1: Price can't be negative vm.expectRevert(PythErrors.NegativeInputPrice.selector); PythUtils.convertToUint(-100, -5, 18); @@ -438,13 +443,15 @@ contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { // 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 + 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 - + assertEq(PythUtils.convertToUint(100, 1, 2), 100_000); // 100 with 3 zeros // Special Cases // Test 11: price = 0, any expo/decimals returns 0 @@ -464,7 +471,7 @@ contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { // 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); @@ -476,22 +483,25 @@ contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { // Test 17: Big price and scaling, but outside of bounds vm.expectRevert(PythErrors.ExponentOverflow.selector); - assertEq(PythUtils.convertToUint(100_000_000, 10, 50),0); + 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, -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, 50, 8), + uint256(int256(type(int64).max)) * 10 ** 58 + ); // 50 + 8 = 58 vm.expectRevert(PythErrors.ExponentOverflow.selector); - assertEq(PythUtils.convertToUint(type(int64).max, 50, 9), 0); + assertEq(PythUtils.convertToUint(type(int64).max, 50, 9), 0); assertEq(PythUtils.convertToUint(type(int64).max, -64, 8), 0); // -64 + 8 = -56 > -58 assertEq(PythUtils.convertToUint(type(int64).max, -50, 1), 0); // -50 + 1 = -49 > -58 @@ -500,50 +510,138 @@ contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { 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); + 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); + 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 + ); // Target exponent can't be less than -255 - assertCrossRateReverts(100, -2, 100, -2, -256, PythErrors.InvalidInputExpo.selector); + assertCrossRateReverts( + 100, + -2, + 100, + -2, + -256, + PythErrors.InvalidInputExpo.selector + ); - // Test 3: Basic Tests with negative exponents - assertCrossRateEquals(500, -8, 500, -8, -5, 100000); + // 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, 8, 100, 8, 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 + 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 // Test 6: Exponent 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); - + 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 + ); // Test 7: Max int64 price and scaling - assertCrossRateEquals(type(int64).max, 0, 1, 0, 0, uint256(int256(type(int64).max))); + assertCrossRateEquals( + type(int64).max, + 0, + 1, + 0, + 0, + uint256(int256(type(int64).max)) + ); assertCrossRateEquals(1, 0, type(int64).max, 0, 0, 0); assertCrossRateEquals(type(int64).max, 0, type(int64).max, 0, 0, 1); // type(int64).max is approx 9.223e18 @@ -551,26 +649,52 @@ contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { // 1 / type(int64).max is approx 1.085e-19 assertCrossRateEquals(1, 0, type(int64).max, 0, -19, 1); // type(int64).max * 10 ** 58 / 1 - assertCrossRateEquals(type(int64).max, 50, 1, -8, 0, uint256(int256(type(int64).max)) * 10 ** 58); + assertCrossRateEquals( + type(int64).max, + 50, + 1, + -8, + 0, + uint256(int256(type(int64).max)) * 10 ** 58 + ); // 1 / (type(int64).max * 10 ** 58) assertCrossRateEquals(1, 0, type(int64).max, 50, 8, 0); // type(int64).max * 10 ** 59 / 1 - assertCrossRateReverts(type(int64).max, 50, 1, -9, 0, PythErrors.ExponentOverflow.selector); + assertCrossRateReverts( + type(int64).max, + 50, + 1, + -9, + 0, + PythErrors.ExponentOverflow.selector + ); // 1 / (type(int64).max * 10 ** 59) - assertCrossRateReverts(1, 0, type(int64).max, 50, 9, PythErrors.ExponentOverflow.selector); - + assertCrossRateReverts( + 1, + 0, + type(int64).max, + 50, + 9, + PythErrors.ExponentOverflow.selector + ); // Realistic Tests // Test case 1: (StEth/Eth / Eth/USD = ETH/BTC) - uint256 price = PythUtils.deriveCrossRate(206487956502, -8, 206741615681, -8, -8); + uint256 price = PythUtils.deriveCrossRate( + 206487956502, + -8, + 206741615681, + -8, + -8 + ); assertApproxEqRel(price, 100000000, 9e17); // $1 - // Test case 2: + // Test case 2: price = PythUtils.deriveCrossRate(520010, -8, 38591, -8, -8); assertApproxEqRel(price, 1347490347, 9e17); // $13.47 - // Test case 3: + // 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 index 488f88f823..15352a380e 100644 --- a/target_chains/ethereum/sdk/solidity/Math.sol +++ b/target_chains/ethereum/sdk/solidity/Math.sol @@ -7,19 +7,20 @@ 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) { + 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. @@ -38,7 +39,11 @@ library Math { * 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) { + function ternary( + bool condition, + uint256 a, + uint256 b + ) internal pure returns (uint256) { unchecked { // branchless ternary works because: // b ^ (a ^ b) == a @@ -68,7 +73,6 @@ library Math { } } - /** * @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or * denominator == 0. @@ -76,7 +80,11 @@ library Math { * 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) { + function mulDiv( + uint256 x, + uint256 y, + uint256 denominator + ) internal pure returns (uint256 result) { unchecked { (uint256 high, uint256 low) = mul512(x, y); @@ -90,7 +98,9 @@ library Math { // Make sure the result is less than 2²⁵⁶. Also prevents denominator == 0. if (denominator <= high) { - panic(ternary(denominator == 0, DIVISION_BY_ZERO, UNDER_OVERFLOW)); + panic( + ternary(denominator == 0, DIVISION_BY_ZERO, UNDER_OVERFLOW) + ); } /////////////////////////////////////////////// @@ -178,10 +188,13 @@ library Math { /** * @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) { + function tryMul( + uint256 a, + uint256 b + ) internal pure returns (bool success, uint256 result) { unchecked { uint256 c = a * b; - /// @solidity memory-safe-assembly + /// @solidity memory-safe-assembly assembly { // Only true when the multiplication doesn't overflow // (c / a == b) || (a == 0) @@ -195,15 +208,17 @@ library Math { /** * @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) { + function tryDiv( + uint256 a, + uint256 b + ) internal pure returns (bool success, uint256 result) { unchecked { success = b > 0; - /// @solidity memory-safe-assembly + /// @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/PythUtils.sol b/target_chains/ethereum/sdk/solidity/PythUtils.sol index e5b9638ecf..3e6d00cf73 100644 --- a/target_chains/ethereum/sdk/solidity/PythUtils.sol +++ b/target_chains/ethereum/sdk/solidity/PythUtils.sol @@ -34,17 +34,21 @@ library PythUtils { // So the delta exponent is targetDecimals + currentExpo int32 deltaExponent = int32(uint32(targetDecimals)) + expo; - // Bounds check: prevent overflow/underflow with base 10 exponentiation + // 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 (deltaExponent > 58 || deltaExponent < -58) + revert PythErrors.ExponentOverflow(); // We can safely cast the price to uint256 because the above condition will revert if the price is negative - uint256 unsignedPrice = uint256(uint64(price)); + uint256 unsignedPrice = uint256(uint64(price)); if (deltaExponent > 0) { - (bool success, uint256 result) = Math.tryMul(unsignedPrice, 10 ** uint32(deltaExponent)); + (bool success, uint256 result) = Math.tryMul( + unsignedPrice, + 10 ** uint32(deltaExponent) + ); // This condition is unreachable since we validated deltaExponent bounds above. // But keeping it here for safety. if (!success) { @@ -52,7 +56,10 @@ library PythUtils { } return result; } else { - (bool success, uint256 result) = Math.tryDiv(unsignedPrice, 10 ** uint(Math.abs(deltaExponent))); + (bool success, uint256 result) = Math.tryDiv( + unsignedPrice, + 10 ** uint(Math.abs(deltaExponent)) + ); // This condition is unreachable since we validated deltaExponent bounds above. // But keeping it here for safety. if (!success) { @@ -89,21 +96,30 @@ library PythUtils { if (expo1 < -255 || expo2 < -255 || targetExponent < -255) { revert PythErrors.InvalidInputExpo(); } - + // note: This value can be negative. int64 deltaExponent = int64(expo1 - (expo2 + targetExponent)); - // Bounds check: prevent overflow/underflow with base 10 exponentiation + // 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 (deltaExponent > 58 || deltaExponent < -58) + revert PythErrors.ExponentOverflow(); uint256 result; if (deltaExponent > 0) { - result = Math.mulDiv(uint64(price1), 10 ** uint64(deltaExponent) , uint64(price2)); + result = Math.mulDiv( + uint64(price1), + 10 ** uint64(deltaExponent), + uint64(price2) + ); } else { - result = Math.mulDiv(uint64(price1), 1, 10 ** uint64(Math.abs(deltaExponent)) * uint64(price2)); + result = Math.mulDiv( + uint64(price1), + 1, + 10 ** uint64(Math.abs(deltaExponent)) * uint64(price2) + ); } return result; diff --git a/target_chains/ethereum/sdk/solidity/abis/PythErrors.json b/target_chains/ethereum/sdk/solidity/abis/PythErrors.json index def11cb07a..8388ff7f02 100644 --- a/target_chains/ethereum/sdk/solidity/abis/PythErrors.json +++ b/target_chains/ethereum/sdk/solidity/abis/PythErrors.json @@ -1,4 +1,14 @@ [ + { + "inputs": [], + "name": "CombinedPriceOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "ExponentOverflow", + "type": "error" + }, { "inputs": [], "name": "InsufficientFee", @@ -24,6 +34,11 @@ "name": "InvalidGovernanceTarget", "type": "error" }, + { + "inputs": [], + "name": "InvalidInputExpo", + "type": "error" + }, { "inputs": [], "name": "InvalidTwapUpdateData", @@ -54,6 +69,11 @@ "name": "InvalidWormholeVaa", "type": "error" }, + { + "inputs": [], + "name": "NegativeInputPrice", + "type": "error" + }, { "inputs": [], "name": "NoFreshUpdate", diff --git a/target_chains/ethereum/sdk/solidity/abis/PythUtils.json b/target_chains/ethereum/sdk/solidity/abis/PythUtils.json index c30f138950..e7e087974e 100644 --- a/target_chains/ethereum/sdk/solidity/abis/PythUtils.json +++ b/target_chains/ethereum/sdk/solidity/abis/PythUtils.json @@ -1,4 +1,37 @@ [ + { + "inputs": [], + "name": "CombinedPriceOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "ExponentOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInputExpo", + "type": "error" + }, + { + "inputs": [], + "name": "NegativeInputPrice", + "type": "error" + }, + { + "inputs": [], + "name": "PRECISION", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -27,5 +60,44 @@ ], "stateMutability": "pure", "type": "function" + }, + { + "inputs": [ + { + "internalType": "int64", + "name": "price1", + "type": "int64" + }, + { + "internalType": "int32", + "name": "expo1", + "type": "int32" + }, + { + "internalType": "int64", + "name": "price2", + "type": "int64" + }, + { + "internalType": "int32", + "name": "expo2", + "type": "int32" + }, + { + "internalType": "int32", + "name": "targetExponent", + "type": "int32" + } + ], + "name": "deriveCrossRate", + "outputs": [ + { + "internalType": "uint256", + "name": "crossRate", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" } ]