diff --git a/src/ERC721Drop.sol b/src/ERC721Drop.sol index 5a5085a..a92b438 100644 --- a/src/ERC721Drop.sol +++ b/src/ERC721Drop.sol @@ -33,12 +33,14 @@ import {IERC721Drop} from "./interfaces/IERC721Drop.sol"; import {IOwnable} from "./interfaces/IOwnable.sol"; import {IERC4906} from "./interfaces/IERC4906.sol"; import {IFactoryUpgradeGate} from "./interfaces/IFactoryUpgradeGate.sol"; +import {ITransferHookExtension} from "./interfaces/ITransferHookExtension.sol"; import {OwnableSkeleton} from "./utils/OwnableSkeleton.sol"; import {FundsReceiver} from "./utils/FundsReceiver.sol"; import {Version} from "./utils/Version.sol"; import {PublicMulticall} from "./utils/PublicMulticall.sol"; import {ERC721DropStorageV1} from "./storage/ERC721DropStorageV1.sol"; import {ERC721DropStorageV2} from "./storage/ERC721DropStorageV2.sol"; +import {ERC721TransferHookStorageV1, TransferHookStorage} from "./storage/ERC721TransferHookStorageV1.sol"; /** @@ -64,7 +66,8 @@ contract ERC721Drop is ERC721DropStorageV1, ERC721DropStorageV2, ERC721Rewards, - ERC721RewardsStorageV1 + ERC721RewardsStorageV1, + ERC721TransferHookStorageV1 { /// @dev This is the max mint batch size for the optimized ERC721A mint contract uint256 internal immutable MAX_MINT_BATCH_SIZE = 8; @@ -856,6 +859,31 @@ contract ERC721Drop is _setOwner(newOwner); } + /// @notice Admin function to set the NFT transfer hook, useful for metadata and non-transferrable NFTs. + /// @dev Set to 0 to disable, address to enable transfer hook. + /// @param newTransferHook new transfer hook to receive before token transfer events + function setTransferHook(address newTransferHook) public onlyAdmin { + if (newTransferHook == address(0) || ITransferHookExtension(newTransferHook).supportsInterface(type(ITransferHookExtension).interfaceId)) { + _setTransferHook(newTransferHook); + } else { + revert InvalidTransferHook(); + } + } + + /// @notice Handles the internal before token transfer hook + /// @param from address transfer is coming from + /// @param to address transfer is going to + /// @param tokenId token id for transfer + /// @param amount number of transfers + function _beforeTokenTransfers(address from, address to, uint256 tokenId, uint256 amount) internal override virtual { + TransferHookStorage storage transferHookStorage = _getTransferHookStorage(); + if (transferHookStorage.transferHookExtension != address(0)) { + ITransferHookExtension(transferHookStorage.transferHookExtension).beforeTokenTransfers(from, to, tokenId, amount); + } + + super._beforeTokenTransfers(from, to, tokenId, amount); + } + /// @notice Set a new metadata renderer /// @param newRenderer new renderer address to use /// @param setupRenderer data to setup new renderer with diff --git a/src/interfaces/ITransferHookExtension.sol b/src/interfaces/ITransferHookExtension.sol new file mode 100644 index 0000000..37f207f --- /dev/null +++ b/src/interfaces/ITransferHookExtension.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface ITransferHookExtension { + function beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) external; + function supportsInterface(bytes4 interfaceId) external returns (bool); +} \ No newline at end of file diff --git a/src/storage/ERC721TransferHookStorageV1.sol b/src/storage/ERC721TransferHookStorageV1.sol new file mode 100644 index 0000000..aa4a26e --- /dev/null +++ b/src/storage/ERC721TransferHookStorageV1.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IERC721Drop} from "../interfaces/IERC721Drop.sol"; + +/// @custom:storage-location erc7201:zora.erc721drop.transferhook +struct TransferHookStorage { + address transferHookExtension; +} + +contract ERC721TransferHookStorageV1 { + error InvalidTransferHook(); + event SetNewTransferHook(address _newTransferHook); + + // keccak256(abi.encode(uint256(keccak256("zora.erc721drop.transferhook")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant TRANSFER_HOOK_STORAGE_LOCATION = + 0x7dd1076582dd9e0dc6a5073ed536c067f2e92ed46866d3076f6f2d9a5e36b400; + + function _getTransferHookStorage() internal pure returns (TransferHookStorage storage $) { + assembly { + $.slot := TRANSFER_HOOK_STORAGE_LOCATION + } + } + + function _setTransferHook(address _newTransferHook) internal { + _getTransferHookStorage().transferHookExtension = _newTransferHook; + emit SetNewTransferHook(_newTransferHook); + } +}