@@ -52,6 +52,7 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
5252 error PastApprovalDeadline ();
5353 error NotOwner ();
5454 error NotCreditorOrDebtor ();
55+ error NotCreditor ();
5556 error NotController (address sender );
5657 error ClaimBound ();
5758 error ClaimNotPending ();
@@ -62,6 +63,8 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
6263 error ZeroAmount ();
6364 error PaymentUnderApproved ();
6465 error OverPaying (uint256 paymentAmount );
66+ error StillInGracePeriod ();
67+ error NoDueBy ();
6568
6669 function _notLocked () internal view {
6770 if (lockState == LockState.Locked) revert Locked ();
@@ -93,6 +96,8 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
9396
9497 event ClaimRescinded (uint256 indexed claimId , address indexed from , string note );
9598
99+ event ClaimImpaired (uint256 indexed claimId );
100+
96101 event CreateClaimApproved (
97102 address indexed user ,
98103 address indexed operator ,
@@ -113,6 +118,8 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
113118
114119 event CancelClaimApproved (address indexed user , address indexed operator , uint256 approvalCount );
115120
121+ event ImpairClaimApproved (address indexed user , address indexed operator , uint256 approvalCount );
122+
116123 constructor (address _extensionRegistry , LockState _lockState )
117124 ERC721 ("BullaClaim " , "CLAIM " )
118125 EIP712 ("BullaClaim " , "1 " )
@@ -242,6 +249,10 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
242249 if (params.dueBy != 0 && (params.dueBy < block .timestamp || params.dueBy > type (uint40 ).max)) {
243250 revert InvalidDueBy ();
244251 }
252+ // validate impairment grace period
253+ if (params.impairmentGracePeriod > type (uint40 ).max) {
254+ revert InvalidDueBy (); // reuse this error for consistency since it's about time validation
255+ }
245256
246257 // you need the permission of the debtor to bind a claim
247258 if (params.binding == ClaimBinding.Bound && from != params.debtor) revert CannotBindClaim ();
@@ -266,6 +277,7 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
266277 if (params.binding != ClaimBinding.Unbound) claim.binding = params.binding;
267278 if (params.payerReceivesClaimOnPayment) claim.payerReceivesClaimOnPayment = true ;
268279 if (params.dueBy != 0 ) claim.dueBy = uint40 (params.dueBy);
280+ if (params.impairmentGracePeriod != 0 ) claim.impairmentGracePeriod = uint40 (params.impairmentGracePeriod);
269281 }
270282
271283 emit ClaimCreated (
@@ -316,10 +328,10 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
316328 _spendPayClaimApproval (from, msg .sender , claimId, amount);
317329
318330 Claim memory claim = getClaim (claimId);
319-
331+
320332 // Only the controller can call this function
321333 if (claim.controller != msg .sender ) revert NotController (msg .sender );
322-
334+
323335 _updateClaimPaymentState (from, claimId, amount);
324336 }
325337
@@ -414,7 +426,7 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
414426
415427 // Update payment state first to follow checks-effects-interactions pattern
416428 _updateClaimPaymentState (from, claimId, paymentAmount);
417-
429+
418430 // Process token transfer after state is updated
419431 claim.token == address (0 )
420432 ? creditor.safeTransferETH (paymentAmount)
@@ -434,7 +446,9 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
434446 if (paymentAmount == 0 ) revert PayingZero ();
435447
436448 // make sure the claim can be paid (not completed, not rejected, not rescinded)
437- if (claim.status != Status.Pending && claim.status != Status.Repaying) revert ClaimNotPending ();
449+ if (claim.status != Status.Pending && claim.status != Status.Repaying && claim.status != Status.Impaired) {
450+ revert ClaimNotPending ();
451+ }
438452
439453 uint256 totalPaidAmount = claim.paidAmount + paymentAmount;
440454 bool claimPaid = totalPaidAmount == claim.claimAmount;
@@ -504,7 +518,9 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
504518 // check if the claim is controlled
505519 if (claim.controller != address (0 ) && msg .sender != claim.controller) revert NotController (msg .sender );
506520 // make sure the claim is in pending status
507- if (claim.status != Status.Pending && claim.status != Status.Repaying) revert ClaimNotPending ();
521+ if (claim.status != Status.Pending && claim.status != Status.Repaying && claim.status != Status.Impaired) {
522+ revert ClaimNotPending ();
523+ }
508524 // make sure the sender is authorized
509525 if (from != creditor && from != claim.debtor) revert NotCreditorOrDebtor ();
510526 // make sure the binding is valid
@@ -553,6 +569,21 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
553569 return ;
554570 }
555571
572+ /// @notice "spends" an operator's impairClaim approval
573+ /// @notice SPEC:
574+ /// A function can call this function to verify and "spend" `from`'s approval of `operator` to impair a claim given:
575+ /// S1. `operator` has > 0 approvalCount from `from` address -> otherwise: reverts
576+ ///
577+ /// RES1: If the above is true, and the approvalCount != type(uint64).max, decrement the approval count by 1 and return
578+ function _spendImpairClaimApproval (address user , address operator ) internal {
579+ ImpairClaimApproval storage approval = approvals[user][operator].impairClaim;
580+
581+ if (approval.approvalCount == 0 ) revert NotApproved ();
582+ if (approval.approvalCount != type (uint64 ).max) approval.approvalCount-- ;
583+
584+ return ;
585+ }
586+
556587 /// @notice allows a creditor to rescind a claim or a debtor to reject a claim
557588 /// @notice SPEC:
558589 /// this function will rescind or reject a claim given:
@@ -581,6 +612,56 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
581612 }
582613 }
583614
615+ /**
616+ * /// IMPAIR CLAIM ///
617+ */
618+
619+ /// @notice allows a creditor to impair a claim
620+ /// @notice SPEC:
621+ /// 1. call impairClaim on behalf of the msg.sender
622+ function impairClaim (uint256 claimId ) external {
623+ _impairClaim (msg .sender , claimId);
624+ }
625+
626+ /// @notice allows an operator to impair a claim on behalf of a creditor
627+ /// @notice SPEC:
628+ /// 1. verify and spend msg.sender's approval to impair claim
629+ /// 2. impair the claim on `from`'s behalf
630+ function impairClaimFrom (address from , uint256 claimId ) external {
631+ _spendImpairClaimApproval (from, msg .sender );
632+
633+ _impairClaim (from, claimId);
634+ }
635+
636+ /// @notice allows a creditor to impair a claim
637+ /// @notice SPEC:
638+ /// this function will impair a claim given:
639+ /// 1. The contract is not locked
640+ /// 2. The claim exists and is not burned
641+ /// 3. The caller is the creditor (owner of the claim NFT)
642+ /// 4. The claim is in pending or repaying status
643+ /// 5. If claim has a dueBy date, the grace period must have passed
644+ /// 6. If claim has no dueBy date (dueBy = 0), it cannot be impaired
645+ function _impairClaim (address from , uint256 claimId ) internal {
646+ _notLocked ();
647+ // load the claim from storage
648+ Claim memory claim = getClaim (claimId);
649+ address creditor = _ownerOf[claimId];
650+
651+ if (claim.controller != address (0 ) && msg .sender != claim.controller) revert NotController (msg .sender );
652+ // make sure the claim can be impaired (pending or repaying)
653+ if (claim.status != Status.Pending && claim.status != Status.Repaying) revert ClaimNotPending ();
654+ // only the creditor can impair a claim
655+ if (from != creditor) revert NotCreditor ();
656+
657+ // Grace period validation
658+ if (claim.dueBy == 0 ) revert NoDueBy ();
659+ if (block .timestamp < claim.dueBy + claim.impairmentGracePeriod) revert StillInGracePeriod ();
660+
661+ claims[claimId].status = Status.Impaired;
662+ emit ClaimImpaired (claimId);
663+ }
664+
584665 /*///////////////////////////////////////////////////////////////
585666 PERMIT FUNCTIONS
586667 //////////////////////////////////////////////////////////////*/
@@ -649,6 +730,14 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
649730 );
650731 }
651732
733+ /// @notice permits an operator to impair claims on user's behalf
734+ /// @dev see BullaClaimPermitLib.sol for spec
735+ function permitImpairClaim (address user , address operator , uint64 approvalCount , bytes calldata signature ) public {
736+ BullaClaimPermitLib.permitImpairClaim (
737+ approvals[user][operator], extensionRegistry, _domainSeparatorV4 (), user, operator, approvalCount, signature
738+ );
739+ }
740+
652741 /*///////////////////////////////////////////////////////////////
653742 VIEW / UTILITY FUNCTIONS
654743 //////////////////////////////////////////////////////////////*/
@@ -667,7 +756,8 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
667756 token: claimStorage.token,
668757 controller: claimStorage.controller,
669758 originalCreditor: claimStorage.originalCreditor,
670- dueBy: claimStorage.dueBy
759+ dueBy: claimStorage.dueBy,
760+ impairmentGracePeriod: claimStorage.impairmentGracePeriod
671761 });
672762 }
673763
0 commit comments