Skip to content

Commit 0cf87b5

Browse files
committed
feat: floor price set to 0.06 USDFC/month per data set
For storage amounts below ~24.576 GiB, the rate will be 0.06 USDFC / month. Above this amount the price is per-byte. Closes: #319
1 parent 5f9f3ed commit 0cf87b5

File tree

5 files changed

+181
-59
lines changed

5 files changed

+181
-59
lines changed

service_contracts/src/FilecoinWarmStorageService.sol

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ contract FilecoinWarmStorageService is
196196
uint256 private immutable STORAGE_PRICE_PER_TIB_PER_MONTH; // 2.5 USDFC per TiB per month without CDN with correct decimals
197197
uint256 private immutable CACHE_MISS_PRICE_PER_TIB_PER_MONTH; // .5 USDFC per TiB per month for CDN with correct decimals
198198
uint256 private immutable CDN_PRICE_PER_TIB_PER_MONTH; // .5 USDFC per TiB per month for CDN with correct decimals
199+
uint256 private immutable MINIMUM_STORAGE_RATE_PER_MONTH; // 0.06 USDFC per month minimum pricing floor
199200

200201
// Fixed lockup amounts for CDN rails
201202
uint256 private immutable DEFAULT_CDN_LOCKUP_AMOUNT; // 0.7 USDFC
@@ -330,6 +331,7 @@ contract FilecoinWarmStorageService is
330331
STORAGE_PRICE_PER_TIB_PER_MONTH = (5 * 10 ** TOKEN_DECIMALS) / 2; // 2.5 USDFC
331332
CACHE_MISS_PRICE_PER_TIB_PER_MONTH = (1 * 10 ** TOKEN_DECIMALS) / 2; // 0.5 USDFC
332333
CDN_PRICE_PER_TIB_PER_MONTH = (1 * 10 ** TOKEN_DECIMALS) / 2; // 0.5 USDFC
334+
MINIMUM_STORAGE_RATE_PER_MONTH = (6 * 10 ** TOKEN_DECIMALS) / 100; // 0.06 USDFC minimum
333335

334336
// Initialize the lockup constants based on the actual token decimals
335337
DEFAULT_CDN_LOCKUP_AMOUNT = (7 * 10 ** TOKEN_DECIMALS) / 10; // 0.7 USDFC
@@ -1189,7 +1191,8 @@ contract FilecoinWarmStorageService is
11891191

11901192
/**
11911193
* @notice Calculate all per-epoch rates based on total storage size
1192-
* @dev Returns storage, cache miss, and CDN rates per TiB per month
1194+
* @dev Returns storage, cache miss, and CDN rates per TiB per month.
1195+
* Storage rate includes minimum pricing floor.
11931196
* @param totalBytes Total size of the stored data in bytes
11941197
* @return storageRate The PDP storage rate per epoch
11951198
* @return cacheMissRate The cache miss rate per epoch
@@ -1200,18 +1203,26 @@ contract FilecoinWarmStorageService is
12001203
view
12011204
returns (uint256 storageRate, uint256 cacheMissRate, uint256 cdnRate)
12021205
{
1203-
storageRate = calculateStorageSizeBasedRatePerEpoch(totalBytes, STORAGE_PRICE_PER_TIB_PER_MONTH);
1206+
storageRate = _calculateStorageRate(totalBytes);
12041207
cacheMissRate = calculateStorageSizeBasedRatePerEpoch(totalBytes, CACHE_MISS_PRICE_PER_TIB_PER_MONTH);
12051208
cdnRate = calculateStorageSizeBasedRatePerEpoch(totalBytes, CDN_PRICE_PER_TIB_PER_MONTH);
12061209
}
12071210

12081211
/**
12091212
* @notice Calculate the storage rate per epoch (internal use)
1213+
* @dev Implements minimum pricing floor and returns the higher of the natural size-based rate or the minimum rate.
12101214
* @param totalBytes Total size of the stored data in bytes
12111215
* @return The storage rate per epoch
12121216
*/
12131217
function _calculateStorageRate(uint256 totalBytes) internal view returns (uint256) {
1214-
return calculateStorageSizeBasedRatePerEpoch(totalBytes, STORAGE_PRICE_PER_TIB_PER_MONTH);
1218+
// Calculate natural size-based rate
1219+
uint256 naturalRate = calculateStorageSizeBasedRatePerEpoch(totalBytes, STORAGE_PRICE_PER_TIB_PER_MONTH);
1220+
1221+
// Calculate minimum rate (floor price converted to per-epoch)
1222+
uint256 minimumRate = MINIMUM_STORAGE_RATE_PER_MONTH / EPOCHS_PER_MONTH;
1223+
1224+
// Return whichever is higher: natural rate or minimum rate
1225+
return naturalRate > minimumRate ? naturalRate : minimumRate;
12151226
}
12161227

