From 8d64936ecd0717a3860f7e0d7ba3240e717bd10f Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Wed, 7 Jun 2023 23:32:57 +0200 Subject: [PATCH] Update EIP-6381: Add bulk actions and presigned emotes Merged by EIP-Bot. --- EIPS/eip-6381.md | 205 +++++++++- .../eip-6381/contracts/EmotableRepository.sol | 271 +++++++++++++ assets/eip-6381/contracts/IERC6381.sol | 60 +++ assets/eip-6381/test/emotableRepository.ts | 360 +++++++++++++++++- 4 files changed, 891 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-6381.md b/EIPS/eip-6381.md index 1fa3003cfd2067..4087da1a3ddcfa 100644 --- a/EIPS/eip-6381.md +++ b/EIPS/eip-6381.md @@ -49,7 +49,7 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL ```solidity /// @title ERC-6381 Emotable Extension for Non-Fungible Tokens /// @dev See https://eips.ethereum.org/EIPS/eip-6381 -/// @dev Note: the ERC-165 identifier for this interface is 0x08eb97a6. +/// @dev Note: the ERC-165 identifier for this interface is 0xd9fac55a. pragma solidity ^0.8.16; @@ -84,6 +84,19 @@ interface IERC6381 /*is IERC165*/ { bytes4 emoji ) external view returns (uint256); + /** + * @notice Used to get the number of emotes for a specific emoji on a set of tokens. + * @param collection An array of addresses of the collections containing the tokens being checked for emoji count + * @param tokenIds An array of IDs of the tokens to check for emoji count + * @param emojis An array of unicode identifiers of the emojis + * @return An array of numbers of emotes with the emoji on the tokens + */ + function bulkEmoteCountOf( + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis + ) external view returns (uint256[] memory); + /** * @notice Used to get the information on whether the specified address has used a specific emoji on a specific * token. @@ -101,11 +114,65 @@ interface IERC6381 /*is IERC165*/ { bytes4 emoji ) external view returns (bool); + /** + * @notice Used to get the information on whether the specified addresses have used specific emojis on specific + * tokens. + * @param collections An array of addresses of the collection smart contracts containing the tokens being checked + * for emoji reactions + * @param emoters An array of addresses of the accounts we are checking for reactions to tokens + * @param tokenIds An array of IDs of the tokens being checked for emoji reactions + * @param emojis An array of the ASCII emoji codes being checked for reactions + * @return An array of boolean values indicating whether the `emoter`s has used the `emoji`s on the tokens (`true`) + * or not (`false`) + */ + function haveEmotersUsedEmotes( + address[] memory emoters, + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis + ) external view returns (bool[] memory); + + /** + * @notice Used to get the message to be signed by the `emoter` in order for the reaction to be submitted by someone + * else. + * @param collection The address of the collection smart contract containing the token being emoted at + * @param tokenId ID of the token being emoted + * @param emoji Unicode identifier of the emoji + * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote + * @param deadline UNIX timestamp of the deadline for the signature to be submitted + * @return The message to be signed by the `emoter` in order for the reaction to be submitted by someone else + */ + function prepareMessageToPresignEmote( + address collection, + uint256 tokenId, + bytes4 emoji, + bool state, + uint256 deadline + ) external view returns (bytes32); + + /** + * @notice Used to get multiple messages to be signed by the `emoter` in order for the reaction to be submitted by someone + * else. + * @param collections An array of addresses of the collection smart contracts containing the tokens being emoted at + * @param tokenIds An array of IDs of the tokens being emoted + * @param emojis An arrau of unicode identifiers of the emojis + * @param states An array of boolean values signifying whether to emote (`true`) or undo (`false`) emote + * @param deadlines An array of UNIX timestamps of the deadlines for the signatures to be submitted + * @return The array of messages to be signed by the `emoter` in order for the reaction to be submitted by someone else + */ + function prepareMessageToPresignEmote( + address collection, + uint256 tokenId, + bytes4 emoji, + bool state, + uint256 deadline + ) external view returns (bytes32); + /** * @notice Used to emote or undo an emote on a token. * @dev Does nothing if attempting to set a pre-existent state. * @dev MUST emit the `Emoted` event is the state of the emote is changed. - * @param collection Address of the collection containing the token being checked for emoji count + * @param collection Address of the collection containing the token being emoted at * @param tokenId ID of the token being emoted * @param emoji Unicode identifier of the emoji * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote @@ -116,9 +183,125 @@ interface IERC6381 /*is IERC165*/ { bytes4 emoji, bool state ) external; + + /** + * @notice Used to emote or undo an emote on multiple tokens. + * @dev Does nothing if attempting to set a pre-existent state. + * @dev MUST emit the `Emoted` event is the state of the emote is changed. + * @dev MUST revert if the lengths of the `collections`, `tokenIds`, `emojis` and `states` arrays are not equal. + * @param collections An array of addresses of the collections containing the tokens being emoted at + * @param tokenIds An array of IDs of the tokens being emoted + * @param emojis An array of unicode identifiers of the emojis + * @param states An array of boolean values signifying whether to emote (`true`) or undo (`false`) emote + */ + function bulkEmote( + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis, + bool[] memory states + ) external; + + /** + * @notice Used to emote or undo an emote on someone else's behalf. + * @dev Does nothing if attempting to set a pre-existent state. + * @dev MUST emit the `Emoted` event is the state of the emote is changed. + * @dev MUST revert if the lengths of the `collections`, `tokenIds`, `emojis` and `states` arrays are not equal. + * @dev MUST revert if the `deadline` has passed. + * @dev MUST revert if the recovered address is the zero address. + * @param emoter The address that presigned the emote + * @param collection The address of the collection smart contract containing the token being emoted at + * @param tokenId IDs of the token being emoted + * @param emoji Unicode identifier of the emoji + * @param states Boolean value signifying whether to emote (`true`) or undo (`false`) emote + * @param deadline UNIX timestamp of the deadline for the signature to be submitted + * @param v `v` value of an ECDSA signature of the message obtained via `prepareMessageToPresignEmote` + * @param r `r` value of an ECDSA signature of the message obtained via `prepareMessageToPresignEmote` + * @param s `s` value of an ECDSA signature of the message obtained via `prepareMessageToPresignEmote` + */ + function presignedEmote( + address emoter, + address collection, + uint256 tokenId, + bytes4 emoji, + bool state, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @notice Used to bulk emote or undo an emote on someone else's behalf. + * @dev Does nothing if attempting to set a pre-existent state. + * @dev MUST emit the `Emoted` event is the state of the emote is changed. + * @dev MUST revert if the lengths of the `collections`, `tokenIds`, `emojis` and `states` arrays are not equal. + * @dev MUST revert if the `deadline` has passed. + * @dev MUST revert if the recovered address is the zero address. + * @param emoters An array of addresses of the accounts that presigned the emotes + * @param collections An array of addresses of the collections containing the tokens being emoted at + * @param tokenIds An array of IDs of the tokens being emoted + * @param emojis An array of unicode identifiers of the emojis + * @param states An array of boolean values signifying whether to emote (`true`) or undo (`false`) emote + * @param deadline UNIX timestamp of the deadline for the signature to be submitted + * @param v An array of `v` values of an ECDSA signatures of the messages obtained via `prepareMessageToPresignEmote` + * @param r An array of `r` values of an ECDSA signatures of the messages obtained via `prepareMessageToPresignEmote` + * @param s An array of `s` values of an ECDSA signatures of the messages obtained via `prepareMessageToPresignEmote` + */ + function bulkPresignedEmote( + address[] memory emoters, + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis, + bool[] memory states, + uint256[] memory deadlines, + uint8[] memory v, + bytes32[] memory r, + bytes32[] memory s + ) external; } ``` +### Message format for presigned emotes + +The message to be signed by the `emoter` in order for the reaction to be submitted by someone else is formatted as follows: + +```solidity +keccak256( + abi.encode( + DOMAIN_SEPARATOR, + collection, + tokenId, + emoji, + state, + deadline + ) + ); +``` + +The values passed when generating the message to be signed are: + +- `DOMAIN_SEPARATOR` - The domain separator of the Emotable repository smart contract +- `collection` - Address of the collection containing the token being emoted at +- `tokenId` - ID of the token being emoted +- `emoji` - Unicode identifier of the emoji +- `state` - Boolean value signifying whether to emote (`true`) or undo (`false`) emote +- `deadline` - UNIX timestamp of the deadline for the signature to be submitted + +The `DOMAIN_SEPARATOR` is generated as follows: + +```solidity +keccak256( + abi.encode( + "ERC-6381: Public Non-Fungible Token Emote Repository", + "1", + block.chainid, + address(this) + ) + ); +``` + +Each chain, that the Emotable repository smart contract is deployed on, will have a different `DOMAIN_SEPARATOR` value due to chain IDs being different. + ### Pre-determined address of the Emotable repository The address of the Emotable repository smart contract is designed to resemble the function it serves. It starts with `0x311073` which is the abstract representation of `EMOTE`. The address is: @@ -138,6 +321,18 @@ The impressions could have been done using user-supplied strings or numeric valu 3. **Should the proposal establish an emotable extension or a common-good repository?**\ Initially we set out to create an emotable extension to be used with any ERC-721 compilant tokens. However, we realized that the proposal would be more useful if it was a common-good repository of emotable tokens. This way, the tokens that can be reacted to are not only the new ones but also the old ones that have been around since before the proposal.\ In line with this decision, we decided to calculate a deterministic address for the repository smart contract. This way, the repository can be used by any NFT collection without the need to search for the address on the given chain. +4. **Should we include only single-action operations, only multi-action operations, or both?**\ +We've considered including only single-action operations, where the user is only able to react with a single emoji to a single token, but we decided to include both single-action and multi-action operations. This way, the users can choose whether they want to emote or undo emote on a single token or on multiple tokens at once.\ +This decision was made for the long-term viability of the proposal. Based on the gas cost of the network and the number of tokens in the collection, the user can choose the most cost-effective way of emoting. +5. **Should we add the ability to emote on someone else's behalf?**\ +While we did not intend to add this as part of the proposal when drafting it, we realized that it would be a useful feature for it. This way, the users can emote on behalf of someone else, for example, if they are not able to do it themselves or if the emote is earned through an off-chain activity. +6. **How do we ensure that emoting on someone else's behalf is legitimate?**\ +We could add delegates to the proposal; when a user delegates their right to emote to someone else, the delegate can emote on their behalf. However, this would add a lot of complexity and additional logic to the proposal.\ +Using ECDSA signatures, we can ensure that the user has given their consent to emote on their behalf. This way, the user can sign a message with the parameters of the emote and the signature can be submitted by someone else. +7. **Should we add chain ID as a parameter when reacting to a token?**\ +During the course of discussion of the proposal, a suggestion arose that we could add chain ID as a parameter when reacting to a token. This would allow the users to emote on the token of one chain on another chain.\ +We decided against this as we feel that additional parameter would rarely be used and would add additional cost to the reaction transactions. If the collection smart contract wants to utilize on-chain emotes to tokens they contain, they require the reactions to be recorded on the same chain. Marketplaces and wallets integrating this proposal will rely on reactions to reside in the same chain as well, because if chain ID parameter was supported this would mean that they would need to query the repository smart contract on all of the chains the repository is deployed in order to get the reactions for a given token.\ +Additionally, if the collection creator wants users to record their reactions on a different chain, they can still direct the users to do just that. The repository does not validate the existence of the token being reacted to, which in theory means that you can react to non-existent token or to a token that does not exist yet. The likelihood of a different collection existing at the same address on another chain is significantly low, so the users can react using the collection's address on another chain and it is very unlikely that they will unintentionally react to another collection's token. ## Backwards Compatibility @@ -161,7 +356,11 @@ See [`EmotableRepository.sol`](../assets/eip-6381/contracts/EmotableRepository.s ## Security Considerations -The same security considerations as with [ERC-721](./eip-721.md) apply: hidden logic may be present in any of the functions, including burn, add asset, accept asset, and more. +The proposal does not envision handling any form of assets from the user, so the assets should not be at risk when interacting with an Emote repository. + +The ability to use ECDSA signatures to emote on someone else's behalf introduces the risk of a replay attack, which the format of the message to be signed guards against. The `DOMAIN_SEPARATOR` used in the message to be signed is unique to the repository smart contract of the chain it is deployed on. This means that the signature is invalid on any other chain and the Emote repositories deployed on them should revert the operation if a replay attack is attempted. + +Another thing to consider is the ability of presigned message reuse. Since the message includes the signature validity deadline, the message can be reused any number of times before the deadline is reached. The proposal only allows for a single reaction with a given emoji to a specific token to be active, so the presigned message can not be abused to increase the reaction count on the token. However, if the service using the repository relies on the ability to revoke the reaction after certain actions, a valid presigned message can be used to re-react to the token. We suggest that the services using the repository in cnjunction with presigned messages use deadlines that invalidate presigned messages after a reasonalby short period of time. Caution is advised when dealing with non-audited contracts. diff --git a/assets/eip-6381/contracts/EmotableRepository.sol b/assets/eip-6381/contracts/EmotableRepository.sol index d66c36b59b9490..445ae2454f4fad 100644 --- a/assets/eip-6381/contracts/EmotableRepository.sol +++ b/assets/eip-6381/contracts/EmotableRepository.sol @@ -5,7 +5,20 @@ pragma solidity ^0.8.16; import "./IERC6381.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +error BulkParametersOfUnequalLength(); +error ExpiredPresignedEmote(); +error InvalidSignature(); + contract EmotableRepository is IERC6381 { + bytes32 public immutable DOMAIN_SEPARATOR = keccak256( + abi.encode( + "ERC-6381: Public Non-Fungible Token Emote Repository", + "1", + block.chainid, + address(this) + ) + ); + // Used to avoid double emoting and control undoing mapping(address => mapping(address => mapping(uint256 => mapping(bytes4 => uint256)))) private _emotesUsedByEmoter; // Cheaper than using a bool @@ -20,6 +33,28 @@ contract EmotableRepository is IERC6381 { return _emotesPerToken[collection][tokenId][emoji]; } + function bulkEmoteCountOf( + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis + ) public view returns (uint256[] memory) { + if( + collections.length != tokenIds.length || + collections.length != emojis.length + ){ + revert BulkParametersOfUnequalLength(); + } + + uint256[] memory counts = new uint256[](collections.length); + for (uint256 i; i < collections.length; ) { + counts[i] = _emotesPerToken[collections[i]][tokenIds[i]][emojis[i]]; + unchecked { + ++i; + } + } + return counts; + } + function hasEmoterUsedEmote( address emoter, address collection, @@ -29,6 +64,30 @@ contract EmotableRepository is IERC6381 { return _emotesUsedByEmoter[emoter][collection][tokenId][emoji] == 1; } + function haveEmotersUsedEmotes( + address[] memory emoters, + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis + ) public view returns (bool[] memory) { + if( + emoters.length != collections.length || + emoters.length != tokenIds.length || + emoters.length != emojis.length + ){ + revert BulkParametersOfUnequalLength(); + } + + bool[] memory states = new bool[](collections.length); + for (uint256 i; i < collections.length; ) { + states[i] = _emotesUsedByEmoter[emoters[i]][collections[i]][tokenIds[i]][emojis[i]] == 1; + unchecked { + ++i; + } + } + return states; + } + function emote( address collection, uint256 tokenId, @@ -51,6 +110,218 @@ contract EmotableRepository is IERC6381 { } } + function bulkEmote( + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis, + bool[] memory states + ) public { + if( + collections.length != tokenIds.length || + collections.length != emojis.length || + collections.length != states.length + ){ + revert BulkParametersOfUnequalLength(); + } + + bool currentVal; + for (uint256 i; i < collections.length; ) { + currentVal = _emotesUsedByEmoter[msg.sender][collections[i]][tokenIds[i]][ + emojis[i] + ] == 1; + if (currentVal != states[i]) { + if (states[i]) { + _emotesPerToken[collections[i]][tokenIds[i]][emojis[i]] += 1; + } else { + _emotesPerToken[collections[i]][tokenIds[i]][emojis[i]] -= 1; + } + _emotesUsedByEmoter[msg.sender][collections[i]][tokenIds[i]][emojis[i]] = states[i] + ? 1 + : 0; + emit Emoted(msg.sender, collections[i], tokenIds[i], emojis[i], states[i]); + } + unchecked { + ++i; + } + } + } + + function prepareMessageToPresignEmote( + address collection, + uint256 tokenId, + bytes4 emoji, + bool state, + uint256 deadline + ) public view returns (bytes32) { + return keccak256( + abi.encode( + DOMAIN_SEPARATOR, + collection, + tokenId, + emoji, + state, + deadline + ) + ); + } + + function bulkPrepareMessagesToPresignEmote( + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis, + bool[] memory states, + uint256[] memory deadlines + ) public view returns (bytes32[] memory) { + if( + collections.length != tokenIds.length || + collections.length != emojis.length || + collections.length != states.length || + collections.length != deadlines.length + ){ + revert BulkParametersOfUnequalLength(); + } + + bytes32[] memory messages = new bytes32[](collections.length); + for (uint256 i; i < collections.length; ) { + messages[i] = keccak256( + abi.encode( + DOMAIN_SEPARATOR, + collections[i], + tokenIds[i], + emojis[i], + states[i], + deadlines[i] + ) + ); + unchecked { + ++i; + } + } + + return messages; + } + + function presignedEmote( + address emoter, + address collection, + uint256 tokenId, + bytes4 emoji, + bool state, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public { + if(block.timestamp > deadline){ + revert ExpiredPresignedEmote(); + } + bytes32 digest = keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256( + abi.encode( + DOMAIN_SEPARATOR, + collection, + tokenId, + emoji, + state, + deadline + ) + ) + ) + ); + address signer = ecrecover(digest, v, r, s); + if(signer != emoter){ + revert InvalidSignature(); + } + + bool currentVal = _emotesUsedByEmoter[signer][collection][tokenId][ + emoji + ] == 1; + if (currentVal != state) { + if (state) { + _emotesPerToken[collection][tokenId][emoji] += 1; + } else { + _emotesPerToken[collection][tokenId][emoji] -= 1; + } + _emotesUsedByEmoter[signer][collection][tokenId][emoji] = state + ? 1 + : 0; + emit Emoted(signer, collection, tokenId, emoji, state); + } + } + + function bulkPresignedEmote( + address[] memory emoters, + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis, + bool[] memory states, + uint256[] memory deadlines, + uint8[] memory v, + bytes32[] memory r, + bytes32[] memory s + ) public { + if( + emoters.length != collections.length || + emoters.length != tokenIds.length || + emoters.length != emojis.length || + emoters.length != states.length || + emoters.length != deadlines.length || + emoters.length != v.length || + emoters.length != r.length || + emoters.length != s.length + ){ + revert BulkParametersOfUnequalLength(); + } + + bytes32 digest; + address signer; + bool currentVal; + for (uint256 i; i < collections.length; ) { + if (block.timestamp > deadlines[i]){ + revert ExpiredPresignedEmote(); + } + digest = keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256( + abi.encode( + DOMAIN_SEPARATOR, + collections[i], + tokenIds[i], + emojis[i], + states[i], + deadlines[i] + ) + ) + ) + ); + signer = ecrecover(digest, v[i], r[i], s[i]); + if(signer != emoters[i]){ + revert InvalidSignature(); + } + + currentVal = _emotesUsedByEmoter[signer][collections[i]][tokenIds[i]][ + emojis[i] + ] == 1; + if (currentVal != states[i]) { + if (states[i]) { + _emotesPerToken[collections[i]][tokenIds[i]][emojis[i]] += 1; + } else { + _emotesPerToken[collections[i]][tokenIds[i]][emojis[i]] -= 1; + } + _emotesUsedByEmoter[signer][collections[i]][tokenIds[i]][emojis[i]] = states[i] + ? 1 + : 0; + emit Emoted(signer, collections[i], tokenIds[i], emojis[i], states[i]); + } + unchecked { + ++i; + } + } + } + function supportsInterface( bytes4 interfaceId ) public view virtual returns (bool) { diff --git a/assets/eip-6381/contracts/IERC6381.sol b/assets/eip-6381/contracts/IERC6381.sol index 0d31de0a6aaf05..a579c5825d9620 100644 --- a/assets/eip-6381/contracts/IERC6381.sol +++ b/assets/eip-6381/contracts/IERC6381.sol @@ -17,6 +17,12 @@ interface IERC6381 { bytes4 emoji ) external view returns (uint256); + function bulkEmoteCountOf( + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis + ) external view returns (uint256[] memory); + function hasEmoterUsedEmote( address emoter, address collection, @@ -24,10 +30,64 @@ interface IERC6381 { bytes4 emoji ) external view returns (bool); + function haveEmotersUsedEmotes( + address[] memory emoters, + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis + ) external view returns (bool[] memory); + + function prepareMessageToPresignEmote( + address collection, + uint256 tokenId, + bytes4 emoji, + bool state, + uint256 deadline + ) external view returns (bytes32); + + function bulkPrepareMessagesToPresignEmote( + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis, + bool[] memory states, + uint256[] memory deadlines + ) external view returns (bytes32[] memory); + function emote( address collection, uint256 tokenId, bytes4 emoji, bool state ) external; + + function bulkEmote( + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis, + bool[] memory states + ) external; + + function presignedEmote( + address emoter, + address collection, + uint256 tokenId, + bytes4 emoji, + bool state, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + function bulkPresignedEmote( + address[] memory emoters, + address[] memory collections, + uint256[] memory tokenIds, + bytes4[] memory emojis, + bool[] memory states, + uint256[] memory deadlines, + uint8[] memory v, + bytes32[] memory r, + bytes32[] memory s + ) external; } \ No newline at end of file diff --git a/assets/eip-6381/test/emotableRepository.ts b/assets/eip-6381/test/emotableRepository.ts index 11dd7d2e1be5ef..18345bf4032d6a 100644 --- a/assets/eip-6381/test/emotableRepository.ts +++ b/assets/eip-6381/test/emotableRepository.ts @@ -40,8 +40,8 @@ describe("RMRKEmotableRepositoryMock", async function () { repository = await loadFixture(emotableRepositoryFixture); }); - it("can support IEmotableRepository", async function () { - expect(await repository.supportsInterface("0x08eb97a6")).to.equal(true); + it("can support IERC6381", async function () { + expect(await repository.supportsInterface("0xd9fac55a")).to.equal(true); }); it("can support IERC165", async function () { @@ -132,5 +132,361 @@ describe("RMRKEmotableRepositoryMock", async function () { await repository.emoteCountOf(token.address, tokenId, emoji2) ).to.equal(bn(0)); }); + + it("can bulk emote", async function () { + expect( + await repository.bulkEmoteCountOf( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([bn(0), bn(0)]); + + expect( + await repository.haveEmotersUsedEmotes( + [owner.address, owner.address], + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([false, false]); + + await expect( + repository.bulkEmote( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2], + [true, true] + ) + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji1, + true + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji2, + true + ); + + expect( + await repository.bulkEmoteCountOf( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([bn(1), bn(1)]); + + expect( + await repository.haveEmotersUsedEmotes( + [owner.address, owner.address], + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([true, true]); + }); + + it("can bulk undo emote", async function () { + await expect( + repository.bulkEmote( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2], + [true, true] + ) + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji1, + true + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji2, + true + ); + + expect( + await repository.bulkEmoteCountOf( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([bn(1), bn(1)]); + + expect( + await repository.haveEmotersUsedEmotes( + [owner.address, owner.address], + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([true, true]); + + await expect( + repository.bulkEmote( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2], + [false, false] + ) + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji1, + false + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji2, + false + ); + + expect( + await repository.bulkEmoteCountOf( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([bn(0), bn(0)]); + + expect( + await repository.haveEmotersUsedEmotes( + [owner.address, owner.address], + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([false, false]); + }); + + it("can bulk emote and unemote at the same time", async function () { + await repository.emote(token.address, tokenId, emoji2, true); + + expect( + await repository.bulkEmoteCountOf( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([bn(0), bn(1)]); + + expect( + await repository.haveEmotersUsedEmotes( + [owner.address, owner.address], + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([false, true]); + + await expect( + repository.bulkEmote( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2], + [true, false] + ) + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji1, + true + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji2, + false + ); + + expect( + await repository.bulkEmoteCountOf( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([bn(1), bn(0)]); + + expect( + await repository.haveEmotersUsedEmotes( + [owner.address, owner.address], + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2] + ) + ).to.eql([true, false]); + }); + + it("can not bulk emote if passing arrays of different length", async function () { + await expect( + repository.bulkEmote( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2], + [true] + ) + ).to.be.revertedWithCustomError( + repository, + "BulkParametersOfUnequalLength" + ); + + await expect( + repository.bulkEmote( + [token.address], + [tokenId, tokenId], + [emoji1, emoji2], + [true, true] + ) + ).to.be.revertedWithCustomError( + repository, + "BulkParametersOfUnequalLength" + ); + + await expect( + repository.bulkEmote( + [token.address, token.address], + [tokenId], + [emoji1, emoji2], + [true, true] + ) + ).to.be.revertedWithCustomError( + repository, + "BulkParametersOfUnequalLength" + ); + + await expect( + repository.bulkEmote( + [token.address, token.address], + [tokenId, tokenId], + [emoji1], + [true, true] + ) + ).to.be.revertedWithCustomError( + repository, + "BulkParametersOfUnequalLength" + ); + }); + + it("can use presigned emote to react to token", async function () { + const message = await repository.prepareMessageToPresignEmote( + token.address, + tokenId, + emoji1, + true, + bn(9999999999) + ); + + const signature = await owner.signMessage(ethers.utils.arrayify(message)); + + const r: string = signature.substring(0, 66); + const s: string = "0x" + signature.substring(66, 130); + const v: number = parseInt(signature.substring(130, 132), 16); + + await expect( + repository + .connect(addrs[0]) + .presignedEmote( + owner.address, + token.address, + tokenId, + emoji1, + true, + bn(9999999999), + v, + r, + s + ) + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji1, + true + ); + }); + + it("can use presigned emotes to bulk react to token", async function () { + const messages = await repository.bulkPrepareMessagesToPresignEmote( + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2], + [true, true], + [bn(9999999999), bn(9999999999)] + ); + + const signature1 = await owner.signMessage( + ethers.utils.arrayify(messages[0]) + ); + const signature2 = await owner.signMessage( + ethers.utils.arrayify(messages[1]) + ); + + const r1: string = signature1.substring(0, 66); + const s1: string = "0x" + signature1.substring(66, 130); + const v1: number = parseInt(signature1.substring(130, 132), 16); + const r2: string = signature2.substring(0, 66); + const s2: string = "0x" + signature2.substring(66, 130); + const v2: number = parseInt(signature2.substring(130, 132), 16); + + await expect( + repository + .connect(addrs[0]) + .bulkPresignedEmote( + [owner.address, owner.address], + [token.address, token.address], + [tokenId, tokenId], + [emoji1, emoji2], + [true, true], + [bn(9999999999), bn(9999999999)], + [v1, v2], + [r1, r2], + [s1, s2] + ) + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji1, + true + ) + .to.emit(repository, "Emoted") + .withArgs( + owner.address, + token.address, + tokenId.toNumber(), + emoji2, + true + ); + }); }); });