Skip to content
Open
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
40 changes: 26 additions & 14 deletions service_contracts/src/FilecoinWarmStorageService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.20;

import {PDPListener} from "@pdp/PDPVerifier.sol";
import {IPDPVerifier} from "@pdp/interfaces/IPDPVerifier.sol";
import {Cids} from "@pdp/Cids.sol";
import {SessionKeyRegistry} from "@session-key-registry/SessionKeyRegistry.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
Expand Down Expand Up @@ -663,7 +664,7 @@ contract FilecoinWarmStorageService is

// Validate payer has sufficient funds and operator approvals for minimum pricing
// If CDN is enabled, validation must account for the additional fixed lockup amounts
validatePayerOperatorApprovalAndFunds(payments, createData.payer, hasCDN);
validatePayerOperatorApprovalAndFunds(payments, createData.payer, hasCDN, 0, new Cids.Cid[](0));

uint256 pdpRailId = payments.createRail(
usdfcTokenAddress, // token address
Expand Down Expand Up @@ -834,6 +835,13 @@ contract FilecoinWarmStorageService is
// Verify the signature
verifyAddPiecesSignature(payer, info.clientDataSetId, pieceData, nonce, metadataKeys, metadataValues, signature);

// Validate payer/operator approvals and available funds for the new pieces
// This checks the payer has sufficient available funds and operator allowances
// to cover the increased per-epoch rate and the 30-day lockup implied by the
// new total leaf count after adding these pieces.
FilecoinPayV1 payments = FilecoinPayV1(paymentsContractAddress);
validatePayerOperatorApprovalAndFunds(payments, payer, false, dataSetId, pieceData);

// Store metadata for each new piece
for (uint256 i = 0; i < pieceData.length; i++) {
uint256 pieceId = firstAdded + i;
Expand Down Expand Up @@ -1177,14 +1185,22 @@ contract FilecoinWarmStorageService is
/// @notice Validates that the payer has sufficient funds and operator approvals for minimum pricing
/// @param payments The FilecoinPayV1 contract instance
/// @param payer The address of the payer
function validatePayerOperatorApprovalAndFunds(FilecoinPayV1 payments, address payer, bool includeCDN)
internal
view
{
// Calculate required lockup for minimum pricing
uint256 minimumLockupRequired = (minimumStorageRatePerMonth * DEFAULT_LOCKUP_PERIOD) / EPOCHS_PER_MONTH;
function validatePayerOperatorApprovalAndFunds(
FilecoinPayV1 payments,
address payer,
bool includeCDN,
uint256 dataSetId,
Cids.Cid[] memory pieceData
) internal view {
uint256 totalBytes = 0;
if (dataSetId != 0 && pieceData.length != 0) {
totalBytes = IPDPVerifier(pdpVerifierAddress).getDataSetLeafCount(dataSetId) * BYTES_PER_LEAF;
}

// If CDN is enabled, include the fixed cache-miss and CDN lockup amounts
// Calculate the minimum storage rate per epoch based on total bytes
uint256 minimumStorageRatePerEpoch = _calculateStorageRate(totalBytes);
// Calculate the minimum lockup required for the payer
uint256 minimumLockupRequired = minimumStorageRatePerEpoch * DEFAULT_LOCKUP_PERIOD;
if (includeCDN) {
minimumLockupRequired += DEFAULT_CACHE_MISS_LOCKUP_AMOUNT + DEFAULT_CDN_LOCKUP_AMOUNT;
}
Expand All @@ -1208,14 +1224,10 @@ contract FilecoinWarmStorageService is

// Verify operator is approved
require(isApproved, Errors.OperatorNotApproved(payer, address(this)));

// Calculate minimum rate per epoch
uint256 minimumRatePerEpoch = minimumStorageRatePerMonth / EPOCHS_PER_MONTH;

// Verify rate allowance is sufficient
require(
rateAllowance >= rateUsage + minimumRatePerEpoch,
Errors.InsufficientRateAllowance(payer, address(this), rateAllowance, rateUsage, minimumRatePerEpoch)
rateAllowance >= rateUsage + minimumStorageRatePerEpoch,
Errors.InsufficientRateAllowance(payer, address(this), rateAllowance, rateUsage, minimumStorageRatePerEpoch)
);

// Verify lockup allowance is sufficient (include CDN extras when applicable)
Expand Down
214 changes: 203 additions & 11 deletions service_contracts/test/FilecoinWarmStorageService.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -803,9 +803,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {

// First batch (3 pieces) with key "meta" => metadataShort
Cids.Cid[] memory pieceData1 = new Cids.Cid[](3);
pieceData1[0].data = bytes("1_0:1111");
pieceData1[1].data = bytes("1_1:111100000");
pieceData1[2].data = bytes("1_2:11110000000000");
pieceData1[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("1_0:1111")));
pieceData1[1] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("1_1:111100000")));
pieceData1[2] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("1_2:11110000000000")));
string[] memory keys1 = new string[](1);
string[] memory values1 = new string[](1);
keys1[0] = "meta";
Expand All @@ -817,8 +817,10 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {

// Second batch (2 pieces) with key "meta" => metadataLong
Cids.Cid[] memory pieceData2 = new Cids.Cid[](2);
pieceData2[0].data = bytes("2_0:22222222222222222222");
pieceData2[1].data = bytes("2_1:222222222222222222220000000000000000000000000000000000000000");
pieceData2[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("2_0:22222222222222222222")));
pieceData2[1] = Cids.CommPv2FromDigest(
0, 4, keccak256(abi.encodePacked("2_1:222222222222222222220000000000000000000000000000000000000000000"))
);
string[] memory keys2 = new string[](1);
string[] memory values2 = new string[](1);
keys2[0] = "meta";
Expand Down Expand Up @@ -1066,7 +1068,8 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
);

