Skip to content

Commit 285465d

Browse files
authored
Chore(evm) add combine pricefeed (#2665)
* chore(evm) Add combine Pricefeeds * update * update * added some test * update * udpate * fix * fix ci * added custom errors * added math.sol * update PythUtils * wip-update * update PythUtils * removed argument * update * chore(evm) Added support for positive expo in PythUtils * chore(evm) Add combine pf * chore(evm) final edit convertToUnit * update * temp update * chore(evm) Combine pricefeeds final * update * removed console.log * update * chore(evm) Add combine Prifeeds
1 parent e57e3bf commit 285465d

File tree

4 files changed

+487
-20
lines changed

4 files changed

+487
-20
lines changed

target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol

Lines changed: 185 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -370,33 +370,208 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils {
370370
}
371371

372372
contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents {
373+
function assertCrossRateEquals(
374+
int64 price1,
375+
int32 expo1,
376+
int64 price2,
377+
int32 expo2,
378+
int32 targetExpo,
379+
uint256 expectedPrice
380+
) internal {
381+
uint256 price = PythUtils.deriveCrossRate(price1, expo1, price2, expo2, targetExpo);
382+
assertEq(price, expectedPrice);
383+
}
384+
385+
function assertCrossRateReverts(
386+
int64 price1,
387+
int32 expo1,
388+
int64 price2,
389+
int32 expo2,
390+
int32 targetExpo,
391+
bytes4 expectedError
392+
) internal {
393+
vm.expectRevert(expectedError);
394+
PythUtils.deriveCrossRate(price1, expo1, price2, expo2, targetExpo);
395+
}
396+
373397
function testConvertToUnit() public {
374-
// Price can't be negative
375-
vm.expectRevert();
398+
399+
// Test 1: Price can't be negative
400+
vm.expectRevert(PythErrors.NegativeInputPrice.selector);
376401
PythUtils.convertToUint(-100, -5, 18);
377402

378-
// Exponent can't be positive
379-
vm.expectRevert();
380-
PythUtils.convertToUint(100, 5, 18);
403+
// Test 2: Exponent can't be less than -255
404+
vm.expectRevert(PythErrors.InvalidInputExpo.selector);
405+
PythUtils.convertToUint(100, -256, 18);
381406

407+
// Test 3: This test will fail as the 10 ** 237 is too large for a uint256
408+
vm.expectRevert(PythErrors.ExponentOverflow.selector);
409+
assertEq(PythUtils.convertToUint(100, -255, 18), 0);
410+
411+
// Test 4: Combined Exponent can't be greater than 58 and less than -58
412+
// See the calculation how we came up with 58 in PythUtils.sol
413+
vm.expectRevert(PythErrors.ExponentOverflow.selector);
414+
assertEq(PythUtils.convertToUint(100, 50, 9), 0); // 50 + 9 = 59 > 58
415+
vm.expectRevert(PythErrors.ExponentOverflow.selector);
416+
assertEq(PythUtils.convertToUint(100, -96, 37), 0); // -96 + 37 = -59 < -58
417+
418+
// Test 5: Negative Exponent Tests
382419
// Price with 18 decimals and exponent -5
383420
assertEq(
384421
PythUtils.convertToUint(100, -5, 18),
385-
1000000000000000 // 100 * 10^13
422+
100_0_000_000_000_000 // 100 * 10^13
386423
);
387-
388424
// Price with 9 decimals and exponent -2
389425
assertEq(
390426
PythUtils.convertToUint(100, -2, 9),
391-
1000000000 // 100 * 10^7
427+
100_0_000_000 // 100 * 10^7
392428
);
393429

394-
// Price with 4 decimals and exponent -5
430+
// Test 6: Price with 4 decimals and exponent -5
395431
assertEq(PythUtils.convertToUint(100, -5, 4), 10);
396432

397-
// Price with 5 decimals and exponent -2
433+
// Test 7: Price with 5 decimals and exponent -2
398434
// @note: We will lose precision here as price is
399435
// 0.00001 and we are targetDecimals is 2.
400436
assertEq(PythUtils.convertToUint(100, -5, 2), 0);
437+
assertEq(PythUtils.convertToUint(123, -8, 5), 0);
438+
439+
// Test 8: Positive Exponent Tests
440+
// Price with 18 decimals and exponent 5
441+
assertEq(PythUtils.convertToUint(100, 5, 18), 100_00_000_000_000_000_000_000_000); // 100 with 23 zeros
442+
// Test 9: Price with 9 decimals and exponent 2
443+
assertEq(PythUtils.convertToUint(100, 2, 9), 100_00_000_000_000); // 100 with 11 zeros
444+
445+
// Test 10: Price with 2 decimals and exponent 1
446+
assertEq(PythUtils.convertToUint(100, 1, 2), 100_000); // 100 with 3 zeros
447+
448+
449+
// Special Cases
450+
// Test 11: price = 0, any expo/decimals returns 0
451+
assertEq(PythUtils.convertToUint(0, -58, 0), 0);
452+
assertEq(PythUtils.convertToUint(0, 0, 0), 0);
453+
assertEq(PythUtils.convertToUint(0, 58, 0), 0);
454+
assertEq(PythUtils.convertToUint(0, -58, 58), 0);
455+
456+
// Test 12: smallest positive price, maximum downward exponent (should round to zero)
457+
assertEq(PythUtils.convertToUint(1, -58, 0), 0);
458+
assertEq(PythUtils.convertToUint(1, -58, 58), 1);
459+
460+
// Test 13: deltaExponent == 0 (should be identical to price)
461+
assertEq(PythUtils.convertToUint(123456, 0, 0), 123456);
462+
assertEq(PythUtils.convertToUint(123456, -5, 5), 123456); // -5 + 5 == 0
463+
464+
// Test 14: deltaExponent > 0 (should shift price up)
465+
assertEq(PythUtils.convertToUint(123456, 5, 0), 12345600000);
466+
assertEq(PythUtils.convertToUint(123456, 5, 2), 1234560000000);
467+
468+
// Test 15: deltaExponent < 0 (should shift price down)
469+
assertEq(PythUtils.convertToUint(123456, -5, 0), 1);
470+
assertEq(PythUtils.convertToUint(123456, -5, 2), 123);
471+
472+
// Test 16: division with truncation
473+
assertEq(PythUtils.convertToUint(999, -2, 0), 9); // 999/100 = 9 (truncated)
474+
assertEq(PythUtils.convertToUint(199, -2, 0), 1); // 199/100 = 1 (truncated)
475+
assertEq(PythUtils.convertToUint(99, -2, 0), 0); // 99/100 = 0 (truncated)
476+
477+
// Test 17: Big price and scaling, but outside of bounds
478+
vm.expectRevert(PythErrors.ExponentOverflow.selector);
479+
assertEq(PythUtils.convertToUint(100_000_000, 10, 50),0);
480+
481+
// Test 18: Big price and scaling
482+
assertEq(PythUtils.convertToUint(100_000_000, -50, 10),0); // -50 + 10 = -40 > -58
483+
vm.expectRevert(PythErrors.ExponentOverflow.selector);
484+
assertEq(PythUtils.convertToUint(100_000_000, 10, 50), 0); // 10 + 50 = 60 > 58
485+
486+
// Test 19: Decimals just save from truncation
487+
assertEq(PythUtils.convertToUint(5, -1, 1), 5); // 5/10*10 = 5
488+
assertEq(PythUtils.convertToUint(5, -1, 2), 50); // 5/10*100 = 50
489+
490+
// 10. Test: Big price and scaling, should be inside the bounds
491+
// We have to convert int64 -> int256 -> uint256 before multiplying by 10 ** 58
492+
assertEq(PythUtils.convertToUint(type(int64).max, 50, 8), uint256(int256(type(int64).max)) * 10 ** 58); // 50 + 8 = 58
493+
vm.expectRevert(PythErrors.ExponentOverflow.selector);
494+
assertEq(PythUtils.convertToUint(type(int64).max, 50, 9), 0);
495+
assertEq(PythUtils.convertToUint(type(int64).max, -64, 8), 0); // -64 + 8 = -56 > -58
496+
assertEq(PythUtils.convertToUint(type(int64).max, -50, 1), 0); // -50 + 1 = -49 > -58
497+
498+
// 11. Test: Big price and scaling, should be inside the bounds
499+
vm.expectRevert(PythErrors.ExponentOverflow.selector);
500+
assertEq(PythUtils.convertToUint(type(int64).max, 50, 9), 0); // 50 + 9 = 59 > 58
501+
vm.expectRevert(PythErrors.ExponentOverflow.selector);
502+
assertEq(PythUtils.convertToUint(type(int64).max, -60, 1), 0); // -60 + 1 = -59 < -58
503+
504+
}
505+
506+
function testDeriveCrossRate() public {
507+
508+
// Test 1: Prices can't be negative
509+
assertCrossRateReverts(-100, -2, 100, -2, 5, PythErrors.NegativeInputPrice.selector);
510+
assertCrossRateReverts(100, -2, -100, -2, 5, PythErrors.NegativeInputPrice.selector);
511+
assertCrossRateReverts(-100, -2, -100, -2, 5, PythErrors.NegativeInputPrice.selector);
512+
513+
// Test 2: Exponent can't be less than -255
514+
assertCrossRateReverts(100, -256, 100, -2, 5, PythErrors.InvalidInputExpo.selector);
515+
assertCrossRateReverts(100, -2, 100, -256, 5, PythErrors.InvalidInputExpo.selector);
516+
assertCrossRateReverts(100, -256, 100, -256, 5, PythErrors.InvalidInputExpo.selector);
517+
// Target exponent can't be less than -255
518+
assertCrossRateReverts(100, -2, 100, -2, -256, PythErrors.InvalidInputExpo.selector);
519+
520+
// Test 3: Basic Tests with negative exponents
521+
assertCrossRateEquals(500, -8, 500, -8, -5, 100000);
522+
assertCrossRateEquals(10_000, -8, 100, -2, -5, 10);
523+
assertCrossRateEquals(10_000, -2, 100, -8, -5, 100_00_000_000_000);
524+
525+
// Test 4: Basic Tests with positive exponents
526+
assertCrossRateEquals(100, 2, 100, 2, -5, 100000); // 100 * 10^2 / 100 * 10^2 = 10000 / 10000 = 1 == 100000 * 10^-5
527+
// We will loose preistion as the the target exponent is 5 making the price 0.00001
528+
assertCrossRateEquals(100, 8, 100, 8, 5, 0);
529+
530+
// Test 5: Different Exponent Tests
531+
assertCrossRateEquals(10_000, -2, 100, -4, 0, 10_000); // 10_000 / 100 = 100 * 10(-2 - -4) = 10_000 with 0 decimals = 10_000
532+
assertCrossRateEquals(10_000, -2, 100, -4, 5, 0); // 10_000 / 100 = 100 * 10(-2 - -4) = 10_000 with 5 decimals = 0
533+
assertCrossRateEquals(10_000, -2, 10_000, -1, 5, 0); // It will truncate to 0
534+
assertCrossRateEquals(10_000, -10, 10_000, -2, 0, 0); // It will truncate to 0
535+
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
536+
537+
// Test 6: Exponent Edge Tests
538+
assertCrossRateEquals(10_000, 0, 100, 0, 0, 100);
539+
assertCrossRateReverts(10_000, 0, 100, 0, -255, PythErrors.ExponentOverflow.selector);
540+
assertCrossRateReverts(10_000, 0, 100, -255, -255, PythErrors.ExponentOverflow.selector);
541+
assertCrossRateReverts(10_000, -255, 100, 0, 0, PythErrors.ExponentOverflow.selector);
542+
assertCrossRateReverts(10_000, -255, 100, -178, -5, PythErrors.ExponentOverflow.selector);
543+
544+
545+
// Test 7: Max int64 price and scaling
546+
assertCrossRateEquals(type(int64).max, 0, 1, 0, 0, uint256(int256(type(int64).max)));
547+
assertCrossRateEquals(1, 0, type(int64).max, 0, 0, 0);
548+
assertCrossRateEquals(type(int64).max, 0, type(int64).max, 0, 0, 1);
549+
// type(int64).max is approx 9.223e18
550+
assertCrossRateEquals(type(int64).max, 0, 1, 0, 18, 9);
551+
// 1 / type(int64).max is approx 1.085e-19
552+
assertCrossRateEquals(1, 0, type(int64).max, 0, -19, 1);
553+
// type(int64).max * 10 ** 58 / 1
554+
assertCrossRateEquals(type(int64).max, 50, 1, -8, 0, uint256(int256(type(int64).max)) * 10 ** 58);
555+
// 1 / (type(int64).max * 10 ** 58)
556+
assertCrossRateEquals(1, 0, type(int64).max, 50, 8, 0);
557+
558+
// type(int64).max * 10 ** 59 / 1
559+
assertCrossRateReverts(type(int64).max, 50, 1, -9, 0, PythErrors.ExponentOverflow.selector);
560+
// 1 / (type(int64).max * 10 ** 59)
561+
assertCrossRateReverts(1, 0, type(int64).max, 50, 9, PythErrors.ExponentOverflow.selector);
562+
563+
564+
// Realistic Tests
565+
// Test case 1: (StEth/Eth / Eth/USD = ETH/BTC)
566+
uint256 price = PythUtils.deriveCrossRate(206487956502, -8, 206741615681, -8, -8);
567+
assertApproxEqRel(price, 100000000, 9e17); // $1
568+
569+
// Test case 2:
570+
price = PythUtils.deriveCrossRate(520010, -8, 38591, -8, -8);
571+
assertApproxEqRel(price, 1347490347, 9e17); // $13.47
572+
573+
// Test case 3:
574+
price = PythUtils.deriveCrossRate(520010, -8, 38591, -8, -12);
575+
assertApproxEqRel(price, 13474903475432, 9e17); // $13.47
401576
}
402577
}

0 commit comments

Comments
 (0)