Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented a Math Library to combine Pyth prices in Solidity - tested and working on Remix IDE #1732

Closed
wants to merge 3 commits into from

Conversation

lopeselio
Copy link

Description

This is a solidity implementation of a Math library on Solana that can easily combine two Pyth Prices into one, for instance: (SOL:USD/ETH:USD=SOL/ETH)

  • The contracts have been deployed and the function calls have been tested in Remix IDE
    image
  • The deployed contracts return accurate figures based on the Unix timestamp for the pair SOL/USD and ETH/USD as SOL/ETH shown below, ref
    image
    image
  • Upon testing the deployed contracts the price returned as result.price is shown as $0.04069877 which is 4069877 * 10^-8, as you can see in the code implementation below
  • The ABIs have been added to abis folder from Remix IDE
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;

import "./PythStructs.sol";
import "./PriceCombine.sol";

// This contract is used to test the library functions and tested in Remix IDE. All ABIs are present in the /abis folder
contract PriceCombineTest {
    using PriceCombine for PythStructs.Price;

    /**
     * @notice Test the getPriceInQuote function
     * @param basePrice The price of the base currency (example: SOL/USD) 13913000000 (represents the current SOL/USD $139.13 with 8 decimal places)
     * @param baseConf The confidence interval of the base currency
     * @param baseExpo The exponent of the base currency (here: -8 -> 8 decimal units) indicates price is scaled by 10^-8
     * @param basePublishTime The publish time of the base currency (UNIX timestamp)
     * @param quotePrice The price of the quote currency (example: ETH/USD) 341853000000 (represents the current ETH/USD $3418.53 with 8 decimal places)
     * @param quoteConf The confidence interval of the quote currency
     * @param quoteExpo The exponent of the quote currency (here: -8 -> 8 decimal units) indicates price is scaled by 10^-8
     * @param quotePublishTime The publish time of the quote currency (UNIX timestamp)
     * @param resultExpo The desired exponent for the result (here: -8)
     * @return The price, confidence interval, exponent, and publish time of the resulting price
     */
    function testGetPriceInQuote(int64 basePrice, uint64 baseConf, int32 baseExpo, uint basePublishTime, 
                                 int64 quotePrice, uint64 quoteConf, int32 quoteExpo, uint quotePublishTime,
                                 int32 resultExpo) public pure returns (int64, uint64, int32, uint) {
        PythStructs.Price memory base = PythStructs.Price(basePrice, baseConf, baseExpo, basePublishTime);
        PythStructs.Price memory quote = PythStructs.Price(quotePrice, quoteConf, quoteExpo, quotePublishTime);
        PythStructs.Price memory result = base.getPriceInQuote(quote, resultExpo);
        return (result.price, result.conf, result.expo, result.publishTime);
    }

    // Add more test functions as needed
}

Public methods implemented in Solidity from the Pyth Rust SDK

The methods like get_price_in_quote, div, normalize, scale_to_exponent, and to_unsigned, are public because they are part of the impl Price block in the Rust based Math Library implementation. Correspondingly in PriceCombine.sol, functions like getPriceInQuote, div, normalize, scaleToExponent, toUnsigned have been written in the Solidity Library

  1. getPriceInQuote
  • Takes the Price of the base currency and the Price of the quote currency.
  • Calls the div method to divide the base price by the quote price.
  • Scales the result to the desired exponent using scale_to_exponent.
  1. div
  • Normalizes the base and quote prices to ensure they are within acceptable ranges.
  • Converts prices to unsigned integers for safe arithmetic operations.
  • Computes the midprice and confidence interval, ensuring the result fits within the numeric limits.
  • Returns the result as a Price struct.
  1. normalize
  • Converts the price to an unsigned integer and adjusts the exponent to keep the values within acceptable ranges.
  • Repeatedly divides the price and confidence by 10 and increments the exponent until the values fit within the limits.
  • price and confidence are normalized to be between MIN_PD_V_I64 and MAX_PD_V_I64 constants
  1. scaleToExponent
  • Adjusts the price and confidence to match the target exponent.
  • Either scales down (if target exponent is larger) or scales up (if target exponent is smaller), handling edge cases gracefully.
  1. toUnsigned
  • Converts a signed integer to an unsigned integer and a sign bit.
  1. Additional error handling checks

This PR has been submitted as a solution to the [Pyth Superteam Bounty] on Superteam earn platform (https://earn.superteam.fun/listings/bounty/create-a-math-library-for-combining-pyth-prices-on-solidity/)

Copy link

vercel bot commented Jun 25, 2024

@lopeselio is attempting to deploy a commit to the pyth-web Team on Vercel.

A member of the Team first needs to authorize it.

@lopeselio
Copy link
Author

Hey @ali-bahjati can you please help review this PR #1732 ?

* @param x here is the signed integer.
* @return The unsigned integer and sign bit.
*/
function toUnsigned(int64 x) public pure returns (uint64, int64) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function can be internal. is it intended to be used for anything other than a helper method within this library?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, we can keep it internal pure

other = normalize(other);

// If the price of the quote currency is zero, return zero
if (other.price == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest you throw an error here or return (bool validOutputPrice, PythStructs.Price memory outputPrice), to avoid any downstream issues with consumption of the 0 price. Being explicit here with the output would help protect users from incorrectly using an invalid price


// Compute the confidence interval
uint64 otherConfidencePct = other.conf * PD_SCALE / otherPrice;
uint128 conf = (base.conf * PD_SCALE / otherPrice) + (otherConfidencePct * midprice) / PD_SCALE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be dividing the sum by PD_SCALE, so you need to include parentheses here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also i presume you want to cast each of the summands to uint128 so that you avoid any overflow in the sum. if so, you need to call uint128(base.conf * PD_SCALE / otherPrice) and likewise for the other summand

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if you want to ensure that conf is less than the max uint64 value, just don't cast to uint128. solidity has built-in overflow protection on summing post 0.8, so you won't need to worry about checking, it'll just throw an overflow error if the two summands sum to more than the max uint64 val

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will make this change

int64(int64(midprice) * baseSign * otherSign),
uint64(conf),
midpriceExpo,
self.publishTime < other.publishTime ? self.publishTime : other.publishTime
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use OpenZeppelin's Math library for better readability: https://docs.openzeppelin.com/contracts/3.x/api/math#Math-min-uint256-uint256-

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I will look into this

PythStructs.Price memory result = div(self, quote);
// Return zero price so the error can be gracefully handled by the caller
if (result.price == 0 && result.conf == 0) {
return PythStructs.Price({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not put this check in scaleToExponent?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to update the check in scaleToExponent

@@ -30,4 +30,4 @@ contract PythStructs {
// Latest available exponentially-weighted moving average price
Price emaPrice;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you remove this change?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

return (result.price, result.conf, result.expo, result.publishTime);
}

// Add more test functions as needed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe make a forge project and add test cases there? There should be a lot more tests before this can be merged

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forge allows for fuzz testing as well which can be useful so that you don't have to manually define all the cases

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Can you provide me more details on forge? Do the forge tests also need to be part of this codebase?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://book.getfoundry.sh/reference/forge/forge

you should be able to include this library in a forge environment and add tests there. there should be tests somewhere before this is merged. you can add tests to the forge-test folder, see the note here:

# We put the tests into the forge-test directory (instead of test) so that
# truffle doesn't try to build them
test = 'forge-test'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for the info

@ali-bahjati
Copy link
Collaborator

We have decided to move forward with #1746

@ali-bahjati ali-bahjati closed this Aug 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants