@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.
- package/.env.example +11 -0
- package/.turbo/turbo-build.log +60 -0
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/_imagine/Enjoy.sol +41 -0
- package/abis/AccessControlUpgradeable.json +250 -0
- package/abis/Address.json +29 -0
- package/abis/Comments.json +62 -0
- package/abis/CommentsDeployerBase.json +15 -0
- package/abis/CommentsImpl.json +1750 -0
- package/abis/CommentsPermitTest.json +847 -0
- package/abis/CommentsTest.json +986 -0
- package/abis/CommentsTestBase.json +577 -0
- package/abis/Comments_mintAndCommentTest.json +690 -0
- package/abis/ContextUpgradeable.json +25 -0
- package/abis/ContractVersionBase.json +15 -0
- package/abis/Create2.json +28 -0
- package/abis/DeployImpl.json +22 -0
- package/abis/DeployNonDeterministic.json +22 -0
- package/abis/DeployScript.json +22 -0
- package/abis/DeterministicDeployerAndCaller.json +315 -0
- package/abis/DeterministicUUPSProxyDeployer.json +167 -0
- package/abis/ECDSA.json +29 -0
- package/abis/EIP712.json +67 -0
- package/abis/EIP712UpgradeableWithChainId.json +25 -0
- package/abis/ERC1155.json +416 -0
- package/abis/ERC1155Holder.json +99 -0
- package/abis/ERC165.json +21 -0
- package/abis/ERC165Upgradeable.json +44 -0
- package/abis/ERC1967Proxy.json +67 -0
- package/abis/ERC1967Utils.json +85 -0
- package/abis/GenerateDeterministicParams.json +22 -0
- package/abis/IAccessControl.json +195 -0
- package/abis/IBeacon.json +15 -0
- package/abis/IComments.json +654 -0
- package/abis/IContractMetadata.json +28 -0
- package/abis/IERC1155.json +295 -0
- package/abis/IERC1155Errors.json +104 -0
- package/abis/IERC1155MetadataURI.json +314 -0
- package/abis/IERC1155Receiver.json +99 -0
- package/abis/IERC1271.json +26 -0
- package/abis/IERC165.json +21 -0
- package/abis/IERC1822Proxiable.json +15 -0
- package/abis/IERC20.json +224 -0
- package/abis/IERC20Errors.json +88 -0
- package/abis/IERC5267.json +51 -0
- package/abis/IERC721.json +287 -0
- package/abis/IERC721Enumerable.json +343 -0
- package/abis/IERC721Errors.json +105 -0
- package/abis/IERC721Metadata.json +332 -0
- package/abis/IERC721TokenReceiver.json +36 -0
- package/abis/IHasContractName.json +15 -0
- package/abis/IImmutableCreate2Factory.json +93 -0
- package/abis/IMulticall3.json +440 -0
- package/abis/IProtocolRewards.json +342 -0
- package/abis/ISafe.json +15 -0
- package/abis/ISymbol.json +15 -0
- package/abis/IVersionedContract.json +15 -0
- package/abis/IZoraCreator1155.json +343 -0
- package/abis/ImmutableCreate2FactoryUtils.json +15 -0
- package/abis/Initializable.json +25 -0
- package/abis/LibString.json +7 -0
- package/abis/Math.json +7 -0
- package/abis/Mock1155.json +547 -0
- package/abis/MockERC20.json +322 -0
- package/abis/MockERC721.json +350 -0
- package/abis/MockMinter.json +64 -0
- package/abis/OwnableUpgradeable.json +99 -0
- package/abis/ProtocolRewards.json +494 -0
- package/abis/Proxy.json +6 -0
- package/abis/ProxyDeployerScript.json +15 -0
- package/abis/ProxyShim.json +112 -0
- package/abis/Script.json +15 -0
- package/abis/ShortStrings.json +18 -0
- package/abis/StdAssertions.json +379 -0
- package/abis/StdInvariant.json +180 -0
- package/abis/Strings.json +18 -0
- package/abis/Test.json +570 -0
- package/abis/UUPSUpgradeable.json +130 -0
- package/abis/UnorderedNoncesUpgradeable.json +42 -0
- package/abis/Vm.json +8627 -0
- package/abis/VmSafe.json +7297 -0
- package/abis/stdError.json +119 -0
- package/abis/stdStorageSafe.json +52 -0
- package/addresses/999999999.json +4 -0
- package/deterministicConfig/comments.json +8 -0
- package/dist/index.cjs +935 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +908 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/wagmiGenerated.d.ts +1354 -0
- package/dist/wagmiGenerated.d.ts.map +1 -0
- package/foundry.toml +24 -0
- package/package/index.ts +4 -0
- package/package/types.ts +5 -0
- package/package/wagmiGenerated.ts +907 -0
- package/package.json +62 -0
- package/remappings.txt +8 -0
- package/script/CommentsDeployerBase.sol +60 -0
- package/script/Deploy.s.sol +66 -0
- package/script/DeployImpl.s.sol +26 -0
- package/script/DeployNonDeterministic.s.sol +43 -0
- package/script/GenerateDeterministicParams.s.sol +55 -0
- package/script/bundle-abis.ts +109 -0
- package/script/storage-check.sh +57 -0
- package/script/update-contract-version.ts +63 -0
- package/scripts/abis.ts +3 -0
- package/scripts/backfillComments.ts +176 -0
- package/scripts/generateCommentsTestData.ts +247 -0
- package/scripts/getCommentsAddresses.ts +10 -0
- package/scripts/queries.ts +73 -0
- package/scripts/queryAndSaveComments.ts +48 -0
- package/scripts/queryQuantityOfComments.ts +53 -0
- package/scripts/signDeployAndCall.ts +51 -0
- package/scripts/turnkey.ts +36 -0
- package/scripts/utils.ts +127 -0
- package/scripts/writeComments.ts +198 -0
- package/slither.config.json +7 -0
- package/src/CommentsImpl.sol +552 -0
- package/src/deployments/CommentsDeployment.sol +14 -0
- package/src/interfaces/IComments.sol +156 -0
- package/src/interfaces/IZoraCreator1155.sol +12 -0
- package/src/proxy/Comments.sol +43 -0
- package/src/utils/EIP712UpgradeableWithChainId.sol +36 -0
- package/src/version/ContractVersionBase.sol +14 -0
- package/test/Comments.t.sol +482 -0
- package/test/CommentsTestBase.sol +86 -0
- package/test/Comments_mintAndComment.t.sol +101 -0
- package/test/Comments_permit.t.sol +397 -0
- package/test/mocks/Mock1155.sol +50 -0
- package/test/mocks/MockMinter.sol +29 -0
- package/test/mocks/ProtocolRewards.sol +1497 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +11 -0
- 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
|
+
}
|