From cf045158f6fa14c8b2bd8d3ca0a796cdfa24d6b6 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Wed, 22 Oct 2025 23:26:20 +1100 Subject: [PATCH 1/4] 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: https://github.com/FilOzone/filecoin-services/issues/319 --- .../abi/FilecoinWarmStorageService.abi.json | 26 +++ .../src/FilecoinWarmStorageService.sol | 20 +- .../test/FilecoinWarmStorageService.t.sol | 178 ++++++++++++++---- .../FilecoinWarmStorageServiceOwner.t.sol | 8 +- .../test/ProviderValidation.t.sol | 20 +- service_contracts/test/mocks/SharedMocks.sol | 2 +- 6 files changed, 194 insertions(+), 60 deletions(-) diff --git a/service_contracts/abi/FilecoinWarmStorageService.abi.json b/service_contracts/abi/FilecoinWarmStorageService.abi.json index eeb21226..b06c6801 100644 --- a/service_contracts/abi/FilecoinWarmStorageService.abi.json +++ b/service_contracts/abi/FilecoinWarmStorageService.abi.json @@ -357,6 +357,11 @@ "name": "epochsPerMonth", "type": "uint256", "internalType": "uint256" + }, + { + "name": "minimumPricePerMonth", + "type": "uint256", + "internalType": "uint256" } ] } @@ -1681,6 +1686,27 @@ } ] }, + { + "type": "error", + "name": "InsufficientFundsForMinimumRate", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "minimumRequired", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + } + ] + }, { "type": "error", "name": "InvalidChallengeCount", diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index 295a4b7b..a539f17a 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -159,6 +159,7 @@ contract FilecoinWarmStorageService is uint256 pricePerTiBCacheMissEgress; // Cache miss egress price per TiB (usage-based) IERC20 tokenAddress; // Address of the USDFC token uint256 epochsPerMonth; // Number of epochs in a month + uint256 minimumPricePerMonth; // Minimum monthly charge for any dataset size (0.06 USDFC) } // Used for announcing upgrades, packed into one slot @@ -197,6 +198,7 @@ contract FilecoinWarmStorageService is uint256 private immutable STORAGE_PRICE_PER_TIB_PER_MONTH; // 2.5 USDFC per TiB per month without CDN with correct decimals uint256 private immutable CDN_EGRESS_PRICE_PER_TIB; // 7 USDFC per TiB of CDN egress uint256 private immutable CACHE_MISS_EGRESS_PRICE_PER_TIB; // 7 USDFC per TiB of cache miss egress + uint256 private immutable MINIMUM_STORAGE_RATE_PER_MONTH; // 0.06 USDFC per month minimum pricing floor // Fixed lockup amounts for CDN rails uint256 private immutable DEFAULT_CDN_LOCKUP_AMOUNT; // 0.7 USDFC @@ -331,6 +333,7 @@ contract FilecoinWarmStorageService is STORAGE_PRICE_PER_TIB_PER_MONTH = (5 * 10 ** TOKEN_DECIMALS) / 2; // 2.5 USDFC CDN_EGRESS_PRICE_PER_TIB = 7 * 10 ** TOKEN_DECIMALS; // 7 USDFC per TiB CACHE_MISS_EGRESS_PRICE_PER_TIB = 7 * 10 ** TOKEN_DECIMALS; // 7 USDFC per TiB + MINIMUM_STORAGE_RATE_PER_MONTH = (6 * 10 ** TOKEN_DECIMALS) / 100; // 0.06 USDFC minimum // Initialize the lockup constants based on the actual token decimals DEFAULT_CDN_LOCKUP_AMOUNT = (7 * 10 ** TOKEN_DECIMALS) / 10; // 0.7 USDFC @@ -1177,21 +1180,29 @@ contract FilecoinWarmStorageService is /** * @notice Calculate storage rate per epoch based on total storage size - * @dev Returns storage rate per TiB per month + * @dev Returns storage rate per TiB per month with minimum pricing floor applied * @param totalBytes Total size of the stored data in bytes * @return storageRate The PDP storage rate per epoch */ function calculateRatesPerEpoch(uint256 totalBytes) external view returns (uint256 storageRate) { - storageRate = calculateStorageSizeBasedRatePerEpoch(totalBytes, STORAGE_PRICE_PER_TIB_PER_MONTH); + storageRate = _calculateStorageRate(totalBytes); } /** * @notice Calculate the storage rate per epoch (internal use) + * @dev Implements minimum pricing floor and returns the higher of the natural size-based rate or the minimum rate. * @param totalBytes Total size of the stored data in bytes * @return The storage rate per epoch */ function _calculateStorageRate(uint256 totalBytes) internal view returns (uint256) { - return calculateStorageSizeBasedRatePerEpoch(totalBytes, STORAGE_PRICE_PER_TIB_PER_MONTH); + // Calculate natural size-based rate + uint256 naturalRate = calculateStorageSizeBasedRatePerEpoch(totalBytes, STORAGE_PRICE_PER_TIB_PER_MONTH); + + // Calculate minimum rate (floor price converted to per-epoch) + uint256 minimumRate = MINIMUM_STORAGE_RATE_PER_MONTH / EPOCHS_PER_MONTH; + + // Return whichever is higher: natural rate or minimum rate + return naturalRate > minimumRate ? naturalRate : minimumRate; } /** @@ -1289,7 +1300,8 @@ contract FilecoinWarmStorageService is pricePerTiBCdnEgress: CDN_EGRESS_PRICE_PER_TIB, pricePerTiBCacheMissEgress: CACHE_MISS_EGRESS_PRICE_PER_TIB, tokenAddress: usdfcTokenAddress, - epochsPerMonth: EPOCHS_PER_MONTH + epochsPerMonth: EPOCHS_PER_MONTH, + minimumPricePerMonth: MINIMUM_STORAGE_RATE_PER_MONTH }); } diff --git a/service_contracts/test/FilecoinWarmStorageService.t.sol b/service_contracts/test/FilecoinWarmStorageService.t.sol index eea02ca9..5126d505 100644 --- a/service_contracts/test/FilecoinWarmStorageService.t.sol +++ b/service_contracts/test/FilecoinWarmStorageService.t.sol @@ -557,13 +557,13 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { mockUSDFC, address(pdpServiceWithPayments), true, // approved - 1000e6, // rate allowance (1000 USDFC) - 1000e6, // lockup allowance (1000 USDFC) + 1000e18, // rate allowance (1000 USDFC) + 1000e18, // lockup allowance (1000 USDFC) 365 days // max lockup period ); // Client deposits funds to the FilecoinPayV1 contract for future payments - uint256 depositAmount = 10e6; // Sufficient funds for initial lockup and future operations + uint256 depositAmount = 10e18; // Sufficient funds for initial lockup and future operations mockUSDFC.approve(address(payments), depositAmount); payments.deposit(mockUSDFC, client, depositAmount); vm.stopPrank(); @@ -682,13 +682,13 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { mockUSDFC, address(pdpServiceWithPayments), true, // approved - 1000e6, // rate allowance (1000 USDFC) - 1000e6, // lockup allowance (1000 USDFC) + 1000e18, // rate allowance (1000 USDFC) + 1000e18, // lockup allowance (1000 USDFC) 365 days // max lockup period ); // Client deposits funds to the FilecoinPayV1 contract for future payments - uint256 depositAmount = 10e6; // Sufficient funds for initial lockup and future operations + uint256 depositAmount = 10e18; // Sufficient funds for initial lockup and future operations mockUSDFC.approve(address(payments), depositAmount); payments.deposit(mockUSDFC, client, depositAmount); vm.stopPrank(); @@ -795,11 +795,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { mockUSDFC, address(pdpServiceWithPayments), true, // approved - 1000e6, // rate allowance (1000 USDFC) - 1000e6, // lockup allowance (1000 USDFC) + 1000e18, // rate allowance (1000 USDFC) + 1000e18, // lockup allowance (1000 USDFC) 365 days // max lockup period ); - uint256 depositAmount = 10e6; // Sufficient funds for initial lockup and future operations + uint256 depositAmount = 10e18; // Sufficient funds for initial lockup and future operations mockUSDFC.approve(address(payments), depositAmount); payments.deposit(mockUSDFC, client, depositAmount); vm.stopPrank(); @@ -909,10 +909,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // Test the values returned by getServicePrice FilecoinWarmStorageService.ServicePricing memory pricing = pdpServiceWithPayments.getServicePrice(); - uint256 decimals = 6; // MockUSDFC uses 6 decimals in tests - uint256 expectedNoCDN = 25 * 10 ** (decimals - 1); // 2.5 USDFC with 6 decimals + uint256 decimals = 18; // MockUSDFC uses 18 decimals in tests + uint256 expectedNoCDN = 25 * 10 ** (decimals - 1); // 2.5 USDFC with 18 decimals uint256 expectedCDNEgress = 7 * 10 ** decimals; // 7 USDFC per TiB of CDN egress uint256 expectedCacheMissEgress = 7 * 10 ** decimals; // 7 USDFC per TiB of cache miss egress + uint256 expectedMinimum = (6 * 10 ** decimals) / 100; // 0.06 USDFC minimum assertEq(pricing.pricePerTiBPerMonthNoCDN, expectedNoCDN, "No CDN price should be 2.5 * 10^decimals"); assertEq(pricing.pricePerTiBCdnEgress, expectedCDNEgress, "CDN egress price should be 7 * 10^decimals per TiB"); @@ -923,19 +924,20 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { ); assertEq(address(pricing.tokenAddress), address(mockUSDFC), "Token address should match USDFC"); assertEq(pricing.epochsPerMonth, 86400, "Epochs per month should be 86400"); + assertEq(pricing.minimumPricePerMonth, expectedMinimum, "Minimum price should be 0.06 * 10^decimals"); // Verify the values are in expected range - assert(pricing.pricePerTiBPerMonthNoCDN < 10 ** 8); // Less than 10^8 - assert(pricing.pricePerTiBCdnEgress < 10 ** 8); // Less than 10^8 - assert(pricing.pricePerTiBCacheMissEgress < 10 ** 8); // Less than 10^8 + assert(pricing.pricePerTiBPerMonthNoCDN < 10 ** 20); // Less than 10^20 + assert(pricing.pricePerTiBCdnEgress < 10 ** 20); // Less than 10^20 + assert(pricing.pricePerTiBCacheMissEgress < 10 ** 20); // Less than 10^20 } function testGetEffectiveRatesValues() public view { // Test the values returned by getEffectiveRates (uint256 serviceFee, uint256 spPayment) = pdpServiceWithPayments.getEffectiveRates(); - uint256 decimals = 6; // MockUSDFC uses 6 decimals in tests - // Total is 2.5 USDFC with 6 decimals + uint256 decimals = 18; // MockUSDFC uses 18 decimals in tests + // Total is 2.5 USDFC with 18 decimals uint256 expectedTotal = 25 * 10 ** (decimals - 1); // Test setup uses 0% commission @@ -943,13 +945,107 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { uint256 expectedSpPayment = expectedTotal; // 100% goes to SP assertEq(serviceFee, expectedServiceFee, "Service fee should be 0 with 0% commission"); - assertEq(spPayment, expectedSpPayment, "SP payment should be 2.5 * 10^6"); - assertEq(serviceFee + spPayment, expectedTotal, "Total should equal 2.5 * 10^6"); + assertEq(spPayment, expectedSpPayment, "SP payment should be 2.5 * 10^18"); + assertEq(serviceFee + spPayment, expectedTotal, "Total should equal 2.5 * 10^18"); // Verify the values are in expected range - assert(serviceFee + spPayment < 10 ** 8); // Less than 10^8 + assert(serviceFee + spPayment < 10 ** 20); // Less than 10^20 } + // Minimum Pricing Tests + function testMinimumPricing_SmallDataSetsPayFloorRate() public view { + // Small datasets should all pay the minimum floor rate of 0.06 USDFC/month + uint256 decimals = 18; + uint256 oneGiB = 1024 * 1024 * 1024; + + // Expected minimum: 0.06 USDFC/month = 6/100 with 18 decimals + uint256 expectedMinPerMonth = (6 * 10 ** decimals) / 100; + uint256 expectedMinPerEpoch = expectedMinPerMonth / 86400; // Convert to per-epoch + + // Test 0 bytes + uint256 rateZero = pdpServiceWithPayments.calculateRatesPerEpoch(0); + assertEq(rateZero, expectedMinPerEpoch, "0 bytes should return 0.06 USDFC/month minimum"); + + // Test 1 GiB + uint256 rateOneGiB = pdpServiceWithPayments.calculateRatesPerEpoch(oneGiB); + assertEq(rateOneGiB, expectedMinPerEpoch, "1 GiB should return minimum rate"); + + // Test 10 GiB + uint256 rateTenGiB = pdpServiceWithPayments.calculateRatesPerEpoch(10 * oneGiB); + assertEq(rateTenGiB, expectedMinPerEpoch, "10 GiB should return minimum rate"); + + // Test 24 GiB (below crossover) + uint256 rateTwentyFourGiB = pdpServiceWithPayments.calculateRatesPerEpoch(24 * oneGiB); + assertEq(rateTwentyFourGiB, expectedMinPerEpoch, "24 GiB should return minimum rate"); + } + + function testMinimumPricing_CrossoverPoint() public view { + // Test the crossover where natural pricing exceeds minimum + // At 2.5 USDFC/TiB: 0.06/2.5*1024 = 24.576 GiB is the crossover + uint256 oneGiB = 1024 * 1024 * 1024; + uint256 decimals = 18; + uint256 expectedMinPerMonth = (6 * 10 ** decimals) / 100; + uint256 expectedMinPerEpoch = expectedMinPerMonth / 86400; + + // 24 GiB: natural rate (0.0586) < minimum (0.06), so returns minimum + uint256 rate24GiB = pdpServiceWithPayments.calculateRatesPerEpoch(24 * oneGiB); + assertEq(rate24GiB, expectedMinPerEpoch, "24 GiB should use minimum floor"); + + // 25 GiB: natural rate (0.0610) > minimum (0.06), so returns natural rate + uint256 rate25GiB = pdpServiceWithPayments.calculateRatesPerEpoch(25 * oneGiB); + assert(rate25GiB > expectedMinPerEpoch); + + // Verify it's actually proportional (not minimum) + uint256 expectedNatural25 = rate25GiB * 86400; // Convert to monthly + uint256 expected25Monthly = (25 * 10 ** decimals * 25) / (1024 * 10); // 25 GiB at 2.5 USDFC/TiB + // Tolerance: actual loss is ~16,000 from integer division, allow 100,000 for safety + assertApproxEqAbs(expectedNatural25, expected25Monthly, 100000, "25 GiB should use natural rate"); + } + + function testMinimumPricing_LargeDataSetsUseProportionalPricing() public view { + // Large datasets should use proportional pricing (natural rate > minimum) + uint256 oneGiB = 1024 * 1024 * 1024; + uint256 decimals = 18; + uint256 expectedMinPerMonth = (6 * 10 ** decimals) / 100; + uint256 expectedMinPerEpoch = expectedMinPerMonth / 86400; + + // Test 48 GiB + uint256 rate48GiB = pdpServiceWithPayments.calculateRatesPerEpoch(48 * oneGiB); + assert(rate48GiB > expectedMinPerEpoch); + + // Test 100 GiB + uint256 rate100GiB = pdpServiceWithPayments.calculateRatesPerEpoch(100 * oneGiB); + assert(rate100GiB > rate48GiB); + + // Test 1 TiB + uint256 oneTiB = oneGiB * 1024; + uint256 rateOneTiB = pdpServiceWithPayments.calculateRatesPerEpoch(oneTiB); + assert(rateOneTiB > rate100GiB); + + // Verify proportional scaling + assertApproxEqRel(rate100GiB, rate48GiB * 100 / 48, 0.01e18, "Rates should scale proportionally"); + } + + function testMinimumPricing_ExactlyPoint06USDFC() public view { + // Verify that minimum pricing is exactly 0.06 USDFC/month for small datasets + uint256 decimals = 18; // MockUSDFC uses 18 decimals in tests + uint256 oneGiB = 1024 * 1024 * 1024; + + // Get rate per epoch for dataset below crossover point + uint256 ratePerEpoch = pdpServiceWithPayments.calculateRatesPerEpoch(oneGiB); + + // Convert to rate per month (86400 epochs per month) + uint256 ratePerMonth = ratePerEpoch * 86400; + + // Expected: exactly 0.06 USDFC with 18 decimals = 60000000000000000 + // Allow tiny tolerance for integer division rounding (0.06 / 86400 rounds down) + uint256 expected = (6 * 10 ** decimals) / 100; + uint256 tolerance = 1; // Allow 1 per epoch difference = 86400 total + + assertApproxEqAbs(ratePerMonth, expected, tolerance * 86400, "Minimum rate should be 0.06 USDFC/month"); + } + + uint256 nextClientDataSetId = 0; // Client-Data Set Tracking Tests @@ -978,9 +1074,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // Setup client payment approval if not already done vm.startPrank(clientAddress); - payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days); - mockUSDFC.approve(address(payments), 100e6); - payments.deposit(mockUSDFC, clientAddress, 100e6); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), 100e18); + payments.deposit(mockUSDFC, clientAddress, 100e18); vm.stopPrank(); // Create data set as approved provider @@ -1164,9 +1260,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // Setup client payment approval if not already done vm.startPrank(clientAddress); - payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days); - mockUSDFC.approve(address(payments), 100e6); - payments.deposit(mockUSDFC, clientAddress, 100e6); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), 100e18); + payments.deposit(mockUSDFC, clientAddress, 100e18); vm.stopPrank(); // Create data set as approved provider @@ -1309,11 +1405,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { mockUSDFC, address(pdpServiceWithPayments), true, - 1000e6, // rate allowance - 1000e6, // lockup allowance + 1000e18, // rate allowance + 1000e18, // lockup allowance 365 days // max lockup period ); - uint256 depositAmount = 100e6; + uint256 depositAmount = 100e18; mockUSDFC.approve(address(payments), depositAmount); payments.deposit(mockUSDFC, client, depositAmount); vm.stopPrank(); @@ -1489,11 +1585,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { mockUSDFC, address(pdpServiceWithPayments), true, - 1000e6, // rate allowance - 1000e6, // lockup allowance + 1000e18, // rate allowance + 1000e18, // lockup allowance 365 days // max lockup period ); - uint256 depositAmount = 100e6; + uint256 depositAmount = 100e18; mockUSDFC.approve(address(payments), depositAmount); payments.deposit(mockUSDFC, client, depositAmount); vm.stopPrank(); @@ -1610,11 +1706,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { mockUSDFC, address(pdpServiceWithPayments), true, - 1000e6, // rate allowance - 1000e6, // lockup allowance + 1000e18, // rate allowance + 1000e18, // lockup allowance 365 days // max lockup period ); - uint256 depositAmount = 100e6; + uint256 depositAmount = 100e18; mockUSDFC.approve(address(payments), depositAmount); payments.deposit(mockUSDFC, client, depositAmount); vm.stopPrank(); @@ -2829,8 +2925,8 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { ); vm.startPrank(client); - payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days); - uint256 depositAmount = 1e6; + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + uint256 depositAmount = 1e18; mockUSDFC.approve(address(payments), depositAmount); payments.deposit(mockUSDFC, client, depositAmount); vm.stopPrank(); @@ -2882,8 +2978,8 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { ); vm.startPrank(client); - payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days); - uint256 depositAmount = 1e6; + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + uint256 depositAmount = 1e18; mockUSDFC.approve(address(payments), depositAmount); payments.deposit(mockUSDFC, client, depositAmount); vm.stopPrank(); @@ -2933,8 +3029,8 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { ); vm.startPrank(client); - payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days); - uint256 depositAmount = 10e6; + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + uint256 depositAmount = 10e18; mockUSDFC.approve(address(payments), depositAmount); payments.deposit(mockUSDFC, client, depositAmount); vm.stopPrank(); @@ -4204,7 +4300,7 @@ contract FilecoinWarmStorageServiceSignatureTest is Test { pdpService = SignatureCheckingService(address(serviceProxy)); // Fund the payer - mockUSDFC.safeTransfer(payer, 1000 * 10 ** 6); // 1000 USDFC + mockUSDFC.safeTransfer(payer, 1000 * 10 ** 18); // 1000 USDFC } // Test the recoverSigner function indirectly through signature verification diff --git a/service_contracts/test/FilecoinWarmStorageServiceOwner.t.sol b/service_contracts/test/FilecoinWarmStorageServiceOwner.t.sol index 53412775..bd15dfd8 100644 --- a/service_contracts/test/FilecoinWarmStorageServiceOwner.t.sol +++ b/service_contracts/test/FilecoinWarmStorageServiceOwner.t.sol @@ -124,7 +124,7 @@ contract FilecoinWarmStorageServiceOwnerTest is MockFVMTest { serviceContract.addApprovedProvider(providerId3); // Setup USDFC tokens for client - usdfcToken.safeTransfer(client, 10000e6); + usdfcToken.safeTransfer(client, 10000e18); // Make signatures pass makeSignaturePass(client); @@ -190,9 +190,9 @@ contract FilecoinWarmStorageServiceOwnerTest is MockFVMTest { // Setup payment approval vm.startPrank(payer); - payments.setOperatorApproval(usdfcToken, address(serviceContract), true, 1000e6, 1000e6, 365 days); - usdfcToken.approve(address(payments), 100e6); - payments.deposit(usdfcToken, payer, 100e6); + payments.setOperatorApproval(usdfcToken, address(serviceContract), true, 1000e18, 1000e18, 365 days); + usdfcToken.approve(address(payments), 100e18); + payments.deposit(usdfcToken, payer, 100e18); vm.stopPrank(); // Create data set diff --git a/service_contracts/test/ProviderValidation.t.sol b/service_contracts/test/ProviderValidation.t.sol index bf079c64..96607a04 100644 --- a/service_contracts/test/ProviderValidation.t.sol +++ b/service_contracts/test/ProviderValidation.t.sol @@ -91,7 +91,7 @@ contract ProviderValidationTest is MockFVMTest { viewContract = new FilecoinWarmStorageServiceStateView(warmStorage); // Transfer tokens to client - usdfc.safeTransfer(client, 10000 * 10 ** 6); + usdfc.safeTransfer(client, 10000 * 10 ** 18); } function testProviderNotRegistered() public { @@ -141,12 +141,12 @@ contract ProviderValidationTest is MockFVMTest { usdfc, address(warmStorage), true, - 1000 * 10 ** 6, // rate allowance - 1000 * 10 ** 6, // lockup allowance + 1000 * 10 ** 18, // rate allowance + 1000 * 10 ** 18, // lockup allowance 365 days // max lockup period ); - usdfc.approve(address(payments), 100 * 10 ** 6); - payments.deposit(usdfc, client, 100 * 10 ** 6); + usdfc.approve(address(payments), 100 * 10 ** 18); + payments.deposit(usdfc, client, 100 * 10 ** 18); vm.stopPrank(); // Create dataset without approval should now succeed @@ -195,15 +195,15 @@ contract ProviderValidationTest is MockFVMTest { // Approve USDFC spending, deposit and set operator vm.startPrank(client); - usdfc.approve(address(payments), 10000 * 10 ** 6); - payments.deposit(usdfc, client, 10000 * 10 ** 6); // Deposit funds + usdfc.approve(address(payments), 10000 * 10 ** 18); + payments.deposit(usdfc, client, 10000 * 10 ** 18); // Deposit funds payments.setOperatorApproval( usdfc, // token address(warmStorage), // operator true, // approved - 10000 * 10 ** 6, // rateAllowance - 10000 * 10 ** 6, // lockupAllowance - 10000 * 10 ** 6 // allowance + 10000 * 10 ** 18, // rateAllowance + 10000 * 10 ** 18, // lockupAllowance + 10000 * 10 ** 18 // allowance ); vm.stopPrank(); diff --git a/service_contracts/test/mocks/SharedMocks.sol b/service_contracts/test/mocks/SharedMocks.sol index 45f5da04..39e0b267 100644 --- a/service_contracts/test/mocks/SharedMocks.sol +++ b/service_contracts/test/mocks/SharedMocks.sol @@ -10,7 +10,7 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER contract MockERC20 is IERC20, IERC20Metadata { string private _name = "USD Filecoin"; string private _symbol = "USDFC"; - uint8 private _decimals = 6; + uint8 private _decimals = 18; mapping(address => uint256) private _balances; mapping(address => mapping(address => uint256)) private _allowances; From 5c05de6d2db02190c189b084f7a009457074c0bb Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 24 Oct 2025 17:11:20 +1100 Subject: [PATCH 2/4] feat: check payer has minimum available balance before creating data set --- service_contracts/src/Errors.sol | 6 + .../src/FilecoinWarmStorageService.sol | 11 ++ .../test/FilecoinWarmStorageService.t.sol | 153 ++++++++++++++++-- 3 files changed, 158 insertions(+), 12 deletions(-) diff --git a/service_contracts/src/Errors.sol b/service_contracts/src/Errors.sol index 03fb45ae..8427b1f6 100644 --- a/service_contracts/src/Errors.sol +++ b/service_contracts/src/Errors.sol @@ -286,4 +286,10 @@ library Errors { /// @param dataSetId The data set ID /// @param pdpEndEpoch The end epoch when the PDP payment rail will finalize error PaymentRailsNotFinalized(uint256 dataSetId, uint256 pdpEndEpoch); + + /// @notice Payer has insufficient available funds to cover the minimum storage rate + /// @param payer The payer address + /// @param minimumRequired The minimum lockup required to cover the minimum storage rate + /// @param available The available funds in the payer's account + error InsufficientFundsForMinimumRate(address payer, uint256 minimumRequired, uint256 available); } diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index a539f17a..31d8dbf0 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -587,6 +587,17 @@ contract FilecoinWarmStorageService is // Create the payment rails using the FilecoinPayV1 contract FilecoinPayV1 payments = FilecoinPayV1(paymentsContractAddress); + + // Pre-check that payer has sufficient available funds to cover minimum storage rate + // This provides early feedback but doesn't guarantee funds will be available when SP calls nextProvingPeriod + (,, uint256 availableFunds,) = payments.getAccountInfoIfSettled(usdfcTokenAddress, createData.payer); + // Calculate required lockup: multiply first to preserve precision + uint256 minimumLockupRequired = (MINIMUM_STORAGE_RATE_PER_MONTH * DEFAULT_LOCKUP_PERIOD) / EPOCHS_PER_MONTH; + require( + availableFunds >= minimumLockupRequired, + Errors.InsufficientFundsForMinimumRate(createData.payer, minimumLockupRequired, availableFunds) + ); + uint256 pdpRailId = payments.createRail( usdfcTokenAddress, // token address createData.payer, // from (payer) diff --git a/service_contracts/test/FilecoinWarmStorageService.t.sol b/service_contracts/test/FilecoinWarmStorageService.t.sol index 5126d505..97bf5a36 100644 --- a/service_contracts/test/FilecoinWarmStorageService.t.sol +++ b/service_contracts/test/FilecoinWarmStorageService.t.sol @@ -1046,6 +1046,135 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { } + // Minimum Funds Validation Tests + function testInsufficientFunds_BelowMinimum() public { + // Setup: Client with insufficient funds (below 0.06 USDFC minimum) + address insufficientClient = makeAddr("insufficientClient"); + uint256 insufficientAmount = 5e16; // 0.05 USDFC (below 0.06 minimum) + + // Transfer tokens from test contract to the test client + mockUSDFC.safeTransfer(insufficientClient, insufficientAmount); + + vm.startPrank(insufficientClient); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), insufficientAmount); + payments.deposit(mockUSDFC, insufficientClient, insufficientAmount); + vm.stopPrank(); + + // Prepare dataset creation data + (string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Insufficient Test"); + FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({ + payer: insufficientClient, + clientDataSetId: 999, + metadataKeys: dsKeys, + metadataValues: dsValues, + signature: FAKE_SIGNATURE + }); + + bytes memory encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + // Expected minimum: (0.06 USDFC * 86400) / 86400 = 0.06 USDFC = 6e16 + uint256 minimumRequired = 6e16; + + // Expect revert with InsufficientFundsForMinimumRate error + makeSignaturePass(insufficientClient); + vm.expectRevert( + abi.encodeWithSelector( + Errors.InsufficientFundsForMinimumRate.selector, insufficientClient, minimumRequired, insufficientAmount + ) + ); + vm.prank(serviceProvider); + mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + } + + function testInsufficientFunds_ExactMinimum() public { + // Setup: Client with exactly the minimum funds (0.06 USDFC) + address exactClient = makeAddr("exactClient"); + uint256 exactAmount = 6e16; // Exactly 0.06 USDFC + + // Transfer tokens from test contract to the test client + mockUSDFC.safeTransfer(exactClient, exactAmount); + + vm.startPrank(exactClient); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), exactAmount); + payments.deposit(mockUSDFC, exactClient, exactAmount); + vm.stopPrank(); + + // Prepare dataset creation data + (string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Exact Minimum Test"); + FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({ + payer: exactClient, + clientDataSetId: 1000, + metadataKeys: dsKeys, + metadataValues: dsValues, + signature: FAKE_SIGNATURE + }); + + bytes memory encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + // Should succeed with exact minimum + makeSignaturePass(exactClient); + vm.prank(serviceProvider); + uint256 dataSetId = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + + // Verify dataset was created + assertEq(dataSetId, 1, "Dataset should be created with exact minimum funds"); + } + + function testInsufficientFunds_JustAboveMinimum() public { + // Setup: Client with slightly more than minimum (0.07 USDFC) + address aboveMinClient = makeAddr("aboveMinClient"); + uint256 aboveMinAmount = 7e16; // 0.07 USDFC (just above 0.06 minimum) + + // Transfer tokens from test contract to the test client + mockUSDFC.safeTransfer(aboveMinClient, aboveMinAmount); + + vm.startPrank(aboveMinClient); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), aboveMinAmount); + payments.deposit(mockUSDFC, aboveMinClient, aboveMinAmount); + vm.stopPrank(); + + // Prepare dataset creation data + (string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Above Minimum Test"); + FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({ + payer: aboveMinClient, + clientDataSetId: 1001, + metadataKeys: dsKeys, + metadataValues: dsValues, + signature: FAKE_SIGNATURE + }); + + bytes memory encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + // Should succeed with funds above minimum + makeSignaturePass(aboveMinClient); + vm.prank(serviceProvider); + uint256 dataSetId = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + + // Verify dataset was created + assertEq(dataSetId, 1, "Dataset should be created with above-minimum funds"); + } + uint256 nextClientDataSetId = 0; // Client-Data Set Tracking Tests @@ -4008,9 +4137,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // Setup approvals and deposit vm.startPrank(client); - payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days); - mockUSDFC.approve(address(payments), 10e6); - payments.deposit(mockUSDFC, client, 10e6); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), 10e18); + payments.deposit(mockUSDFC, client, 10e18); vm.stopPrank(); // Create dataset @@ -4054,9 +4183,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // Setup approvals and deposit vm.startPrank(client); - payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days); - mockUSDFC.approve(address(payments), 10e6); - payments.deposit(mockUSDFC, client, 10e6); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), 10e18); + payments.deposit(mockUSDFC, client, 10e18); vm.stopPrank(); // Create dataset @@ -4088,9 +4217,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { function testAddPiecesNonceUniquePerPayer() public { // Setup approvals and deposit vm.startPrank(client); - payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days); - mockUSDFC.approve(address(payments), 20e6); - payments.deposit(mockUSDFC, client, 20e6); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), 20e18); + payments.deposit(mockUSDFC, client, 20e18); vm.stopPrank(); (string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Dataset 1"); @@ -4157,9 +4286,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { function testNonceCannotBeReusedAcrossOperations() public { // Setup: Approvals and deposit vm.startPrank(client); - payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days); - mockUSDFC.approve(address(payments), 10e6); - payments.deposit(mockUSDFC, client, 10e6); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), 10e18); + payments.deposit(mockUSDFC, client, 10e18); vm.stopPrank(); // Use nonce 777 to create a dataset From 66162594698c375ff56aa73274ac97d3c567bb94 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 24 Oct 2025 23:06:59 +1100 Subject: [PATCH 3/4] feat(warm-storage): validate operator approvals in dataSetCreated Add comprehensive validation of operator approval settings alongside existing minimum balance check during createDataSet operation. Validates operator is approved with sufficient rate allowance, lockup allowance, and max lockup period. --- .../abi/FilecoinWarmStorageService.abi.json | 104 +++++++ service_contracts/src/Errors.sol | 34 +++ .../src/FilecoinWarmStorageService.sol | 62 +++- .../test/FilecoinWarmStorageService.t.sol | 280 +++++++++++++++++- 4 files changed, 470 insertions(+), 10 deletions(-) diff --git a/service_contracts/abi/FilecoinWarmStorageService.abi.json b/service_contracts/abi/FilecoinWarmStorageService.abi.json index b06c6801..d6a5df2c 100644 --- a/service_contracts/abi/FilecoinWarmStorageService.abi.json +++ b/service_contracts/abi/FilecoinWarmStorageService.abi.json @@ -1707,6 +1707,94 @@ } ] }, + { + "type": "error", + "name": "InsufficientLockupAllowance", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "lockupAllowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "lockupUsage", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumLockupRequired", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientMaxLockupPeriod", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "maxLockupPeriod", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requiredLockupPeriod", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientRateAllowance", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "rateAllowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rateUsage", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumRateRequired", + "type": "uint256", + "internalType": "uint256" + } + ] + }, { "type": "error", "name": "InvalidChallengeCount", @@ -1983,6 +2071,22 @@ } ] }, + { + "type": "error", + "name": "OperatorNotApproved", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ] + }, { "type": "error", "name": "OwnableInvalidOwner", diff --git a/service_contracts/src/Errors.sol b/service_contracts/src/Errors.sol index 8427b1f6..62c65599 100644 --- a/service_contracts/src/Errors.sol +++ b/service_contracts/src/Errors.sol @@ -292,4 +292,38 @@ library Errors { /// @param minimumRequired The minimum lockup required to cover the minimum storage rate /// @param available The available funds in the payer's account error InsufficientFundsForMinimumRate(address payer, uint256 minimumRequired, uint256 available); + + /// @notice Operator is not approved for the payer + /// @param payer The payer address + /// @param operator The operator address (warm storage service) + error OperatorNotApproved(address payer, address operator); + + /// @notice Operator has insufficient rate allowance for the minimum storage rate + /// @param payer The payer address + /// @param operator The operator address (warm storage service) + /// @param rateAllowance The total rate allowance approved + /// @param rateUsage The current rate usage + /// @param minimumRateRequired The minimum rate required per epoch + error InsufficientRateAllowance( + address payer, address operator, uint256 rateAllowance, uint256 rateUsage, uint256 minimumRateRequired + ); + + /// @notice Operator has insufficient lockup allowance for the minimum lockup + /// @param payer The payer address + /// @param operator The operator address (warm storage service) + /// @param lockupAllowance The total lockup allowance approved + /// @param lockupUsage The current lockup usage + /// @param minimumLockupRequired The minimum lockup required + error InsufficientLockupAllowance( + address payer, address operator, uint256 lockupAllowance, uint256 lockupUsage, uint256 minimumLockupRequired + ); + + /// @notice Operator's max lockup period is insufficient for the default lockup period + /// @param payer The payer address + /// @param operator The operator address (warm storage service) + /// @param maxLockupPeriod The maximum lockup period approved + /// @param requiredLockupPeriod The required lockup period + error InsufficientMaxLockupPeriod( + address payer, address operator, uint256 maxLockupPeriod, uint256 requiredLockupPeriod + ); } diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index 31d8dbf0..301d853e 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -588,15 +588,8 @@ contract FilecoinWarmStorageService is // Create the payment rails using the FilecoinPayV1 contract FilecoinPayV1 payments = FilecoinPayV1(paymentsContractAddress); - // Pre-check that payer has sufficient available funds to cover minimum storage rate - // This provides early feedback but doesn't guarantee funds will be available when SP calls nextProvingPeriod - (,, uint256 availableFunds,) = payments.getAccountInfoIfSettled(usdfcTokenAddress, createData.payer); - // Calculate required lockup: multiply first to preserve precision - uint256 minimumLockupRequired = (MINIMUM_STORAGE_RATE_PER_MONTH * DEFAULT_LOCKUP_PERIOD) / EPOCHS_PER_MONTH; - require( - availableFunds >= minimumLockupRequired, - Errors.InsufficientFundsForMinimumRate(createData.payer, minimumLockupRequired, availableFunds) - ); + // Validate payer has sufficient funds and operator approvals for minimum pricing + validatePayerOperatorApprovalAndFunds(payments, createData.payer); uint256 pdpRailId = payments.createRail( usdfcTokenAddress, // token address @@ -1110,6 +1103,57 @@ contract FilecoinWarmStorageService is } } + /// @notice Validates that the payer has sufficient funds and operator approvals for minimum pricing + /// @param payments The FilecoinPayV1 contract instance + /// @param payer The address of the payer + function validatePayerOperatorApprovalAndFunds(FilecoinPayV1 payments, address payer) internal view { + // Calculate required lockup for minimum pricing + uint256 minimumLockupRequired = (MINIMUM_STORAGE_RATE_PER_MONTH * DEFAULT_LOCKUP_PERIOD) / EPOCHS_PER_MONTH; + + // Check that payer has sufficient available funds + (,, uint256 availableFunds,) = payments.getAccountInfoIfSettled(usdfcTokenAddress, payer); + require( + availableFunds >= minimumLockupRequired, + Errors.InsufficientFundsForMinimumRate(payer, minimumLockupRequired, availableFunds) + ); + + // Check operator approval settings + ( + bool isApproved, + uint256 rateAllowance, + uint256 lockupAllowance, + uint256 rateUsage, + uint256 lockupUsage, + uint256 maxLockupPeriod + ) = payments.operatorApprovals(usdfcTokenAddress, payer, address(this)); + + // Verify operator is approved + require(isApproved, Errors.OperatorNotApproved(payer, address(this))); + + // Calculate minimum rate per epoch + uint256 minimumRatePerEpoch = MINIMUM_STORAGE_RATE_PER_MONTH / EPOCHS_PER_MONTH; + + // Verify rate allowance is sufficient + require( + rateAllowance >= rateUsage + minimumRatePerEpoch, + Errors.InsufficientRateAllowance(payer, address(this), rateAllowance, rateUsage, minimumRatePerEpoch) + ); + + // Verify lockup allowance is sufficient + require( + lockupAllowance >= lockupUsage + minimumLockupRequired, + Errors.InsufficientLockupAllowance( + payer, address(this), lockupAllowance, lockupUsage, minimumLockupRequired + ) + ); + + // Verify max lockup period is sufficient + require( + maxLockupPeriod >= DEFAULT_LOCKUP_PERIOD, + Errors.InsufficientMaxLockupPeriod(payer, address(this), maxLockupPeriod, DEFAULT_LOCKUP_PERIOD) + ); + } + function updatePaymentRates(uint256 dataSetId, uint256 leafCount) internal { // Revert if no payment rail is configured for this data set require(dataSetInfo[dataSetId].pdpRailId != 0, Errors.NoPDPPaymentRail(dataSetId)); diff --git a/service_contracts/test/FilecoinWarmStorageService.t.sol b/service_contracts/test/FilecoinWarmStorageService.t.sol index 97bf5a36..921c73cd 100644 --- a/service_contracts/test/FilecoinWarmStorageService.t.sol +++ b/service_contracts/test/FilecoinWarmStorageService.t.sol @@ -1045,7 +1045,6 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { assertApproxEqAbs(ratePerMonth, expected, tolerance * 86400, "Minimum rate should be 0.06 USDFC/month"); } - // Minimum Funds Validation Tests function testInsufficientFunds_BelowMinimum() public { // Setup: Client with insufficient funds (below 0.06 USDFC minimum) @@ -1175,6 +1174,285 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { assertEq(dataSetId, 1, "Dataset should be created with above-minimum funds"); } + // Operator Approval Validation Tests + function testOperatorApproval_NotApproved() public { + // Setup: Client with sufficient funds but no operator approval + address testClient = makeAddr("testClient"); + uint256 depositAmount = 10e18; // 10 USDFC (plenty of funds) + + // Transfer tokens and deposit + mockUSDFC.safeTransfer(testClient, depositAmount); + + vm.startPrank(testClient); + // Don't set operator approval (or explicitly set to false) + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), false, 0, 0, 0); + mockUSDFC.approve(address(payments), depositAmount); + payments.deposit(mockUSDFC, testClient, depositAmount); + vm.stopPrank(); + + // Prepare dataset creation data + (string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Not Approved Test"); + FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({ + payer: testClient, + clientDataSetId: 2000, + metadataKeys: dsKeys, + metadataValues: dsValues, + signature: FAKE_SIGNATURE + }); + + bytes memory encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + // Expect revert with OperatorNotApproved error + makeSignaturePass(testClient); + vm.expectRevert( + abi.encodeWithSelector(Errors.OperatorNotApproved.selector, testClient, address(pdpServiceWithPayments)) + ); + vm.prank(serviceProvider); + mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + } + + function testOperatorApproval_InsufficientRateAllowance() public { + // Setup: Client with sufficient funds but insufficient rate allowance + address testClient = makeAddr("testClient2"); + uint256 depositAmount = 10e18; // 10 USDFC (plenty of funds) + + // Calculate minimum rate per epoch + // MINIMUM_STORAGE_RATE_PER_MONTH = 0.06 USDFC = 6e16 + // EPOCHS_PER_MONTH = 2880 * 30 = 86400 + // minimumRatePerEpoch = 6e16 / 86400 = 694444444444 (integer division) + uint256 minimumRatePerEpoch = 694444444444; + uint256 insufficientRateAllowance = minimumRatePerEpoch - 1; // Just below minimum + + // Transfer tokens and set up approvals + mockUSDFC.safeTransfer(testClient, depositAmount); + + vm.startPrank(testClient); + // Set operator approval with insufficient rate allowance + payments.setOperatorApproval( + mockUSDFC, + address(pdpServiceWithPayments), + true, // approved + insufficientRateAllowance, // rate allowance too low + 1000e18, // lockup allowance sufficient + 365 days // max lockup period sufficient + ); + mockUSDFC.approve(address(payments), depositAmount); + payments.deposit(mockUSDFC, testClient, depositAmount); + vm.stopPrank(); + + // Prepare dataset creation data + (string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Insufficient Rate Test"); + FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({ + payer: testClient, + clientDataSetId: 2001, + metadataKeys: dsKeys, + metadataValues: dsValues, + signature: FAKE_SIGNATURE + }); + + bytes memory encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + // Expect revert with InsufficientRateAllowance error + makeSignaturePass(testClient); + vm.expectRevert( + abi.encodeWithSelector( + Errors.InsufficientRateAllowance.selector, + testClient, + address(pdpServiceWithPayments), + insufficientRateAllowance, + 0, // rateUsage is 0 initially + minimumRatePerEpoch + ) + ); + vm.prank(serviceProvider); + mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + } + + function testOperatorApproval_InsufficientLockupAllowance() public { + // Setup: Client with sufficient funds but insufficient lockup allowance + address testClient = makeAddr("testClient3"); + uint256 depositAmount = 10e18; // 10 USDFC (plenty of funds) + + // Calculate minimum lockup required + // MINIMUM_STORAGE_RATE_PER_MONTH = 0.06 USDFC = 6e16 + // DEFAULT_LOCKUP_PERIOD = 86400 + // EPOCHS_PER_MONTH = 86400 + // minimumLockupRequired = (6e16 * 86400) / 86400 = 6e16 + uint256 minimumLockupRequired = 6e16; + uint256 insufficientLockupAllowance = minimumLockupRequired - 1; // Just below minimum + + // Transfer tokens and set up approvals + mockUSDFC.safeTransfer(testClient, depositAmount); + + vm.startPrank(testClient); + // Set operator approval with insufficient lockup allowance + payments.setOperatorApproval( + mockUSDFC, + address(pdpServiceWithPayments), + true, // approved + 1000e18, // rate allowance sufficient + insufficientLockupAllowance, // lockup allowance too low + 365 days // max lockup period sufficient + ); + mockUSDFC.approve(address(payments), depositAmount); + payments.deposit(mockUSDFC, testClient, depositAmount); + vm.stopPrank(); + + // Prepare dataset creation data + (string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Insufficient Lockup Test"); + FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({ + payer: testClient, + clientDataSetId: 2002, + metadataKeys: dsKeys, + metadataValues: dsValues, + signature: FAKE_SIGNATURE + }); + + bytes memory encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + // Expect revert with InsufficientLockupAllowance error + makeSignaturePass(testClient); + vm.expectRevert( + abi.encodeWithSelector( + Errors.InsufficientLockupAllowance.selector, + testClient, + address(pdpServiceWithPayments), + insufficientLockupAllowance, + 0, // lockupUsage is 0 initially + minimumLockupRequired + ) + ); + vm.prank(serviceProvider); + mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + } + + function testOperatorApproval_InsufficientMaxLockupPeriod() public { + // Setup: Client with sufficient funds but insufficient max lockup period + address testClient = makeAddr("testClient4"); + uint256 depositAmount = 10e18; // 10 USDFC (plenty of funds) + + // Get the default lockup period + // DEFAULT_LOCKUP_PERIOD = 2880 * 30 = 86400 + uint256 defaultLockupPeriod = 2880 * 30; + uint256 insufficientMaxLockupPeriod = defaultLockupPeriod - 1; // Just below required + + // Transfer tokens and set up approvals + mockUSDFC.safeTransfer(testClient, depositAmount); + + vm.startPrank(testClient); + // Set operator approval with insufficient max lockup period + payments.setOperatorApproval( + mockUSDFC, + address(pdpServiceWithPayments), + true, // approved + 1000e18, // rate allowance sufficient + 1000e18, // lockup allowance sufficient + insufficientMaxLockupPeriod // max lockup period too low + ); + mockUSDFC.approve(address(payments), depositAmount); + payments.deposit(mockUSDFC, testClient, depositAmount); + vm.stopPrank(); + + // Prepare dataset creation data + (string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Insufficient Period Test"); + FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({ + payer: testClient, + clientDataSetId: 2003, + metadataKeys: dsKeys, + metadataValues: dsValues, + signature: FAKE_SIGNATURE + }); + + bytes memory encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + // Expect revert with InsufficientMaxLockupPeriod error + makeSignaturePass(testClient); + vm.expectRevert( + abi.encodeWithSelector( + Errors.InsufficientMaxLockupPeriod.selector, + testClient, + address(pdpServiceWithPayments), + insufficientMaxLockupPeriod, + defaultLockupPeriod + ) + ); + vm.prank(serviceProvider); + mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + } + + function testOperatorApproval_AllSufficient() public { + // Setup: Client with all approvals sufficient + address testClient = makeAddr("testClient5"); + uint256 depositAmount = 10e18; // 10 USDFC (plenty of funds) + + // Transfer tokens and set up sufficient approvals + mockUSDFC.safeTransfer(testClient, depositAmount); + + vm.startPrank(testClient); + // Set operator approval with all sufficient values + payments.setOperatorApproval( + mockUSDFC, + address(pdpServiceWithPayments), + true, // approved + 1000e18, // rate allowance more than sufficient + 1000e18, // lockup allowance more than sufficient + 365 days // max lockup period more than sufficient + ); + mockUSDFC.approve(address(payments), depositAmount); + payments.deposit(mockUSDFC, testClient, depositAmount); + vm.stopPrank(); + + // Prepare dataset creation data + (string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "All Sufficient Test"); + FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({ + payer: testClient, + clientDataSetId: 2004, + metadataKeys: dsKeys, + metadataValues: dsValues, + signature: FAKE_SIGNATURE + }); + + bytes memory encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + // Should succeed with all approvals sufficient + makeSignaturePass(testClient); + vm.prank(serviceProvider); + uint256 dataSetId = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + + // Verify dataset was created + assertEq(dataSetId, 1, "Dataset should be created with sufficient approvals"); + } + uint256 nextClientDataSetId = 0; // Client-Data Set Tracking Tests From 51efea9172abdf2da6a87c9ddf97ebdd9cd33f0e Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 24 Oct 2025 23:23:07 +1100 Subject: [PATCH 4/4] fix: s/calculateRatesPerEpoch/calculateRatePerEpoch --- .../abi/FilecoinWarmStorageService.abi.json | 2 +- .../src/FilecoinWarmStorageService.sol | 5 ++--- .../test/FilecoinWarmStorageService.t.sol | 20 +++++++++---------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/service_contracts/abi/FilecoinWarmStorageService.abi.json b/service_contracts/abi/FilecoinWarmStorageService.abi.json index d6a5df2c..d4a74ce0 100644 --- a/service_contracts/abi/FilecoinWarmStorageService.abi.json +++ b/service_contracts/abi/FilecoinWarmStorageService.abi.json @@ -101,7 +101,7 @@ }, { "type": "function", - "name": "calculateRatesPerEpoch", + "name": "calculateRatePerEpoch", "inputs": [ { "name": "totalBytes", diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index 301d853e..08becbb9 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -1161,8 +1161,7 @@ contract FilecoinWarmStorageService is uint256 totalBytes = leafCount * BYTES_PER_LEAF; FilecoinPayV1 payments = FilecoinPayV1(paymentsContractAddress); - // Update the PDP rail payment rate with the new rate and no one-time - // payment + // Update the PDP rail payment rate with the new rate and no one-time payment uint256 pdpRailId = dataSetInfo[dataSetId].pdpRailId; uint256 newStorageRatePerEpoch = _calculateStorageRate(totalBytes); payments.modifyRailPayment( @@ -1239,7 +1238,7 @@ contract FilecoinWarmStorageService is * @param totalBytes Total size of the stored data in bytes * @return storageRate The PDP storage rate per epoch */ - function calculateRatesPerEpoch(uint256 totalBytes) external view returns (uint256 storageRate) { + function calculateRatePerEpoch(uint256 totalBytes) external view returns (uint256 storageRate) { storageRate = _calculateStorageRate(totalBytes); } diff --git a/service_contracts/test/FilecoinWarmStorageService.t.sol b/service_contracts/test/FilecoinWarmStorageService.t.sol index 921c73cd..220475da 100644 --- a/service_contracts/test/FilecoinWarmStorageService.t.sol +++ b/service_contracts/test/FilecoinWarmStorageService.t.sol @@ -963,19 +963,19 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { uint256 expectedMinPerEpoch = expectedMinPerMonth / 86400; // Convert to per-epoch // Test 0 bytes - uint256 rateZero = pdpServiceWithPayments.calculateRatesPerEpoch(0); + uint256 rateZero = pdpServiceWithPayments.calculateRatePerEpoch(0); assertEq(rateZero, expectedMinPerEpoch, "0 bytes should return 0.06 USDFC/month minimum"); // Test 1 GiB - uint256 rateOneGiB = pdpServiceWithPayments.calculateRatesPerEpoch(oneGiB); + uint256 rateOneGiB = pdpServiceWithPayments.calculateRatePerEpoch(oneGiB); assertEq(rateOneGiB, expectedMinPerEpoch, "1 GiB should return minimum rate"); // Test 10 GiB - uint256 rateTenGiB = pdpServiceWithPayments.calculateRatesPerEpoch(10 * oneGiB); + uint256 rateTenGiB = pdpServiceWithPayments.calculateRatePerEpoch(10 * oneGiB); assertEq(rateTenGiB, expectedMinPerEpoch, "10 GiB should return minimum rate"); // Test 24 GiB (below crossover) - uint256 rateTwentyFourGiB = pdpServiceWithPayments.calculateRatesPerEpoch(24 * oneGiB); + uint256 rateTwentyFourGiB = pdpServiceWithPayments.calculateRatePerEpoch(24 * oneGiB); assertEq(rateTwentyFourGiB, expectedMinPerEpoch, "24 GiB should return minimum rate"); } @@ -988,11 +988,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { uint256 expectedMinPerEpoch = expectedMinPerMonth / 86400; // 24 GiB: natural rate (0.0586) < minimum (0.06), so returns minimum - uint256 rate24GiB = pdpServiceWithPayments.calculateRatesPerEpoch(24 * oneGiB); + uint256 rate24GiB = pdpServiceWithPayments.calculateRatePerEpoch(24 * oneGiB); assertEq(rate24GiB, expectedMinPerEpoch, "24 GiB should use minimum floor"); // 25 GiB: natural rate (0.0610) > minimum (0.06), so returns natural rate - uint256 rate25GiB = pdpServiceWithPayments.calculateRatesPerEpoch(25 * oneGiB); + uint256 rate25GiB = pdpServiceWithPayments.calculateRatePerEpoch(25 * oneGiB); assert(rate25GiB > expectedMinPerEpoch); // Verify it's actually proportional (not minimum) @@ -1010,16 +1010,16 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { uint256 expectedMinPerEpoch = expectedMinPerMonth / 86400; // Test 48 GiB - uint256 rate48GiB = pdpServiceWithPayments.calculateRatesPerEpoch(48 * oneGiB); + uint256 rate48GiB = pdpServiceWithPayments.calculateRatePerEpoch(48 * oneGiB); assert(rate48GiB > expectedMinPerEpoch); // Test 100 GiB - uint256 rate100GiB = pdpServiceWithPayments.calculateRatesPerEpoch(100 * oneGiB); + uint256 rate100GiB = pdpServiceWithPayments.calculateRatePerEpoch(100 * oneGiB); assert(rate100GiB > rate48GiB); // Test 1 TiB uint256 oneTiB = oneGiB * 1024; - uint256 rateOneTiB = pdpServiceWithPayments.calculateRatesPerEpoch(oneTiB); + uint256 rateOneTiB = pdpServiceWithPayments.calculateRatePerEpoch(oneTiB); assert(rateOneTiB > rate100GiB); // Verify proportional scaling @@ -1032,7 +1032,7 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { uint256 oneGiB = 1024 * 1024 * 1024; // Get rate per epoch for dataset below crossover point - uint256 ratePerEpoch = pdpServiceWithPayments.calculateRatesPerEpoch(oneGiB); + uint256 ratePerEpoch = pdpServiceWithPayments.calculateRatePerEpoch(oneGiB); // Convert to rate per month (86400 epochs per month) uint256 ratePerMonth = ratePerEpoch * 86400;