Skip to content

Commit 6801108

Browse files
rjan90wjmelements
andauthored
feat(pricing): make pricing constants mutable with owner-controlled u… (#325)
Makes the three pricing rate constants mutable storage variables instead of immutable, enabling future price adjustments without requiring contract upgrades. ## Changes - **Pricing Variables**: Converted `STORAGE_PRICE_PER_TIB_PER_MONTH`, `CACHE_MISS_PRICE_PER_TIB_PER_MONTH`, and `CDN_PRICE_PER_TIB_PER_MONTH` from immutable constants to mutable storage variables - **New Function**: Added `updatePricing()` - owner-only function that allows selective updates to pricing rates (pass 0 to keep existing value) - **New Getter**: Added `getCurrentPricingRates()` - public view function to query current pricing values - **New Event**: Added `PricingUpdated` event to track pricing changes - **Lockup Amounts**: `DEFAULT_CDN_LOCKUP_AMOUNT` and `DEFAULT_CACHE_MISS_LOCKUP_AMOUNT` remain immutable as they are not expected to change ## Impact - New price rates will apply to all active data sets on their next proving period update (via `nextProvingPeriod()`) --------- Co-authored-by: William Morriss <[email protected]>
1 parent 2d31493 commit 6801108

11 files changed

+257
-13
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
2323
- Use `getDataSetStatusDetails()` to check termination status separately from Active/Inactive status
2424
- Subgraph schema updated with status enum and history tracking
2525
- **Calibnet**: Reduced DEFAULT_CHALLENGE_WINDOW_SIZE from 30 epochs to 20 epochs for faster testing iteration
26+
- Made storage pricing and minimum rate mutable storage variables instead of immutable constants ([#306](https://github.com/FilOzone/filecoin-services/issues/306))
27+
- `storagePricePerTibPerMonth` (initially 2.5 USDFC, max 10 USDFC)
28+
- `minimumStorageRatePerMonth` (initially 0.06 USDFC, max 0.24 USDFC)
29+
- Added `updatePricing(uint256 newStoragePrice, uint256 newMinimumRate)` function to allow owner to update pricing rates without contract upgrades
30+
- Pass 0 to keep existing value unchanged
31+
- At least one price must be non-zero
32+
- Validates against 4x upper bounds (10 USDFC storage, 0.24 USDFC minimum rate)
33+
- Price updates apply immediately to existing payment rails when recalculated
34+
- Added `getCurrentPricingRates()` function to query current storage price and minimum rate
35+
- Added `PricingUpdated(uint256 storagePrice, uint256 minimumRate)` event to track pricing changes
36+
- Added `AtLeastOnePriceMustBeNonZero` and `PriceExceedsMaximum` errors for pricing validation
2637

2738
## [0.3.0] - 2025-10-08 - M3.1 Calibration Network Deployment
2839

service_contracts/abi/Errors.abi.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
}
1111
]
1212
},
13+
{
14+
"type": "error",
15+
"name": "AtLeastOnePriceMustBeNonZero",
16+
"inputs": []
17+
},
1318
{
1419
"type": "error",
1520
"name": "CDNPaymentAlreadyTerminated",
@@ -742,6 +747,27 @@
742747
}
743748
]
744749
},
750+
{
751+
"type": "error",
752+
"name": "PriceExceedsMaximum",
753+
"inputs": [
754+
{
755+
"name": "priceType",
756+
"type": "uint8",
757+
"internalType": "enum Errors.PriceType"
758+
},
759+
{
760+
"name": "maxAllowed",
761+
"type": "uint256",
762+
"internalType": "uint256"
763+
},
764+
{
765+
"name": "actual",
766+
"type": "uint256",
767+
"internalType": "uint256"
768+
}
769+
]
770+
},
745771
{
746772
"type": "error",
747773
"name": "ProofAlreadySubmitted",

service_contracts/abi/FilecoinWarmStorageService.abi.json

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,24 @@
793793
"outputs": [],
794794
"stateMutability": "nonpayable"
795795
},
796+
{
797+
"type": "function",
798+
"name": "updatePricing",
799+
"inputs": [
800+
{
801+
"name": "newStoragePrice",
802+
"type": "uint256",
803+
"internalType": "uint256"
804+
},
805+
{
806+
"name": "newMinimumRate",
807+
"type": "uint256",
808+
"internalType": "uint256"
809+
}
810+
],
811+
"outputs": [],
812+
"stateMutability": "nonpayable"
813+
},
796814
{
797815
"type": "function",
798816
"name": "updateServiceCommission",
@@ -1286,6 +1304,25 @@
12861304
],
12871305
"anonymous": false
12881306
},
1307+
{
1308+
"type": "event",
1309+
"name": "PricingUpdated",
1310+
"inputs": [
1311+
{
1312+
"name": "storagePrice",
1313+
"type": "uint256",
1314+
"indexed": false,
1315+
"internalType": "uint256"
1316+
},
1317+
{
1318+
"name": "minimumRate",
1319+
"type": "uint256",
1320+
"indexed": false,
1321+
"internalType": "uint256"
1322+
}
1323+
],
1324+
"anonymous": false
1325+
},
12891326
{
12901327
"type": "event",
12911328
"name": "ProviderApproved",
@@ -1436,6 +1473,11 @@
14361473
}
14371474
]
14381475
},
1476+
{
1477+
"type": "error",
1478+
"name": "AtLeastOnePriceMustBeNonZero",
1479+
"inputs": []
1480+
},
14391481
{
14401482
"type": "error",
14411483
"name": "CDNPaymentAlreadyTerminated",
@@ -2141,6 +2183,27 @@
21412183
}
21422184
]
21432185
},
2186+
{
2187+
"type": "error",
2188+
"name": "PriceExceedsMaximum",
2189+
"inputs": [
2190+
{
2191+
"name": "priceType",
2192+
"type": "uint8",
2193+
"internalType": "enum Errors.PriceType"
2194+
},
2195+
{
2196+
"name": "maxAllowed",
2197+
"type": "uint256",
2198+
"internalType": "uint256"
2199+
},
2200+
{
2201+
"name": "actual",
2202+
"type": "uint256",
2203+
"internalType": "uint256"
2204+
}
2205+
]
2206+
},
21442207
{
21452208
"type": "error",
21462209
"name": "ProofAlreadySubmitted",

service_contracts/abi/FilecoinWarmStorageServiceStateLibrary.abi.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,30 @@
295295
],
296296
"stateMutability": "view"
297297
},
298+
{
299+
"type": "function",
300+
"name": "getCurrentPricingRates",
301+
"inputs": [
302+
{
303+
"name": "service",
304+
"type": "FilecoinWarmStorageService",
305+
"internalType": "contract FilecoinWarmStorageService"
306+
}
307+
],
308+
"outputs": [
309+
{
310+
"name": "storagePrice",
311+
"type": "uint256",
312+
"internalType": "uint256"
313+
},
314+
{
315+
"name": "minimumRate",
316+
"type": "uint256",
317+
"internalType": "uint256"
318+
}
319+
],
320+
"stateMutability": "view"
321+
},
298322
{
299323
"type": "function",
300324
"name": "getDataSet",

service_contracts/abi/FilecoinWarmStorageServiceStateView.abi.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,24 @@
258258
],
259259
"stateMutability": "view"
260260
},
261+
{
262+
"type": "function",
263+
"name": "getCurrentPricingRates",
264+
"inputs": [],
265+
"outputs": [
266+
{
267+
"name": "storagePrice",
268+
"type": "uint256",
269+
"internalType": "uint256"
270+
},
271+
{
272+
"name": "minimumRate",
273+
"type": "uint256",
274+
"internalType": "uint256"
275+
}
276+
],
277+
"stateMutability": "view"
278+
},
261279
{
262280
"type": "function",
263281
"name": "getDataSet",

service_contracts/src/Errors.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ library Errors {
3838
Service
3939
}
4040

41+
enum PriceType {
42+
/// Storage price per TiB per month
43+
Storage,
44+
/// Minimum monthly storage rate (floor price)
45+
MinimumRate
46+
}
47+
4148
/// @notice An expected contract or participant address was the zero address
4249
/// @dev Used for parameter validation when a non-zero address is required
4350
/// @param field The specific address field that was zero (see enum {AddressField})
@@ -336,4 +343,13 @@ library Errors {
336343
error InsufficientMaxLockupPeriod(
337344
address payer, address operator, uint256 maxLockupPeriod, uint256 requiredLockupPeriod
338345
);
346+
347+
/// @notice At least one price parameter must be non-zero when updating pricing
348+
error AtLeastOnePriceMustBeNonZero();
349+
350+
/// @notice Price update exceeds the maximum allowed value
351+
/// @param priceType The type of price being updated (see enum {PriceType})
352+
/// @param maxAllowed The maximum allowed value for this price type
353+
/// @param actual The attempted value that exceeds the maximum
354+
error PriceExceedsMaximum(PriceType priceType, uint256 maxAllowed, uint256 actual);
339355
}

service_contracts/src/FilecoinWarmStorageService.sol

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ contract FilecoinWarmStorageService is
115115
event ProviderApproved(uint256 indexed providerId);
116116
event ProviderUnapproved(uint256 indexed providerId);
117117

118+
event PricingUpdated(uint256 storagePrice, uint256 minimumRate);
119+
118120
// =========================================================================
119121
// Structs
120122

@@ -211,16 +213,18 @@ contract FilecoinWarmStorageService is
211213
bytes32 private constant WITH_CDN_STRING_STORAGE_REPR =
212214
0x7769746843444e0000000000000000000000000000000000000000000000000e;
213215

214-
// Pricing constants
215-
uint256 private immutable STORAGE_PRICE_PER_TIB_PER_MONTH; // 2.5 USDFC per TiB per month without CDN with correct decimals
216+
// Pricing constants (CDN egress pricing is immutable)
216217
uint256 private immutable CDN_EGRESS_PRICE_PER_TIB; // 7 USDFC per TiB of CDN egress
217218
uint256 private immutable CACHE_MISS_EGRESS_PRICE_PER_TIB; // 7 USDFC per TiB of cache miss egress
218-
uint256 private immutable MINIMUM_STORAGE_RATE_PER_MONTH; // 0.06 USDFC per month minimum pricing floor
219219

220220
// Fixed lockup amounts for CDN rails
221221
uint256 private immutable DEFAULT_CDN_LOCKUP_AMOUNT; // 0.7 USDFC
222222
uint256 private immutable DEFAULT_CACHE_MISS_LOCKUP_AMOUNT; // 0.3 USDFC
223223

224+
// Maximum pricing bounds (4x initial values)
225+
uint256 private immutable MAX_STORAGE_PRICE_PER_TIB_PER_MONTH; // 10 USDFC (4x 2.5)
226+
uint256 private immutable MAX_MINIMUM_STORAGE_RATE_PER_MONTH; // 0.24 USDFC (4x 0.06)
227+
224228
// Token decimals
225229
uint8 private immutable TOKEN_DECIMALS;
226230

@@ -290,6 +294,10 @@ contract FilecoinWarmStorageService is
290294

291295
PlannedUpgrade private nextUpgrade;
292296

297+
// Pricing rates (mutable for future adjustments)
298+
uint256 private storagePricePerTibPerMonth;
299+
uint256 private minimumStorageRatePerMonth;
300+
293301
event UpgradeAnnounced(PlannedUpgrade plannedUpgrade);
294302

295303
// =========================================================================
@@ -346,11 +354,13 @@ contract FilecoinWarmStorageService is
346354
// Read token decimals from the USDFC token contract
347355
TOKEN_DECIMALS = _usdfc.decimals();
348356

349-
// Initialize the fee constants based on the actual token decimals
350-
STORAGE_PRICE_PER_TIB_PER_MONTH = (5 * 10 ** TOKEN_DECIMALS) / 2; // 2.5 USDFC
357+
// Initialize the immutable pricing constants based on the actual token decimals
351358
CDN_EGRESS_PRICE_PER_TIB = 7 * 10 ** TOKEN_DECIMALS; // 7 USDFC per TiB
352359
CACHE_MISS_EGRESS_PRICE_PER_TIB = 7 * 10 ** TOKEN_DECIMALS; // 7 USDFC per TiB
353-
MINIMUM_STORAGE_RATE_PER_MONTH = (6 * 10 ** TOKEN_DECIMALS) / 100; // 0.06 USDFC minimum
360+
361+
// Initialize maximum pricing bounds (4x initial values)
362+
MAX_STORAGE_PRICE_PER_TIB_PER_MONTH = 10 * 10 ** TOKEN_DECIMALS; // 10 USDFC (4x 2.5)
363+
MAX_MINIMUM_STORAGE_RATE_PER_MONTH = (24 * 10 ** TOKEN_DECIMALS) / 100; // 0.24 USDFC (4x 0.06)
354364

355365
// Initialize the lockup constants based on the actual token decimals
356366
DEFAULT_CDN_LOCKUP_AMOUNT = (7 * 10 ** TOKEN_DECIMALS) / 10; // 0.7 USDFC
@@ -401,6 +411,10 @@ contract FilecoinWarmStorageService is
401411

402412
// Set commission rate
403413
serviceCommissionBps = 0; // 0%
414+
415+
// Initialize mutable pricing variables
416+
storagePricePerTibPerMonth = (5 * 10 ** TOKEN_DECIMALS) / 2; // 2.5 USDFC
417+
minimumStorageRatePerMonth = (6 * 10 ** TOKEN_DECIMALS) / 100; // 0.06 USDFC
404418
}
405419

