diff --git a/package-lock.json b/package-lock.json index b3e1df662b..87a4d858a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59453,7 +59453,47 @@ "devDependencies": { "abi_generator": "*", "prettier": "^2.7.1", - "prettier-plugin-solidity": "^1.0.0-rc.1" + "prettier-plugin-solidity": "^1.0.0-rc.1", + "solc": "^0.8.25" + } + }, + "target_chains/ethereum/sdk/solidity/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "target_chains/ethereum/sdk/solidity/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "target_chains/ethereum/sdk/solidity/node_modules/solc": { + "version": "0.8.25", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.25.tgz", + "integrity": "sha512-7P0TF8gPeudl1Ko3RGkyY6XVCxe2SdD/qQhtns1vl3yAbK/PDifKDLHGtx1t7mX3LgR7ojV7Fg/Kc6Q9D2T8UQ==", + "dev": true, + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=10.0.0" } }, "target_chains/solana/sdk/js/pyth_solana_receiver": { @@ -71066,7 +71106,37 @@ "requires": { "abi_generator": "*", "prettier": "^2.7.1", - "prettier-plugin-solidity": "^1.0.0-rc.1" + "prettier-plugin-solidity": "^1.0.0-rc.1", + "solc": "*" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "solc": { + "version": "0.8.25", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.25.tgz", + "integrity": "sha512-7P0TF8gPeudl1Ko3RGkyY6XVCxe2SdD/qQhtns1vl3yAbK/PDifKDLHGtx1t7mX3LgR7ojV7Fg/Kc6Q9D2T8UQ==", + "dev": true, + "requires": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + } + } } }, "@pythnetwork/pyth-solana-receiver": { 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 db9bf94c48..73149425b3 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol @@ -12,6 +12,7 @@ import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; import "@pythnetwork/pyth-sdk-solidity/IPythEvents.sol"; import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; +import "@pythnetwork/pyth-sdk-solidity/PythUtils.sol"; import "forge-std/Test.sol"; import "./WormholeTestUtils.t.sol"; @@ -275,3 +276,35 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { } } } + +contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { + function testConvertToUnit() public { + // Price can't be negative + vm.expectRevert(); + PythUtils.convertToUint(-100, -5, 18); + + // Exponent can't be positive + vm.expectRevert(); + PythUtils.convertToUint(100, 5, 18); + + // Price with 18 decimals and exponent -5 + assertEq( + PythUtils.convertToUint(100, -5, 18), + 1000000000000000 // 100 * 10^13 + ); + + // Price with 9 decimals and exponent -2 + assertEq( + PythUtils.convertToUint(100, -2, 9), + 1000000000 // 100 * 10^7 + ); + + // Price with 4 decimals and exponent -5 + assertEq(PythUtils.convertToUint(100, -5, 4), 10); + + // 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); + } +} diff --git a/target_chains/ethereum/sdk/solidity/PythUtils.sol b/target_chains/ethereum/sdk/solidity/PythUtils.sol new file mode 100644 index 0000000000..04b7f51f34 --- /dev/null +++ b/target_chains/ethereum/sdk/solidity/PythUtils.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +library PythUtils { + /// @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 + /// @param targetDecimals The target number of decimals + /// @return The price as a uint256 + /// @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 + function convertToUint( + int64 price, + int32 expo, + uint8 targetDecimals + ) public pure returns (uint256) { + if (price < 0 || expo > 0 || expo < -255) { + revert(); + } + + uint8 priceDecimals = uint8(uint32(-1 * expo)); + + if (targetDecimals >= priceDecimals) { + return + uint(uint64(price)) * + 10 ** uint32(targetDecimals - priceDecimals); + } else { + return + uint(uint64(price)) / + 10 ** uint32(priceDecimals - targetDecimals); + } + } +} diff --git a/target_chains/ethereum/sdk/solidity/abis/PythUtils.json b/target_chains/ethereum/sdk/solidity/abis/PythUtils.json new file mode 100644 index 0000000000..c30f138950 --- /dev/null +++ b/target_chains/ethereum/sdk/solidity/abis/PythUtils.json @@ -0,0 +1,31 @@ +[ + { + "inputs": [ + { + "internalType": "int64", + "name": "price", + "type": "int64" + }, + { + "internalType": "int32", + "name": "expo", + "type": "int32" + }, + { + "internalType": "uint8", + "name": "targetDecimals", + "type": "uint8" + } + ], + "name": "convertToUint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + } +] diff --git a/target_chains/ethereum/sdk/solidity/package.json b/target_chains/ethereum/sdk/solidity/package.json index 2865a73ba8..4a9d3e7257 100644 --- a/target_chains/ethereum/sdk/solidity/package.json +++ b/target_chains/ethereum/sdk/solidity/package.json @@ -9,7 +9,7 @@ }, "scripts": { "format": "npx prettier --write .", - "generate-abi": "npx generate-abis IPyth IPythEvents AbstractPyth MockPyth PythErrors", + "generate-abi": "npx generate-abis IPyth IPythEvents AbstractPyth MockPyth PythErrors PythUtils", "check-abi": "git diff --exit-code abis", "build": "solcjs --bin MockPyth.sol --base-path . -o build/" }, @@ -25,8 +25,9 @@ }, "homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/ethereum/sdk/solidity", "devDependencies": { + "abi_generator": "*", "prettier": "^2.7.1", "prettier-plugin-solidity": "^1.0.0-rc.1", - "abi_generator": "*" + "solc": "^0.8.25" } }