Skip to content
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions service_contracts/abi/Errors.abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
}
]
},
{
"type": "error",
"name": "AtLeastOnePriceMustBeNonZero",
"inputs": []
},
{
"type": "error",
"name": "CDNPaymentAlreadyTerminated",
Expand Down Expand Up @@ -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",
Expand Down
63 changes: 63 additions & 0 deletions service_contracts/abi/FilecoinWarmStorageService.abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1436,6 +1473,11 @@
}
]
},
{
"type": "error",
"name": "AtLeastOnePriceMustBeNonZero",
"inputs": []
},
{
"type": "error",
"name": "CDNPaymentAlreadyTerminated",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 18 additions & 0 deletions service_contracts/abi/FilecoinWarmStorageServiceStateView.abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions service_contracts/src/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down Expand Up @@ -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);
}
74 changes: 61 additions & 13 deletions service_contracts/src/FilecoinWarmStorageService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ contract FilecoinWarmStorageService is
event ProviderApproved(uint256 indexed providerId);
event ProviderUnapproved(uint256 indexed providerId);

event PricingUpdated(uint256 storagePrice, uint256 minimumRate);

// =========================================================================
// Structs

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);

// =========================================================================
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Comment on lines +496 to +513
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love having ifs here and am not sure "Pass 0 to keep existing value unchanged." is all that helpful. It might help with footguns, perhaps, but now there's no way to set a floor of 0, we'd have to use 1 to be our floor if we decided to undo that.
But, not a big deal if we're just trying to get this over the line at this point.


emit PricingUpdated(storagePricePerTibPerMonth, minimumStorageRatePerMonth);
}

/**
* @notice Adds a provider ID to the approved list
* @dev Only callable by the contract owner. Reverts if already approved.
Expand Down Expand Up @@ -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);
Expand All @@ -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(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
});
}

Expand All @@ -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;
Expand Down
Loading
Loading