Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/Fees.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ library PDPFees {
// 0.1 FIL
uint256 constant SYBIL_FEE = FIL_TO_ATTO_FIL / 10;

// Fixed PDP proof fee per TiB (0.00023 FIL = 0.00023 * 1e18 AttoFIL = 230000000000000000 AttoFIL)
// Based on fixed conversion rate: 1 FIL = 2.88 USD, with fee = 0.00067 USD / 2.88 USD = 0.00023 FIL per TiB
uint256 public constant FEE_PER_TIB = 230000000000000000;

// 1 TiB in bytes (2^40)
uint256 constant TIB_IN_BYTES = 2 ** 40;

// 2 USD/Tib/month is the current reward earned by Storage Providers
uint256 constant ESTIMATED_MONTHLY_TIB_STORAGE_REWARD_USD = 2;
// 1% of reward per period
Expand All @@ -19,9 +26,6 @@ library PDPFees {
// 5% of reward per period for gas limit right bound
uint256 constant GAS_LIMIT_RIGHT_PERCENTAGE = 5;
uint256 constant USD_DECIMALS = 1e18;

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

Expand Down Expand Up @@ -74,6 +78,21 @@ library PDPFees {
}
}

/// @notice Calculates the proof fee based on a fixed fee per TiB.
/// @param rawSize The raw size of the proof in bytes.
/// @param feePerTiB The fee per TiB in AttoFIL (can be overridden or use default).
/// @return proof fee in AttoFIL
/// @dev The proof fee is calculated as: (rawSize / TIB_IN_BYTES) * feePerTiB
function flatProofFee(uint256 rawSize, uint256 feePerTiB) internal pure returns (uint256) {
require(rawSize > 0, "failed to validate: raw size must be greater than 0");
require(feePerTiB > 0, "failed to validate: fee per TiB must be greater than 0");

// Calculate fee based on dataset size in TiB
// fee = (rawSize / TIB_IN_BYTES) * feePerTiB
// To avoid precision loss, we compute: (rawSize * feePerTiB) / TIB_IN_BYTES
return (rawSize * feePerTiB) / TIB_IN_BYTES;
}

// sybil fee adds cost to adding state to the pdp verifier contract to prevent
// wasteful state growth. 0.1 FIL
function sybilFee() internal pure returns (uint256) {
Expand Down
60 changes: 48 additions & 12 deletions src/PDPVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {
event PiecesAdded(uint256 indexed setId, uint256[] pieceIds, Cids.Cid[] pieceCids);
event PiecesRemoved(uint256 indexed setId, uint256[] pieceIds);

event ProofFeePaid(uint256 indexed setId, uint256 fee, uint64 price, int32 expo);
event ProofFeePaid(uint256 indexed setId, uint256 fee);
event FeeUpdateProposed(uint256 currentFee, uint256 newFee, uint256 effectiveTime);

event PossessionProven(uint256 indexed setId, IPDPTypes.PieceIdAndOffset[] challenges);
event NextProvingPeriod(uint256 indexed setId, uint256 challengeEpoch, uint256 leafCount);
Expand Down Expand Up @@ -144,6 +145,11 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {
mapping(uint256 => address) dataSetProposedStorageProvider;
mapping(uint256 => uint256) dataSetLastProvenEpoch;

// Fee update mechanism
uint256 public proofFeePerTiB;
uint256 public proposedProofFeePerTiB;
uint256 public feeUpdateEffectiveTime;

// Methods

/// @custom:oz-upgrades-unsafe-allow constructor
Expand All @@ -162,7 +168,11 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {

event ContractUpgraded(string version, address implementation);

function migrate() external onlyOwner reinitializer(2) {
function migrate() external onlyOwner reinitializer(3) {
// Initialize the proof fee per TiB with the default value
proofFeePerTiB = PDPFees.FEE_PER_TIB;
proposedProofFeePerTiB = 0;
feeUpdateEffectiveTime = 0;
emit ContractUpgraded(VERSION, ERC1967Utils.getImplementation());
}

Expand All @@ -174,6 +184,37 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {
require(success, "Burn failed");
}

/// @notice Get the currently active proof fee per TiB
/// @return The active proof fee per TiB in AttoFIL
function getActiveProofFeePerTiB() public view returns (uint256) {
// If there's a proposed fee and it's now effective, return it
if (proposedProofFeePerTiB > 0 && block.timestamp >= feeUpdateEffectiveTime) {
return proposedProofFeePerTiB;
}
// Otherwise return the current fee (or default if not set)
return proofFeePerTiB > 0 ? proofFeePerTiB : PDPFees.FEE_PER_TIB;
}

/// @notice Propose a new proof fee per TiB (owner only)
/// @param newFeePerTiB The new fee per TiB in AttoFIL
/// @dev The new fee becomes effective 7 days after proposal
function updateProofFee(uint256 newFeePerTiB) external onlyOwner {
require(newFeePerTiB > 0, "Fee must be greater than 0");

uint256 currentFee = getActiveProofFeePerTiB();

// Apply any pending fee update before proposing a new one
if (proposedProofFeePerTiB > 0 && block.timestamp >= feeUpdateEffectiveTime) {
proofFeePerTiB = proposedProofFeePerTiB;
}

// Set the new proposed fee with 7 days delay (7 * 24 * 60 * 60 = 604800 seconds)
proposedProofFeePerTiB = newFeePerTiB;
feeUpdateEffectiveTime = block.timestamp + 604800;

emit FeeUpdateProposed(currentFee, newFeePerTiB, feeUpdateEffectiveTime);
}

// Returns the current challenge finality value
function getChallengeFinality() public view returns (uint256) {
return challengeFinality;
Expand Down Expand Up @@ -599,23 +640,18 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable {

function calculateProofFee(uint256 setId, uint256 estimatedGasFee) public view returns (uint256) {
uint256 rawSize = 32 * challengeRange[setId];
(uint64 filUsdPrice, int32 filUsdPriceExpo) = getFILUSDPrice();
Copy link
Contributor

Choose a reason for hiding this comment

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

can we now remove the other usd oracling code or is it used elsewhere?

Copy link

Choose a reason for hiding this comment

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

afaik, it is not used for anything else

Copy link
Author

Choose a reason for hiding this comment

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

The oracle code (getFILUSDPrice, PYTH constants) is kept for backward compatibility since it's still called in tests and may be used by external contracts. It's no longer used for proof fee calculation but remains available as a public function.

uint256 feePerTiB = getActiveProofFeePerTiB();

return PDPFees.proofFeeWithGasFeeBound(
estimatedGasFee, filUsdPrice, filUsdPriceExpo, rawSize, block.number - dataSetLastProvenEpoch[setId]
);
return PDPFees.flatProofFee(rawSize, feePerTiB);
}

function calculateAndBurnProofFee(uint256 setId, uint256 gasUsed) internal returns (uint256 refund) {
uint256 estimatedGasFee = gasUsed * block.basefee;
uint256 rawSize = 32 * challengeRange[setId];
(uint64 filUsdPrice, int32 filUsdPriceExpo) = getFILUSDPrice();
uint256 feePerTiB = getActiveProofFeePerTiB();

uint256 proofFee = PDPFees.proofFeeWithGasFeeBound(
estimatedGasFee, filUsdPrice, filUsdPriceExpo, rawSize, block.number - dataSetLastProvenEpoch[setId]
);
uint256 proofFee = PDPFees.flatProofFee(rawSize, feePerTiB);
burnFee(proofFee);
emit ProofFeePaid(setId, proofFee, filUsdPrice, filUsdPriceExpo);
emit ProofFeePaid(setId, proofFee);

return msg.value - proofFee; // burnFee asserts that proofFee <= msg.value;
}
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/IPDPEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ interface IPDPEvents {
event DataSetEmpty(uint256 indexed setId);
event PiecesAdded(uint256 indexed setId, uint256[] pieceIds, Cids.Cid[] pieceCids);
event PiecesRemoved(uint256 indexed setId, uint256[] pieceIds);
event ProofFeePaid(uint256 indexed setId, uint256 fee, uint64 price, int32 expo);
event ProofFeePaid(uint256 indexed setId, uint256 fee);
event FeeUpdateProposed(uint256 currentFee, uint256 newFee, uint256 effectiveTime);
event PossessionProven(uint256 indexed setId, IPDPTypes.PieceIdAndOffset[] challenges);
event NextProvingPeriod(uint256 indexed setId, uint256 challengeEpoch, uint256 leafCount);
event ContractUpgraded(string version, address newImplementation);
Expand Down
69 changes: 69 additions & 0 deletions test/Fees.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,73 @@ contract PDPFeesTest is Test {
uint256 expectedFee = 1; // Should be gasLimitRight - estimatedGasFee = 1
assertEq(fee, expectedFee, "Fee should be 1 when estimatedGasFee is just below right boundary");
}

// Tests for new flat fee calculation
function testFlatProofFeeOneTiB() public pure {
uint256 rawSize = PDPFees.TIB_IN_BYTES; // 1 TiB
uint256 feePerTiB = PDPFees.FEE_PER_TIB;

uint256 fee = PDPFees.flatProofFee(rawSize, feePerTiB);

assertEq(fee, PDPFees.FEE_PER_TIB, "Fee for 1 TiB should equal FEE_PER_TIB");
}

function testFlatProofFeeTwoTiB() public pure {
uint256 rawSize = 2 * PDPFees.TIB_IN_BYTES; // 2 TiB
uint256 feePerTiB = PDPFees.FEE_PER_TIB;

uint256 fee = PDPFees.flatProofFee(rawSize, feePerTiB);

assertEq(fee, 2 * PDPFees.FEE_PER_TIB, "Fee for 2 TiB should be twice FEE_PER_TIB");
}

function testFlatProofFeeHalfTiB() public pure {
uint256 rawSize = PDPFees.TIB_IN_BYTES / 2; // 0.5 TiB
uint256 feePerTiB = PDPFees.FEE_PER_TIB;

uint256 fee = PDPFees.flatProofFee(rawSize, feePerTiB);

assertEq(fee, PDPFees.FEE_PER_TIB / 2, "Fee for 0.5 TiB should be half of FEE_PER_TIB");
}

function testFlatProofFeeSmallSize() public pure {
uint256 rawSize = 32 * 1000; // 1000 leaves (32 bytes each)
uint256 feePerTiB = PDPFees.FEE_PER_TIB;

uint256 fee = PDPFees.flatProofFee(rawSize, feePerTiB);
uint256 expectedFee = (rawSize * feePerTiB) / PDPFees.TIB_IN_BYTES;

assertEq(fee, expectedFee, "Fee should be proportional to size");
assertTrue(fee > 0, "Fee should be positive even for small datasets");
}

function testFlatProofFeeLargeSize() public pure {
uint256 rawSize = 100 * PDPFees.TIB_IN_BYTES; // 100 TiB
uint256 feePerTiB = PDPFees.FEE_PER_TIB;

uint256 fee = PDPFees.flatProofFee(rawSize, feePerTiB);

assertEq(fee, 100 * PDPFees.FEE_PER_TIB, "Fee for 100 TiB should be 100x FEE_PER_TIB");
}

function testFlatProofFeeCustomRate() public pure {
uint256 rawSize = PDPFees.TIB_IN_BYTES; // 1 TiB
uint256 customFee = 500000000000000000; // 0.0005 FIL per TiB

uint256 fee = PDPFees.flatProofFee(rawSize, customFee);

assertEq(fee, customFee, "Fee should use custom rate");
}

/// forge-config: default.allow_internal_expect_revert = true
function testFlatProofFeeZeroSize() public {
vm.expectRevert("failed to validate: raw size must be greater than 0");
PDPFees.flatProofFee(0, PDPFees.FEE_PER_TIB);
}

/// forge-config: default.allow_internal_expect_revert = true
function testFlatProofFeeZeroRate() public {
vm.expectRevert("failed to validate: fee per TiB must be greater than 0");
PDPFees.flatProofFee(PDPFees.TIB_IN_BYTES, 0);
}
}
130 changes: 130 additions & 0 deletions test/PDPVerifier.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1969,3 +1969,133 @@ contract PDPVerifierStorageProviderListenerTest is Test {
pdpVerifier.claimDataSetStorageProvider(setId, empty);
}
}

contract PDPVerifierFeeUpdateTest is Test {
PDPVerifier pdpVerifier;
address owner;
address notOwner;

function setUp() public {
PDPVerifier pdpVerifierImpl = new PDPVerifier();
uint256 challengeFinality = 2;
bytes memory initializeData = abi.encodeWithSelector(PDPVerifier.initialize.selector, challengeFinality);
MyERC1967Proxy proxy = new MyERC1967Proxy(address(pdpVerifierImpl), initializeData);
pdpVerifier = PDPVerifier(address(proxy));

// Call migrate to initialize fee variables
pdpVerifier.migrate();

owner = address(this);
notOwner = address(0x1234);
}

function testInitialFeeIsDefault() public view {
uint256 activeFee = pdpVerifier.getActiveProofFeePerTiB();
assertEq(activeFee, PDPFees.FEE_PER_TIB, "Initial fee should equal default FEE_PER_TIB");
}

function testUpdateProofFeeAsOwner() public {
uint256 newFee = 500000000000000000; // 0.0005 FIL per TiB
uint256 currentTime = block.timestamp;

vm.expectEmit(true, true, true, true);
emit IPDPEvents.FeeUpdateProposed(PDPFees.FEE_PER_TIB, newFee, currentTime + 604800);

pdpVerifier.updateProofFee(newFee);

// Fee should not be effective yet
assertEq(pdpVerifier.getActiveProofFeePerTiB(), PDPFees.FEE_PER_TIB, "Fee should not change immediately");

// Proposed fee should be set
assertEq(pdpVerifier.proposedProofFeePerTiB(), newFee, "Proposed fee should be set");
assertEq(pdpVerifier.feeUpdateEffectiveTime(), currentTime + 604800, "Effective time should be set");
}

function testUpdateProofFeeNotOwner() public {
uint256 newFee = 500000000000000000;

vm.prank(notOwner);
vm.expectRevert();
pdpVerifier.updateProofFee(newFee);
}

function testFeeBecomesEffectiveAfter7Days() public {
uint256 newFee = 500000000000000000;
uint256 currentTime = block.timestamp;

pdpVerifier.updateProofFee(newFee);

// Still not effective
assertEq(pdpVerifier.getActiveProofFeePerTiB(), PDPFees.FEE_PER_TIB);

// Advance time by 7 days
vm.warp(currentTime + 604800);

// Now it should be effective
assertEq(pdpVerifier.getActiveProofFeePerTiB(), newFee, "Fee should be effective after 7 days");
}

function testFeeNotEffectiveBefore7Days() public {
uint256 newFee = 500000000000000000;
uint256 currentTime = block.timestamp;

pdpVerifier.updateProofFee(newFee);

// Advance time by 6 days
vm.warp(currentTime + 604800 - 1);

// Should still be old fee
assertEq(pdpVerifier.getActiveProofFeePerTiB(), PDPFees.FEE_PER_TIB, "Fee should not be effective before 7 days");
}

function testUpdateProofFeeZeroReverts() public {
vm.expectRevert("Fee must be greater than 0");
pdpVerifier.updateProofFee(0);
}

function testMultipleFeeUpdates() public {
uint256 newFee1 = 500000000000000000;
uint256 newFee2 = 700000000000000000;
uint256 currentTime = block.timestamp;

// First update
pdpVerifier.updateProofFee(newFee1);

// Advance time by 7 days to make first update effective
vm.warp(currentTime + 604800);
assertEq(pdpVerifier.getActiveProofFeePerTiB(), newFee1);

// Second update (should apply the pending first update and set new one)
pdpVerifier.updateProofFee(newFee2);

// Should still see newFee1 as active
assertEq(pdpVerifier.getActiveProofFeePerTiB(), newFee1);

// Advance another 7 days
vm.warp(currentTime + 604800 + 604800);

// Now newFee2 should be active
assertEq(pdpVerifier.getActiveProofFeePerTiB(), newFee2);
}

function testProposeFeeUpdateBeforePreviousBecameEffective() public {
uint256 newFee1 = 500000000000000000;
uint256 newFee2 = 700000000000000000;
uint256 currentTime = block.timestamp;

// First update
pdpVerifier.updateProofFee(newFee1);

// Advance time by 3 days (not enough for first to be effective)
vm.warp(currentTime + 3 days);

// Second update (should keep first as proposed until it's effective)
pdpVerifier.updateProofFee(newFee2);

// Should still see default fee
assertEq(pdpVerifier.getActiveProofFeePerTiB(), PDPFees.FEE_PER_TIB);

// The new proposed fee should be newFee2
assertEq(pdpVerifier.proposedProofFeePerTiB(), newFee2);
}
}
Loading