@@ -370,33 +370,208 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils {
370
370
}
371
371
372
372
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
+
373
397
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 );
376
401
PythUtils.convertToUint (- 100 , - 5 , 18 );
377
402
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 );
381
406
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
382
419
// Price with 18 decimals and exponent -5
383
420
assertEq (
384
421
PythUtils.convertToUint (100 , - 5 , 18 ),
385
- 1000000000000000 // 100 * 10^13
422
+ 100_0_000_000_000_000 // 100 * 10^13
386
423
);
387
-
388
424
// Price with 9 decimals and exponent -2
389
425
assertEq (
390
426
PythUtils.convertToUint (100 , - 2 , 9 ),
391
- 1000000000 // 100 * 10^7
427
+ 100_0_000_000 // 100 * 10^7
392
428
);
393
429
394
- // Price with 4 decimals and exponent -5
430
+ // Test 6: Price with 4 decimals and exponent -5
395
431
assertEq (PythUtils.convertToUint (100 , - 5 , 4 ), 10 );
396
432
397
- // Price with 5 decimals and exponent -2
433
+ // Test 7: Price with 5 decimals and exponent -2
398
434
// @note: We will lose precision here as price is
399
435
// 0.00001 and we are targetDecimals is 2.
400
436
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
401
576
}
402
577
}
0 commit comments