Skip to content

WIP: Chore(evm) add combine pricefeed #2665

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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 98 additions & 6 deletions target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -370,25 +370,50 @@ 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,
int64 expectedPrice
) internal {
int64 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();
vm.expectRevert(PythErrors.NegativeInputPrice.selector);
PythUtils.convertToUint(-100, -5, 18);

// Exponent can't be positive
vm.expectRevert();
PythUtils.convertToUint(100, 5, 18);
// Exponent can't be less than -255
vm.expectRevert(PythErrors.InvalidInputExpo.selector);
PythUtils.convertToUint(100, -256, 18);

// 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
Expand All @@ -398,5 +423,72 @@ contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents {
// @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);

// 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 with23 zeros

// Price with 9 decimals and exponent 2
assertEq(PythUtils.convertToUint(100, 2, 9), 100_00_000_000_000); // 100 with 11 zeros

// Price with 4 decimals and exponent 5
assertEq(PythUtils.convertToUint(100, 1, 2), 100_000); // 100 with 3 zeros


// Edge Cases
// This test will fail as the 10 ** 237 is too large for a uint256
// assertEq(PythUtils.convertToUint(100, -255, 18), 0);
// assertEq(PythUtils.convertToUint(100, 255, 18), 100_00_000_000_000_000_000_000_000);
}

function testCombinePrices() public {

// Basic Tests
assertCrossRateEquals(500, -8, 500, -8, -5, 100000);
assertCrossRateEquals(10_000, -8, 100, -2, -5, 10);
assertCrossRateEquals(10_000, -2, 100, -8, -4, 1_000_000_000_000);

// Negative Price Tests
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);

// Positive Exponent Tests
assertCrossRateReverts(100, 2, 100, -2, -5, PythErrors.InvalidInputExpo.selector);
assertCrossRateReverts(100, -2, 100, 2, -5, PythErrors.InvalidInputExpo.selector);
assertCrossRateReverts(100, 2, 100, 2, -5, PythErrors.InvalidInputExpo.selector);

// Invalid Target Exponent Tests
assertCrossRateReverts(100, -2, 100, -2, 1, PythErrors.InvalidTargetExpo.selector);

// Different Exponent Tests
assertCrossRateEquals(10_000, -2, 100, -4, -4, 100_000_000);
assertCrossRateEquals(10_000, -2, 10_000, -1, -2, 10);
assertCrossRateEquals(10_000, -10, 10_000, -2, 0, 0); // It will truncate to 0

// Exponent Edge Tests
assertCrossRateEquals(10_000, 0, 100, 0, 0, 100);
assertCrossRateEquals(10_000, 0, 100, 0, -255, 100);
// assertCrossRateEquals(10_000, 0, 100, -255, -255, 100, -255);
// assertCrossRateEquals(10_000, -255, 100, 0, 0, 100, 0);

// // End Range Tests
// successTest(int64(type(int64).max), 0, int64(type(int64).max), 0, 1, 0);
// successTest(int64(type(int64).max), 0, 1, 0, int64(type(int64).max), 0);
// successTest(1, 0, int64(type(int64).max), 0, 1 / int64(type(int64).max), 0);
// revertTest(10_000, -2, 10_000, -256);

// // More Realistic Tests
// // Test case 1: (StEth/Eth / Eth/USD = ETH/BTC)
// (int64 price, int32 expo) = PythUtils.deriveCrossRate(206487956502, -8, 206741615681, -8);
// assertApproxEqRel(price, 100000000, 9e17); // $1
// assertEq(expo, -8);

// // Test case 2:
// (price, expo) = PythUtils.deriveCrossRate(520010, -8, 38591, -8);
// assertApproxEqRel(price, 1347490347, 9e17); // $1
// assertEq(expo, -8);
}
}
208 changes: 208 additions & 0 deletions target_chains/ethereum/sdk/solidity/Math.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// 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


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)
}
}
}

}
8 changes: 8 additions & 0 deletions target_chains/ethereum/sdk/solidity/PythErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 target exponent is invalid.
error InvalidTargetExpo();
// The combined price is greater than int64.max.
error CombinedPriceOverflow();
}
Loading
Loading