@zoralabs/comments-contracts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/.env.example +11 -0
  2. package/.turbo/turbo-build.log +60 -0
  3. package/LICENSE +21 -0
  4. package/README.md +70 -0
  5. package/_imagine/Enjoy.sol +41 -0
  6. package/abis/AccessControlUpgradeable.json +250 -0
  7. package/abis/Address.json +29 -0
  8. package/abis/Comments.json +62 -0
  9. package/abis/CommentsDeployerBase.json +15 -0
  10. package/abis/CommentsImpl.json +1750 -0
  11. package/abis/CommentsPermitTest.json +847 -0
  12. package/abis/CommentsTest.json +986 -0
  13. package/abis/CommentsTestBase.json +577 -0
  14. package/abis/Comments_mintAndCommentTest.json +690 -0
  15. package/abis/ContextUpgradeable.json +25 -0
  16. package/abis/ContractVersionBase.json +15 -0
  17. package/abis/Create2.json +28 -0
  18. package/abis/DeployImpl.json +22 -0
  19. package/abis/DeployNonDeterministic.json +22 -0
  20. package/abis/DeployScript.json +22 -0
  21. package/abis/DeterministicDeployerAndCaller.json +315 -0
  22. package/abis/DeterministicUUPSProxyDeployer.json +167 -0
  23. package/abis/ECDSA.json +29 -0
  24. package/abis/EIP712.json +67 -0
  25. package/abis/EIP712UpgradeableWithChainId.json +25 -0
  26. package/abis/ERC1155.json +416 -0
  27. package/abis/ERC1155Holder.json +99 -0
  28. package/abis/ERC165.json +21 -0
  29. package/abis/ERC165Upgradeable.json +44 -0
  30. package/abis/ERC1967Proxy.json +67 -0
  31. package/abis/ERC1967Utils.json +85 -0
  32. package/abis/GenerateDeterministicParams.json +22 -0
  33. package/abis/IAccessControl.json +195 -0
  34. package/abis/IBeacon.json +15 -0
  35. package/abis/IComments.json +654 -0
  36. package/abis/IContractMetadata.json +28 -0
  37. package/abis/IERC1155.json +295 -0
  38. package/abis/IERC1155Errors.json +104 -0
  39. package/abis/IERC1155MetadataURI.json +314 -0
  40. package/abis/IERC1155Receiver.json +99 -0
  41. package/abis/IERC1271.json +26 -0
  42. package/abis/IERC165.json +21 -0
  43. package/abis/IERC1822Proxiable.json +15 -0
  44. package/abis/IERC20.json +224 -0
  45. package/abis/IERC20Errors.json +88 -0
  46. package/abis/IERC5267.json +51 -0
  47. package/abis/IERC721.json +287 -0
  48. package/abis/IERC721Enumerable.json +343 -0
  49. package/abis/IERC721Errors.json +105 -0
  50. package/abis/IERC721Metadata.json +332 -0
  51. package/abis/IERC721TokenReceiver.json +36 -0
  52. package/abis/IHasContractName.json +15 -0
  53. package/abis/IImmutableCreate2Factory.json +93 -0
  54. package/abis/IMulticall3.json +440 -0
  55. package/abis/IProtocolRewards.json +342 -0
  56. package/abis/ISafe.json +15 -0
  57. package/abis/ISymbol.json +15 -0
  58. package/abis/IVersionedContract.json +15 -0
  59. package/abis/IZoraCreator1155.json +343 -0
  60. package/abis/ImmutableCreate2FactoryUtils.json +15 -0
  61. package/abis/Initializable.json +25 -0
  62. package/abis/LibString.json +7 -0
  63. package/abis/Math.json +7 -0
  64. package/abis/Mock1155.json +547 -0
  65. package/abis/MockERC20.json +322 -0
  66. package/abis/MockERC721.json +350 -0
  67. package/abis/MockMinter.json +64 -0
  68. package/abis/OwnableUpgradeable.json +99 -0
  69. package/abis/ProtocolRewards.json +494 -0
  70. package/abis/Proxy.json +6 -0
  71. package/abis/ProxyDeployerScript.json +15 -0
  72. package/abis/ProxyShim.json +112 -0
  73. package/abis/Script.json +15 -0
  74. package/abis/ShortStrings.json +18 -0
  75. package/abis/StdAssertions.json +379 -0
  76. package/abis/StdInvariant.json +180 -0
  77. package/abis/Strings.json +18 -0
  78. package/abis/Test.json +570 -0
  79. package/abis/UUPSUpgradeable.json +130 -0
  80. package/abis/UnorderedNoncesUpgradeable.json +42 -0
  81. package/abis/Vm.json +8627 -0
  82. package/abis/VmSafe.json +7297 -0
  83. package/abis/stdError.json +119 -0
  84. package/abis/stdStorageSafe.json +52 -0
  85. package/addresses/999999999.json +4 -0
  86. package/deterministicConfig/comments.json +8 -0
  87. package/dist/index.cjs +935 -0
  88. package/dist/index.cjs.map +1 -0
  89. package/dist/index.d.ts +2 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +908 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/types.d.ts +4 -0
  94. package/dist/types.d.ts.map +1 -0
  95. package/dist/wagmiGenerated.d.ts +1354 -0
  96. package/dist/wagmiGenerated.d.ts.map +1 -0
  97. package/foundry.toml +24 -0
  98. package/package/index.ts +4 -0
  99. package/package/types.ts +5 -0
  100. package/package/wagmiGenerated.ts +907 -0
  101. package/package.json +62 -0
  102. package/remappings.txt +8 -0
  103. package/script/CommentsDeployerBase.sol +60 -0
  104. package/script/Deploy.s.sol +66 -0
  105. package/script/DeployImpl.s.sol +26 -0
  106. package/script/DeployNonDeterministic.s.sol +43 -0
  107. package/script/GenerateDeterministicParams.s.sol +55 -0
  108. package/script/bundle-abis.ts +109 -0
  109. package/script/storage-check.sh +57 -0
  110. package/script/update-contract-version.ts +63 -0
  111. package/scripts/abis.ts +3 -0
  112. package/scripts/backfillComments.ts +176 -0
  113. package/scripts/generateCommentsTestData.ts +247 -0
  114. package/scripts/getCommentsAddresses.ts +10 -0
  115. package/scripts/queries.ts +73 -0
  116. package/scripts/queryAndSaveComments.ts +48 -0
  117. package/scripts/queryQuantityOfComments.ts +53 -0
  118. package/scripts/signDeployAndCall.ts +51 -0
  119. package/scripts/turnkey.ts +36 -0
  120. package/scripts/utils.ts +127 -0
  121. package/scripts/writeComments.ts +198 -0
  122. package/slither.config.json +7 -0
  123. package/src/CommentsImpl.sol +552 -0
  124. package/src/deployments/CommentsDeployment.sol +14 -0
  125. package/src/interfaces/IComments.sol +156 -0
  126. package/src/interfaces/IZoraCreator1155.sol +12 -0
  127. package/src/proxy/Comments.sol +43 -0
  128. package/src/utils/EIP712UpgradeableWithChainId.sol +36 -0
  129. package/src/version/ContractVersionBase.sol +14 -0
  130. package/test/Comments.t.sol +482 -0
  131. package/test/CommentsTestBase.sol +86 -0
  132. package/test/Comments_mintAndComment.t.sol +101 -0
  133. package/test/Comments_permit.t.sol +397 -0
  134. package/test/mocks/Mock1155.sol +50 -0
  135. package/test/mocks/MockMinter.sol +29 -0
  136. package/test/mocks/ProtocolRewards.sol +1497 -0
  137. package/tsconfig.build.json +10 -0
  138. package/tsconfig.json +9 -0
  139. package/tsup.config.ts +11 -0
  140. package/wagmi.config.ts +14 -0