// Expected minimum: (0.06 USDFC * 86400) / 86400 = 0.06 USDFC = 6e16
uint256 minimumRequired = 6e16;
uint256 minimumRequiredPerEpoch = (uint256(6e16) / (2880 * 30));
uint256 minimumRequired = minimumRequiredPerEpoch * (2880 * 30);

// Expect revert with InsufficientLockupFunds error
makeSignaturePass(insufficientClient);
Expand Down Expand Up @@ -1161,6 +1164,194 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
assertEq(dataSetId, 1, "Dataset should be created with above-minimum funds");
}

function testAddPiecesFundingValidation() public {
uint256 minimumAmount = 6e16; // 0.06 USDFC

// Test 1: Insufficient funds with small piece (below minimum rate)
address client1 = makeAddr("client1");
mockUSDFC.safeTransfer(client1, minimumAmount);

vm.startPrank(client1);
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
mockUSDFC.approve(address(payments), minimumAmount);
payments.deposit(mockUSDFC, client1, minimumAmount);
vm.stopPrank();

(string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Test");
FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({
payer: client1,
clientDataSetId: 1001,
metadataKeys: dsKeys,
metadataValues: dsValues,
signature: FAKE_SIGNATURE
});

bytes memory encodedCreateData = abi.encode(
createData.payer,
createData.clientDataSetId,
createData.metadataKeys,
createData.metadataValues,
createData.signature
);

makeSignaturePass(client1);
vm.prank(serviceProvider);
uint256 dataSetId1 = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData);

vm.prank(client1);
payments.withdraw(mockUSDFC, 59999999999961600); // Withdraw almost all funds

Cids.Cid[] memory pieceData1 = new Cids.Cid[](1);
pieceData1[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("piece1")));
string[] memory emptyKeys = new string[](0);
string[] memory emptyValues = new string[](0);