406420
function announcePlannedUpgrade(PlannedUpgrade calldata plannedUpgrade) external onlyOwner {
@@ -485,6 +499,40 @@ contract FilecoinWarmStorageService is
485499
serviceCommissionBps = newCommissionBps;
486500
}
487501

502+
/**
503+
* @notice Updates the pricing rates for storage services
504+
* @dev Only callable by the contract owner. Pass 0 to keep existing value unchanged.
505+
* Price updates apply immediately to existing payment rails when they're recalculated
506+
* (e.g., during piece additions/deletions or proving period updates).
507+
* Maximum allowed values: 10 USDFC for storage, 0.24 USDFC for minimum rate.
508+
* @param newStoragePrice New storage price per TiB per month (0 = no change, max 10 USDFC)
509+
* @param newMinimumRate New minimum monthly storage rate (0 = no change, max 0.24 USDFC)
510+
*/
511+
function updatePricing(uint256 newStoragePrice, uint256 newMinimumRate) external onlyOwner {
512+
require(newStoragePrice > 0 || newMinimumRate > 0, Errors.AtLeastOnePriceMustBeNonZero());
513+
514+
if (newStoragePrice > 0) {
515+
require(
516+
newStoragePrice <= MAX_STORAGE_PRICE_PER_TIB_PER_MONTH,
517+
Errors.PriceExceedsMaximum(
518+
Errors.PriceType.Storage, MAX_STORAGE_PRICE_PER_TIB_PER_MONTH, newStoragePrice
519+
)
520+
);
521+
storagePricePerTibPerMonth = newStoragePrice;
522+
}
523+
if (newMinimumRate > 0) {
524+
require(
525+
newMinimumRate <= MAX_MINIMUM_STORAGE_RATE_PER_MONTH,
526+
Errors.PriceExceedsMaximum(
527+
Errors.PriceType.MinimumRate, MAX_MINIMUM_STORAGE_RATE_PER_MONTH, newMinimumRate
528+
)
529+
);
530+
minimumStorageRatePerMonth = newMinimumRate;
531+
}
532+
533+
emit PricingUpdated(storagePricePerTibPerMonth, minimumStorageRatePerMonth);
534+
}
535+
488536
/**
489537
* @notice Adds a provider ID to the approved list
490538
* @dev Only callable by the contract owner. Reverts if already approved.
@@ -1127,7 +1175,7 @@ contract FilecoinWarmStorageService is
11271175
/// @param payer The address of the payer
11281176
function validatePayerOperatorApprovalAndFunds(FilecoinPayV1 payments, address payer) internal view {
11291177
// Calculate required lockup for minimum pricing
1130-
uint256 minimumLockupRequired = (MINIMUM_STORAGE_RATE_PER_MONTH * DEFAULT_LOCKUP_PERIOD) / EPOCHS_PER_MONTH;
1178+
uint256 minimumLockupRequired = (minimumStorageRatePerMonth * DEFAULT_LOCKUP_PERIOD) / EPOCHS_PER_MONTH;
11311179

11321180
// Check that payer has sufficient available funds
11331181
(,, uint256 availableFunds,) = payments.getAccountInfoIfSettled(usdfcTokenAddress, payer);
@@ -1150,7 +1198,7 @@ contract FilecoinWarmStorageService is
11501198
require(isApproved, Errors.OperatorNotApproved(payer, address(this)));
11511199

11521200
// Calculate minimum rate per epoch
1153-
uint256 minimumRatePerEpoch = MINIMUM_STORAGE_RATE_PER_MONTH / EPOCHS_PER_MONTH;
1201+
uint256 minimumRatePerEpoch = minimumStorageRatePerMonth / EPOCHS_PER_MONTH;
11541202

11551203
// Verify rate allowance is sufficient
11561204
require(
@@ -1271,10 +1319,10 @@ contract FilecoinWarmStorageService is
12711319
*/
12721320
function _calculateStorageRate(uint256 totalBytes) internal view returns (uint256) {
12731321
// Calculate natural size-based rate
1274-
uint256 naturalRate = calculateStorageSizeBasedRatePerEpoch(totalBytes, STORAGE_PRICE_PER_TIB_PER_MONTH);
1322+
uint256 naturalRate = calculateStorageSizeBasedRatePerEpoch(totalBytes, storagePricePerTibPerMonth);
12751323

12761324
// Calculate minimum rate (floor price converted to per-epoch)
1277-
uint256 minimumRate = MINIMUM_STORAGE_RATE_PER_MONTH / EPOCHS_PER_MONTH;
1325+
uint256 minimumRate = minimumStorageRatePerMonth / EPOCHS_PER_MONTH;
12781326

12791327
// Return whichever is higher: natural rate or minimum rate
12801328
return naturalRate > minimumRate ? naturalRate : minimumRate;
@@ -1371,12 +1419,12 @@ contract FilecoinWarmStorageService is
13711419
*/
13721420
function getServicePrice() external view returns (ServicePricing memory pricing) {
13731421
pricing = ServicePricing({
1374-
pricePerTiBPerMonthNoCDN: STORAGE_PRICE_PER_TIB_PER_MONTH,
1422+
pricePerTiBPerMonthNoCDN: storagePricePerTibPerMonth,
13751423
pricePerTiBCdnEgress: CDN_EGRESS_PRICE_PER_TIB,
13761424
pricePerTiBCacheMissEgress: CACHE_MISS_EGRESS_PRICE_PER_TIB,
13771425
tokenAddress: usdfcTokenAddress,
13781426
epochsPerMonth: EPOCHS_PER_MONTH,
1379-
minimumPricePerMonth: MINIMUM_STORAGE_RATE_PER_MONTH
1427+
minimumPricePerMonth: minimumStorageRatePerMonth
13801428
});
13811429
}
13821430

@@ -1386,7 +1434,7 @@ contract FilecoinWarmStorageService is
13861434
* @return spPayment SP payment (per TiB per month)
13871435
*/
13881436
function getEffectiveRates() external view returns (uint256 serviceFee, uint256 spPayment) {
1389-
uint256 total = STORAGE_PRICE_PER_TIB_PER_MONTH;
1437+
uint256 total = storagePricePerTibPerMonth;
13901438

13911439
serviceFee = (total * serviceCommissionBps) / COMMISSION_MAX_BPS;
13921440
spPayment = total - serviceFee;

0 commit comments

Comments
 (0)