Skip to content

Commit 5eaddfd

Browse files
feat: Update PDP proof fee (#214)
* feat: Update PDP proof fee * feat: make PDP fee calc use verifier-managed rate; stateless Fees.sol; update interfaces/tests * feat(pdp): address review feedback for FIL-based fee update -Remove manual apply; auto-apply new fee on read paths after delay -Commit any elapsed pending update in updateProofFee to avoid “reset” -Use transitionTime > 0 check (supports proposed fee = 0) -Pack fee fields into FeeStatus and expose only views (uint96) -Shift-based TiB conversion in PDPFees: (feePerTiB * rawSize) >> 40 -Trim IPDPVerifier to feePerTiB() only; drop mutation endpoints -Update tests to use getters and verify delayed auto-apply * Update src/PDPVerifier.sol * Update src/PDPVerifier.sol * Update src/PDPVerifier.sol * Update src/PDPVerifier.sol * Update src/PDPVerifier.sol * Update src/PDPVerifier.sol --------- Co-authored-by: William Morriss <[email protected]>
1 parent b605eae commit 5eaddfd

File tree

7 files changed

+192
-334
lines changed

7 files changed

+192
-334
lines changed

src/Fees.sol

Lines changed: 9 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,68 +10,23 @@ library PDPFees {
1010
// 0.1 FIL
1111
uint256 constant SYBIL_FEE = FIL_TO_ATTO_FIL / 10;
1212

13-
// 2 USD/Tib/month is the current reward earned by Storage Providers
14-
uint256 constant ESTIMATED_MONTHLY_TIB_STORAGE_REWARD_USD = 2;
15-
// 1% of reward per period
16-
uint256 constant PROOF_FEE_PERCENTAGE = 1;
17-
// 4% of reward per period for gas limit left bound
18-
uint256 constant GAS_LIMIT_LEFT_PERCENTAGE = 4;
19-
// 5% of reward per period for gas limit right bound
20-
uint256 constant GAS_LIMIT_RIGHT_PERCENTAGE = 5;
21-
uint256 constant USD_DECIMALS = 1e18;
13+
// Default FIL-based proof fee: 0.00023 FIL per TiB (used for initialization)
14+
// Based on: 0.00067 USD per TiB / 2.88 USD per FIL = 0.00023 FIL per TiB
15+
uint96 constant DEFAULT_FEE_PER_TIB = 230000 gwei; // 0.00023 FIL in attoFIL
2216

2317
// 1 TiB in bytes (2^40)
2418
uint256 constant TIB_IN_BYTES = 2 ** 40;
25-
// Number of epochs per month (30 days * 2880 epochs per day)
26-
uint256 constant EPOCHS_PER_MONTH = 86400;
2719

28-
/// @notice Calculates the proof fee based on the gas fee and the raw size of the proof.
29-
/// @param estimatedGasFee The estimated gas fee in AttoFIL.
30-
/// @param filUsdPrice The price of FIL in USD.
31-
/// @param filUsdPriceExpo The exponent of the price of FIL in USD.
20+
/// @notice Calculates the proof fee based on the dataset size and a provided per-TiB fee.
3221
/// @param rawSize The raw size of the proof in bytes.
33-
/// @param nProofEpochs The number of proof epochs.
22+
/// @param feePerTiB The fee rate per TiB in AttoFIL (source of truth lives in PDPVerifier).
3423
/// @return proof fee in AttoFIL
35-
/// @dev The proof fee is calculated based on the gas fee and the raw size of the proof
36-
/// The fee is 1% of the projected reward and is reduced in the case gas cost of proving is too high.
37-
function proofFeeWithGasFeeBound(
38-
uint256 estimatedGasFee, // in AttoFIL
39-
uint64 filUsdPrice,
40-
int32 filUsdPriceExpo,
41-
uint256 rawSize,
42-
uint256 nProofEpochs
43-
) internal view returns (uint256) {
44-
require(
45-
estimatedGasFee > 0 || block.basefee == 0, "failed to validate: estimated gas fee must be greater than 0"
46-
);
47-
require(filUsdPrice > 0, "failed to validate: AttoFIL price must be greater than 0");
24+
/// @dev The proof fee is calculated as: fee_perTiB * datasetSize_in_TiB
25+
function calculateProofFee(uint256 rawSize, uint96 feePerTiB) internal pure returns (uint256) {
4826
require(rawSize > 0, "failed to validate: raw size must be greater than 0");
4927

50-
// Calculate reward per epoch per byte (in AttoFIL)
51-
uint256 rewardPerEpochPerByte;
52-
if (filUsdPriceExpo >= 0) {
53-
rewardPerEpochPerByte = (ESTIMATED_MONTHLY_TIB_STORAGE_REWARD_USD * FIL_TO_ATTO_FIL)
54-
/ (TIB_IN_BYTES * EPOCHS_PER_MONTH * filUsdPrice * (10 ** uint32(filUsdPriceExpo)));
55-
} else {
56-
rewardPerEpochPerByte = (
57-
ESTIMATED_MONTHLY_TIB_STORAGE_REWARD_USD * FIL_TO_ATTO_FIL * (10 ** uint32(-filUsdPriceExpo))
58-
) / (TIB_IN_BYTES * EPOCHS_PER_MONTH * filUsdPrice);
59-
}
60-
61-
// Calculate total reward for the proving period
62-
uint256 estimatedCurrentReward = rewardPerEpochPerByte * nProofEpochs * rawSize;
63-
64-
// Calculate gas limits
65-
uint256 gasLimitRight = (estimatedCurrentReward * GAS_LIMIT_RIGHT_PERCENTAGE) / 100;
66-
uint256 gasLimitLeft = (estimatedCurrentReward * GAS_LIMIT_LEFT_PERCENTAGE) / 100;
67-
68-
if (estimatedGasFee >= gasLimitRight) {
69-
return 0; // No proof fee if gas fee is above right limit
70-
} else if (estimatedGasFee >= gasLimitLeft) {
71-
return gasLimitRight - estimatedGasFee; // Partial discount on proof fee
72-
} else {
73-
return (estimatedCurrentReward * PROOF_FEE_PERCENTAGE) / 100;
74-
}
28+
// Calculate fee as: (feePerTiB * rawSize) >> 40 (since TIB_IN_BYTES == 2**40)
29+
return (feePerTiB * rawSize) >> 40;
7530
}
7631

7732
// sybil fee adds cost to adding state to the pdp verifier contract to prevent

src/PDPVerifier.sol

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import {ERC1967Utils} from "../lib/openzeppelin-contracts/contracts/proxy/ERC196
99
import {Initializable} from "../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
1010
import {UUPSUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
1111
import {OwnableUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
12-
import {IPyth} from "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
13-
import {PythStructs} from "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
1412
import {IPDPTypes} from "./interfaces/IPDPTypes.sol";
1513

1614
/// @title PDPListener
@@ -46,10 +44,7 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {
4644
uint256 public constant MAX_ENQUEUED_REMOVALS = 2000;
4745
address public constant RANDOMNESS_PRECOMPILE = 0xfE00000000000000000000000000000000000006;
4846
uint256 public constant EXTRA_DATA_MAX_SIZE = 2048;
49-
IPyth public constant PYTH = IPyth(0xA2aa501b19aff244D90cc15a4Cf739D2725B5729);
50-
51-
// FIL/USD price feed query ID on the Pyth network
52-
bytes32 public constant FIL_USD_PRICE_FEED_ID = 0x150ac9b959aee0051e4091f0ef5216d941f590e1c5e7f91cf7635b5c11628c0e;
47+
uint256 public constant SECONDS_IN_DAY = 86400;
5348
uint256 public constant NO_CHALLENGE_SCHEDULED = 0;
5449
uint256 public constant NO_PROVEN_EPOCH = 0;
5550

@@ -64,7 +59,8 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {
6459
event PiecesAdded(uint256 indexed setId, uint256[] pieceIds, Cids.Cid[] pieceCids);
6560
event PiecesRemoved(uint256 indexed setId, uint256[] pieceIds);
6661

67-
event ProofFeePaid(uint256 indexed setId, uint256 fee, uint64 price, int32 expo);
62+
event ProofFeePaid(uint256 indexed setId, uint256 fee);
63+
event FeeUpdateProposed(uint256 currentFee, uint256 newFee, uint256 effectiveTime);
6864

6965
event PossessionProven(uint256 indexed setId, IPDPTypes.PieceIdAndOffset[] challenges);
7066
event NextProvingPeriod(uint256 indexed setId, uint256 challengeEpoch, uint256 leafCount);
@@ -142,6 +138,15 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {
142138
mapping(uint256 => address) dataSetProposedStorageProvider;
143139
mapping(uint256 => uint256) dataSetLastProvenEpoch;
144140

141+
// Packed fee status
142+
struct FeeStatus {
143+
uint96 currentFeePerTiB;
144+
uint96 nextFeePerTiB;
145+
uint64 transitionTime;
146+
}
147+
148+
FeeStatus private feeStatus;
149+
145150
// Methods
146151

147152
/// @custom:oz-upgrades-unsafe-allow constructor
@@ -154,6 +159,7 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {
154159
__UUPSUpgradeable_init();
155160
challengeFinality = _challengeFinality;
156161
nextDataSetId = 1; // Data sets start at 1
162+
feeStatus.nextFeePerTiB = PDPFees.DEFAULT_FEE_PER_TIB;
157163
}
158164

159165
string public constant VERSION = "2.1.0";
@@ -575,7 +581,7 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {
575581
//
576582
// (add 32 bytes to the `callDataSize` to also account for the `setId` calldata param)
577583
uint256 gasUsed = (initialGas - gasleft()) + ((calculateCallDataSize(proofs) + 32) * 1300);
578-
uint256 refund = calculateAndBurnProofFee(setId, gasUsed);
584+
uint256 refund = calculateAndBurnProofFee(setId);
579585

580586
{
581587
address listenerAddr = dataSetListener[setId];
@@ -595,29 +601,43 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {
595601
}
596602
}
597603

598-
function calculateProofFee(uint256 setId, uint256 estimatedGasFee) public view returns (uint256) {
604+
function calculateProofFee(uint256 setId) public view returns (uint256) {
599605
uint256 rawSize = 32 * challengeRange[setId];
600-
(uint64 filUsdPrice, int32 filUsdPriceExpo) = getFILUSDPrice();
606+
return calculateProofFeeForSize(rawSize);
607+
}
601608

602-
return PDPFees.proofFeeWithGasFeeBound(
603-
estimatedGasFee, filUsdPrice, filUsdPriceExpo, rawSize, block.number - dataSetLastProvenEpoch[setId]
604-
);
609+
function calculateProofFeeForSize(uint256 rawSize) public view returns (uint256) {
610+
require(rawSize > 0, "failed to validate: raw size must be greater than 0");
611+
return PDPFees.calculateProofFee(rawSize, _currentFeePerTiB());
605612
}
606613

607-
function calculateAndBurnProofFee(uint256 setId, uint256 gasUsed) internal returns (uint256 refund) {
608-
uint256 estimatedGasFee = gasUsed * block.basefee;
614+
function calculateAndBurnProofFee(uint256 setId) internal returns (uint256 refund) {
609615
uint256 rawSize = 32 * challengeRange[setId];
610-
(uint64 filUsdPrice, int32 filUsdPriceExpo) = getFILUSDPrice();
616+
uint256 proofFee = calculateProofFeeForSize(rawSize);
611617

612-
uint256 proofFee = PDPFees.proofFeeWithGasFeeBound(
613-
estimatedGasFee, filUsdPrice, filUsdPriceExpo, rawSize, block.number - dataSetLastProvenEpoch[setId]
614-
);
615618
burnFee(proofFee);
616-
emit ProofFeePaid(setId, proofFee, filUsdPrice, filUsdPriceExpo);
619+
emit ProofFeePaid(setId, proofFee);
617620

618621
return msg.value - proofFee; // burnFee asserts that proofFee <= msg.value;
619622
}
620623

624+
function _currentFeePerTiB() internal view returns (uint96) {
625+
return block.timestamp >= feeStatus.transitionTime ? feeStatus.nextFeePerTiB : feeStatus.currentFeePerTiB;
626+
}
627+
628+
// Public getters for packed fee status
629+
function feePerTiB() public view returns (uint96) {
630+
return _currentFeePerTiB();
631+
}
632+
633+
function proposedFeePerTiB() public view returns (uint96) {
634+
return feeStatus.nextFeePerTiB;
635+
}
636+
637+
function feeEffectiveTime() public view returns (uint64) {
638+
return feeStatus.transitionTime;
639+
}
640+
621641
function calculateCallDataSize(IPDPTypes.Proof[] calldata proofs) internal pure returns (uint256) {
622642
uint256 callDataSize = 0;
623643
for (uint256 i = 0; i < proofs.length; i++) {
@@ -837,10 +857,15 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {
837857
return BitOps.ctz(index + 1);
838858
}
839859

840-
// Add function to get FIL/USD price
841-
function getFILUSDPrice() public view returns (uint64, int32) {
842-
PythStructs.Price memory priceData = PYTH.getPriceUnsafe(FIL_USD_PRICE_FEED_ID);
843-
require(priceData.price > 0, "failed to validate: price must be greater than 0");
844-
return (uint64(priceData.price), priceData.expo);
860+
/// @notice Proposes a new proof fee with 7-day delay
861+
/// @param newFeePerTiB The new fee per TiB in AttoFIL
862+
function updateProofFee(uint256 newFeePerTiB) external onlyOwner {
863+
// Auto-commit any pending update that has reached its transition time
864+
if (block.timestamp >= feeStatus.transitionTime) {
865+
feeStatus.currentFeePerTiB = feeStatus.nextFeePerTiB;
866+
}
867+
feeStatus.nextFeePerTiB = uint96(newFeePerTiB);
868+
feeStatus.transitionTime = uint64(block.timestamp + 7 days);
869+
emit FeeUpdateProposed(feeStatus.currentFeePerTiB, newFeePerTiB, feeStatus.transitionTime);
845870
}
846871
}

src/interfaces/IPDPEvents.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ interface IPDPEvents {
1515
event DataSetEmpty(uint256 indexed setId);
1616
event PiecesAdded(uint256 indexed setId, uint256[] pieceIds, Cids.Cid[] pieceCids);
1717
event PiecesRemoved(uint256 indexed setId, uint256[] pieceIds);
18-
event ProofFeePaid(uint256 indexed setId, uint256 fee, uint64 price, int32 expo);
18+
event ProofFeePaid(uint256 indexed setId, uint256 fee);
19+
event FeeUpdateProposed(uint256 currentFee, uint256 newFee, uint256 effectiveTime);
1920
event PossessionProven(uint256 indexed setId, IPDPTypes.PieceIdAndOffset[] challenges);
2021
event NextProvingPeriod(uint256 indexed setId, uint256 challengeEpoch, uint256 leafCount);
2122
event ContractUpgraded(string version, address newImplementation);

src/interfaces/IPDPVerifier.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,7 @@ interface IPDPVerifier is IPDPEvents {
4040
external
4141
view
4242
returns (IPDPTypes.PieceIdAndOffset[] memory);
43+
44+
// Fee view: returns the current effective fee per TiB
45+
function feePerTiB() external view returns (uint96);
4346
}

0 commit comments

Comments
 (0)