makeSignaturePass(client1);
uint256 leafCount1 = 0;
for (uint256 i = 0; i < pieceData1.length; i++) {
(uint256 padding, uint8 height,) = Cids.validateCommPv2(pieceData1[i]);
leafCount1 += Cids.leafCount(padding, height);
}
uint256 lockupRequired1 = pdpServiceWithPayments.calculateRatePerEpoch(leafCount1 * 32) * 2880 * 30;
(uint256 availableFunds1,) = getAccountInfo(mockUSDFC, client1);

vm.expectRevert(
abi.encodeWithSelector(Errors.InsufficientLockupFunds.selector, client1, lockupRequired1, availableFunds1)
);
vm.prank(serviceProvider);
mockPDPVerifier.addPieces(
pdpServiceWithPayments, dataSetId1, 0, pieceData1, 0, FAKE_SIGNATURE, emptyKeys, emptyValues
);

// Test 2: Sufficient funds with small piece (below minimum rate)
address client2 = makeAddr("client2");
mockUSDFC.safeTransfer(client2, minimumAmount);

vm.startPrank(client2);
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
mockUSDFC.approve(address(payments), minimumAmount);
payments.deposit(mockUSDFC, client2, minimumAmount);
vm.stopPrank();

createData.payer = client2;
createData.clientDataSetId = 1002;
encodedCreateData = abi.encode(
createData.payer,
createData.clientDataSetId,
createData.metadataKeys,
createData.metadataValues,
createData.signature
);

makeSignaturePass(client2);
vm.prank(serviceProvider);
uint256 dataSetId2 = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData);

Cids.Cid[] memory pieceData2 = new Cids.Cid[](1);
pieceData2[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("piece2")));

vm.prank(serviceProvider);
mockPDPVerifier.addPieces(
pdpServiceWithPayments, dataSetId2, 0, pieceData2, 0, FAKE_SIGNATURE, emptyKeys, emptyValues
);

// Test 3: Insufficient funds with large pieces (above minimum rate)
address client3 = makeAddr("client3");
mockUSDFC.safeTransfer(client3, minimumAmount);

vm.startPrank(client3);
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
mockUSDFC.approve(address(payments), minimumAmount);
payments.deposit(mockUSDFC, client3, minimumAmount);
vm.stopPrank();

createData.payer = client3;
createData.clientDataSetId = 1003;
encodedCreateData = abi.encode(
createData.payer,
createData.clientDataSetId,
createData.metadataKeys,
createData.metadataValues,
createData.signature
);

makeSignaturePass(client3);
vm.prank(serviceProvider);
uint256 dataSetId3 = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData);

// Construct 5 large pieces
uint256 totalPieces = 5;
Cids.Cid[] memory pieceData3 = new Cids.Cid[](totalPieces);
string[] memory keys3 = new string[](MAX_KEYS_PER_PIECE);
string[] memory values3 = new string[](MAX_KEYS_PER_PIECE);

for (uint256 p = 0; p < totalPieces; p++) {
pieceData3[p] = Cids.CommPv2FromDigest(0, 28, keccak256(abi.encodePacked("file", Strings.toString(p))));
}
for (uint256 k = 0; k < MAX_KEYS_PER_PIECE; k++) {
keys3[k] = _generateKey(k);
values3[k] = _makeStringOfLength(128);
}

makeSignaturePass(client3);

uint256 leafCount3 = 0;
for (uint256 i = 0; i < pieceData3.length; i++) {
(uint256 padding, uint8 height,) = Cids.validateCommPv2(pieceData3[i]);
leafCount3 += Cids.leafCount(padding, height);
}

uint256 lockupRequired3 = pdpServiceWithPayments.calculateRatePerEpoch(leafCount3 * 32) * 2880 * 30;
(uint256 availableFunds3,) = getAccountInfo(mockUSDFC, client3);

vm.expectRevert(
abi.encodeWithSelector(Errors.InsufficientLockupFunds.selector, client3, lockupRequired3, availableFunds3)
);
vm.prank(serviceProvider);
mockPDPVerifier.addPieces(pdpServiceWithPayments, dataSetId3, 0, pieceData3, 0, FAKE_SIGNATURE, keys3, values3);

