Skip to content

Commit 5917514

Browse files
authored
Merge pull request #10 from kostysh/feat/checkout
Feat/checkout
2 parents e51f1db + 43d1feb commit 5917514

File tree

4 files changed

+451
-108
lines changed

4 files changed

+451
-108
lines changed

contracts/DealsRegistry.sol

Lines changed: 141 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

contracts/Market.sol

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,14 @@ contract Market is Ownable, Pausable, DealsRegistry, ERC721Token {
126126
}
127127

128128
/**
129-
* @dev Executes logic before a deal is canceled
129+
* @dev Executes logic after a deal is canceled
130130
* @param offerId The ID of the offer
131131
*/
132-
function _beforeCancel(bytes32 offerId) internal override(DealsRegistry) {
133-
if (deals[offerId].status != DealStatus.Created) {
134-
uint256 tokenId = offerTokens[offerId];
132+
function _afterCancel(bytes32 offerId) internal override(DealsRegistry) {
133+
uint256 tokenId = offerTokens[offerId];
134+
135+
// If token has been minted we must burn it
136+
if (tokenId != 0) {
135137
safeBurn(tokenId);
136138
delete tokenOffers[tokenId];
137139
delete offerTokens[offerId];

0 commit comments

Comments
 (0)