@@ -370,33 +370,208 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils {
370370}
371371
372372contract 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