// Test 4: Sufficient funds with large pieces (above minimum rate)
address client4 = makeAddr("client4");
uint256 sufficientAmount = 10e16; // 0.10 USDFC
mockUSDFC.safeTransfer(client4, sufficientAmount);

vm.startPrank(client4);
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
mockUSDFC.approve(address(payments), sufficientAmount);
payments.deposit(mockUSDFC, client4, sufficientAmount);
vm.stopPrank();

createData.payer = client4;
createData.clientDataSetId = 1004;
encodedCreateData = abi.encode(
createData.payer,
createData.clientDataSetId,
createData.metadataKeys,
createData.metadataValues,
createData.signature
);

makeSignaturePass(client4);
vm.prank(serviceProvider);
uint256 dataSetId4 = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData);

Cids.Cid[] memory pieceData4 = new Cids.Cid[](totalPieces);
string[] memory keys4 = new string[](MAX_KEYS_PER_PIECE);
string[] memory values4 = new string[](MAX_KEYS_PER_PIECE);

for (uint256 p = 0; p < totalPieces; p++) {
pieceData4[p] = Cids.CommPv2FromDigest(0, 28, keccak256(abi.encodePacked("file", Strings.toString(p))));
}
for (uint256 k = 0; k < MAX_KEYS_PER_PIECE; k++) {
keys4[k] = _generateKey(k);
values4[k] = _makeStringOfLength(128);
}

makeSignaturePass(client4);
vm.prank(serviceProvider);
mockPDPVerifier.addPieces(pdpServiceWithPayments, dataSetId4, 0, pieceData4, 0, FAKE_SIGNATURE, keys4, values4);
}

// Operator Approval Validation Tests
function testOperatorApproval_NotApproved() public {
// Setup: Client with sufficient funds but no operator approval
Expand Down Expand Up @@ -1277,7 +1468,8 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
// DEFAULT_LOCKUP_PERIOD = 86400
// EPOCHS_PER_MONTH = 86400
// minimumLockupRequired = (6e16 * 86400) / 86400 = 6e16
uint256 minimumLockupRequired = 6e16;
uint256 minimumLockupRatePerEpoch = (uint256(6e16) / (2880 * 30));
uint256 minimumLockupRequired = minimumLockupRatePerEpoch * (2880 * 30);
uint256 insufficientLockupAllowance = minimumLockupRequired - 1; // Just below minimum

// Transfer tokens and set up approvals
Expand Down Expand Up @@ -4558,7 +4750,7 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {

// Prepare piece data
Cids.Cid[] memory pieceData = new Cids.Cid[](1);
pieceData[0].data = bytes("test_piece_1");
pieceData[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("test_piece_1")));
string[] memory keys = new string[](0);
string[] memory values = new string[](0);

Expand Down Expand Up @@ -4604,7 +4796,7 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {

// Prepare piece data
Cids.Cid[] memory pieceData = new Cids.Cid[](1);
pieceData[0].data = bytes("test_piece");
pieceData[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("test_piece_1")));
string[] memory keys = new string[](0);
string[] memory values = new string[](0);

Expand Down Expand Up @@ -4678,7 +4870,7 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {

// Prepare piece data
Cids.Cid[] memory pieceData = new Cids.Cid[](1);
pieceData[0].data = bytes("test");
pieceData[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("test_piece_1")));
string[] memory keys = new string[](0);
string[] memory values = new string[](0);

Expand Down Expand Up @@ -4725,7 +4917,7 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {

// Prepare piece data
Cids.Cid[] memory pieceData = new Cids.Cid[](1);
pieceData[0].data = bytes("test");
pieceData[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("test_piece_1")));
string[] memory keys = new string[](0);
string[] memory values = new string[](0);

Expand Down
Loading
Loading