Skip to content

Commit 6d4914e

Browse files
committedApr 19, 2023
feat: 🎸 Added DealsRegistry contracts and testing utils
1 parent 09ac29e commit 6d4914e

17 files changed

+700
-27
lines changed
 

‎.prettierrc

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"plugins": ["prettier-plugin-solidity"],
23
"semi": true,
34
"trailingComma": "all",
45
"singleQuote": true,
@@ -8,7 +9,8 @@
89
{
910
"files": "*.sol",
1011
"options": {
11-
"printWidth": 80,
12+
"parser": "solidity-parse",
13+
"printWidth": 90,
1214
"tabWidth": 2,
1315
"useTabs": false,
1416
"singleQuote": false,

‎.solhint.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "solhint:default",
3+
"plugins": ["prettier"]
4+
}

‎contracts/DealsRegistry.sol

+239
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.19;
3+
4+
import "@openzeppelin/contracts/utils/Context.sol";
5+
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
6+
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
7+
import "./utils/IERC20.sol";
8+
import "./utils/StringUtils.sol";
9+
import "./utils/SignatureUtils.sol";
10+
11+
abstract contract DealsRegistry is Context, EIP712 {
12+
using SignatureChecker for address;
13+
using StringUtils for string;
14+
using SignatureUtils for bytes;
15+
16+
bytes32 public constant PAYMENT_OPTION_TYPE_HASH =
17+
keccak256("PaymentOption(string id,uint256 price,address asset)");
18+
19+
bytes32 public constant CANCEL_OPTION_TYPE_HASH =
20+
keccak256("CancelOption(uint256 time,uint256 penalty)");
21+
22+
bytes32 public constant OFFER_TYPE_HASH =
23+
keccak256(
24+
// solhint-disable-next-line max-line-length
25+
"Offer(bytes32 id,uint256 expire,bytes32 supplierId,uint256 chainId,bytes32 requestHash,bytes32 optionsHash,bytes32 paymentHash,bytes32 cancelHash,bool transferable,uint256 checkIn)"
26+
);
27+
28+
struct PaymentOption {
29+
/// @dev Unique paymentOptions option Id
30+
string id;
31+
/// @dev Asset price in WEI
32+
uint256 price;
33+
/// @dev ERC20 asset contract address
34+
address asset;
35+
}
36+
37+
struct CancelOption {
38+
/// @dev Seconds before checkIn
39+
uint256 time;
40+
/// @dev Percents of total sum
41+
uint256 penalty;
42+
}
43+
44+
struct Offer {
45+
/// @dev Offer Id
46+
bytes32 id;
47+
/// @dev Expiration time
48+
uint256 expire;
49+
/// @dev Unique supplier Id registered on the protocol contract
50+
bytes32 supplierId;
51+
/// @dev Target network chain Id
52+
uint256 chainId;
53+
/// @dev <keccak256(encode(request))>
54+
bytes32 requestHash;
55+
/// @dev <keccak256(encode(offer.options))>
56+
bytes32 optionsHash;
57+
/// @dev <keccak256(encode(offer.payment))>
58+
bytes32 paymentHash;
59+
/// @dev <keccak256(encode(offer.cancel(sorted by time DESC) || []))>
60+
bytes32 cancelHash;
61+
/// @dev makes the deal NFT transferable or not
62+
bool transferable;
63+
/// @dev check-in time in seconds
64+
uint256 checkIn;
65+
}
66+
67+
enum DealStatus {
68+
Created, // Just created
69+
Claimed, // Claimed by the supplier
70+
Rejected, // Rejected by the supplier
71+
Cancelled, // Cancelled by the buyer
72+
Disputed // Dispute started
73+
}
74+
75+
struct Deal {
76+
Offer offer;
77+
uint256 price;
78+
address asset;
79+
DealStatus status;
80+
}
81+
82+
/// @dev Mapping of an offer Id on a Deal
83+
mapping(bytes32 => Deal) private _deals;
84+
85+
/// @dev Emitted when a Deal is created by a buyer
86+
event DealCreated(bytes32 indexed offerId, address indexed buyer);
87+
88+
/// @dev Thrown when a user attempts to make a deal using an offer with an invalid signature
89+
error InvalidOfferSignature();
90+
91+
/// @dev Thrown when a user attempts to make a deal providing an invalid payment options
92+
error InvalidPaymentOptions();
93+
94+
/// @dev Thrown when a user attempts to make a deal providing an invalid payment option Id
95+
error InvalidPaymentId();
96+
97+
/// @dev Thrown when a Deal funds transfer is failed
98+
error DealTransferFailed();
99+
100+
/**
101+
* @dev DealsRegistry constructor
102+
* @param name EIP712 contract name
103+
* @param version EIP712 contract version
104+
*/
105+
constructor(string memory name, string memory version) EIP712(name, version) {}
106+
107+
/// @dev Creates a hash of a PaymentOption
108+
function hash(PaymentOption memory paymentOptions) internal pure returns (bytes32) {
109+
return
110+
keccak256(
111+
abi.encode(
112+
PAYMENT_OPTION_TYPE_HASH,
113+
paymentOptions.id,
114+
paymentOptions.price,
115+
paymentOptions.asset
116+
)
117+
);
118+
}
119+
120+
/// @dev Creates a hash of a CancelOption
121+
function hash(CancelOption memory cancel) internal pure returns (bytes32) {
122+
return keccak256(abi.encode(CANCEL_OPTION_TYPE_HASH, cancel.time, cancel.penalty));
123+
}
124+
125+
/// @dev Creates a hash of an array of PaymentOption
126+
function hash(PaymentOption[] memory paymentOptions) internal pure returns (bytes32) {
127+
bytes32[] memory hashes = new bytes32[](paymentOptions.length);
128+
129+
for (uint256 i = 0; i < paymentOptions.length; i++) {
130+
hashes[i] = hash(paymentOptions[i]);
131+
}
132+
133+
return keccak256(abi.encodePacked(hashes));
134+
}
135+
136+
/// @dev Creates a hash of an array of CancelOption
137+
function hash(CancelOption[] memory cancel) internal pure returns (bytes32) {
138+
bytes32[] memory hashes = new bytes32[](cancel.length);
139+
140+
for (uint256 i = 0; i < cancel.length; i++) {
141+
hashes[i] = hash(cancel[i]);
142+
}
143+
144+
return keccak256(abi.encodePacked(hashes));
145+
}
146+
147+
/// @dev Creates a hash of an Offer
148+
function hash(Offer memory offer) internal pure returns (bytes32) {
149+
return
150+
keccak256(
151+
abi.encode(
152+
OFFER_TYPE_HASH,
153+
offer.id,
154+
offer.expire,
155+
offer.supplierId,
156+
offer.chainId,
157+
offer.requestHash,
158+
offer.optionsHash,
159+
offer.paymentHash,
160+
offer.cancelHash,
161+
offer.transferable,
162+
offer.checkIn
163+
)
164+
);
165+
}
166+
167+
/**
168+
* @dev Creates a Deal on a base of an offer
169+
* @param offer An offer payload
170+
* @param paymentOptions Raw offered payment options array
171+
* @param paymentId Payment option Id
172+
* @param signs Signatures: [0] - offer: ECDSA/ERC1271; [1] - asset permit: ECDSA (optional)
173+
*
174+
* NOTE: `permit` signature can be ECDSA of type only
175+
*/
176+
function deal(
177+
Offer memory offer,
178+
PaymentOption[] memory paymentOptions,
179+
string memory paymentId,
180+
bytes[] memory signs
181+
) public {
182+
address buyer = _msgSender();
183+
bytes32 offerHash = hash(offer);
184+
185+
if (!buyer.isValidSignatureNow(offerHash, signs[0])) {
186+
revert InvalidOfferSignature();
187+
}
188+
189+
bytes32 paymentHash = hash(paymentOptions);
190+
191+
if (paymentHash != offer.paymentHash) {
192+
revert InvalidPaymentOptions();
193+
}
194+
195+
uint256 price;
196+
address asset;
197+
198+
for (uint256 i = 0; i < paymentOptions.length; i++) {
199+
if (paymentOptions[i].id.equal(paymentId)) {
200+
price = paymentOptions[i].price;
201+
asset = paymentOptions[i].asset;
202+
break;
203+
}
204+
}
205+
206+
if (asset == address(0)) {
207+
revert InvalidPaymentId();
208+
}
209+
210+
_beforeDealCreated(offer, price, asset, signs);
211+
212+
if (signs.length > 1) {
213+
(uint8 v, bytes32 r, bytes32 s) = signs[1].split();
214+
IERC20(asset).permit(buyer, address(this), price, offer.expire, v, r, s);
215+
} else if (!IERC20(asset).transferFrom(buyer, address(this), price)) {
216+
revert DealTransferFailed();
217+
}
218+
219+
_deals[offer.id] = Deal(offer, price, asset, DealStatus.Created);
220+
221+
emit DealCreated(offer.id, buyer);
222+
223+
_afterDealCreated(offer, price, asset, signs);
224+
}
225+
226+
function _beforeDealCreated(
227+
Offer memory offer,
228+
uint256 price,
229+
address asset,
230+
bytes[] memory signs
231+
) internal virtual {}
232+
233+
function _afterDealCreated(
234+
Offer memory offer,
235+
uint256 price,
236+
address asset,
237+
bytes[] memory signs
238+
) internal virtual {}
239+
}

‎contracts/ERC1155Token.sol

+3-6
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@ import "@openzeppelin/contracts/access/Ownable.sol";
66
import "@openzeppelin/contracts/security/Pausable.sol";
77
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
88

9-
abstract contract ERC1155Token is
10-
ERC1155(""),
11-
Ownable,
12-
Pausable,
13-
ERC1155Supply
14-
{
9+
abstract contract ERC1155Token is ERC1155(""), Ownable, Pausable, ERC1155Supply {
1510
function pause() public onlyOwner {
1611
_pause();
1712
}
@@ -30,4 +25,6 @@ abstract contract ERC1155Token is
3025
) internal override(ERC1155, ERC1155Supply) whenNotPaused {
3126
super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
3227
}
28+
29+
uint256[50] private __gap;
3330
}

‎contracts/Market.sol

+4-5
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
pragma solidity ^0.8.19;
33

44
import "./ERC1155Token.sol";
5+
import "./DealsRegistry.sol";
56

6-
contract Market is ERC1155Token {
7-
constructor(address owner) {
7+
contract Market is ERC1155Token, DealsRegistry {
8+
constructor(address owner) DealsRegistry("Market", "1") {
89
transferOwnership(owner);
910
}
1011

11-
function uri(
12-
uint256 id
13-
) public view override(ERC1155) returns (string memory) {
12+
function uri(uint256 id) public view override(ERC1155) returns (string memory) {
1413
// Generate uri that depends on the id
1514
return "";
1615
}

‎contracts/test/MockERC20Dec18.sol

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.13;
3+
4+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
import "@openzeppelin/contracts/access/Ownable.sol";
6+
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
7+
import "@openzeppelin/contracts/security/Pausable.sol";
8+
import "@openzeppelin/contracts/access/AccessControl.sol";
9+
10+
/// @custom:security-contact security@windingtree.com
11+
contract MockERC20Dec18 is ERC20, Ownable, ERC20Burnable, Pausable {
12+
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
13+
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
14+
15+
constructor(
16+
string memory name,
17+
string memory symbol,
18+
address owner
19+
) ERC20(name, symbol) {
20+
transferOwnership(owner);
21+
}
22+
23+
function pause() public onlyOwner {
24+
_pause();
25+
}
26+
27+
function unpause() public onlyOwner {
28+
_unpause();
29+
}
30+
31+
function mint(address to, uint256 amount) public onlyOwner {
32+
_mint(to, amount);
33+
}
34+
35+
function _beforeTokenTransfer(
36+
address from,
37+
address to,
38+
uint256 amount
39+
) internal virtual override whenNotPaused {
40+
super._beforeTokenTransfer(from, to, amount);
41+
}
42+
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.13;
3+
4+
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
5+
import "./MockERC20Dec18.sol";
6+
7+
/// @custom:security-contact security@windingtree.com
8+
contract MockERC20Dec18Permit is MockERC20Dec18, ERC20Permit {
9+
constructor(
10+
string memory name,
11+
string memory symbol,
12+
address owner
13+
) MockERC20Dec18(name, symbol, owner) ERC20Permit(name) {}
14+
15+
function _beforeTokenTransfer(
16+
address from,
17+
address to,
18+
uint256 amount
19+
) internal override(ERC20, MockERC20Dec18) whenNotPaused {
20+
super._beforeTokenTransfer(from, to, amount);
21+
}
22+
}

‎contracts/utils/IERC20.sol

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.19;
3+
4+
/// @dev Simple ERC20 token interface
5+
interface IERC20 {
6+
function decimals() external view returns (uint256);
7+
8+
function transfer(address, uint256) external returns (bool);
9+
10+
function transferFrom(address, address, uint256) external returns (bool);
11+
12+
function permit(
13+
address owner,
14+
address spender,
15+
uint256 value,
16+
uint256 deadline,
17+
uint8 v,
18+
bytes32 r,
19+
bytes32 s
20+
) external;
21+
}

0 commit comments

Comments
 (0)
Please sign in to comment.