From f6c1e64c63fb27410c4a3bacfff510da2a979787 Mon Sep 17 00:00:00 2001 From: Isabella Smallcombe Date: Wed, 27 Sep 2023 10:34:21 -0400 Subject: [PATCH 1/2] fix: token transfer hook for single token transfers --- foundry.toml | 6 +++--- src/interfaces/ITransferHookReceiver.sol | 17 +++++++++++++++++ src/nft/ZoraCreator1155Impl.sol | 21 +++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/foundry.toml b/foundry.toml index d5d3af477..46a4a6152 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,7 @@ fs_permissions = [{access = "read", path = "./addresses"}, {access = "read", path = "./chainConfigs"}, {access = "read", path = "./package.json"}] libs = ['_imagine', 'node_modules', 'script'] optimizer = true -optimizer_runs = 250 +optimizer_runs = 150 out = 'out' solc_version = '0.8.17' src = 'src' @@ -10,7 +10,7 @@ via_ir = true [profile.optimized] optimizer = true -optimizer_runs = 250 +optimizer_runs = 150 out = 'out' script = 'src' solc_version = '0.8.17' @@ -19,7 +19,7 @@ test = 'src' via_ir = true [profile.fast_compilation] -optimizer_runs = 200 +optimizer_runs = 150 solc_version = '0.8.17' [rpc_endpoints] diff --git a/src/interfaces/ITransferHookReceiver.sol b/src/interfaces/ITransferHookReceiver.sol index 0721fd919..fa2608e77 100644 --- a/src/interfaces/ITransferHookReceiver.sol +++ b/src/interfaces/ITransferHookReceiver.sol @@ -22,5 +22,22 @@ interface ITransferHookReceiver is IERC165Upgradeable { bytes memory data ) external; + /// @notice Token transfer batch callback + /// @param target target contract for transfer + /// @param operator operator address for transfer + /// @param from user address for amount transferred + /// @param to user address for amount transferred + /// @param id token id transferred + /// @param amount value transferred + /// @param data data as perscribed by 1155 standard + function onTokenTransfer( + address target, + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) external; // IERC165 type required } diff --git a/src/nft/ZoraCreator1155Impl.sol b/src/nft/ZoraCreator1155Impl.sol index c03e79c3b..baee2df54 100644 --- a/src/nft/ZoraCreator1155Impl.sol +++ b/src/nft/ZoraCreator1155Impl.sol @@ -671,6 +671,27 @@ contract ZoraCreator1155Impl is emit ConfigUpdated(msg.sender, ConfigUpdate.TRANSFER_HOOK, config); } + /// @notice Hook before token transfer that checks for a transfer hook integration + /// @param operator operator moving the tokens + /// @param from from address + /// @param to to address + /// @param id token id to move + /// @param amount amount of token + /// @param data data of token + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal override { + super._beforeTokenTransfer(operator, from, to, id, amount, data); + if (address(config.transferHook) != address(0)) { + config.transferHook.onTokenTransfer(address(this), operator, from, to, id, amount, data); + } + } + /// @notice Hook before token transfer that checks for a transfer hook integration /// @param operator operator moving the tokens /// @param from from address From 58332f342b10be99042b764096b9a9d84ff33957 Mon Sep 17 00:00:00 2001 From: Isabella Smallcombe Date: Wed, 27 Sep 2023 11:00:25 -0400 Subject: [PATCH 2/2] feat: add unit tests --- test/nft/ZoraCreator1155.t.sol | 98 ++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/test/nft/ZoraCreator1155.t.sol b/test/nft/ZoraCreator1155.t.sol index 70e3fc9d9..fad64176e 100644 --- a/test/nft/ZoraCreator1155.t.sol +++ b/test/nft/ZoraCreator1155.t.sol @@ -7,6 +7,7 @@ import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.so import {RewardsSettings} from "@zoralabs/protocol-rewards/src/abstract/RewardSplits.sol"; import {MathUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/math/MathUpgradeable.sol"; import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; +import {ITransferHookReceiver} from "../../src/interfaces/ITransferHookReceiver.sol"; import {Zora1155} from "../../src/proxies/Zora1155.sol"; import {ZoraCreatorFixedPriceSaleStrategy} from "../../src/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; @@ -22,6 +23,39 @@ import {SimpleMinter} from "../mock/SimpleMinter.sol"; import {SimpleRenderer} from "../mock/SimpleRenderer.sol"; import {MockUpgradeGate} from "../mock/MockUpgradeGate.sol"; + +contract MockTransferHookReceiver is ITransferHookReceiver { + mapping(uint256 => bool) public hasTransfer; + function onTokenTransferBatch( + address target, + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) external { + for (uint256 i = 0; i < ids.length; i++) { + hasTransfer[ids[i]] = true; + } + } + function onTokenTransfer( + address target, + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) external { + hasTransfer[id] = true; + } + function supportsInterface(bytes4 testInterface) external override view returns (bool) { + return testInterface == type(ITransferHookReceiver).interfaceId; + } +} + + contract ZoraCreator1155Test is Test { using stdJson for string; @@ -467,6 +501,70 @@ contract ZoraCreator1155Test is Test { assertEq(target.balanceOf(recipient, tokenId2), quantity2); } + + function test_adminMintBatchWithHook(uint256 quantity1, uint256 quantity2) external { + vm.assume(quantity1 < 1000); + vm.assume(quantity2 < 1000); + init(); + + vm.prank(admin); + uint256 tokenId1 = target.setupNewToken("test", 1000); + + vm.prank(admin); + uint256 tokenId2 = target.setupNewToken("test", 1000); + + uint256[] memory tokenIds = new uint256[](2); + uint256[] memory quantities = new uint256[](2); + tokenIds[0] = tokenId1; + tokenIds[1] = tokenId2; + quantities[0] = quantity1; + quantities[1] = quantity2; + + MockTransferHookReceiver testHook = new MockTransferHookReceiver(); + + vm.prank(admin); + target.setTransferHook(testHook); + + vm.prank(admin); + target.adminMintBatch(recipient, tokenIds, quantities, ""); + + IZoraCreator1155TypesV1.TokenData memory tokenData1 = target.getTokenInfo(tokenId1); + IZoraCreator1155TypesV1.TokenData memory tokenData2 = target.getTokenInfo(tokenId2); + + assertEq(testHook.hasTransfer(tokenId1), true); + assertEq(testHook.hasTransfer(tokenId2), true); + assertEq(testHook.hasTransfer(1000), false); + + assertEq(tokenData1.totalMinted, quantity1); + assertEq(tokenData2.totalMinted, quantity2); + assertEq(target.balanceOf(recipient, tokenId1), quantity1); + assertEq(target.balanceOf(recipient, tokenId2), quantity2); + } + + function test_adminMintWithHook(uint256 quantity1) external { + vm.assume(quantity1 < 1000); + init(); + + vm.prank(admin); + uint256 tokenId1 = target.setupNewToken("test", 1000); + + MockTransferHookReceiver testHook = new MockTransferHookReceiver(); + + vm.prank(admin); + target.setTransferHook(testHook); + + vm.prank(admin); + target.adminMint(recipient, tokenId1, quantity1, ""); + + IZoraCreator1155TypesV1.TokenData memory tokenData1 = target.getTokenInfo(tokenId1); + + assertEq(testHook.hasTransfer(tokenId1), true); + assertEq(testHook.hasTransfer(1000), false); + + assertEq(tokenData1.totalMinted, quantity1); + assertEq(target.balanceOf(recipient, tokenId1), quantity1); + } + function test_adminMintBatchWithSchedule(uint256 quantity1, uint256 quantity2) external { vm.assume(quantity1 < 900); vm.assume(quantity2 < 900);