12171228
/**

service_contracts/test/FilecoinWarmStorageService.t.sol

Lines changed: 152 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -557,13 +557,13 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
557557
mockUSDFC,
558558
address(pdpServiceWithPayments),
559559
true, // approved
560-
1000e6, // rate allowance (1000 USDFC)
561-
1000e6, // lockup allowance (1000 USDFC)
560+
1000e18, // rate allowance (1000 USDFC)
561+
1000e18, // lockup allowance (1000 USDFC)
562562
365 days // max lockup period
563563
);
564564

565565
// Client deposits funds to the FilecoinPayV1 contract for future payments
566-
uint256 depositAmount = 10e6; // Sufficient funds for initial lockup and future operations
566+
uint256 depositAmount = 10e18; // Sufficient funds for initial lockup and future operations
567567
mockUSDFC.approve(address(payments), depositAmount);
568568
payments.deposit(mockUSDFC, client, depositAmount);
569569
vm.stopPrank();
@@ -682,13 +682,13 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
682682
mockUSDFC,
683683
address(pdpServiceWithPayments),
684684
true, // approved
685-
1000e6, // rate allowance (1000 USDFC)
686-
1000e6, // lockup allowance (1000 USDFC)
685+
1000e18, // rate allowance (1000 USDFC)
686+
1000e18, // lockup allowance (1000 USDFC)
687687
365 days // max lockup period
688688
);
689689

690690
// Client deposits funds to the FilecoinPayV1 contract for future payments
691-
uint256 depositAmount = 10e6; // Sufficient funds for initial lockup and future operations
691+
uint256 depositAmount = 10e18; // Sufficient funds for initial lockup and future operations
692692
mockUSDFC.approve(address(payments), depositAmount);
693693
payments.deposit(mockUSDFC, client, depositAmount);
694694
vm.stopPrank();
@@ -795,11 +795,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
795795
mockUSDFC,
796796
address(pdpServiceWithPayments),
797797
true, // approved
798-
1000e6, // rate allowance (1000 USDFC)
799-
1000e6, // lockup allowance (1000 USDFC)
798+
1000e18, // rate allowance (1000 USDFC)
799+
1000e18, // lockup allowance (1000 USDFC)
800800
365 days // max lockup period
801801
);
802-
uint256 depositAmount = 10e6; // Sufficient funds for initial lockup and future operations
802+
uint256 depositAmount = 10e18; // Sufficient funds for initial lockup and future operations
803803
mockUSDFC.approve(address(payments), depositAmount);
804804
payments.deposit(mockUSDFC, client, depositAmount);
805805
vm.stopPrank();
@@ -909,38 +909,149 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
909909
// Test the values returned by getServicePrice
910910
FilecoinWarmStorageService.ServicePricing memory pricing = pdpServiceWithPayments.getServicePrice();
911911

912-
uint256 decimals = 6; // MockUSDFC uses 6 decimals in tests
913-
uint256 expectedNoCDN = 25 * 10 ** (decimals - 1); // 2.5 USDFC with 6 decimals
914-
uint256 expectedWithCDN = 3 * 10 ** decimals; // 3 USDFC with 6 decimals (2.5 + 0.5 CDN)
912+
uint256 decimals = 18; // MockUSDFC uses 18 decimals in tests
913+
uint256 expectedNoCDN = 25 * 10 ** (decimals - 1); // 2.5 USDFC with 18 decimals
914+
uint256 expectedWithCDN = 3 * 10 ** decimals; // 3 USDFC with 18 decimals (2.5 + 0.5 CDN)
915915

916916
assertEq(pricing.pricePerTiBPerMonthNoCDN, expectedNoCDN, "No CDN price should be 2.5 * 10^decimals");
917917
assertEq(pricing.pricePerTiBPerMonthWithCDN, expectedWithCDN, "With CDN price should be 3 * 10^decimals");
918918
assertEq(address(pricing.tokenAddress), address(mockUSDFC), "Token address should match USDFC");
919919
assertEq(pricing.epochsPerMonth, 86400, "Epochs per month should be 86400");
920920

921921
// Verify the values are in expected range
922-
assert(pricing.pricePerTiBPerMonthNoCDN < 10 ** 8); // Less than 10^8
923-
assert(pricing.pricePerTiBPerMonthWithCDN < 10 ** 8); // Less than 10^8
922+
assert(pricing.pricePerTiBPerMonthNoCDN < 10 ** 20); // Less than 10^20
923+
assert(pricing.pricePerTiBPerMonthWithCDN < 10 ** 20); // Less than 10^20
924924
}
925925

926926
function testGetEffectiveRatesValues() public view {
927927
// Test the values returned by getEffectiveRates
928928
(uint256 serviceFee, uint256 spPayment) = pdpServiceWithPayments.getEffectiveRates();
929929

930-
uint256 decimals = 6; // MockUSDFC uses 6 decimals in tests
931-
// Total is 2.5 USDFC with 6 decimals
930+
uint256 decimals = 18; // MockUSDFC uses 18 decimals in tests
931+
// Total is 2.5 USDFC with 18 decimals
932932
uint256 expectedTotal = 25 * 10 ** (decimals - 1);
933933

934934
// Test setup uses 0% commission
935935
uint256 expectedServiceFee = 0; // 0% commission
936936
uint256 expectedSpPayment = expectedTotal; // 100% goes to SP
937937

938938
assertEq(serviceFee, expectedServiceFee, "Service fee should be 0 with 0% commission");
939-
assertEq(spPayment, expectedSpPayment, "SP payment should be 2.5 * 10^6");
940-
assertEq(serviceFee + spPayment, expectedTotal, "Total should equal 2.5 * 10^6");
939+
assertEq(spPayment, expectedSpPayment, "SP payment should be 2.5 * 10^18");
940+
assertEq(serviceFee + spPayment, expectedTotal, "Total should equal 2.5 * 10^18");
941941

942942
// Verify the values are in expected range
943-
assert(serviceFee + spPayment < 10 ** 8); // Less than 10^8
943+
assert(serviceFee + spPayment < 10 ** 20); // Less than 10^20
944+
}
945+
946+
// Minimum Pricing Tests
947+
function testMinimumPricing_SmallDataSetsPayFloorRate() public view {
948+
// Small datasets should all pay the minimum floor rate of 0.06 USDFC/month
949+
uint256 decimals = 18;
950+
uint256 oneGiB = 1024 * 1024 * 1024;
951+
952+
// Expected minimum: 0.06 USDFC/month = 6/100 with 18 decimals
953+
uint256 expectedMinPerMonth = (6 * 10 ** decimals) / 100;
954+
uint256 expectedMinPerEpoch = expectedMinPerMonth / 86400; // Convert to per-epoch
955+
956+
// Test 0 bytes
957+
(uint256 rateZero,,) = pdpServiceWithPayments.calculateRatesPerEpoch(0);
958+
assertEq(rateZero, expectedMinPerEpoch, "0 bytes should return 0.06 USDFC/month minimum");
959+
960+
// Test 1 GiB
961+
(uint256 rateOneGiB,,) = pdpServiceWithPayments.calculateRatesPerEpoch(oneGiB);
962+
assertEq(rateOneGiB, expectedMinPerEpoch, "1 GiB should return minimum rate");
963+
964+
// Test 10 GiB
965+
(uint256 rateTenGiB,,) = pdpServiceWithPayments.calculateRatesPerEpoch(10 * oneGiB);
966+
assertEq(rateTenGiB, expectedMinPerEpoch, "10 GiB should return minimum rate");
967+
968+
// Test 24 GiB (below crossover)
969+
(uint256 rateTwentyFourGiB,,) = pdpServiceWithPayments.calculateRatesPerEpoch(24 * oneGiB);
970+
assertEq(rateTwentyFourGiB, expectedMinPerEpoch, "24 GiB should return minimum rate");
971+
}
972+
973+
function testMinimumPricing_CrossoverPoint() public view {
974+
// Test the crossover where natural pricing exceeds minimum
975+
// At 2.5 USDFC/TiB: 0.06/2.5*1024 = 24.576 GiB is the crossover
976+
uint256 oneGiB = 1024 * 1024 * 1024;
977+
uint256 decimals = 18;
978+
uint256 expectedMinPerMonth = (6 * 10 ** decimals) / 100;
979+
uint256 expectedMinPerEpoch = expectedMinPerMonth / 86400;
980+
981+
// 24 GiB: natural rate (0.0586) < minimum (0.06), so returns minimum
982+
(uint256 rate24GiB,,) = pdpServiceWithPayments.calculateRatesPerEpoch(24 * oneGiB);
983+
assertEq(rate24GiB, expectedMinPerEpoch, "24 GiB should use minimum floor");
984+
985+
// 25 GiB: natural rate (0.0610) > minimum (0.06), so returns natural rate
986+
(uint256 rate25GiB,,) = pdpServiceWithPayments.calculateRatesPerEpoch(25 * oneGiB);
987+
assert(rate25GiB > expectedMinPerEpoch);
988+
989+
// Verify it's actually proportional (not minimum)
990+
uint256 expectedNatural25 = rate25GiB * 86400; // Convert to monthly
991+
uint256 expected25Monthly = (25 * 10 ** decimals * 25) / (1024 * 10); // 25 GiB at 2.5 USDFC/TiB
992+
// Tolerance: actual loss is ~16,000 from integer division, allow 100,000 for safety
993+
assertApproxEqAbs(expectedNatural25, expected25Monthly, 100000, "25 GiB should use natural rate");
994+
}
995+
996+
function testMinimumPricing_LargeDataSetsUseProportionalPricing() public view {
997+
// Large datasets should use proportional pricing (natural rate > minimum)
998+
uint256 oneGiB = 1024 * 1024 * 1024;
999+
uint256 decimals = 18;
1000+
uint256 expectedMinPerMonth = (6 * 10 ** decimals) / 100;
1001+
uint256 expectedMinPerEpoch = expectedMinPerMonth / 86400;
1002+
1003+
// Test 48 GiB
1004+
(uint256 rate48GiB,,) = pdpServiceWithPayments.calculateRatesPerEpoch(48 * oneGiB);
1005+
assert(rate48GiB > expectedMinPerEpoch);
1006+
1007+
// Test 100 GiB
1008+
(uint256 rate100GiB,,) = pdpServiceWithPayments.calculateRatesPerEpoch(100 * oneGiB);
1009+
assert(rate100GiB > rate48GiB);
1010+
1011+
// Test 1 TiB
1012+
uint256 oneTiB = oneGiB * 1024;
1013+
(uint256 rateOneTiB,,) = pdpServiceWithPayments.calculateRatesPerEpoch(oneTiB);
1014+
assert(rateOneTiB > rate100GiB);
1015+
1016+
// Verify proportional scaling
1017+
assertApproxEqRel(rate100GiB, rate48GiB * 100 / 48, 0.01e18, "Rates should scale proportionally");
1018+
}
1019+
1020+
function testMinimumPricing_ExactlyPoint06USDFC() public view {
1021+
// Verify that minimum pricing is exactly 0.06 USDFC/month for small datasets
1022+
uint256 decimals = 18; // MockUSDFC uses 18 decimals in tests
1023+
uint256 oneGiB = 1024 * 1024 * 1024;
1024+
1025+
// Get rate per epoch for dataset below crossover point
1026+
(uint256 ratePerEpoch,,) = pdpServiceWithPayments.calculateRatesPerEpoch(oneGiB);
1027+
1028+
// Convert to rate per month (86400 epochs per month)
1029+
uint256 ratePerMonth = ratePerEpoch * 86400;
1030+
1031+
// Expected: exactly 0.06 USDFC with 18 decimals = 60000000000000000
1032+
// Allow tiny tolerance for integer division rounding (0.06 / 86400 rounds down)
1033+
uint256 expected = (6 * 10 ** decimals) / 100;
1034+
uint256 tolerance = 1; // Allow 1 per epoch difference = 86400 total
1035+
1036+
assertApproxEqAbs(ratePerMonth, expected, tolerance * 86400, "Minimum rate should be 0.06 USDFC/month");
1037+
}
1038+
1039+
function testMinimumPricing_CDNRatesUnaffected() public view {
1040+
// Verify that CDN rates remain proportional (not affected by minimum pricing)
1041+
uint256 oneGiB = 1024 * 1024 * 1024;
1042+
1043+
// Get rates for small dataset
1044+
(, uint256 cacheMissRateSmall, uint256 cdnRateSmall) = pdpServiceWithPayments.calculateRatesPerEpoch(oneGiB);
1045+
1046+
// Get rates for larger dataset (12 GiB)
1047+
(, uint256 cacheMissRateLarge, uint256 cdnRateLarge) =
1048+
pdpServiceWithPayments.calculateRatesPerEpoch(12 * oneGiB);
1049+
1050+
// CDN rates should scale proportionally with size (allow small rounding tolerance)
1051+
assertApproxEqAbs(
1052+
cacheMissRateLarge, cacheMissRateSmall * 12, 100, "Cache miss rate should scale proportionally"
1053+
);
1054+
assertApproxEqAbs(cdnRateLarge, cdnRateSmall * 12, 100, "CDN rate should scale proportionally");
9441055
}
9451056

9461057
uint256 nextClientDataSetId = 0;
@@ -971,9 +1082,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
9711082

9721083
// Setup client payment approval if not already done
9731084
vm.startPrank(clientAddress);
974-
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days);
975-
mockUSDFC.approve(address(payments), 100e6);
976-
payments.deposit(mockUSDFC, clientAddress, 100e6);
1085+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
1086+
mockUSDFC.approve(address(payments), 100e18);
1087+
payments.deposit(mockUSDFC, clientAddress, 100e18);
9771088
vm.stopPrank();
9781089

9791090
// Create data set as approved provider
@@ -1157,9 +1268,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
11571268

11581269
// Setup client payment approval if not already done
11591270
vm.startPrank(clientAddress);
1160-
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days);
1161-
mockUSDFC.approve(address(payments), 100e6);
1162-
payments.deposit(mockUSDFC, clientAddress, 100e6);
1271+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
1272+
mockUSDFC.approve(address(payments), 100e18);
1273+
payments.deposit(mockUSDFC, clientAddress, 100e18);
11631274
vm.stopPrank();
11641275

11651276
// Create data set as approved provider
@@ -1302,11 +1413,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
13021413
mockUSDFC,
13031414
address(pdpServiceWithPayments),
13041415
true,
1305-
1000e6, // rate allowance
1306-
1000e6, // lockup allowance
1416+
1000e18, // rate allowance
1417+
1000e18, // lockup allowance
13071418
365 days // max lockup period
13081419
);
1309-
uint256 depositAmount = 100e6;
1420+
uint256 depositAmount = 100e18;
13101421
mockUSDFC.approve(address(payments), depositAmount);
13111422
payments.deposit(mockUSDFC, client, depositAmount);
13121423
vm.stopPrank();
@@ -1482,11 +1593,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
14821593
mockUSDFC,
14831594
address(pdpServiceWithPayments),
14841595
true,
1485-
1000e6, // rate allowance
1486-
1000e6, // lockup allowance
1596+
1000e18, // rate allowance
1597+
1000e18, // lockup allowance
14871598
365 days // max lockup period
14881599
);
1489-
uint256 depositAmount = 100e6;
1600+
uint256 depositAmount = 100e18;
14901601
mockUSDFC.approve(address(payments), depositAmount);
14911602
payments.deposit(mockUSDFC, client, depositAmount);
14921603
vm.stopPrank();
@@ -1603,11 +1714,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
16031714
mockUSDFC,
16041715
address(pdpServiceWithPayments),
16051716
true,
1606-
1000e6, // rate allowance
1607-
1000e6, // lockup allowance
1717+
1000e18, // rate allowance
1718+
1000e18, // lockup allowance
16081719
365 days // max lockup period
16091720
);
1610-
uint256 depositAmount = 100e6;
1721+
uint256 depositAmount = 100e18;
16111722
mockUSDFC.approve(address(payments), depositAmount);
16121723
payments.deposit(mockUSDFC, client, depositAmount);
16131724
vm.stopPrank();
@@ -2822,8 +2933,8 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
28222933
);
28232934

28242935
vm.startPrank(client);
2825-
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days);
2826-
uint256 depositAmount = 1e6;
2936+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
2937+
uint256 depositAmount = 1e18;
28272938
mockUSDFC.approve(address(payments), depositAmount);
28282939
payments.deposit(mockUSDFC, client, depositAmount);
28292940
vm.stopPrank();
@@ -2875,8 +2986,8 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
28752986
);
28762987

28772988
vm.startPrank(client);
2878-
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days);
2879-
uint256 depositAmount = 1e6;
2989+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
2990+
uint256 depositAmount = 1e18;
28802991
mockUSDFC.approve(address(payments), depositAmount);
28812992
payments.deposit(mockUSDFC, client, depositAmount);
28822993
vm.stopPrank();
@@ -2926,8 +3037,8 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
29263037
);
29273038

29283039
vm.startPrank(client);
2929-
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days);
2930-
uint256 depositAmount = 10e6;
3040+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
3041+
uint256 depositAmount = 10e18;
29313042
mockUSDFC.approve(address(payments), depositAmount);
29323043
payments.deposit(mockUSDFC, client, depositAmount);
29333044
vm.stopPrank();
@@ -4197,7 +4308,7 @@ contract FilecoinWarmStorageServiceSignatureTest is Test {
41974308
pdpService = SignatureCheckingService(address(serviceProxy));
41984309

41994310
// Fund the payer
4200-
mockUSDFC.safeTransfer(payer, 1000 * 10 ** 6); // 1000 USDFC
4311+
mockUSDFC.safeTransfer(payer, 1000 * 10 ** 18); // 1000 USDFC
42014312
}
42024313

42034314
// Test the recoverSigner function indirectly through signature verification

0 commit comments

Comments
 (0)