@@ -0,0 +1,552 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
5
+ import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
6
+ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
7
+ import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
8
+ import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
9
+ import {IHasContractName} from "@zoralabs/shared-contracts/interfaces/IContractMetadata.sol";
10
+ import {ContractVersionBase} from "./version/ContractVersionBase.sol";
11
+ import {IZoraCreator1155} from "./interfaces/IZoraCreator1155.sol";
12
+ import {IComments} from "./interfaces/IComments.sol";
13
+ import {IProtocolRewards} from "@zoralabs/protocol-rewards/src/interfaces/IProtocolRewards.sol";
14
+ import {UnorderedNoncesUpgradeable} from "@zoralabs/shared-contracts/utils/UnorderedNoncesUpgradeable.sol";
15
+ import {EIP712UpgradeableWithChainId} from "./utils/EIP712UpgradeableWithChainId.sol";
16
+ import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
17
+
18
+ /// @title CommentsImpl
19
+ /// @notice Contract for comments and sparking (liking with value) Zora 1155 posts.
20
+ /// @dev Implements comment creation, sparking, and backfilling functionality. Implementation contract
21
+ /// meant to be used with a UUPS upgradeable proxy contract.
22
+ contract CommentsImpl is IComments, AccessControlUpgradeable, UUPSUpgradeable, ContractVersionBase, EIP712UpgradeableWithChainId, UnorderedNoncesUpgradeable {
23
+ // this is the zora creator multisig that can upgrade the contract
24
+ bytes32 public constant BACKFILLER_ROLE = keccak256("BACKFILLER_ROLE");
25
+ // allows to delegate comment
26
+ bytes32 public constant DELEGATE_COMMENTOR = keccak256("DELEGATE_COMMENTOR");
27
+ uint256 public constant PERMISSION_BIT_ADMIN = 2 ** 1;
28
+ uint256 public constant ZORA_REWARD_PCT = 10;
29
+ uint256 public constant REFERRER_REWARD_PCT = 20;
30
+ uint256 public constant ZORA_REWARD_NO_REFERRER_PCT = 30;
31
+ uint256 internal constant BPS_TO_PERCENT_2_DECIMAL_PERCISION = 100;
32
+
33
+ string public constant DOMAIN_NAME = "ZoraComments";
34
+ string public constant DOMAIN_VERSION = "1";
35
+
36
+ /// @custom:storage-location erc7201:comments.storage.CommentsStorage
37
+ struct CommentsStorage {
38
+ mapping(bytes32 => Comment) comments;
39
+ // account that receives rewards Zora Rewards for from a portion of the sparks value
40
+ address zoraRecipient;
41
+ // Global autoincrementing nonce
42
+ uint256 nonce;
43
+ }
44
+
45
+ // keccak256(abi.encode(uint256(keccak256("comments.storage.CommentsStorage")) - 1)) & ~bytes32(uint256(0xff))
46
+ bytes32 private constant CommentsStorageLocation = 0x9e5d0d3a4c7e8d5b9e8f9d9d5b9e8f9d9d5b9e8f9d9d5b9e8f9d9d5b9e8f9d00;
47
+
48
+ function _getCommentsStorage() private pure returns (CommentsStorage storage $) {
49
+ assembly {
50
+ $.slot := CommentsStorageLocation
51
+ }
52
+ }
53
+
54
+ function comments(bytes32 commentId) internal view returns (Comment storage) {
55
+ return _getCommentsStorage().comments[commentId];
56
+ }
57
+
58
+ function commentSparksQuantity(bytes32 commentId) external view returns (uint64) {
59
+ return comments(commentId).totalSparks;
60
+ }
61
+
62
+ function nextNonce() external view returns (bytes32) {
63
+ return bytes32(_getCommentsStorage().nonce);
64
+ }
65
+
66
+ uint256 public immutable sparkValue;
67
+
68
+ IProtocolRewards public immutable protocolRewards;
69
+
70
+ // the sparks value could also be a comment
71
+ constructor(uint256 _sparkValue, address _protocolRewards) {
72
+ _disableInitializers();
73
+
74
+ sparkValue = _sparkValue;
75
+ protocolRewards = IProtocolRewards(_protocolRewards);
76
+ }
77
+
78
+ function initialize(address _zoraRecipient, address defaultAdmin, address backfiller, address[] calldata delegateCommenters) public initializer {
79
+ __AccessControl_init();
80
+ __UUPSUpgradeable_init();
81
+ __EIP712_init(DOMAIN_NAME, DOMAIN_VERSION);
82
+
83
+ _getCommentsStorage().zoraRecipient = _zoraRecipient;
84
+
85
+ _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
86
+ _grantRole(BACKFILLER_ROLE, backfiller);
87
+
88
+ for (uint256 i = 0; i < delegateCommenters.length; i++) {
89
+ _grantRole(DELEGATE_COMMENTOR, delegateCommenters[i]);
90
+ }
91
+ }
92
+
93
+ /// @notice Hashes a comment identifier to generate a unique ID
94
+ /// @param commentIdentifier The comment identifier to hash
95
+ /// @return The hashed comment identifier
96
+ function hashCommentIdentifier(CommentIdentifier memory commentIdentifier) public pure returns (bytes32) {
97
+ return keccak256(abi.encode(commentIdentifier));
98
+ }
99
+
100
+ /// @notice Hashes a comment identifier and checks if a comment exists with that id
101
+ /// @param commentIdentifier The comment identifier to check
102
+ /// @return commentId The hashed comment identifier
103
+ /// @return exists Whether the comment exists
104
+ function hashAndCheckCommentExists(CommentIdentifier memory commentIdentifier) public view returns (bytes32 commentId, bool exists) {
105
+ commentId = hashCommentIdentifier(commentIdentifier);
106
+ exists = comments(commentId).exists;
107
+ }
108
+
109
+ /// @notice Validates that a comment exists and returns its ID
110
+ /// @param commentIdentifier The comment identifier to validate
111
+ /// @return commentId The hashed comment identifier
112
+ function hashAndValidateCommentExists(CommentIdentifier memory commentIdentifier) public view returns (bytes32 commentId) {
113
+ bool exists;
114
+ (commentId, exists) = hashAndCheckCommentExists(commentIdentifier);
115
+ if (!exists) {
116
+ revert CommentDoesntExist();
117
+ }
118
+ }
119
+
120
+ /// @notice Creates a new comment. Equivalant sparks value in eth must be sent with the transaction. Must be a holder or creator of the referenced 1155 token.
121
+ /// If not the owner, must send 1 spark.
122
+ /// @param contractAddress The address of the contract
123
+ /// @param tokenId The token ID
124
+ /// @param commenter The address of the commenter
125
+ /// @param text The text content of the comment
126
+ /// @param replyTo The identifier of the comment being replied to (if any)
127
+ /// @return commentIdentifier The identifier of the created comment, including the nonce
128
+ function comment(
129
+ address commenter,
130
+ address contractAddress,
131
+ uint256 tokenId,
132
+ string calldata text,
133
+ CommentIdentifier calldata replyTo,
134
+ address referrer
135
+ ) external payable returns (CommentIdentifier memory commentIdentifier) {
136
+ uint64 sparksQuantity = 0;
137
+ if (msg.value != 0) {
138
+ sparksQuantity = 1;
139
+ }
140
+ _validateSparksQuantityMatchesValue(sparksQuantity, msg.value);
141
+
142
+ commentIdentifier = _createCommentIdentifier(contractAddress, tokenId, commenter);
143
+
144
+ _comment(msg.sender, commentIdentifier, text, sparksQuantity, replyTo, referrer, true);
145
+ }
146
+
147
+ // allows another contract to call this function to signify a caller commented, and is trusted
148
+ // to provide who the original commentor was. also allows no sparks to be sent.
149
+ function delegateComment(
150
+ address commenter,
151
+ address contractAddress,
152
+ uint256 tokenId,
153
+ string calldata text,
154
+ CommentIdentifier calldata replyTo
155
+ ) external onlyRole(DELEGATE_COMMENTOR) returns (CommentIdentifier memory commentIdentifier) {
156
+ commentIdentifier = _createCommentIdentifier(contractAddress, tokenId, commenter);
157
+
158
+ _comment(commentIdentifier.commenter, commentIdentifier, text, 0, replyTo, address(0), false);
159
+ }
160
+
161
+ function _createCommentIdentifier(address contractAddress, uint256 tokenId, address commenter) private returns (CommentIdentifier memory) {
162
+ CommentsStorage storage $ = _getCommentsStorage();
163
+ return CommentIdentifier({commenter: commenter, contractAddress: contractAddress, tokenId: tokenId, nonce: bytes32($.nonce++)});
164
+ }
165
+
166
+ function _comment(
167
+ address commenter,
168
+ CommentIdentifier memory commentIdentifier,
169
+ string memory text,
170
+ uint64 sparksQuantity,
171
+ CommentIdentifier memory replyTo,
172
+ address referrer,
173
+ bool mustSendAtLeastOneSpark
174
+ ) internal returns (bytes32) {
175
+ if (commentIdentifier.commenter != commenter) {
176
+ revert CommenterMismatch(commentIdentifier.commenter, commenter);
177
+ }
178
+
179
+ (bytes32 commentId, bytes32 replyToId) = _validateComment(commentIdentifier, replyTo, text, sparksQuantity, mustSendAtLeastOneSpark);
180
+
181
+ _saveCommentAndTransferSparks(commentId, commentIdentifier, text, sparksQuantity, replyToId, replyTo, block.timestamp, referrer);
182
+
183
+ return commentId;
184
+ }
185
+
186
+ function _validateIdentifiersMatch(CommentIdentifier memory commentIdentifier, CommentIdentifier memory replyTo) internal pure {
187
+ if (commentIdentifier.contractAddress != replyTo.contractAddress || commentIdentifier.tokenId != replyTo.tokenId) {
188
+ revert CommentAddressOrTokenIdsDoNotMatch(commentIdentifier.contractAddress, commentIdentifier.tokenId, replyTo.contractAddress, replyTo.tokenId);
189
+ }
190
+ }
191
+
192
+ function _validateComment(
193
+ CommentIdentifier memory commentIdentifier,
194
+ CommentIdentifier memory replyTo,
195
+ string memory text,
196
+ uint64 sparksQuantity,
197
+ bool mustSendAtLeastOneSpark
198
+ ) internal view returns (bytes32 commentId, bytes32 replyToId) {
199
+ // verify that the commenter specified in the identifier is the one expected
200
+ commentId = hashCommentIdentifier(commentIdentifier);
201
+
202
+ if (replyTo.commenter != address(0)) {
203
+ replyToId = hashAndValidateCommentExists(replyTo);
204
+ _validateIdentifiersMatch(commentIdentifier, replyTo);
205
+ }
206
+
207
+ if (bytes(text).length == 0) {
208
+ revert EmptyComment();
209
+ }
210
+
211
+ _validateCommentorAndSparksQuantity(commentIdentifier, sparksQuantity, mustSendAtLeastOneSpark);
212
+ }
213
+
214
+ function _validateCommentorAndSparksQuantity(
215
+ CommentIdentifier memory commentIdentifier,
216
+ uint64 sparksQuantity,
217
+ bool mustSendAtLeastOneSpark
218
+ ) internal view {
219
+ // check that the commenter is a token admin - if they are, then they can comment for free
220
+ if (_isTokenAdmin(commentIdentifier.contractAddress, commentIdentifier.tokenId, commentIdentifier.commenter)) {
221
+ return;
222
+ }
223
+ // if they aren't, then they must be a token holder, and have included at least 1 spark
224
+ if (!_isTokenHolder(commentIdentifier.contractAddress, commentIdentifier.tokenId, commentIdentifier.commenter)) {
225
+ revert NotTokenHolderOrAdmin();
226
+ }
227
+
228
+ if (mustSendAtLeastOneSpark && sparksQuantity == 0) {
229
+ revert MustSendAtLeastOneSpark();
230
+ }
231
+ }
232
+
233
+ bytes4 constant zoraRewardReason = bytes4(keccak256("zoraRewardForCommentDeposited()"));
234
+ bytes4 constant referrerRewardReason = bytes4(keccak256("referrerRewardForCommentDeposited()"));
235
+ bytes4 constant sparksRecipientRewardReason = bytes4(keccak256("sparksRecipientRewardForCommentDeposited()"));
236
+
237
+ function _getRewardDeposits(
238
+ address sparksRecipient,
239
+ address referrer,
240
+ uint256 sparksValue
241
+ ) internal view returns (address[] memory, uint256[] memory, bytes4[] memory) {
242
+ uint256 recipientCount = referrer != address(0) ? 3 : 2;
243
+ address[] memory recipients = new address[](recipientCount);
244
+ uint256[] memory amounts = new uint256[](recipientCount);
245
+ bytes4[] memory reasons = new bytes4[](recipientCount);
246
+
247
+ address zoraRecipient = _getCommentsStorage().zoraRecipient;
248
+
249
+ if (referrer != address(0)) {
250
+ uint256 zoraReward = (ZORA_REWARD_PCT * sparksValue) / BPS_TO_PERCENT_2_DECIMAL_PERCISION;
251
+ recipients[0] = zoraRecipient;
252
+ amounts[0] = zoraReward;
253
+ reasons[0] = zoraRewardReason;
254
+
255
+ uint256 referrerReward = (REFERRER_REWARD_PCT * sparksValue) / BPS_TO_PERCENT_2_DECIMAL_PERCISION;
256
+ recipients[1] = referrer;
257
+ amounts[1] = referrerReward;
258
+ reasons[1] = referrerRewardReason;
259
+
260
+ uint256 sparksRecipientReward = sparksValue - zoraReward - referrerReward;
261
+ recipients[2] = sparksRecipient;
262
+ amounts[2] = sparksRecipientReward;
263
+ reasons[2] = sparksRecipientRewardReason;
264
+ } else {
265
+ uint256 zoraRewardNoReferrer = (ZORA_REWARD_NO_REFERRER_PCT * sparksValue) / BPS_TO_PERCENT_2_DECIMAL_PERCISION;
266
+ recipients[0] = zoraRecipient;
267
+ amounts[0] = zoraRewardNoReferrer;
268
+ reasons[0] = zoraRewardReason;
269
+
270
+ uint256 sparkRecipientReward = sparksValue - zoraRewardNoReferrer;
271
+ recipients[1] = sparksRecipient;
272
+ amounts[1] = sparkRecipientReward;
273
+ reasons[1] = sparksRecipientRewardReason;
274
+ }
275
+
276
+ return (recipients, amounts, reasons);
277
+ }
278
+
279
+ function _transferSparksValueToRecipient(address sparksRecipient, address referrer, uint256 sparksValue, string memory depositBatchComment) internal {
280
+ (address[] memory recipients, uint256[] memory amounts, bytes4[] memory reasons) = _getRewardDeposits(sparksRecipient, referrer, sparksValue);
281
+ protocolRewards.depositBatch{value: sparksValue}(recipients, amounts, reasons, depositBatchComment);
282
+ }
283
+
284
+ function _isTokenAdmin(address contractAddress, uint256 tokenId, address user) internal view returns (bool) {
285
+ // TOOD: investigate is there a better way to know the token creator?
286
+ return IZoraCreator1155(contractAddress).isAdminOrRole(user, tokenId, PERMISSION_BIT_ADMIN);
287
+ }
288
+
289
+ function _isTokenHolder(address contractAddress, uint256 tokenId, address user) internal view returns (bool) {
290
+ return IERC1155(contractAddress).balanceOf(user, tokenId) > 0;
291
+ }
292
+
293
+ function _getCommentSparksRecipient(CommentIdentifier memory commentIdentifier, CommentIdentifier memory replyTo) internal view returns (address) {
294
+ // if there is no reply to, then creator reward recipient of the 1155 token gets the sparks
295
+ // otherwise, the replay to commenter gets the sparks
296
+ if (replyTo.commenter == address(0)) {
297
+ return _getCreatorRewardRecipient(commentIdentifier);
298
+ }
299
+
300
+ return replyTo.commenter;
301
+ }
302
+
303
+ // executes the comment. assumes sparks have already been transferred to recipient, and data has been validated
304
+ // assume that the commentId and replyToId are valid
305
+ function _saveCommentAndTransferSparks(
306
+ bytes32 commentId,
307
+ CommentIdentifier memory commentIdentifier,
308
+ string memory text,
309
+ uint64 sparksQuantity,
310
+ bytes32 replyToId,
311
+ CommentIdentifier memory replyToIdentifier,
312
+ uint256 timestamp,
313
+ address referrer
314
+ ) internal {
315
+ _saveComment(commentId, commentIdentifier, text, sparksQuantity, replyToId, replyToIdentifier, timestamp, referrer);
316
+ string memory depositBatchComment = "Comment";
317
+
318
+ // update reason if replying to a comment
319
+ if (replyToId != 0) {
320
+ depositBatchComment = "Comment Reply";
321
+ }
322
+
323
+ if (sparksQuantity > 0) {
324
+ address sparksRecipient = _getCommentSparksRecipient(commentIdentifier, replyToIdentifier);
325
+ _transferSparksValueToRecipient(sparksRecipient, referrer, sparksQuantity * sparkValue, depositBatchComment);
326
+ }
327
+ }
328
+
329
+ function _saveComment(
330
+ bytes32 commentId,
331
+ CommentIdentifier memory commentIdentifier,
332
+ string memory text,
333
+ uint64 sparksQuantity,
334
+ bytes32 replyToId,
335
+ CommentIdentifier memory replyToIdentifier,
336
+ uint256 timestamp,
337
+ address referrer
338
+ ) internal {
339
+ if (comments(commentId).exists) {
340
+ revert DuplicateComment(commentId);
341
+ }
342
+ comments(commentId).exists = true;
343
+
344
+ emit Commented(commentId, commentIdentifier, replyToId, replyToIdentifier, sparksQuantity, text, timestamp, referrer);
345
+ }
346
+
347
+ /// @notice Sparks a comment. Equivalant sparks value in eth to sparksQuantity must be sent with the transaction. Sparking a comment is
348
+ /// similar to liking it, except it is liked with the value of sparks attached. The spark value gets sent to the commentor, with a fee taken out.
349
+ /// @param commentIdentifier The identifier of the comment to spark
350
+ /// @param sparksQuantity The quantity of sparks to send
351
+ /// @param referrer The referrer of the comment
352
+ function sparkComment(CommentIdentifier calldata commentIdentifier, uint64 sparksQuantity, address referrer) public payable {
353
+ if (sparksQuantity == 0) {
354
+ revert MustSendAtLeastOneSpark();
355
+ }
356
+ _validateSparksQuantityMatchesValue(sparksQuantity, msg.value);
357
+ _sparkComment(commentIdentifier, msg.sender, sparksQuantity, referrer);
358
+ }
359
+
360
+ function _validateSparksQuantityMatchesValue(uint64 sparksQuantity, uint256 value) internal view {
361
+ if (value != sparksQuantity * sparkValue) {
362
+ revert IncorrectETHAmountForSparks(value, sparksQuantity * sparkValue);
363
+ }
364
+ }
365
+
366
+ function _sparkComment(CommentIdentifier memory commentIdentifier, address sparker, uint64 sparksQuantity, address referrer) internal {
367
+ if (sparker == commentIdentifier.commenter) {
368
+ revert CannotSparkOwnComment();
369
+ }
370
+ bytes32 commentId = hashCommentIdentifier(commentIdentifier);
371
+ if (!comments(commentId).exists) {
372
+ revert CommentDoesntExist();
373
+ }
374
+
375
+ comments(commentId).totalSparks += uint64(sparksQuantity);
376
+
377
+ _transferSparksValueToRecipient(commentIdentifier.commenter, referrer, sparksQuantity * sparkValue, "Sparked Comment");
378
+
379
+ emit SparkedComment(commentId, commentIdentifier, sparksQuantity, sparker, block.timestamp);
380
+ }
381
+
382
+ bytes32 constant PERMIT_COMMENT_DOMAIN =
383
+ keccak256(
384
+ "PermitComment(address contractAddress,uint256 tokenId,address commenter,CommentIdentifier replyTo,bytes text,uint64 sparksQuantity,uint256 deadline,bytes32 nonce,address referrer,uint256 sourceChainId,uint256 destinationChainId)CommentIdentifier(address contractAddress,uint256 tokenId,address commenter,bytes32 nonce)"
385
+ );
386
+
387
+ bytes32 constant COMMENT_IDENTIFIER_DOMAIN = keccak256("CommentIdentifier(address contractAddress,uint256 tokenId,address commenter,bytes32 nonce)");
388
+
389
+ function _hashCommentIdentifier(CommentIdentifier memory commentIdentifier) internal pure returns (bytes32) {
390
+ return
391
+ keccak256(
392
+ abi.encode(
393
+ COMMENT_IDENTIFIER_DOMAIN,
394
+ commentIdentifier.contractAddress,
395
+ commentIdentifier.tokenId,
396
+ commentIdentifier.commenter,
397
+ commentIdentifier.nonce
398
+ )
399
+ );
400
+ }
401
+
402
+ function _hashTypedDataV4WithCustomChain(bytes32 permitDomain, bytes memory permitData, uint256 sourceChainId) internal view returns (bytes32) {
403
+ return _hashTypedDataV4(keccak256(abi.encode(permitDomain, permitData)), sourceChainId);
404
+ }
405
+
406
+ function hashPermitComment(PermitComment calldata permit) public view returns (bytes32) {
407
+ return
408
+ _hashTypedDataV4WithCustomChain(
409
+ PERMIT_COMMENT_DOMAIN,
410
+ abi.encode(
411
+ permit.contractAddress,
412
+ permit.tokenId,
413
+ permit.commenter,
414
+ _hashCommentIdentifier(permit.replyTo),
415
+ keccak256(bytes(permit.text)),
416
+ permit.sparksQuantity,
417
+ permit.deadline,
418
+ permit.nonce,
419
+ permit.referrer,
420
+ permit.sourceChainId,
421
+ permit.destinationChainId
422
+ ),
423
+ permit.sourceChainId
424
+ );
425
+ }
426
+
427
+ function _validatePermit(bytes32 digest, bytes32 nonce, bytes calldata signature, address signer, uint256 deadline) internal {
428
+ if (block.timestamp > deadline) {
429
+ revert ERC2612ExpiredSignature(deadline);
430
+ }
431
+
432
+ _useCheckedNonce(signer, uint256(nonce));
433
+ _validateSignerIsCommenter(digest, signature, signer);
434
+ }
435
+
436
+ function permitComment(PermitComment calldata permit, bytes calldata signature) public payable {
437
+ if (permit.destinationChainId != block.chainid) {
438
+ revert IncorrectDestinationChain(permit.destinationChainId);
439
+ }
440
+
441
+ bytes32 digest = hashPermitComment(permit);
442
+ _validatePermit(digest, permit.nonce, signature, permit.commenter, permit.deadline);
443
+
444
+ CommentIdentifier memory commentIdentifier = _createCommentIdentifier(permit.contractAddress, permit.tokenId, permit.commenter);
445
+ (bytes32 commentId, bytes32 replyToId) = _validateComment(commentIdentifier, permit.replyTo, permit.text, permit.sparksQuantity, true);
446
+
447
+ _saveCommentAndTransferSparks(
448
+ commentId,
449
+ commentIdentifier,
450
+ permit.text,
451
+ permit.sparksQuantity,
452
+ replyToId,
453
+ permit.replyTo,
454
+ block.timestamp,
455
+ permit.referrer
456
+ );
457
+ }
458
+
459
+ bytes32 constant PERMIT_SPARK_COMMENT_DOMAIN =
460
+ keccak256(
461
+ "PermitSparkComment(CommentIdentifier comment,address sparker,uint64 sparksQuantity,uint256 deadline,bytes32 nonce,address referrer,uint256 sourceChainId,uint256 destinationChainId)CommentIdentifier(address contractAddress,uint256 tokenId,address commenter,bytes32 nonce)"
462
+ );
463
+
464
+ function hashPermitSparkComment(PermitSparkComment calldata permit) public view returns (bytes32) {
465
+ return
466
+ _hashTypedDataV4WithCustomChain(
467
+ PERMIT_SPARK_COMMENT_DOMAIN,
468
+ abi.encode(
469
+ _hashCommentIdentifier(permit.comment),
470
+ permit.sparker,
471
+ permit.sparksQuantity,
472
+ permit.deadline,
473
+ permit.nonce,
474
+ permit.referrer,
475
+ permit.sourceChainId,
476
+ permit.destinationChainId
477
+ ),
478
+ permit.sourceChainId
479
+ );
480
+ }
481
+
482
+ function permitSparkComment(PermitSparkComment calldata permit, bytes calldata signature) public payable {
483
+ if (permit.destinationChainId != block.chainid) {
484
+ revert IncorrectDestinationChain(permit.destinationChainId);
485
+ }
486
+
487
+ bytes32 digest = hashPermitSparkComment(permit);
488
+ _validatePermit(digest, permit.nonce, signature, permit.sparker, permit.deadline);
489
+
490
+ if (permit.sparksQuantity == 0) {
491
+ revert MustSendAtLeastOneSpark();
492
+ }
493
+
494
+ _validateSparksQuantityMatchesValue(permit.sparksQuantity, msg.value);
495
+
496
+ _sparkComment(permit.comment, permit.sparker, permit.sparksQuantity, permit.referrer);
497
+ }
498
+
499
+ function _validateSignerIsCommenter(bytes32 digest, bytes calldata signature, address signer) internal view {
500
+ if (!SignatureChecker.isValidSignatureNow(signer, digest, signature)) {
501
+ revert InvalidSignature();
502
+ }
503
+ }
504
+
505
+ /// @notice Backfills comments created by other contracts. Only callable by an account with the backfiller role.
506
+ /// @param commentIdentifiers Array of comment identifiers
507
+ /// @param texts Array of comment texts
508
+ /// @param timestamps Array of comment timestamps
509
+ /// @param originalTransactionHashes Array of original transaction hashes
510
+ function backfillBatchAddComment(
511
+ CommentIdentifier[] calldata commentIdentifiers,
512
+ string[] calldata texts,
513
+ uint256[] calldata timestamps,
514
+ bytes32[] calldata originalTransactionHashes
515
+ ) public onlyRole(BACKFILLER_ROLE) {
516
+ if (commentIdentifiers.length != texts.length || texts.length != timestamps.length || timestamps.length != originalTransactionHashes.length) {
517
+ revert ArrayLengthMismatch();
518
+ }
519
+
520
+ for (uint256 i = 0; i < commentIdentifiers.length; i++) {
521
+ bytes32 commentId = hashCommentIdentifier(commentIdentifiers[i]);
522
+
523
+ if (comments(commentId).exists) {
524
+ revert DuplicateComment(commentId);
525
+ }
526
+ comments(commentId).exists = true;
527
+
528
+ // create blank replyTo - assume that these were created without replyTo
529
+ emit BackfilledComment(commentId, commentIdentifiers[i], texts[i], timestamps[i], originalTransactionHashes[i]);
530
+ }
531
+ }
532
+
533
+ function _getCreatorRewardRecipient(CommentIdentifier memory commentIdentifier) internal view returns (address) {
534
+ // TODO: Implement logic to get creator reward recipient
535
+ return IZoraCreator1155(commentIdentifier.contractAddress).getCreatorRewardRecipient(commentIdentifier.tokenId);
536
+ }
537
+
538
+ function contractName() public pure returns (string memory) {
539
+ return "Zora Comments";
540
+ }
541
+
542
+ function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {
543
+ // check that new implementation's contract name matches the current contract name
544
+ if (!_equals(IHasContractName(newImplementation).contractName(), this.contractName())) {
545
+ revert UpgradeToMismatchedContractName(this.contractName(), IHasContractName(newImplementation).contractName());
546
+ }
547
+ }
548
+
549
+ function _equals(string memory a, string memory b) internal pure returns (bool) {
550
+ return (keccak256(bytes(a)) == keccak256(bytes(b)));
551
+ }
552
+ }
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import {CommentsImpl} from "../CommentsImpl.sol";
5
+
6
+ library CommentsDeployment {
7
+ address internal constant SPARKS_1155 = 0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B;
8
+ uint256 internal constant SPARKS_TOKEN_ID = 1;
9
+ uint256 internal constant SPARK_VALUE = 0.000001 ether;
10
+
11
+ function commentsImplCreationCode() internal pure returns (bytes memory) {
12
+ return abi.encodePacked(type(CommentsImpl).creationCode, abi.encode(SPARKS_1155, SPARKS_TOKEN_ID, SPARK_VALUE));
13
+ }
14
+ }