@@ -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 /**
@@ -96,8 +98,9 @@ abstract contract DealsRegistry is
9698 Created, // Just created
9799 Claimed, // Claimed by the supplier
98100 Rejected, // Rejected by the supplier
101+ Refunded, // Refunded by the supplier
99102 Cancelled, // Cancelled by the buyer
100- CheckedId , // Checked In
103+ CheckedIn , // Checked In
101104 CheckedOut, // Checked Out
102105 Disputed // Dispute started
103106 }
@@ -168,6 +171,9 @@ abstract contract DealsRegistry is
168171 /// @dev Thrown when a user attempts to claim the deal in non-created status
169172 error NotAllowedStatus ();
170173
174+ /// @dev Thrown when a user attempts to do something that not allowed at a moment
175+ error NotAllowedTime ();
176+
171177 /// @dev Thrown when a user attempts to cancel the deal using invalid cancellation options
172178 error InvalidCancelOptions ();
173179
@@ -188,8 +194,11 @@ abstract contract DealsRegistry is
188194
189195 allowedStatuses["reject " ] = [DealStatus.Created];
190196 allowedStatuses["cancel " ] = [DealStatus.Created, DealStatus.Claimed];
197+ allowedStatuses["refund " ] = [DealStatus.Claimed, DealStatus.CheckedIn];
191198 allowedStatuses["claim " ] = [DealStatus.Created];
192199 allowedStatuses["checkIn " ] = [DealStatus.Claimed];
200+ allowedStatuses["checkOut " ] = [DealStatus.CheckedIn];
201+ allowedStatuses["dispute " ] = [DealStatus.CheckedIn, DealStatus.CheckedOut];
193202 }
194203
195204 /// Modifiers
@@ -382,6 +391,34 @@ abstract contract DealsRegistry is
382391 */
383392 function _afterReject (bytes32 offerId , bytes32 reason ) internal virtual {}
384393
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+
385422 /**
386423 * @dev Hook function that runs before the deal is claimed.
387424 * Allows inheriting smart contracts to perform custom logic.
@@ -424,18 +461,18 @@ abstract contract DealsRegistry is
424461 ) internal virtual {}
425462
426463 /**
427- * @dev Hook function that runs before the deal is canceled .
464+ * @dev Hook function that runs before the deal is checked out .
428465 * Allows inheriting smart contracts to perform custom logic.
429466 * @param offerId The offerId of the deal
430467 */
431- function _beforeCancel (bytes32 offerId ) internal virtual whenNotPaused {}
468+ function _beforeCheckOut (bytes32 offerId ) internal virtual whenNotPaused {}
432469
433470 /**
434- * @dev Hook function that runs after the deal is canceled .
471+ * @dev Hook function that runs after the deal is checked out .
435472 * Allows inheriting smart contracts to perform custom logic.
436473 * @param offerId The offerId of the deal
437474 */
438- function _afterCancel (bytes32 offerId ) internal virtual {}
475+ function _afterCheckOut (bytes32 offerId ) internal virtual {}
439476
440477 /// Features
441478
@@ -467,6 +504,7 @@ abstract contract DealsRegistry is
467504 address buyer = _msgSender ();
468505
469506 /// @dev variable scoping used to avoid stack too deep errors
507+ /// The `supplier` storage variable is required is the frame of this scope only
470508 {
471509 bytes32 offerHash = _hashTypedDataV4 (hash (offer));
472510 Supplier storage supplier = suppliers[offer.supplierId];
@@ -560,15 +598,16 @@ abstract contract DealsRegistry is
560598 )
561599 external
562600 dealExists (offerId)
563- inStatuses (offerId, allowedStatuses["reject " ])
564601 onlySigner (offerId)
602+ inStatuses (offerId, allowedStatuses["reject " ])
565603 {
566604 Deal storage storedDeal = deals[offerId];
567605
568- _beforeReject (offerId, reason);
569-
606+ // Moving to the Rejected status before all to avoid reentrancy
570607 storedDeal.status = DealStatus.Rejected;
571608
609+ _beforeReject (offerId, reason);
610+
572611 if (
573612 ! IERC20 (storedDeal.asset).transfer (storedDeal.buyer, storedDeal.price)
574613 ) {
@@ -580,6 +619,42 @@ abstract contract DealsRegistry is
580619 _afterReject (offerId, reason);
581620 }
582621
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+
583658 /**
584659 * @dev Cancels the deal
585660 * @param offerId The deal offer Id
@@ -609,22 +684,28 @@ abstract contract DealsRegistry is
609684 revert NotAllowedAuth ();
610685 }
611686
612- _beforeCancel ((offerId));
687+ DealStatus callStatus = storedDeal.status;
688+
689+ // Moving to the Cancelled status before all to avoid reentrancy
690+ storedDeal.status = DealStatus.Cancelled;
691+
692+ _beforeCancel (offerId);
613693
614- if (storedDeal.status == DealStatus.Created) {
694+ if (callStatus == DealStatus.Created) {
615695 // Full refund
616696 if (
617697 ! IERC20 (storedDeal.asset).transfer (storedDeal.buyer, storedDeal.price)
618698 ) {
619699 revert DealFundsTransferFailed ();
620700 }
621701 } else if (
622- storedDeal.status == DealStatus.Claimed &&
702+ callStatus == DealStatus.Claimed &&
623703 block .timestamp < storedDeal.offer.checkIn
624704 ) {
625705 if (storedDeal.offer.cancelHash != hash (_cancelOptions)) {
626706 revert InvalidCancelOptions ();
627707 }
708+
628709 // Using offer cancellation rules
629710 uint256 selectedTime;
630711 uint256 selectedPenalty;
@@ -672,7 +753,6 @@ abstract contract DealsRegistry is
672753 revert NotAllowedStatus ();
673754 }
674755
675- storedDeal.status = DealStatus.Cancelled;
676756 emit Status (offerId, DealStatus.Cancelled, sender);
677757
678758 _afterCancel (offerId);
@@ -693,8 +773,8 @@ abstract contract DealsRegistry is
693773 )
694774 external
695775 dealExists (offerId)
696- inStatuses (offerId, allowedStatuses["claim " ])
697776 onlySigner (offerId)
777+ inStatuses (offerId, allowedStatuses["claim " ])
698778 {
699779 Deal storage storedDeal = deals[offerId];
700780
@@ -784,12 +864,58 @@ abstract contract DealsRegistry is
784864 // Execute before checkIn hook
785865 _beforeCheckIn (offerId, signs);
786866
787- storedDeal.status = DealStatus.CheckedId ;
788- emit Status (offerId, DealStatus.CheckedId , sender);
867+ storedDeal.status = DealStatus.CheckedIn ;
868+ emit Status (offerId, DealStatus.CheckedIn , sender);
789869
790870 // Execute after checkIn hook
791871 _afterCheckIn (offerId, signs);
792872 }
793873
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+ */
885+ function checkOut (
886+ bytes32 offerId
887+ )
888+ external
889+ dealExists (offerId)
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+ }
919+
794920 uint256 [50 ] private __gap;
795921}
0 commit comments