Skip to content

Commit 0b31223

Browse files
committed
feat: check payer has minimum available balance before creating data set
1 parent ee1449d commit 0b31223

File tree

3 files changed

+158
-12
lines changed

3 files changed

+158
-12
lines changed

service_contracts/src/Errors.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,10 @@ library Errors {
286286
/// @param dataSetId The data set ID
287287
/// @param pdpEndEpoch The end epoch when the PDP payment rail will finalize
288288
error PaymentRailsNotFinalized(uint256 dataSetId, uint256 pdpEndEpoch);
289+
290+
/// @notice Payer has insufficient available funds to cover the minimum storage rate
291+
/// @param payer The payer address
292+
/// @param minimumRequired The minimum lockup required to cover the minimum storage rate
293+
/// @param available The available funds in the payer's account
294+
error InsufficientFundsForMinimumRate(address payer, uint256 minimumRequired, uint256 available);
289295
}

service_contracts/src/FilecoinWarmStorageService.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,17 @@ contract FilecoinWarmStorageService is
586586

587587
// Create the payment rails using the FilecoinPayV1 contract
588588
FilecoinPayV1 payments = FilecoinPayV1(paymentsContractAddress);
589+
590+
// Pre-check that payer has sufficient available funds to cover minimum storage rate
591+
// This provides early feedback but doesn't guarantee funds will be available when SP calls nextProvingPeriod
592+
(,, uint256 availableFunds,) = payments.getAccountInfoIfSettled(usdfcTokenAddress, createData.payer);
593+
// Calculate required lockup: multiply first to preserve precision
594+
uint256 minimumLockupRequired = (MINIMUM_STORAGE_RATE_PER_MONTH * DEFAULT_LOCKUP_PERIOD) / EPOCHS_PER_MONTH;
595+
require(
596+
availableFunds >= minimumLockupRequired,
597+
Errors.InsufficientFundsForMinimumRate(createData.payer, minimumLockupRequired, availableFunds)
598+
);
599+
589600
uint256 pdpRailId = payments.createRail(
590601
usdfcTokenAddress, // token address
591602
createData.payer, // from (payer)

service_contracts/test/FilecoinWarmStorageService.t.sol

Lines changed: 141 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,135 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
10561056
assertApproxEqAbs(cdnRateLarge, cdnRateSmall * 12, 100, "CDN rate should scale proportionally");
10571057
}
10581058

1059+
// Minimum Funds Validation Tests
1060+
function testInsufficientFunds_BelowMinimum() public {
1061+
// Setup: Client with insufficient funds (below 0.06 USDFC minimum)
1062+
address insufficientClient = makeAddr("insufficientClient");
1063+
uint256 insufficientAmount = 5e16; // 0.05 USDFC (below 0.06 minimum)
1064+
1065+
// Transfer tokens from test contract to the test client
1066+
mockUSDFC.safeTransfer(insufficientClient, insufficientAmount);
1067+
1068+
vm.startPrank(insufficientClient);
1069+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
1070+
mockUSDFC.approve(address(payments), insufficientAmount);
1071+
payments.deposit(mockUSDFC, insufficientClient, insufficientAmount);
1072+
vm.stopPrank();
1073+
1074+
// Prepare dataset creation data
1075+
(string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Insufficient Test");
1076+
FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({
1077+
payer: insufficientClient,
1078+
clientDataSetId: 999,
1079+
metadataKeys: dsKeys,
1080+
metadataValues: dsValues,
1081+
signature: FAKE_SIGNATURE
1082+
});
1083+
1084+
bytes memory encodedCreateData = abi.encode(
1085+
createData.payer,
1086+
createData.clientDataSetId,
1087+
createData.metadataKeys,
1088+
createData.metadataValues,
1089+
createData.signature
1090+
);
1091+
1092+
// Expected minimum: (0.06 USDFC * 86400) / 86400 = 0.06 USDFC = 6e16
1093+
uint256 minimumRequired = 6e16;
1094+
1095+
// Expect revert with InsufficientFundsForMinimumRate error
1096+
makeSignaturePass(insufficientClient);
1097+
vm.expectRevert(
1098+
abi.encodeWithSelector(
1099+
Errors.InsufficientFundsForMinimumRate.selector, insufficientClient, minimumRequired, insufficientAmount
1100+
)
1101+
);
1102+
vm.prank(serviceProvider);
1103+
mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData);
1104+
}
1105+
1106+
function testInsufficientFunds_ExactMinimum() public {
1107+
// Setup: Client with exactly the minimum funds (0.06 USDFC)
1108+
address exactClient = makeAddr("exactClient");
1109+
uint256 exactAmount = 6e16; // Exactly 0.06 USDFC
1110+
1111+
// Transfer tokens from test contract to the test client
1112+
mockUSDFC.safeTransfer(exactClient, exactAmount);
1113+
1114+
vm.startPrank(exactClient);
1115+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
1116+
mockUSDFC.approve(address(payments), exactAmount);
1117+
payments.deposit(mockUSDFC, exactClient, exactAmount);
1118+
vm.stopPrank();
1119+
1120+
// Prepare dataset creation data
1121+
(string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Exact Minimum Test");
1122+
FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({
1123+
payer: exactClient,
1124+
clientDataSetId: 1000,
1125+
metadataKeys: dsKeys,
1126+
metadataValues: dsValues,
1127+
signature: FAKE_SIGNATURE
1128+
});
1129+
1130+
bytes memory encodedCreateData = abi.encode(
1131+
createData.payer,
1132+
createData.clientDataSetId,
1133+
createData.metadataKeys,
1134+
createData.metadataValues,
1135+
createData.signature
1136+
);
1137+
1138+
// Should succeed with exact minimum
1139+
makeSignaturePass(exactClient);
1140+
vm.prank(serviceProvider);
1141+
uint256 dataSetId = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData);
1142+
1143+
// Verify dataset was created
1144+
assertEq(dataSetId, 1, "Dataset should be created with exact minimum funds");
1145+
}
1146+
1147+
function testInsufficientFunds_JustAboveMinimum() public {
1148+
// Setup: Client with slightly more than minimum (0.07 USDFC)
1149+
address aboveMinClient = makeAddr("aboveMinClient");
1150+
uint256 aboveMinAmount = 7e16; // 0.07 USDFC (just above 0.06 minimum)
1151+
1152+
// Transfer tokens from test contract to the test client
1153+
mockUSDFC.safeTransfer(aboveMinClient, aboveMinAmount);
1154+
1155+
vm.startPrank(aboveMinClient);
1156+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
1157+
mockUSDFC.approve(address(payments), aboveMinAmount);
1158+
payments.deposit(mockUSDFC, aboveMinClient, aboveMinAmount);
1159+
vm.stopPrank();
1160+
1161+
// Prepare dataset creation data
1162+
(string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Above Minimum Test");
1163+
FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({
1164+
payer: aboveMinClient,
1165+
clientDataSetId: 1001,
1166+
metadataKeys: dsKeys,
1167+
metadataValues: dsValues,
1168+
signature: FAKE_SIGNATURE
1169+
});
1170+
1171+
bytes memory encodedCreateData = abi.encode(
1172+
createData.payer,
1173+
createData.clientDataSetId,
1174+
createData.metadataKeys,
1175+
createData.metadataValues,
1176+
createData.signature
1177+
);
1178+
1179+
// Should succeed with funds above minimum
1180+
makeSignaturePass(aboveMinClient);
1181+
vm.prank(serviceProvider);
1182+
uint256 dataSetId = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData);
1183+
1184+
// Verify dataset was created
1185+
assertEq(dataSetId, 1, "Dataset should be created with above-minimum funds");
1186+
}
1187+
10591188
uint256 nextClientDataSetId = 0;
10601189

10611190
// Client-Data Set Tracking Tests
@@ -4018,9 +4147,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
40184147

40194148
// Setup approvals and deposit
40204149
vm.startPrank(client);
4021-
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days);
4022-
mockUSDFC.approve(address(payments), 10e6);
4023-
payments.deposit(mockUSDFC, client, 10e6);
4150+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
4151+
mockUSDFC.approve(address(payments), 10e18);
4152+
payments.deposit(mockUSDFC, client, 10e18);
40244153
vm.stopPrank();
40254154

40264155
// Create dataset
@@ -4064,9 +4193,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
40644193

40654194
// Setup approvals and deposit
40664195
vm.startPrank(client);
4067-
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days);
4068-
mockUSDFC.approve(address(payments), 10e6);
4069-
payments.deposit(mockUSDFC, client, 10e6);
4196+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
4197+
mockUSDFC.approve(address(payments), 10e18);
4198+
payments.deposit(mockUSDFC, client, 10e18);
40704199
vm.stopPrank();
40714200

40724201
// Create dataset
@@ -4098,9 +4227,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
40984227
function testAddPiecesNonceUniquePerPayer() public {
40994228
// Setup approvals and deposit
41004229
vm.startPrank(client);
4101-
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days);
4102-
mockUSDFC.approve(address(payments), 20e6);
4103-
payments.deposit(mockUSDFC, client, 20e6);
4230+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
4231+
mockUSDFC.approve(address(payments), 20e18);
4232+
payments.deposit(mockUSDFC, client, 20e18);
41044233
vm.stopPrank();
41054234

41064235
(string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Dataset 1");
@@ -4167,9 +4296,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest {
41674296
function testNonceCannotBeReusedAcrossOperations() public {
41684297
// Setup: Approvals and deposit
41694298
vm.startPrank(client);
4170-
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e6, 1000e6, 365 days);
4171-
mockUSDFC.approve(address(payments), 10e6);
4172-
payments.deposit(mockUSDFC, client, 10e6);
4299+
payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days);
4300+
mockUSDFC.approve(address(payments), 10e18);
4301+
payments.deposit(mockUSDFC, client, 10e18);
41734302
vm.stopPrank();
41744303

41754304
// Use nonce 777 to create a dataset

0 commit comments

Comments
 (0)