diff --git a/CHANGELOG.md b/CHANGELOG.md index 795f87d2..0a6b5ab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Use `getDataSetStatusDetails()` to check termination status separately from Active/Inactive status - Subgraph schema updated with status enum and history tracking - **Calibnet**: Reduced DEFAULT_CHALLENGE_WINDOW_SIZE from 30 epochs to 20 epochs for faster testing iteration +- Made storage pricing and minimum rate mutable storage variables instead of immutable constants ([#306](https://github.com/FilOzone/filecoin-services/issues/306)) + - `storagePricePerTibPerMonth` (initially 2.5 USDFC, max 10 USDFC) + - `minimumStorageRatePerMonth` (initially 0.06 USDFC, max 0.24 USDFC) +- Added `updatePricing(uint256 newStoragePrice, uint256 newMinimumRate)` function to allow owner to update pricing rates without contract upgrades + - Pass 0 to keep existing value unchanged + - At least one price must be non-zero + - Validates against 4x upper bounds (10 USDFC storage, 0.24 USDFC minimum rate) + - Price updates apply immediately to existing payment rails when recalculated +- Added `getCurrentPricingRates()` function to query current storage price and minimum rate +- Added `PricingUpdated(uint256 storagePrice, uint256 minimumRate)` event to track pricing changes +- Added `AtLeastOnePriceMustBeNonZero` and `PriceExceedsMaximum` errors for pricing validation ## [0.3.0] - 2025-10-08 - M3.1 Calibration Network Deployment diff --git a/service_contracts/abi/Errors.abi.json b/service_contracts/abi/Errors.abi.json index b65ad4a8..988c5fa1 100644 --- a/service_contracts/abi/Errors.abi.json +++ b/service_contracts/abi/Errors.abi.json @@ -10,6 +10,11 @@ } ] }, + { + "type": "error", + "name": "AtLeastOnePriceMustBeNonZero", + "inputs": [] + }, { "type": "error", "name": "CDNPaymentAlreadyTerminated", @@ -715,6 +720,27 @@ } ] }, + { + "type": "error", + "name": "PriceExceedsMaximum", + "inputs": [ + { + "name": "priceType", + "type": "uint8", + "internalType": "enum Errors.PriceType" + }, + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, { "type": "error", "name": "ProofAlreadySubmitted", diff --git a/service_contracts/abi/FilecoinWarmStorageService.abi.json b/service_contracts/abi/FilecoinWarmStorageService.abi.json index d4a74ce0..d2e9ca0e 100644 --- a/service_contracts/abi/FilecoinWarmStorageService.abi.json +++ b/service_contracts/abi/FilecoinWarmStorageService.abi.json @@ -793,6 +793,24 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "updatePricing", + "inputs": [ + { + "name": "newStoragePrice", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "newMinimumRate", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "updateServiceCommission", @@ -1286,6 +1304,25 @@ ], "anonymous": false }, + { + "type": "event", + "name": "PricingUpdated", + "inputs": [ + { + "name": "storagePrice", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "minimumRate", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, { "type": "event", "name": "ProviderApproved", @@ -1436,6 +1473,11 @@ } ] }, + { + "type": "error", + "name": "AtLeastOnePriceMustBeNonZero", + "inputs": [] + }, { "type": "error", "name": "CDNPaymentAlreadyTerminated", @@ -2125,6 +2167,27 @@ } ] }, + { + "type": "error", + "name": "PriceExceedsMaximum", + "inputs": [ + { + "name": "priceType", + "type": "uint8", + "internalType": "enum Errors.PriceType" + }, + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, { "type": "error", "name": "ProofAlreadySubmitted", diff --git a/service_contracts/abi/FilecoinWarmStorageServiceStateLibrary.abi.json b/service_contracts/abi/FilecoinWarmStorageServiceStateLibrary.abi.json index 2cb33fa8..02816c8a 100644 --- a/service_contracts/abi/FilecoinWarmStorageServiceStateLibrary.abi.json +++ b/service_contracts/abi/FilecoinWarmStorageServiceStateLibrary.abi.json @@ -295,6 +295,30 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getCurrentPricingRates", + "inputs": [ + { + "name": "service", + "type": "FilecoinWarmStorageService", + "internalType": "contract FilecoinWarmStorageService" + } + ], + "outputs": [ + { + "name": "storagePrice", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumRate", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getDataSet", diff --git a/service_contracts/abi/FilecoinWarmStorageServiceStateView.abi.json b/service_contracts/abi/FilecoinWarmStorageServiceStateView.abi.json index 8bdf9744..deeabdc0 100644 --- a/service_contracts/abi/FilecoinWarmStorageServiceStateView.abi.json +++ b/service_contracts/abi/FilecoinWarmStorageServiceStateView.abi.json @@ -258,6 +258,24 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getCurrentPricingRates", + "inputs": [], + "outputs": [ + { + "name": "storagePrice", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumRate", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getDataSet", diff --git a/service_contracts/src/Errors.sol b/service_contracts/src/Errors.sol index 62c65599..09caf1d4 100644 --- a/service_contracts/src/Errors.sol +++ b/service_contracts/src/Errors.sol @@ -36,6 +36,13 @@ library Errors { Service } + enum PriceType { + /// Storage price per TiB per month + Storage, + /// Minimum monthly storage rate (floor price) + MinimumRate + } + /// @notice An expected contract or participant address was the zero address /// @dev Used for parameter validation when a non-zero address is required /// @param field The specific address field that was zero (see enum {AddressField}) @@ -326,4 +333,13 @@ library Errors { error InsufficientMaxLockupPeriod( address payer, address operator, uint256 maxLockupPeriod, uint256 requiredLockupPeriod ); + + /// @notice At least one price parameter must be non-zero when updating pricing + error AtLeastOnePriceMustBeNonZero(); + + /// @notice Price update exceeds the maximum allowed value + /// @param priceType The type of price being updated (see enum {PriceType}) + /// @param maxAllowed The maximum allowed value for this price type + /// @param actual The attempted value that exceeds the maximum + error PriceExceedsMaximum(PriceType priceType, uint256 maxAllowed, uint256 actual); } diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index b9154ecd..d2e85b4b 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -97,6 +97,8 @@ contract FilecoinWarmStorageService is event ProviderApproved(uint256 indexed providerId); event ProviderUnapproved(uint256 indexed providerId); + event PricingUpdated(uint256 storagePrice, uint256 minimumRate); + // ========================================================================= // Structs @@ -193,16 +195,18 @@ contract FilecoinWarmStorageService is bytes32 private constant WITH_CDN_STRING_STORAGE_REPR = 0x7769746843444e0000000000000000000000000000000000000000000000000e; - // Pricing constants - uint256 private immutable STORAGE_PRICE_PER_TIB_PER_MONTH; // 2.5 USDFC per TiB per month without CDN with correct decimals + // Pricing constants (CDN egress pricing is immutable) 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 uint256 private immutable DEFAULT_CACHE_MISS_LOCKUP_AMOUNT; // 0.3 USDFC + // Maximum pricing bounds (4x initial values) + uint256 private immutable MAX_STORAGE_PRICE_PER_TIB_PER_MONTH; // 10 USDFC (4x 2.5) + uint256 private immutable MAX_MINIMUM_STORAGE_RATE_PER_MONTH; // 0.24 USDFC (4x 0.06) + // Token decimals uint8 private immutable TOKEN_DECIMALS; @@ -272,6 +276,10 @@ contract FilecoinWarmStorageService is PlannedUpgrade private nextUpgrade; + // Pricing rates (mutable for future adjustments) + uint256 private storagePricePerTibPerMonth; + uint256 private minimumStorageRatePerMonth; + event UpgradeAnnounced(PlannedUpgrade plannedUpgrade); // ========================================================================= @@ -328,11 +336,13 @@ contract FilecoinWarmStorageService is // Read token decimals from the USDFC token contract TOKEN_DECIMALS = _usdfc.decimals(); - // Initialize the fee constants based on the actual token decimals - STORAGE_PRICE_PER_TIB_PER_MONTH = (5 * 10 ** TOKEN_DECIMALS) / 2; // 2.5 USDFC + // Initialize the immutable pricing constants based on the actual token decimals 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 maximum pricing bounds (4x initial values) + MAX_STORAGE_PRICE_PER_TIB_PER_MONTH = 10 * 10 ** TOKEN_DECIMALS; // 10 USDFC (4x 2.5) + MAX_MINIMUM_STORAGE_RATE_PER_MONTH = (24 * 10 ** TOKEN_DECIMALS) / 100; // 0.24 USDFC (4x 0.06) // Initialize the lockup constants based on the actual token decimals DEFAULT_CDN_LOCKUP_AMOUNT = (7 * 10 ** TOKEN_DECIMALS) / 10; // 0.7 USDFC @@ -383,6 +393,10 @@ contract FilecoinWarmStorageService is // Set commission rate serviceCommissionBps = 0; // 0% + + // Initialize mutable pricing variables + storagePricePerTibPerMonth = (5 * 10 ** TOKEN_DECIMALS) / 2; // 2.5 USDFC + minimumStorageRatePerMonth = (6 * 10 ** TOKEN_DECIMALS) / 100; // 0.06 USDFC } function announcePlannedUpgrade(PlannedUpgrade calldata plannedUpgrade) external onlyOwner { @@ -467,6 +481,40 @@ contract FilecoinWarmStorageService is serviceCommissionBps = newCommissionBps; } + /** + * @notice Updates the pricing rates for storage services + * @dev Only callable by the contract owner. Pass 0 to keep existing value unchanged. + * Price updates apply immediately to existing payment rails when they're recalculated + * (e.g., during piece additions/deletions or proving period updates). + * Maximum allowed values: 10 USDFC for storage, 0.24 USDFC for minimum rate. + * @param newStoragePrice New storage price per TiB per month (0 = no change, max 10 USDFC) + * @param newMinimumRate New minimum monthly storage rate (0 = no change, max 0.24 USDFC) + */ + function updatePricing(uint256 newStoragePrice, uint256 newMinimumRate) external onlyOwner { + require(newStoragePrice > 0 || newMinimumRate > 0, Errors.AtLeastOnePriceMustBeNonZero()); + + if (newStoragePrice > 0) { + require( + newStoragePrice <= MAX_STORAGE_PRICE_PER_TIB_PER_MONTH, + Errors.PriceExceedsMaximum( + Errors.PriceType.Storage, MAX_STORAGE_PRICE_PER_TIB_PER_MONTH, newStoragePrice + ) + ); + storagePricePerTibPerMonth = newStoragePrice; + } + if (newMinimumRate > 0) { + require( + newMinimumRate <= MAX_MINIMUM_STORAGE_RATE_PER_MONTH, + Errors.PriceExceedsMaximum( + Errors.PriceType.MinimumRate, MAX_MINIMUM_STORAGE_RATE_PER_MONTH, newMinimumRate + ) + ); + minimumStorageRatePerMonth = newMinimumRate; + } + + emit PricingUpdated(storagePricePerTibPerMonth, minimumStorageRatePerMonth); + } + /** * @notice Adds a provider ID to the approved list * @dev Only callable by the contract owner. Reverts if already approved. @@ -1097,7 +1145,7 @@ contract FilecoinWarmStorageService is /// @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; + uint256 minimumLockupRequired = (minimumStorageRatePerMonth * DEFAULT_LOCKUP_PERIOD) / EPOCHS_PER_MONTH; // Check that payer has sufficient available funds (,, uint256 availableFunds,) = payments.getAccountInfoIfSettled(usdfcTokenAddress, payer); @@ -1120,7 +1168,7 @@ contract FilecoinWarmStorageService is require(isApproved, Errors.OperatorNotApproved(payer, address(this))); // Calculate minimum rate per epoch - uint256 minimumRatePerEpoch = MINIMUM_STORAGE_RATE_PER_MONTH / EPOCHS_PER_MONTH; + uint256 minimumRatePerEpoch = minimumStorageRatePerMonth / EPOCHS_PER_MONTH; // Verify rate allowance is sufficient require( @@ -1241,10 +1289,10 @@ contract FilecoinWarmStorageService is */ function _calculateStorageRate(uint256 totalBytes) internal view returns (uint256) { // Calculate natural size-based rate - uint256 naturalRate = calculateStorageSizeBasedRatePerEpoch(totalBytes, STORAGE_PRICE_PER_TIB_PER_MONTH); + uint256 naturalRate = calculateStorageSizeBasedRatePerEpoch(totalBytes, storagePricePerTibPerMonth); // Calculate minimum rate (floor price converted to per-epoch) - uint256 minimumRate = MINIMUM_STORAGE_RATE_PER_MONTH / EPOCHS_PER_MONTH; + uint256 minimumRate = minimumStorageRatePerMonth / EPOCHS_PER_MONTH; // Return whichever is higher: natural rate or minimum rate return naturalRate > minimumRate ? naturalRate : minimumRate; @@ -1341,12 +1389,12 @@ contract FilecoinWarmStorageService is */ function getServicePrice() external view returns (ServicePricing memory pricing) { pricing = ServicePricing({ - pricePerTiBPerMonthNoCDN: STORAGE_PRICE_PER_TIB_PER_MONTH, + pricePerTiBPerMonthNoCDN: storagePricePerTibPerMonth, pricePerTiBCdnEgress: CDN_EGRESS_PRICE_PER_TIB, pricePerTiBCacheMissEgress: CACHE_MISS_EGRESS_PRICE_PER_TIB, tokenAddress: usdfcTokenAddress, epochsPerMonth: EPOCHS_PER_MONTH, - minimumPricePerMonth: MINIMUM_STORAGE_RATE_PER_MONTH + minimumPricePerMonth: minimumStorageRatePerMonth }); } @@ -1356,7 +1404,7 @@ contract FilecoinWarmStorageService is * @return spPayment SP payment (per TiB per month) */ function getEffectiveRates() external view returns (uint256 serviceFee, uint256 spPayment) { - uint256 total = STORAGE_PRICE_PER_TIB_PER_MONTH; + uint256 total = storagePricePerTibPerMonth; serviceFee = (total * serviceCommissionBps) / COMMISSION_MAX_BPS; spPayment = total - serviceFee; diff --git a/service_contracts/src/FilecoinWarmStorageServiceStateView.sol b/service_contracts/src/FilecoinWarmStorageServiceStateView.sol index 4e5068b9..211917b9 100644 --- a/service_contracts/src/FilecoinWarmStorageServiceStateView.sol +++ b/service_contracts/src/FilecoinWarmStorageServiceStateView.sol @@ -70,6 +70,10 @@ contract FilecoinWarmStorageServiceStateView is IPDPProvingSchedule { return service.getClientDataSets(client); } + function getCurrentPricingRates() external view returns (uint256 storagePrice, uint256 minimumRate) { + return service.getCurrentPricingRates(); + } + function getDataSet(uint256 dataSetId) external view diff --git a/service_contracts/src/lib/FilecoinWarmStorageServiceLayout.sol b/service_contracts/src/lib/FilecoinWarmStorageServiceLayout.sol index df5617a7..fb8182af 100644 --- a/service_contracts/src/lib/FilecoinWarmStorageServiceLayout.sol +++ b/service_contracts/src/lib/FilecoinWarmStorageServiceLayout.sol @@ -25,3 +25,5 @@ bytes32 constant APPROVED_PROVIDER_IDS_SLOT = bytes32(uint256(16)); bytes32 constant VIEW_CONTRACT_ADDRESS_SLOT = bytes32(uint256(17)); bytes32 constant FIL_BEAM_CONTROLLER_ADDRESS_SLOT = bytes32(uint256(18)); bytes32 constant NEXT_UPGRADE_SLOT = bytes32(uint256(19)); +bytes32 constant STORAGE_PRICE_PER_TIB_PER_MONTH_SLOT = bytes32(uint256(20)); +bytes32 constant MINIMUM_STORAGE_RATE_PER_MONTH_SLOT = bytes32(uint256(21)); diff --git a/service_contracts/src/lib/FilecoinWarmStorageServiceStateInternalLibrary.sol b/service_contracts/src/lib/FilecoinWarmStorageServiceStateInternalLibrary.sol index 117708eb..8df5e514 100644 --- a/service_contracts/src/lib/FilecoinWarmStorageServiceStateInternalLibrary.sol +++ b/service_contracts/src/lib/FilecoinWarmStorageServiceStateInternalLibrary.sol @@ -533,4 +533,20 @@ library FilecoinWarmStorageServiceStateInternalLibrary { nextImplementation = address(uint160(uint256(upgradeInfo))); afterEpoch = uint96(uint256(upgradeInfo) >> 160); } + + /** + * @notice Get the current pricing rates + * @return storagePrice Current storage price per TiB per month + * @return minimumRate Current minimum monthly storage rate + */ + function getCurrentPricingRates(FilecoinWarmStorageService service) + internal + view + returns (uint256 storagePrice, uint256 minimumRate) + { + return ( + uint256(service.extsload(StorageLayout.STORAGE_PRICE_PER_TIB_PER_MONTH_SLOT)), + uint256(service.extsload(StorageLayout.MINIMUM_STORAGE_RATE_PER_MONTH_SLOT)) + ); + } } diff --git a/service_contracts/src/lib/FilecoinWarmStorageServiceStateLibrary.sol b/service_contracts/src/lib/FilecoinWarmStorageServiceStateLibrary.sol index 9e5e0344..edd6a7d7 100644 --- a/service_contracts/src/lib/FilecoinWarmStorageServiceStateLibrary.sol +++ b/service_contracts/src/lib/FilecoinWarmStorageServiceStateLibrary.sol @@ -529,4 +529,20 @@ library FilecoinWarmStorageServiceStateLibrary { nextImplementation = address(uint160(uint256(upgradeInfo))); afterEpoch = uint96(uint256(upgradeInfo) >> 160); } + + /** + * @notice Get the current pricing rates + * @return storagePrice Current storage price per TiB per month + * @return minimumRate Current minimum monthly storage rate + */ + function getCurrentPricingRates(FilecoinWarmStorageService service) + public + view + returns (uint256 storagePrice, uint256 minimumRate) + { + return ( + uint256(service.extsload(StorageLayout.STORAGE_PRICE_PER_TIB_PER_MONTH_SLOT)), + uint256(service.extsload(StorageLayout.MINIMUM_STORAGE_RATE_PER_MONTH_SLOT)) + ); + } }