From 95be41d4a5effd08e5530cde8dd066ad686000c1 Mon Sep 17 00:00:00 2001 From: 0xHaku <32897506+0xHaku@users.noreply.github.com> Date: Sat, 29 Jun 2024 18:58:29 +0900 Subject: [PATCH 1/7] add Price Library --- target_chains/ethereum/sdk/solidity/Price.sol | 459 ++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 target_chains/ethereum/sdk/solidity/Price.sol diff --git a/target_chains/ethereum/sdk/solidity/Price.sol b/target_chains/ethereum/sdk/solidity/Price.sol new file mode 100644 index 0000000000..0f546dc50d --- /dev/null +++ b/target_chains/ethereum/sdk/solidity/Price.sol @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./PythStructs.sol"; + +library Price { + // Constants for working with Pyth's number representation + int32 private constant PD_EXPO = -9; + uint64 private constant PD_SCALE = 1_000_000_000; + uint64 private constant MAX_PD_V_U64 = (1 << 28) - 1; + + // Custom errors + error DivisionByZero(); + error ConfidenceOverflow(); + error ExponentsMustMatch(); + error InvalidDiscount(); + error InvalidPremiumRates(); + + /** + * @dev Get the current price in a different quote currency. + * @param self The base price + * @param quote The quote price + * @param resultExpo The desired exponent for the result + * @return The price in the quote currency + */ + function getPriceInQuote( + PythStructs.Price memory self, + PythStructs.Price memory quote, + int32 resultExpo + ) public pure returns (PythStructs.Price memory) { + PythStructs.Price memory divResult = div(self, quote); + return scale_to_exponent(divResult, resultExpo); + } + + /** + * @dev Get the valuation price of a collateral position. + * @param self The original price + * @param deposits Quantity of token deposited in the protocol + * @param deposits_endpoint Deposits right endpoint for the affine combination + * @param rate_discount_initial Initial discounted rate at 0 deposits (units given by discount_exponent) + * @param rate_discount_final Final discounted rate at deposits_endpoint deposits (units given by discount_exponent) + * @param discount_exponent The exponent to apply to the discounts + * @return The valuation price of the collateral + */ + function get_collateral_valuation_price( + PythStructs.Price memory self, + uint64 deposits, + uint64 deposits_endpoint, + uint64 rate_discount_initial, + uint64 rate_discount_final, + int32 discount_exponent + ) internal pure returns (PythStructs.Price memory) { + // rate_discount_initial should be >= rate_discount_final + if (rate_discount_initial < rate_discount_final) { + revert InvalidDiscount(); + } + + // get price versions of discounts + PythStructs.Price memory initial_percentage = PythStructs.Price({ + price: int64(rate_discount_initial), + conf: 0, + expo: discount_exponent, + publishTime: 0 + }); + + PythStructs.Price memory final_percentage = PythStructs.Price({ + price: int64(rate_discount_final), + conf: 0, + expo: discount_exponent, + publishTime: 0 + }); + + // get the interpolated discount as a price + PythStructs.Price memory discount_interpolated = affine_combination( + 0, + initial_percentage, + int64(deposits_endpoint), + final_percentage, + int64(deposits), + -9 + ); + + PythStructs.Price memory price_discounted = scale_to_exponent( + mul(self, discount_interpolated), + self.expo + ); + + return + PythStructs.Price({ + price: price_discounted.price, + conf: self.conf, + expo: price_discounted.expo, + publishTime: self.publishTime + }); + } + + /** + * @dev Get the valuation of a borrow position according to various parameters. + * @param borrows The quantity of token borrowed from the protocol + * @param borrows_endpoint The borrows right endpoint for the affine combination + * @param rate_premium_initial The initial premium at 0 borrows + * @param rate_premium_final The final premium at borrows_endpoint borrows + * @param premium_exponent The exponent to apply to the premiums above + * @return The price of the borrow valuation + */ + function get_borrow_valuation_price( + PythStructs.Price memory self, + uint64 borrows, + uint64 borrows_endpoint, + uint64 rate_premium_initial, + uint64 rate_premium_final, + int32 premium_exponent + ) internal pure returns (PythStructs.Price memory) { + // Valuation price should not decrease as amount of borrow grows, so rate_premium_initial + // should <= rate_premium_final + if (rate_premium_initial > rate_premium_final) { + revert InvalidPremiumRates(); + } + + // Get price versions of premiums + PythStructs.Price memory initial_percentage = PythStructs.Price({ + price: int64(rate_premium_initial), + conf: 0, + expo: premium_exponent, + publishTime: 0 + }); + + PythStructs.Price memory final_percentage = PythStructs.Price({ + price: int64(rate_premium_final), + conf: 0, + expo: premium_exponent, + publishTime: 0 + }); + + // Get the interpolated premium as a price + PythStructs.Price memory premium_interpolated = affine_combination( + 0, + initial_percentage, + int64(borrows_endpoint), + final_percentage, + int64(borrows), + -9 + ); + + // Get price premium, convert back to the original exponents we received the price in + PythStructs.Price memory price_premium = scale_to_exponent( + mul(self, premium_interpolated), + self.expo + ); + + return + PythStructs.Price({ + price: price_premium.price, + conf: self.conf, + expo: price_premium.expo, + publishTime: self.publishTime + }); + } + + /** + * @dev Perform an affine combination of two prices located at x coordinates x1 and x2, for query x coordinate x_query. + * @param x1 The x coordinate of the first point + * @param y1 The y coordinate of the first point, represented as a Price struct + * @param x2 The x coordinate of the second point, must be greater than x1 + * @param y2 The y coordinate of the second point, represented as a Price struct + * @param x_query The query x coordinate, at which we wish to impute a y value + * @param pre_add_expo The exponent to scale to, before final addition; essentially the final precision you want + * @return The price at the query x coordinate + */ + function affine_combination( + int64 x1, + PythStructs.Price memory y1, + int64 x2, + PythStructs.Price memory y2, + int64 x_query, + int32 pre_add_expo + ) internal pure returns (PythStructs.Price memory) { + if (x2 <= x1) { + revert("x2 must be greater than x1"); + } + + // Get the deltas for the x coordinates + // 1. compute A = x_query - x1 + int64 delta_q1 = x_query - x1; + // 2. compute B = x2 - x_query + int64 delta_2q = x2 - x_query; + // 3. compute C = x2 - x1 + int64 delta_21 = x2 - x1; + + // Get the relevant fractions of the deltas, with scaling + // 4. compute D = A / C + PythStructs.Price memory frac_q1 = fraction(delta_q1, delta_21); + // 5. compute E = B / C + PythStructs.Price memory frac_2q = fraction(delta_2q, delta_21); + + // Calculate products for left and right + // 6. compute F = y2 * D + PythStructs.Price memory left = mul(y2, frac_q1); + // 7. compute G = y1 * E + PythStructs.Price memory right = mul(y1, frac_2q); + + // Scaling + left = scale_to_exponent(left, pre_add_expo); + right = scale_to_exponent(right, pre_add_expo); + + // 8. compute H = F + G + return add(left, right); + } + + /** + * @dev Divide one price by another, propagating uncertainty. + * @param self The numerator price + * @param other The denominator price + * @return The resulting price + */ + function div( + PythStructs.Price memory self, + PythStructs.Price memory other + ) internal pure returns (PythStructs.Price memory) { + PythStructs.Price memory base = normalize(self); + other = normalize(other); + + if (other.price == 0) { + revert DivisionByZero(); + } + + (uint64 basePrice, int64 baseSign) = toUnsigned(base.price); + (uint64 otherPrice, int64 otherSign) = toUnsigned(other.price); + + uint64 midprice = (basePrice * PD_SCALE) / otherPrice; + int32 midpriceExpo = base.expo - other.expo + PD_EXPO; + + uint64 otherConfidencePct = (other.conf * PD_SCALE) / otherPrice; + uint128 conf = ((uint128(base.conf) * PD_SCALE) / otherPrice) + + ((uint128(otherConfidencePct) * midprice) / PD_SCALE); + + if (conf >= type(uint64).max) { + revert ConfidenceOverflow(); + } + + return + PythStructs.Price({ + price: int64(midprice) * baseSign * otherSign, + conf: uint64(conf), + expo: midpriceExpo, + publishTime: min(self.publishTime, other.publishTime) + }); + } + + /** + * @dev Add two prices, propagating uncertainty. + * @param self The first price + * @param other The second price + * @return The sum of the two prices + */ + function add( + PythStructs.Price memory self, + PythStructs.Price memory other + ) internal pure returns (PythStructs.Price memory) { + if (self.expo != other.expo) { + revert ExponentsMustMatch(); + } + + int64 price = self.price + other.price; + uint64 conf = self.conf + other.conf; + + return + PythStructs.Price({ + price: price, + conf: conf, + expo: self.expo, + publishTime: min(self.publishTime, other.publishTime) + }); + } + + /** + * @dev Multiply a price by a constant. + * @param self The price + * @param c The constant + * @param e The exponent of the constant + * @return The resulting price + */ + function cmul( + PythStructs.Price memory self, + int64 c, + int32 e + ) internal pure returns (PythStructs.Price memory) { + return + mul( + self, + PythStructs.Price({ + price: c, + conf: 0, + expo: e, + publishTime: self.publishTime + }) + ); + } + + /** + * @dev Multiply two prices, propagating uncertainty. + * @param self The first price + * @param other The second price + * @return The product of the two prices + */ + function mul( + PythStructs.Price memory self, + PythStructs.Price memory other + ) internal pure returns (PythStructs.Price memory) { + PythStructs.Price memory base = normalize(self); + other = normalize(other); + + (uint64 basePrice, int64 baseSign) = toUnsigned(base.price); + (uint64 otherPrice, int64 otherSign) = toUnsigned(other.price); + + uint64 midprice = basePrice * otherPrice; + int32 midpriceExpo = base.expo + other.expo; + + uint64 conf = base.conf * otherPrice + other.conf * basePrice; + + return + PythStructs.Price({ + price: int64(midprice) * baseSign * otherSign, + conf: conf, + expo: midpriceExpo, + publishTime: min(self.publishTime, other.publishTime) + }); + } + + /** + * @dev Normalize a price to be between MIN_PD_V_I64 and MAX_PD_V_I64. + * @param self The price to normalize + * @return The normalized price + */ + function normalize( + PythStructs.Price memory self + ) internal pure returns (PythStructs.Price memory) { + (uint64 p, int64 s) = toUnsigned(self.price); + uint64 c = self.conf; + int32 e = self.expo; + + while (p > MAX_PD_V_U64 || c > MAX_PD_V_U64) { + p /= 10; + c /= 10; + e += 1; + } + + return + PythStructs.Price({ + price: int64(p) * s, + conf: c, + expo: e, + publishTime: self.publishTime + }); + } + + /** + * @dev Scale a price to a target exponent. + * @param self The price to scale + * @param targetExpo The target exponent + * @return The scaled price + */ + function scale_to_exponent( + PythStructs.Price memory self, + int32 targetExpo + ) internal pure returns (PythStructs.Price memory) { + int32 delta = targetExpo - self.expo; + if (delta == 0) return self; + + int64 p = self.price; + uint64 c = self.conf; + + if (delta > 0) { + while (delta > 0 && (p != 0 || c != 0)) { + p /= 10; + c /= 10; + delta -= 1; + } + } else { + while (delta < 0) { + p *= 10; + c *= 10; + delta += 1; + } + } + + return + PythStructs.Price({ + price: p, + conf: c, + expo: targetExpo, + publishTime: self.publishTime + }); + } + + /** + * @dev Convert a signed integer to unsigned and a sign bit. + * @param x The signed integer + * @return The unsigned value and sign + */ + function toUnsigned(int64 x) internal pure returns (uint64, int64) { + if (x == type(int64).min) { + return (uint64(type(int64).max) + 1, -1); + } else if (x < 0) { + return (uint64(-x), -1); + } else { + return (uint64(x), 1); + } + } + + /** + * @dev Helper function to create a fraction of two integers as a Price struct. + * @param x The numerator + * @param y The denominator + * @return The fraction as a Price struct + */ + function fraction( + int64 x, + int64 y + ) internal pure returns (PythStructs.Price memory) { + // Convert x and y to Prices + PythStructs.Price memory x_as_price = PythStructs.Price({ + price: x, + conf: 0, + expo: 0, + publishTime: 0 + }); + + PythStructs.Price memory y_as_price = PythStructs.Price({ + price: y, + conf: 0, + expo: 0, + publishTime: 0 + }); + + // Get the relevant fraction + return div(x_as_price, y_as_price); + } + + /** + * @dev Helper function to return the minimum of two uint64 values. + * @param a The first value + * @param b The second value + * @return The minimum of a and b + */ + function min(uint64 a, uint64 b) internal pure returns (uint64) { + return a < b ? a : b; + } + + /** + * @dev Helper function to return the minimum of two uint256 values. + * @param a The first value + * @param b The second value + * @return The minimum of a and b + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} From 223d68565d9cad5a2c75f1f8ce77e15a03b3285f Mon Sep 17 00:00:00 2001 From: 0xHaku <32897506+0xHaku@users.noreply.github.com> Date: Sat, 29 Jun 2024 19:00:41 +0900 Subject: [PATCH 2/7] fix visibility --- target_chains/ethereum/sdk/solidity/Price.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/target_chains/ethereum/sdk/solidity/Price.sol b/target_chains/ethereum/sdk/solidity/Price.sol index 0f546dc50d..4f41a86cd6 100644 --- a/target_chains/ethereum/sdk/solidity/Price.sol +++ b/target_chains/ethereum/sdk/solidity/Price.sol @@ -49,7 +49,7 @@ library Price { uint64 rate_discount_initial, uint64 rate_discount_final, int32 discount_exponent - ) internal pure returns (PythStructs.Price memory) { + ) public pure returns (PythStructs.Price memory) { // rate_discount_initial should be >= rate_discount_final if (rate_discount_initial < rate_discount_final) { revert InvalidDiscount(); @@ -110,7 +110,7 @@ library Price { uint64 rate_premium_initial, uint64 rate_premium_final, int32 premium_exponent - ) internal pure returns (PythStructs.Price memory) { + ) public pure returns (PythStructs.Price memory) { // Valuation price should not decrease as amount of borrow grows, so rate_premium_initial // should <= rate_premium_final if (rate_premium_initial > rate_premium_final) { From 3f10fc690d552983626915b8d0d643674ead1654 Mon Sep 17 00:00:00 2001 From: Haku <32897506+0xHaku@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:15:40 +0900 Subject: [PATCH 3/7] fix functions name style --- target_chains/ethereum/sdk/solidity/Price.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/target_chains/ethereum/sdk/solidity/Price.sol b/target_chains/ethereum/sdk/solidity/Price.sol index 4f41a86cd6..a7e271a303 100644 --- a/target_chains/ethereum/sdk/solidity/Price.sol +++ b/target_chains/ethereum/sdk/solidity/Price.sol @@ -42,7 +42,7 @@ library Price { * @param discount_exponent The exponent to apply to the discounts * @return The valuation price of the collateral */ - function get_collateral_valuation_price( + function getCollateralValuationPrice( PythStructs.Price memory self, uint64 deposits, uint64 deposits_endpoint, @@ -103,7 +103,7 @@ library Price { * @param premium_exponent The exponent to apply to the premiums above * @return The price of the borrow valuation */ - function get_borrow_valuation_price( + function getBorrowValuationPrice( PythStructs.Price memory self, uint64 borrows, uint64 borrows_endpoint, @@ -167,7 +167,7 @@ library Price { * @param pre_add_expo The exponent to scale to, before final addition; essentially the final precision you want * @return The price at the query x coordinate */ - function affine_combination( + function affineCombination( int64 x1, PythStructs.Price memory y1, int64 x2, @@ -360,7 +360,7 @@ library Price { * @param targetExpo The target exponent * @return The scaled price */ - function scale_to_exponent( + function scaleToExponent( PythStructs.Price memory self, int32 targetExpo ) internal pure returns (PythStructs.Price memory) { From 7986f74850ecb6cccb3581edbaa4fa66ec862b1c Mon Sep 17 00:00:00 2001 From: Haku <32897506+0xHaku@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:23:47 +0900 Subject: [PATCH 4/7] apply review comments --- target_chains/ethereum/sdk/solidity/Price.sol | 144 ++++++++---------- 1 file changed, 63 insertions(+), 81 deletions(-) diff --git a/target_chains/ethereum/sdk/solidity/Price.sol b/target_chains/ethereum/sdk/solidity/Price.sol index a7e271a303..cbcfd605e3 100644 --- a/target_chains/ethereum/sdk/solidity/Price.sol +++ b/target_chains/ethereum/sdk/solidity/Price.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +import "@openzeppelin/contracts/utils/math/Math.sol"; + import "./PythStructs.sol"; library Price { @@ -18,23 +20,23 @@ library Price { /** * @dev Get the current price in a different quote currency. - * @param self The base price + * @param price The base price * @param quote The quote price * @param resultExpo The desired exponent for the result * @return The price in the quote currency */ function getPriceInQuote( - PythStructs.Price memory self, + PythStructs.Price memory price, PythStructs.Price memory quote, int32 resultExpo ) public pure returns (PythStructs.Price memory) { - PythStructs.Price memory divResult = div(self, quote); + PythStructs.Price memory divResult = div(price, quote); return scale_to_exponent(divResult, resultExpo); } /** * @dev Get the valuation price of a collateral position. - * @param self The original price + * @param price The original price * @param deposits Quantity of token deposited in the protocol * @param deposits_endpoint Deposits right endpoint for the affine combination * @param rate_discount_initial Initial discounted rate at 0 deposits (units given by discount_exponent) @@ -43,7 +45,7 @@ library Price { * @return The valuation price of the collateral */ function getCollateralValuationPrice( - PythStructs.Price memory self, + PythStructs.Price memory price, uint64 deposits, uint64 deposits_endpoint, uint64 rate_discount_initial, @@ -81,16 +83,16 @@ library Price { ); PythStructs.Price memory price_discounted = scale_to_exponent( - mul(self, discount_interpolated), - self.expo + mul(price, discount_interpolated), + price.expo ); return PythStructs.Price({ price: price_discounted.price, - conf: self.conf, + conf: price.conf, expo: price_discounted.expo, - publishTime: self.publishTime + publishTime: price.publishTime }); } @@ -104,7 +106,7 @@ library Price { * @return The price of the borrow valuation */ function getBorrowValuationPrice( - PythStructs.Price memory self, + PythStructs.Price memory price, uint64 borrows, uint64 borrows_endpoint, uint64 rate_premium_initial, @@ -144,16 +146,16 @@ library Price { // Get price premium, convert back to the original exponents we received the price in PythStructs.Price memory price_premium = scale_to_exponent( - mul(self, premium_interpolated), - self.expo + mul(price, premium_interpolated), + price.expo ); return PythStructs.Price({ price: price_premium.price, - conf: self.conf, + conf: price.conf, expo: price_premium.expo, - publishTime: self.publishTime + publishTime: price.publishTime }); } @@ -209,28 +211,28 @@ library Price { /** * @dev Divide one price by another, propagating uncertainty. - * @param self The numerator price - * @param other The denominator price + * @param price The numerator price + * @param price2 The denominator price * @return The resulting price */ function div( - PythStructs.Price memory self, - PythStructs.Price memory other + PythStructs.Price memory price, + PythStructs.Price memory price2 ) internal pure returns (PythStructs.Price memory) { - PythStructs.Price memory base = normalize(self); - other = normalize(other); + PythStructs.Price memory base = normalize(price); + price2 = normalize(price2); - if (other.price == 0) { + if (price2.price == 0) { revert DivisionByZero(); } (uint64 basePrice, int64 baseSign) = toUnsigned(base.price); - (uint64 otherPrice, int64 otherSign) = toUnsigned(other.price); + (uint64 otherPrice, int64 otherSign) = toUnsigned(price2.price); uint64 midprice = (basePrice * PD_SCALE) / otherPrice; - int32 midpriceExpo = base.expo - other.expo + PD_EXPO; + int32 midpriceExpo = base.expo - price2.expo + PD_EXPO; - uint64 otherConfidencePct = (other.conf * PD_SCALE) / otherPrice; + uint64 otherConfidencePct = (price2.conf * PD_SCALE) / otherPrice; uint128 conf = ((uint128(base.conf) * PD_SCALE) / otherPrice) + ((uint128(otherConfidencePct) * midprice) / PD_SCALE); @@ -243,101 +245,101 @@ library Price { price: int64(midprice) * baseSign * otherSign, conf: uint64(conf), expo: midpriceExpo, - publishTime: min(self.publishTime, other.publishTime) + publishTime: Math.min(price.publishTime, price2.publishTime) }); } /** * @dev Add two prices, propagating uncertainty. - * @param self The first price - * @param other The second price + * @param price The first price + * @param price2 The second price * @return The sum of the two prices */ function add( - PythStructs.Price memory self, - PythStructs.Price memory other + PythStructs.Price memory price, + PythStructs.Price memory price2 ) internal pure returns (PythStructs.Price memory) { - if (self.expo != other.expo) { + if (price.expo != price2.expo) { revert ExponentsMustMatch(); } - int64 price = self.price + other.price; - uint64 conf = self.conf + other.conf; + int64 price = price.price + price2.price; + uint64 conf = price.conf + price2.conf; return PythStructs.Price({ price: price, conf: conf, - expo: self.expo, - publishTime: min(self.publishTime, other.publishTime) + expo: price.expo, + publishTime: Math.min(price.publishTime, price2.publishTime) }); } /** * @dev Multiply a price by a constant. - * @param self The price + * @param price The price * @param c The constant * @param e The exponent of the constant * @return The resulting price */ function cmul( - PythStructs.Price memory self, + PythStructs.Price memory price, int64 c, int32 e ) internal pure returns (PythStructs.Price memory) { return mul( - self, + price, PythStructs.Price({ price: c, conf: 0, expo: e, - publishTime: self.publishTime + publishTime: price.publishTime }) ); } /** * @dev Multiply two prices, propagating uncertainty. - * @param self The first price - * @param other The second price + * @param price The first price + * @param price2 The second price * @return The product of the two prices */ function mul( - PythStructs.Price memory self, - PythStructs.Price memory other + PythStructs.Price memory price, + PythStructs.Price memory price2 ) internal pure returns (PythStructs.Price memory) { - PythStructs.Price memory base = normalize(self); - other = normalize(other); + PythStructs.Price memory base = normalize(price); + price2 = normalize(price2); (uint64 basePrice, int64 baseSign) = toUnsigned(base.price); - (uint64 otherPrice, int64 otherSign) = toUnsigned(other.price); + (uint64 otherPrice, int64 otherSign) = toUnsigned(price2.price); uint64 midprice = basePrice * otherPrice; - int32 midpriceExpo = base.expo + other.expo; + int32 midpriceExpo = base.expo + price2.expo; - uint64 conf = base.conf * otherPrice + other.conf * basePrice; + uint64 conf = base.conf * otherPrice + price2.conf * basePrice; return PythStructs.Price({ price: int64(midprice) * baseSign * otherSign, conf: conf, expo: midpriceExpo, - publishTime: min(self.publishTime, other.publishTime) + publishTime: Math.min(price.publishTime, price2.publishTime) }); } /** * @dev Normalize a price to be between MIN_PD_V_I64 and MAX_PD_V_I64. - * @param self The price to normalize + * @param price The price to normalize * @return The normalized price */ function normalize( - PythStructs.Price memory self + PythStructs.Price memory price ) internal pure returns (PythStructs.Price memory) { - (uint64 p, int64 s) = toUnsigned(self.price); - uint64 c = self.conf; - int32 e = self.expo; + (uint64 p, int64 s) = toUnsigned(price.price); + uint64 c = price.conf; + int32 e = price.expo; while (p > MAX_PD_V_U64 || c > MAX_PD_V_U64) { p /= 10; @@ -350,25 +352,25 @@ library Price { price: int64(p) * s, conf: c, expo: e, - publishTime: self.publishTime + publishTime: price.publishTime }); } /** * @dev Scale a price to a target exponent. - * @param self The price to scale + * @param price The price to scale * @param targetExpo The target exponent * @return The scaled price */ function scaleToExponent( - PythStructs.Price memory self, + PythStructs.Price memory price, int32 targetExpo ) internal pure returns (PythStructs.Price memory) { - int32 delta = targetExpo - self.expo; - if (delta == 0) return self; + int32 delta = targetExpo - price.expo; + if (delta == 0) return price; - int64 p = self.price; - uint64 c = self.conf; + int64 p = price.price; + uint64 c = price.conf; if (delta > 0) { while (delta > 0 && (p != 0 || c != 0)) { @@ -389,7 +391,7 @@ library Price { price: p, conf: c, expo: targetExpo, - publishTime: self.publishTime + publishTime: price.publishTime }); } @@ -436,24 +438,4 @@ library Price { // Get the relevant fraction return div(x_as_price, y_as_price); } - - /** - * @dev Helper function to return the minimum of two uint64 values. - * @param a The first value - * @param b The second value - * @return The minimum of a and b - */ - function min(uint64 a, uint64 b) internal pure returns (uint64) { - return a < b ? a : b; - } - - /** - * @dev Helper function to return the minimum of two uint256 values. - * @param a The first value - * @param b The second value - * @return The minimum of a and b - */ - function min(uint256 a, uint256 b) internal pure returns (uint256) { - return a < b ? a : b; - } } From 76b0a128ac46a9d65d394f4c52f3791f55f30ddd Mon Sep 17 00:00:00 2001 From: Haku <32897506+0xHaku@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:13:54 +0900 Subject: [PATCH 5/7] rename price to pythprice --- .../ethereum/sdk/solidity/{Price.sol => PythPrice.sol} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename target_chains/ethereum/sdk/solidity/{Price.sol => PythPrice.sol} (99%) diff --git a/target_chains/ethereum/sdk/solidity/Price.sol b/target_chains/ethereum/sdk/solidity/PythPrice.sol similarity index 99% rename from target_chains/ethereum/sdk/solidity/Price.sol rename to target_chains/ethereum/sdk/solidity/PythPrice.sol index cbcfd605e3..da37e138c3 100644 --- a/target_chains/ethereum/sdk/solidity/Price.sol +++ b/target_chains/ethereum/sdk/solidity/PythPrice.sol @@ -5,7 +5,7 @@ import "@openzeppelin/contracts/utils/math/Math.sol"; import "./PythStructs.sol"; -library Price { +library PythPrice { // Constants for working with Pyth's number representation int32 private constant PD_EXPO = -9; uint64 private constant PD_SCALE = 1_000_000_000; From bc71898fa48206a64a59f32d1b074b7ef26ef63d Mon Sep 17 00:00:00 2001 From: Haku <32897506+0xHaku@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:09:33 +0900 Subject: [PATCH 6/7] fix snake case to camel case --- .../ethereum/sdk/solidity/PythPrice.sol | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/target_chains/ethereum/sdk/solidity/PythPrice.sol b/target_chains/ethereum/sdk/solidity/PythPrice.sol index da37e138c3..257dfbedef 100644 --- a/target_chains/ethereum/sdk/solidity/PythPrice.sol +++ b/target_chains/ethereum/sdk/solidity/PythPrice.sol @@ -31,7 +31,7 @@ library PythPrice { int32 resultExpo ) public pure returns (PythStructs.Price memory) { PythStructs.Price memory divResult = div(price, quote); - return scale_to_exponent(divResult, resultExpo); + return scaleToExponent(divResult, resultExpo); } /** @@ -73,7 +73,7 @@ library PythPrice { }); // get the interpolated discount as a price - PythStructs.Price memory discount_interpolated = affine_combination( + PythStructs.Price memory discount_interpolated = affineCombination( 0, initial_percentage, int64(deposits_endpoint), @@ -82,7 +82,7 @@ library PythPrice { -9 ); - PythStructs.Price memory price_discounted = scale_to_exponent( + PythStructs.Price memory price_discounted = scaleToExponent( mul(price, discount_interpolated), price.expo ); @@ -135,7 +135,7 @@ library PythPrice { }); // Get the interpolated premium as a price - PythStructs.Price memory premium_interpolated = affine_combination( + PythStructs.Price memory premium_interpolated = affineCombination( 0, initial_percentage, int64(borrows_endpoint), @@ -145,7 +145,7 @@ library PythPrice { ); // Get price premium, convert back to the original exponents we received the price in - PythStructs.Price memory price_premium = scale_to_exponent( + PythStructs.Price memory price_premium = scaleToExponent( mul(price, premium_interpolated), price.expo ); @@ -202,8 +202,8 @@ library PythPrice { PythStructs.Price memory right = mul(y1, frac_2q); // Scaling - left = scale_to_exponent(left, pre_add_expo); - right = scale_to_exponent(right, pre_add_expo); + left = scaleToExponent(left, pre_add_expo); + right = scaleToExponent(right, pre_add_expo); // 8. compute H = F + G return add(left, right); @@ -263,13 +263,10 @@ library PythPrice { revert ExponentsMustMatch(); } - int64 price = price.price + price2.price; - uint64 conf = price.conf + price2.conf; - return PythStructs.Price({ - price: price, - conf: conf, + price: price.price + price2.price, + conf: price.conf + price2.conf, expo: price.expo, publishTime: Math.min(price.publishTime, price2.publishTime) }); From b0844f7d6ec36e1a5e008eb62783d4a60dd0146b Mon Sep 17 00:00:00 2001 From: Haku <32897506+0xHaku@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:24:51 +0900 Subject: [PATCH 7/7] add price test with harness --- .../contracts/forge-test/Pyth.Price.t.sol | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 target_chains/ethereum/contracts/forge-test/Pyth.Price.t.sol diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.Price.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.Price.t.sol new file mode 100644 index 0000000000..9779c142cf --- /dev/null +++ b/target_chains/ethereum/contracts/forge-test/Pyth.Price.t.sol @@ -0,0 +1,281 @@ +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import "@pythnetwork/pyth-sdk-solidity/PythPrice.sol"; +import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; + +contract PythPriceTest is Test { + PythPriceHarness harness; + uint64 private constant PD_SCALE = 1_000_000_000; + + function setUp() public { + harness = new PythPriceHarness(); + } + + function testDivNormalOperation() public { + PythStructs.Price memory price1 = PythStructs.Price({ + price: 100, + conf: 5, + expo: -8, + publishTime: 1000 + }); + PythStructs.Price memory price2 = PythStructs.Price({ + price: 50, + conf: 2, + expo: -8, + publishTime: 1100 + }); + PythStructs.Price memory result = harness.div(price1, price2); + assertEq(result.price, 2 * 10 ** 9, "Division result should be 2"); + assertEq(result.expo, -9, "Exponent should be -9"); + assertEq( + result.publishTime, + 1000, + "PublishTime should be the minimum of the two" + ); + } + + function testDivByZero() public { + PythStructs.Price memory price1 = PythStructs.Price({ + price: 100, + conf: 5, + expo: -8, + publishTime: 1000 + }); + PythStructs.Price memory price2 = PythStructs.Price({ + price: 0, + conf: 2, + expo: -8, + publishTime: 1100 + }); + vm.expectRevert(abi.encodeWithSignature("DivisionByZero()")); + harness.div(price1, price2); + } + + function testAddSameExponent() public { + PythStructs.Price memory price1 = PythStructs.Price({ + price: 100, + conf: 5, + expo: -8, + publishTime: 1000 + }); + PythStructs.Price memory price2 = PythStructs.Price({ + price: 50, + conf: 2, + expo: -8, + publishTime: 1100 + }); + PythStructs.Price memory result = harness.add(price1, price2); + assertEq(result.price, 150, "Addition result should be 150"); + assertEq(result.conf, 7, "Confidence should be summed"); + assertEq(result.expo, -8, "Exponent should remain the same"); + assertEq( + result.publishTime, + 1000, + "PublishTime should be the minimum of the two" + ); + } + + function testAddDifferentExponent() public { + PythStructs.Price memory price1 = PythStructs.Price({ + price: 100, + conf: 5, + expo: -8, + publishTime: 1000 + }); + PythStructs.Price memory price2 = PythStructs.Price({ + price: 50, + conf: 2, + expo: -7, + publishTime: 1100 + }); + vm.expectRevert(abi.encodeWithSignature("ExponentsMustMatch()")); + harness.add(price1, price2); + } + + function testMulNormalOperation() public { + PythStructs.Price memory price1 = PythStructs.Price({ + price: 100, + conf: 5, + expo: -8, + publishTime: 1000 + }); + PythStructs.Price memory price2 = PythStructs.Price({ + price: 50, + conf: 2, + expo: -8, + publishTime: 1100 + }); + PythStructs.Price memory result = harness.mul(price1, price2); + assertEq(result.price, 5000, "Multiplication result should be 5000"); + assertEq(result.expo, -16, "Exponents should be added"); + assertEq( + result.publishTime, + 1000, + "PublishTime should be the minimum of the two" + ); + } + + function testNormalize() public { + PythStructs.Price memory price = PythStructs.Price({ + price: 2 * int64(PD_SCALE), + conf: 3 * PD_SCALE, + expo: 0, + publishTime: 1000 + }); + PythStructs.Price memory result = harness.normalize(price); + assertEq( + result.price, + (2 * int64(PD_SCALE)) / 100, + "Price should be normalized to 10^9" + ); + assertEq( + result.conf, + (3 * PD_SCALE) / 100, + "Confidence should be normalized" + ); + assertEq(result.expo, 2, "Exponent should be adjusted"); + } + + function testScaleToExponentUpscale() public { + PythStructs.Price memory price = PythStructs.Price({ + price: 1000, + conf: 50, + expo: -8, + publishTime: 1000 + }); + PythStructs.Price memory result = harness.scaleToExponent(price, -6); + assertEq(result.price, 10, "Price should be scaled up"); + assertEq( + result.conf, + 0, + "Confidence should be scaled (rounded down to 0)" + ); + assertEq(result.expo, -6, "Exponent should be adjusted to target"); + } + + function testToUnsignedPositive() public { + (uint64 unsignedValue, int64 sign) = harness.toUnsigned(100); + assertEq(unsignedValue, 100, "Unsigned value should be 100"); + assertEq(sign, 1, "Sign should be positive"); + } + + function testToUnsignedNegative() public { + (uint64 unsignedValue, int64 sign) = harness.toUnsigned(-100); + assertEq(unsignedValue, 100, "Unsigned value should be 100"); + assertEq(sign, -1, "Sign should be negative"); + } + + function testFraction() public { + PythStructs.Price memory result = harness.fraction(10, 5); + assertEq(result.price, 2 * 10 ** 9, "Fraction result should be 2"); + assertEq(result.expo, -9, "Exponent should be -9"); + } + + function testAffineCombination() public { + PythStructs.Price memory y1 = PythStructs.Price({ + price: 100, + conf: 10, + expo: -4, + publishTime: 0 + }); + PythStructs.Price memory y2 = PythStructs.Price({ + price: 100, + conf: 10, + expo: -4, + publishTime: 0 + }); + PythStructs.Price memory result = harness.affineCombination( + 0, + y1, + 10, + y2, + 5, + -9 + ); + assertEq(result.price, 10 ** 7, "Affine combination should be 150"); + assertEq(result.expo, -9, "Exponent should be -9"); + } + + function testCmul() public { + PythStructs.Price memory price = PythStructs.Price({ + price: 100, + conf: 5, + expo: -8, + publishTime: 1000 + }); + PythStructs.Price memory result = harness.cmul(price, 2, 0); + assertEq(result.price, 200, "Multiplication result should be 200"); + assertEq(result.expo, -8, "Exponent should remain the same"); + } +} + +contract PythPriceHarness { + using PythPrice for *; + + function div( + PythStructs.Price memory price, + PythStructs.Price memory price2 + ) public pure returns (PythStructs.Price memory) { + return PythPrice.div(price, price2); + } + + function add( + PythStructs.Price memory price, + PythStructs.Price memory price2 + ) public pure returns (PythStructs.Price memory) { + return PythPrice.add(price, price2); + } + + function mul( + PythStructs.Price memory price, + PythStructs.Price memory price2 + ) public pure returns (PythStructs.Price memory) { + return PythPrice.mul(price, price2); + } + + function normalize( + PythStructs.Price memory price + ) public pure returns (PythStructs.Price memory) { + return PythPrice.normalize(price); + } + + function scaleToExponent( + PythStructs.Price memory price, + int32 targetExpo + ) public pure returns (PythStructs.Price memory) { + return PythPrice.scaleToExponent(price, targetExpo); + } + + function toUnsigned(int64 x) public pure returns (uint64, int64) { + return PythPrice.toUnsigned(x); + } + + function fraction( + int64 x, + int64 y + ) public pure returns (PythStructs.Price memory) { + return PythPrice.fraction(x, y); + } + + function affineCombination( + int64 x1, + PythStructs.Price memory y1, + int64 x2, + PythStructs.Price memory y2, + int64 x_query, + int32 pre_add_expo + ) public pure returns (PythStructs.Price memory) { + return + PythPrice.affineCombination(x1, y1, x2, y2, x_query, pre_add_expo); + } + + function cmul( + PythStructs.Price memory price, + int64 c, + int32 e + ) public pure returns (PythStructs.Price memory) { + return PythPrice.cmul(price, c, e); + } +}