Skip to content

Commit a0bef72

Browse files
authored
feat: SPRegistry minDealDurationDays maxDealDurationDays (#40)
* feat(SPRegistry): add deal duration limits - interface, storage, setter, mock * fix(SPRegistry): add upper-bound validation and missing revert test for duration limits - Add MAX_DEAL_DURATION_DAYS constant (mirrors PoRepMarket, prevents silent misconfiguration) - Add DurationExceedsProtocolMax error - Validate min/max duration against protocol max in setDealDurationLimits and registerProviderFor - Add testSetDealDurationLimitsRevertsUnregistered (W2 coverage gap) - Add boundary acceptance/rejection tests for protocol max - Regenerate ABI * fix(SPRegistry): move MAX_DEAL_DURATION_DAYS to PoRepTypes and fix guard order - add MAX_DEAL_DURATION_DAYS constant to PoRepTypes library (single source of truth) - PoRepMarket and SPRegistry re-export from PoRepTypes (ABIs unchanged) - reorder duration validation: DurationExceedsProtocolMax before MinDurationExceedsMax - add guard-order tests confirming correct error precedence on combined-invalid inputs * refactor(SPRegistry): extract duration validation into _ensureDurationLimitsValid
1 parent ff01adc commit a0bef72

File tree

7 files changed

+940
-115
lines changed

7 files changed

+940
-115
lines changed

abis/SPRegistry.json

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@
3030
],
3131
"stateMutability": "view"
3232
},
33+
{
34+
"type": "function",
35+
"name": "MAX_DEAL_DURATION_DAYS",
36+
"inputs": [],
37+
"outputs": [
38+
{
39+
"name": "",
40+
"type": "uint32",
41+
"internalType": "uint32"
42+
}
43+
],
44+
"stateMutability": "view"
45+
},
3346
{
3447
"type": "function",
3548
"name": "MAX_PROVIDERS",
@@ -313,6 +326,16 @@
313326
"name": "pricePerSectorPerMonth",
314327
"type": "uint256",
315328
"internalType": "uint256"
329+
},
330+
{
331+
"name": "minDealDurationDays",
332+
"type": "uint32",
333+
"internalType": "uint32"
334+
},
335+
{
336+
"name": "maxDealDurationDays",
337+
"type": "uint32",
338+
"internalType": "uint32"
316339
}
317340
]
318341
}
@@ -575,6 +598,16 @@
575598
"name": "payee",
576599
"type": "address",
577600
"internalType": "address"
601+
},
602+
{
603+
"name": "minDealDurationDays",
604+
"type": "uint32",
605+
"internalType": "uint32"
606+
},
607+
{
608+
"name": "maxDealDurationDays",
609+
"type": "uint32",
610+
"internalType": "uint32"
578611
}
579612
],
580613
"outputs": [],
@@ -692,6 +725,29 @@
692725
"outputs": [],
693726
"stateMutability": "nonpayable"
694727
},
728+
{
729+
"type": "function",
730+
"name": "setDealDurationLimits",
731+
"inputs": [
732+
{
733+
"name": "provider",
734+
"type": "uint64",
735+
"internalType": "CommonTypes.FilActorId"
736+
},
737+
{
738+
"name": "minDealDurationDays",
739+
"type": "uint32",
740+
"internalType": "uint32"
741+
},
742+
{
743+
"name": "maxDealDurationDays",
744+
"type": "uint32",
745+
"internalType": "uint32"
746+
}
747+
],
748+
"outputs": [],
749+
"stateMutability": "nonpayable"
750+
},
695751
{
696752
"type": "function",
697753
"name": "setPayee",
@@ -920,6 +976,31 @@
920976
],
921977
"anonymous": false
922978
},
979+
{
980+
"type": "event",
981+
"name": "DealDurationLimitsUpdated",
982+
"inputs": [
983+
{
984+
"name": "provider",
985+
"type": "uint64",
986+
"indexed": true,
987+
"internalType": "CommonTypes.FilActorId"
988+
},
989+
{
990+
"name": "minDealDurationDays",
991+
"type": "uint32",
992+
"indexed": false,
993+
"internalType": "uint32"
994+
},
995+
{
996+
"name": "maxDealDurationDays",
997+
"type": "uint32",
998+
"indexed": false,
999+
"internalType": "uint32"
1000+
}
1001+
],
1002+
"anonymous": false
1003+
},
9231004
{
9241005
"type": "event",
9251006
"name": "Initialized",
@@ -1317,6 +1398,22 @@
13171398
}
13181399
]
13191400
},
1401+
{
1402+
"type": "error",
1403+
"name": "DurationExceedsProtocolMax",
1404+
"inputs": [
1405+
{
1406+
"name": "durationDays",
1407+
"type": "uint32",
1408+
"internalType": "uint32"
1409+
},
1410+
{
1411+
"name": "maxDays",
1412+
"type": "uint32",
1413+
"internalType": "uint32"
1414+
}
1415+
]
1416+
},
13201417
{
13211418
"type": "error",
13221419
"name": "ERC1967InvalidImplementation",
@@ -1438,6 +1535,22 @@
14381535
}
14391536
]
14401537
},
1538+
{
1539+
"type": "error",
1540+
"name": "MinDurationExceedsMax",
1541+
"inputs": [
1542+
{
1543+
"name": "minDays",
1544+
"type": "uint32",
1545+
"internalType": "uint32"
1546+
},
1547+
{
1548+
"name": "maxDays",
1549+
"type": "uint32",
1550+
"internalType": "uint32"
1551+
}
1552+
]
1553+
},
14411554
{
14421555
"type": "error",
14431556
"name": "NotAdminOrOperator",

src/PoRepMarket.sol

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,10 @@ contract PoRepMarket is IPoRepMarket, Initializable, AccessControlUpgradeable, U
2727
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
2828

2929
/**
30-
* @notice Maximum Filecoin storage deal duration: 1278 days (~3.5 years),
31-
* per FIP-0052 (NV21 actor policy update).
32-
* References:
33-
* https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0052.md
34-
* https://github.com/filecoin-project/core-devs/blob/master/Network%20Upgrades/v21.md
30+
* @notice Maximum deal duration in days. See PoRepTypes.MAX_DEAL_DURATION_DAYS.
31+
* @dev Any provider limit above this is unreachable: PoRepMarket rejects deals with durationDays > 1278.
3532
*/
36-
uint32 public constant MAX_DEAL_DURATION_DAYS = 1278;
33+
uint32 public constant MAX_DEAL_DURATION_DAYS = PoRepTypes.MAX_DEAL_DURATION_DAYS;
3734

3835
/// @custom:storage-location erc7201:porepmarket.storage.DealProposalsStorage
3936
struct DealProposalsStorage {

src/SPRegistry.sol

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet
1010
import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol";
1111
import {ISPRegistry} from "./interfaces/ISPRegistry.sol";
1212
import {SLITypes} from "./types/SLITypes.sol";
13+
import {PoRepTypes} from "./types/PoRepTypes.sol";
1314
import {MinerUtils} from "./lib/MinerUtils.sol";
1415

1516
/**
@@ -45,6 +46,13 @@ contract SPRegistry is Initializable, AccessControlUpgradeable, UUPSUpgradeable,
4546
*/
4647
uint256 public constant MAX_PROVIDERS = 500;
4748

49+
/**
50+
* @notice Maximum deal duration in days, sourced from PoRepTypes.MAX_DEAL_DURATION_DAYS.
51+
* @dev Any provider limit above this is unreachable: PoRepMarket rejects deals with durationDays > 1278.
52+
*/
53+
uint32 public constant MAX_DEAL_DURATION_DAYS = PoRepTypes.MAX_DEAL_DURATION_DAYS;
54+
55+
// solhint-disable-next-line gas-struct-packing
4856
struct ProviderData {
4957
address organization;
5058
address payee;
@@ -55,6 +63,8 @@ contract SPRegistry is Initializable, AccessControlUpgradeable, UUPSUpgradeable,
5563
uint256 committedBytes;
5664
uint256 pendingBytes;
5765
uint256 pricePerSectorPerMonth;
66+
uint32 minDealDurationDays;
67+
uint32 maxDealDurationDays;
5868
}
5969

6070
/// @custom:storage-location erc7201:porepmarket.storage.SPRegistryStorage
@@ -185,6 +195,17 @@ contract SPRegistry is Initializable, AccessControlUpgradeable, UUPSUpgradeable,
185195
*/
186196
event PayeeUpdated(CommonTypes.FilActorId indexed provider, address indexed oldPayee, address indexed newPayee);
187197

198+
/**
199+
* @notice DealDurationLimitsUpdated event
200+
* @dev DealDurationLimitsUpdated event is emitted when a provider's deal duration limits change
201+
* @param provider The provider actor ID
202+
* @param minDealDurationDays The minimum deal duration in days (0 = no minimum)
203+
* @param maxDealDurationDays The maximum deal duration in days (0 = no maximum)
204+
*/
205+
event DealDurationLimitsUpdated(
206+
CommonTypes.FilActorId indexed provider, uint32 minDealDurationDays, uint32 maxDealDurationDays
207+
);
208+
188209
/**
189210
* @notice Error indicating that a provider is already registered
190211
* @dev 0xf91794e7
@@ -300,6 +321,8 @@ contract SPRegistry is Initializable, AccessControlUpgradeable, UUPSUpgradeable,
300321
error AvailableBelowCommittedPlusPending(
301322
CommonTypes.FilActorId provider, uint256 availableBytes, uint256 committedBytes, uint256 pendingBytes
302323
);
324+
error MinDurationExceedsMax(uint32 minDays, uint32 maxDays);
325+
error DurationExceedsProtocolMax(uint32 durationDays, uint32 maxDays);
303326

304327
/**
305328
* @notice Constructor
@@ -459,7 +482,9 @@ contract SPRegistry is Initializable, AccessControlUpgradeable, UUPSUpgradeable,
459482
availableBytes: p.availableBytes,
460483
committedBytes: p.committedBytes,
461484
pendingBytes: p.pendingBytes,
462-
pricePerSectorPerMonth: p.pricePerSectorPerMonth
485+
pricePerSectorPerMonth: p.pricePerSectorPerMonth,
486+
minDealDurationDays: p.minDealDurationDays,
487+
maxDealDurationDays: p.maxDealDurationDays
463488
});
464489
}
465490

@@ -511,6 +536,9 @@ contract SPRegistry is Initializable, AccessControlUpgradeable, UUPSUpgradeable,
511536

512537
if (!_meetsRequirements(p.capabilities, requirements)) continue;
513538

539+
if (p.minDealDurationDays != 0 && terms.durationDays < p.minDealDurationDays) continue;
540+
if (p.maxDealDurationDays != 0 && terms.durationDays > p.maxDealDurationDays) continue;
541+
514542
if (p.pendingBytes < lowestPending) {
515543
lowestPending = p.pendingBytes;
516544
bestProvider = CommonTypes.FilActorId.wrap(id);
@@ -619,19 +647,24 @@ contract SPRegistry is Initializable, AccessControlUpgradeable, UUPSUpgradeable,
619647
* @param availableBytes The provider's available storage capacity
620648
* @param pricePerSectorPerMonth The provider's auto-approve price per sector per month (0 to skip)
621649
* @param payee The payment recipient address (address(0) defaults to organization)
650+
* @param minDealDurationDays Minimum deal duration in days (0 = no minimum)
651+
* @param maxDealDurationDays Maximum deal duration in days (0 = no maximum)
622652
*/
623653
function registerProviderFor(
624654
CommonTypes.FilActorId provider,
625655
address organization,
626656
SLITypes.SLIThresholds calldata capabilities,
627657
uint256 availableBytes,
628658
uint256 pricePerSectorPerMonth,
629-
address payee
659+
address payee,
660+
uint32 minDealDurationDays,
661+
uint32 maxDealDurationDays
630662
) external {
631663
_onlyAdminOrOperator();
632664
if (organization == address(0)) revert InvalidOrganizationAddress();
633665
if (capabilities.retrievabilityBps > 10_000) revert InvalidRetrievabilityBps(capabilities.retrievabilityBps);
634666
if (capabilities.indexingPct > 100) revert InvalidIndexingPct(capabilities.indexingPct);
667+
_ensureDurationLimitsValid(minDealDurationDays, maxDealDurationDays);
635668

636669
_registerProvider(provider, organization, payee);
637670

@@ -640,10 +673,13 @@ contract SPRegistry is Initializable, AccessControlUpgradeable, UUPSUpgradeable,
640673
$._providers[id].capabilities = capabilities;
641674
$._providers[id].availableBytes = availableBytes;
642675
$._providers[id].pricePerSectorPerMonth = pricePerSectorPerMonth;
676+
$._providers[id].minDealDurationDays = minDealDurationDays;
677+
$._providers[id].maxDealDurationDays = maxDealDurationDays;
643678

644679
emit CapabilitiesUpdated(provider, capabilities);
645680
emit AvailableSpaceUpdated(provider, availableBytes);
646681
emit PriceUpdated(provider, 0, pricePerSectorPerMonth);
682+
emit DealDurationLimitsUpdated(provider, minDealDurationDays, maxDealDurationDays);
647683
}
648684

649685
/// @inheritdoc ISPRegistry
@@ -667,6 +703,25 @@ contract SPRegistry is Initializable, AccessControlUpgradeable, UUPSUpgradeable,
667703
emit PayeeUpdated(provider, oldPayee, payee);
668704
}
669705

706+
/// @inheritdoc ISPRegistry
707+
function setDealDurationLimits(
708+
CommonTypes.FilActorId provider,
709+
uint32 minDealDurationDays,
710+
uint32 maxDealDurationDays
711+
) external {
712+
_ensureProviderRegistered(provider);
713+
_ensureProviderNotBlocked(provider);
714+
_onlyProviderControllerOrAdmin(provider);
715+
_ensureDurationLimitsValid(minDealDurationDays, maxDealDurationDays);
716+
717+
SPRegistryStorage storage $ = _getSPRegistryStorage();
718+
uint64 id = CommonTypes.FilActorId.unwrap(provider);
719+
$._providers[id].minDealDurationDays = minDealDurationDays;
720+
$._providers[id].maxDealDurationDays = maxDealDurationDays;
721+
722+
emit DealDurationLimitsUpdated(provider, minDealDurationDays, maxDealDurationDays);
723+
}
724+
670725
/// @inheritdoc ISPRegistry
671726
function getPayee(CommonTypes.FilActorId provider) external view returns (address) {
672727
SPRegistryStorage storage $ = _getSPRegistryStorage();
@@ -729,6 +784,23 @@ contract SPRegistry is Initializable, AccessControlUpgradeable, UUPSUpgradeable,
729784
}
730785
}
731786

787+
/**
788+
* @notice Ensures deal duration limits are within the protocol maximum and internally consistent
789+
* @param minDealDurationDays The minimum deal duration to validate
790+
* @param maxDealDurationDays The maximum deal duration to validate
791+
*/
792+
function _ensureDurationLimitsValid(uint32 minDealDurationDays, uint32 maxDealDurationDays) internal pure {
793+
if (minDealDurationDays > MAX_DEAL_DURATION_DAYS) {
794+
revert DurationExceedsProtocolMax(minDealDurationDays, MAX_DEAL_DURATION_DAYS);
795+
}
796+
if (maxDealDurationDays != 0 && maxDealDurationDays > MAX_DEAL_DURATION_DAYS) {
797+
revert DurationExceedsProtocolMax(maxDealDurationDays, MAX_DEAL_DURATION_DAYS);
798+
}
799+
if (minDealDurationDays != 0 && maxDealDurationDays != 0 && minDealDurationDays > maxDealDurationDays) {
800+
revert MinDurationExceedsMax(minDealDurationDays, maxDealDurationDays);
801+
}
802+
}
803+
732804
/**
733805
* @notice Ensures a provider is not blocked
734806
* @param provider The provider actor ID to check

src/interfaces/ISPRegistry.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {SLITypes} from "../types/SLITypes.sol";
99
* @notice Interface for storage provider registration, matching, and capacity management
1010
*/
1111
interface ISPRegistry {
12+
// solhint-disable-next-line gas-struct-packing
1213
struct ProviderInfo {
1314
address organization;
1415
address payee;
@@ -20,6 +21,8 @@ interface ISPRegistry {
2021
uint256 pendingBytes;
2122
/// @notice Monthly ERC20 token price per 32 GiB sector in smallest units (0 = manual approval)
2223
uint256 pricePerSectorPerMonth;
24+
uint32 minDealDurationDays;
25+
uint32 maxDealDurationDays;
2326
}
2427

2528
/**
@@ -157,6 +160,18 @@ interface ISPRegistry {
157160
*/
158161
function setPayee(CommonTypes.FilActorId provider, address payee) external;
159162

163+
/**
164+
* @notice Set the acceptable deal duration range for a provider
165+
* @param provider The provider to update
166+
* @param minDealDurationDays Minimum deal duration in days (0 = no minimum)
167+
* @param maxDealDurationDays Maximum deal duration in days (0 = no maximum)
168+
*/
169+
function setDealDurationLimits(
170+
CommonTypes.FilActorId provider,
171+
uint32 minDealDurationDays,
172+
uint32 maxDealDurationDays
173+
) external;
174+
160175
/**
161176
* @notice Get the payment recipient address for a provider
162177
* @param provider The provider actor ID

src/types/PoRepTypes.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ import {SLITypes} from "./SLITypes.sol";
1010
* @notice Shared types for PoRepMarket deal proposals and states
1111
*/
1212
library PoRepTypes {
13+
/**
14+
* @notice Maximum Filecoin storage deal duration: 1278 days (~3.5 years),
15+
* per FIP-0052 (NV21 actor policy update).
16+
* References:
17+
* https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0052.md
18+
* https://github.com/filecoin-project/core-devs/blob/master/Network%20Upgrades/v21.md
19+
*/
20+
uint32 internal constant MAX_DEAL_DURATION_DAYS = 1278;
21+
1322
/**
1423
* @notice DealState enum
1524
* @dev Represents the various states a deal can be

0 commit comments

Comments
 (0)