@@ -75,6 +75,7 @@ abstract contract DealsRegistry is
7575 * @param cancelHash The hash of the cancellation option used for the deal
7676 * @param transferable Indicates whether the deal NFT is transferable or not
7777 * @param checkIn The check-in time for the deal (in seconds since the Unix epoch)
78+ * @param checkOut The check-out time for the deal (in seconds since the Unix epoch)
7879 */
7980 struct Offer {
8081 bytes32 id;
@@ -87,6 +88,7 @@ abstract contract DealsRegistry is
8788 bytes32 cancelHash;
8889 bool transferable;
8990 uint256 checkIn;
91+ uint256 checkOut;
9092 }
9193
9294 /**
@@ -169,6 +171,9 @@ abstract contract DealsRegistry is
169171 /// @dev Thrown when a user attempts to claim the deal in non-created status
170172 error NotAllowedStatus ();
171173
174+ /// @dev Thrown when a user attempts to do something that not allowed at a moment
175+ error NotAllowedTime ();
176+
172177 /// @dev Thrown when a user attempts to cancel the deal using invalid cancellation options
173178 error InvalidCancelOptions ();
174179
@@ -193,6 +198,7 @@ abstract contract DealsRegistry is
193198 allowedStatuses["claim " ] = [DealStatus.Created];
194199 allowedStatuses["checkIn " ] = [DealStatus.Claimed];
195200 allowedStatuses["checkOut " ] = [DealStatus.CheckedIn];
201+ allowedStatuses["dispute " ] = [DealStatus.CheckedIn, DealStatus.CheckedOut];
196202 }
197203
198204 /// Modifiers
@@ -385,6 +391,34 @@ abstract contract DealsRegistry is
385391 */
386392 function _afterReject (bytes32 offerId , bytes32 reason ) internal virtual {}
387393
394+ /**
395+ * @dev Hook function that runs before the deal is canceled.
396+ * Allows inheriting smart contracts to perform custom logic.
397+ * @param offerId The offerId of the deal
398+ */
399+ function _beforeCancel (bytes32 offerId ) internal virtual whenNotPaused {}
400+
401+ /**
402+ * @dev Hook function that runs after the deal is canceled.
403+ * Allows inheriting smart contracts to perform custom logic.
404+ * @param offerId The offerId of the deal
405+ */
406+ function _afterCancel (bytes32 offerId ) internal virtual {}
407+
408+ /**
409+ * @dev Hook function that runs before the deal is refunded.
410+ * Allows inheriting smart contracts to perform custom logic.
411+ * @param offerId The offerId of the deal
412+ */
413+ function _beforeRefund (bytes32 offerId ) internal virtual whenNotPaused {}
414+
415+ /**
416+ * @dev Hook function that runs after the deal is refunded.
417+ * Allows inheriting smart contracts to perform custom logic.
418+ * @param offerId The offerId of the deal
419+ */
420+ function _afterRefund (bytes32 offerId ) internal virtual {}
421+
388422 /**
389423 * @dev Hook function that runs before the deal is claimed.
390424 * Allows inheriting smart contracts to perform custom logic.
@@ -427,18 +461,18 @@ abstract contract DealsRegistry is
427461 ) internal virtual {}
428462
429463 /**
430- * @dev Hook function that runs before the deal is canceled .
464+ * @dev Hook function that runs before the deal is checked out .
431465 * Allows inheriting smart contracts to perform custom logic.
432466 * @param offerId The offerId of the deal
433467 */
434- function _beforeCancel (bytes32 offerId ) internal virtual whenNotPaused {}
468+ function _beforeCheckOut (bytes32 offerId ) internal virtual whenNotPaused {}
435469
436470 /**
437- * @dev Hook function that runs after the deal is canceled .
471+ * @dev Hook function that runs after the deal is checked out .
438472 * Allows inheriting smart contracts to perform custom logic.
439473 * @param offerId The offerId of the deal
440474 */
441- function _afterCancel (bytes32 offerId ) internal virtual {}
475+ function _afterCheckOut (bytes32 offerId ) internal virtual {}
442476
443477 /// Features
444478
@@ -470,6 +504,7 @@ abstract contract DealsRegistry is
470504 address buyer = _msgSender ();
471505
472506 /// @dev variable scoping used to avoid stack too deep errors
507+ /// The `supplier` storage variable is required is the frame of this scope only
473508 {
474509 bytes32 offerHash = _hashTypedDataV4 (hash (offer));
475510 Supplier storage supplier = suppliers[offer.supplierId];
@@ -563,15 +598,16 @@ abstract contract DealsRegistry is
563598 )
564599 external
565600 dealExists (offerId)
566- inStatuses (offerId, allowedStatuses["reject " ])
567601 onlySigner (offerId)
602+ inStatuses (offerId, allowedStatuses["reject " ])
568603 {
569604 Deal storage storedDeal = deals[offerId];
570605
571- _beforeReject (offerId, reason);
572-
606+ // Moving to the Rejected status before all to avoid reentrancy
573607 storedDeal.status = DealStatus.Rejected;
574608
609+ _beforeReject (offerId, reason);
610+
575611 if (
576612 ! IERC20 (storedDeal.asset).transfer (storedDeal.buyer, storedDeal.price)
577613 ) {
@@ -583,6 +619,42 @@ abstract contract DealsRegistry is
583619 _afterReject (offerId, reason);
584620 }
585621
622+ /**
623+ * @dev Refunds the deal
624+ * @param offerId The deal offer Id
625+ *
626+ * Requirements:
627+ *
628+ * - the deal must exists
629+ * - the deal must be in status DealStatus.CheckedIn
630+ * - must be called by the signer address of the deal offer supplier
631+ */
632+ function refund (
633+ bytes32 offerId
634+ )
635+ external
636+ dealExists (offerId)
637+ onlySigner (offerId)
638+ inStatuses (offerId, allowedStatuses["refund " ])
639+ {
640+ Deal storage storedDeal = deals[offerId];
641+
642+ // Moving to the Refunded status before all to avoid reentrancy
643+ storedDeal.status = DealStatus.Refunded;
644+
645+ _beforeRefund (offerId);
646+
647+ if (
648+ ! IERC20 (storedDeal.asset).transfer (storedDeal.buyer, storedDeal.price)
649+ ) {
650+ revert DealFundsTransferFailed ();
651+ }
652+
653+ emit Status (offerId, DealStatus.Refunded, _msgSender ());
654+
655+ _afterRefund (offerId);
656+ }
657+
586658 /**
587659 * @dev Cancels the deal
588660 * @param offerId The deal offer Id
@@ -612,22 +684,28 @@ abstract contract DealsRegistry is
612684 revert NotAllowedAuth ();
613685 }
614686
615- _beforeCancel ((offerId));
687+ DealStatus callStatus = storedDeal.status;
688+
689+ // Moving to the Cancelled status before all to avoid reentrancy
690+ storedDeal.status = DealStatus.Cancelled;
616691
617- if (storedDeal.status == DealStatus.Created) {
692+ _beforeCancel (offerId);
693+
694+ if (callStatus == DealStatus.Created) {
618695 // Full refund
619696 if (
620697 ! IERC20 (storedDeal.asset).transfer (storedDeal.buyer, storedDeal.price)
621698 ) {
622699 revert DealFundsTransferFailed ();
623700 }
624701 } else if (
625- storedDeal.status == DealStatus.Claimed &&
702+ callStatus == DealStatus.Claimed &&
626703 block .timestamp < storedDeal.offer.checkIn
627704 ) {
628705 if (storedDeal.offer.cancelHash != hash (_cancelOptions)) {
629706 revert InvalidCancelOptions ();
630707 }
708+
631709 // Using offer cancellation rules
632710 uint256 selectedTime;
633711 uint256 selectedPenalty;
@@ -675,7 +753,6 @@ abstract contract DealsRegistry is
675753 revert NotAllowedStatus ();
676754 }
677755
678- storedDeal.status = DealStatus.Cancelled;
679756 emit Status (offerId, DealStatus.Cancelled, sender);
680757
681758 _afterCancel (offerId);
@@ -696,8 +773,8 @@ abstract contract DealsRegistry is
696773 )
697774 external
698775 dealExists (offerId)
699- inStatuses (offerId, allowedStatuses["claim " ])
700776 onlySigner (offerId)
777+ inStatuses (offerId, allowedStatuses["claim " ])
701778 {
702779 Deal storage storedDeal = deals[offerId];
703780
@@ -794,13 +871,51 @@ abstract contract DealsRegistry is
794871 _afterCheckIn (offerId, signs);
795872 }
796873
874+ /**
875+ * @dev Checks out the deal and sends funds to the supplier
876+ * @param offerId The deal offer Id
877+ *
878+ * Requirements:
879+ *
880+ * - the deal must exists
881+ * - must be called by the supplier's signer only
882+ * - the deal must be in status DealStatus.CheckIn
883+ * - must be called after checkOut time only
884+ */
797885 function checkOut (
798886 bytes32 offerId
799887 )
800888 external
801889 dealExists (offerId)
802- inStatuses (offerId, allowedStatuses["checkIn " ])
803- {}
890+ onlySigner (offerId)
891+ inStatuses (offerId, allowedStatuses["checkOut " ])
892+ {
893+ Deal storage storedDeal = deals[offerId];
894+
895+ if (block .timestamp < storedDeal.offer.checkOut) {
896+ revert NotAllowedTime ();
897+ }
898+
899+ // Moving to CheckedOut status before all to avoid reentrancy
900+ storedDeal.status = DealStatus.CheckedOut;
901+
902+ // Execute before checkOut hook
903+ _beforeCheckOut (offerId);
904+
905+ if (
906+ ! IERC20 (storedDeal.asset).transfer (
907+ suppliers[storedDeal.offer.supplierId].owner,
908+ storedDeal.price
909+ )
910+ ) {
911+ revert DealFundsTransferFailed ();
912+ }
913+
914+ emit Status (offerId, DealStatus.CheckedOut, _msgSender ());
915+
916+ // Execute after checkOut hook
917+ _afterCheckOut (offerId);
918+ }
804919
805920 uint256 [50 ] private __gap;
806921}
0 commit comments