diff --git a/src/ERC721ACH.sol b/src/ERC721ACH.sol index cbae07a..666ddef 100644 --- a/src/ERC721ACH.sol +++ b/src/ERC721ACH.sol @@ -5,6 +5,7 @@ import {ERC721AC} from "ERC721C/erc721c/ERC721AC.sol"; import {IERC721A} from "erc721a/contracts/IERC721A.sol"; import {IBeforeTokenTransfersHook} from "./interfaces/IBeforeTokenTransfersHook.sol"; import {IAfterTokenTransfersHook} from "./interfaces/IAfterTokenTransfersHook.sol"; +import {IOwnerOfHook} from "./interfaces/IOwnerOfHook.sol"; import {IERC721ACH} from "./interfaces/IERC721ACH.sol"; /** @@ -93,6 +94,20 @@ contract ERC721ACH is ERC721AC, IERC721ACH { } } + function ownerOf(uint256 tokenId) public view virtual override returns (address) { + + IOwnerOfHook ownerOfHook = IOwnerOfHook(hooks[HookType.OwnerOf]); + + if ( + address(ownerOfHook) != address(0) && + ownerOfHook.useOwnerOfHook(tokenId) + ) { + return ownerOfHook.ownerOfOverrideHook(tokenId); + } + + return super.ownerOf(tokenId); + } + /** * @notice Returns the contract address for a specified hook type. diff --git a/src/interfaces/IERC721ACH.sol b/src/interfaces/IERC721ACH.sol index 4b2ee59..a0f7493 100644 --- a/src/interfaces/IERC721ACH.sol +++ b/src/interfaces/IERC721ACH.sol @@ -13,8 +13,9 @@ interface IERC721ACH { /// @notice Hook for custom logic before a token transfer occurs. BeforeTokenTransfers, /// @notice Hook for custom logic after a token transfer occurs. - AfterTokenTransfers - + AfterTokenTransfers, + /// @notice Hook for custom logic for ownerOf() function. + OwnerOf } diff --git a/src/interfaces/IOwnerOfHook.sol b/src/interfaces/IOwnerOfHook.sol new file mode 100644 index 0000000..74e009d --- /dev/null +++ b/src/interfaces/IOwnerOfHook.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +/// @title IOwnerOfHook +/// @dev Interface that defines hooks for retrieving the owner of a token. +interface IOwnerOfHook { + + /** + @notice Emitted when the owner of hook is used. + @param tokenId The ID of the token whose owner is being retrieved. + @param owner The address of the owner of the token. + */ + event OwnerOfHookUsed( + uint256 tokenId, + address owner + ); + + /** + @notice Checks if the owner retrieval function should use the custom hook. + @param tokenId The ID of the token whose owner is being retrieved. + @return A boolean indicating whether or not to use the custom hook for the owner retrieval function. + */ + function useOwnerOfHook( + uint256 tokenId + ) external view returns (bool); + + /** + @notice Provides a custom implementation for the owner retrieval process. + @param tokenId The ID of the token whose owner is being retrieved. + @return The address of the owner of the token. + */ + function ownerOfOverrideHook( + uint256 tokenId + ) external view returns (address); +} diff --git a/test/hooks/OwnerOfHook.t.sol b/test/hooks/OwnerOfHook.t.sol new file mode 100644 index 0000000..8e997ba --- /dev/null +++ b/test/hooks/OwnerOfHook.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {Vm} from "forge-std/Vm.sol"; +import {DSTest} from "ds-test/test.sol"; +import {ERC721ACHMock} from "../utils/ERC721ACHMock.sol"; +import {IERC721A} from "lib/ERC721A/contracts/IERC721A.sol"; +import {OwnerOfHookMock} from "../utils/hooks/OwnerOfHookMock.sol"; +import {IERC721ACH} from "../../src/interfaces/IERC721ACH.sol"; + +contract OwnerOfHookTest is DSTest { + Vm public constant vm = Vm(HEVM_ADDRESS); + address public constant DEFAULT_OWNER_ADDRESS = address(0xC0FFEE); + address public constant DEFAULT_BUYER_ADDRESS = address(0xBABE); + ERC721ACHMock erc721Mock; + OwnerOfHookMock hookMock; + + IERC721ACH.HookType constant OwnerOf = IERC721ACH.HookType.OwnerOf; + + function setUp() public { + erc721Mock = new ERC721ACHMock(DEFAULT_OWNER_ADDRESS); + hookMock = new OwnerOfHookMock(); + } + + function test_ownerOfHook() public { + assertEq(address(0), address(erc721Mock.getHook(OwnerOf))); + } + + function test_setOwnerOfHook() public { + assertEq(address(0), address(erc721Mock.getHook(OwnerOf))); + + // calling an admin function without being the contract owner should revert + vm.expectRevert(); + erc721Mock.setHook(OwnerOf, address(hookMock)); + + vm.prank(DEFAULT_OWNER_ADDRESS); + erc721Mock.setHook(OwnerOf, address(hookMock)); + assertEq(address(hookMock), address(erc721Mock.getHook(OwnerOf))); + } + + function test_ownerOfHook(uint256 tokenId) public { + vm.assume(tokenId > 0); + vm.assume(tokenId < 10); + + test_setOwnerOfHook(); + erc721Mock.mint(DEFAULT_BUYER_ADDRESS, tokenId); + + assertEq(DEFAULT_BUYER_ADDRESS, erc721Mock.ownerOf(tokenId)); + + hookMock.setHooksEnabled(true); + vm.expectRevert(OwnerOfHookMock.OwnerOfHook_Executed.selector); + erc721Mock.ownerOf(tokenId); + } +} diff --git a/test/utils/hooks/OwnerOfHookMock.sol b/test/utils/hooks/OwnerOfHookMock.sol new file mode 100644 index 0000000..8b97ab1 --- /dev/null +++ b/test/utils/hooks/OwnerOfHookMock.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {IOwnerOfHook} from "../../../src/interfaces/IOwnerOfHook.sol"; + +contract OwnerOfHookMock is IOwnerOfHook { + /// @notice hook was executed + error OwnerOfHook_Executed(); + + bool public hooksEnabled; + address public fixedOwner; + + /// @notice toggle ownerOf hook. + function setHooksEnabled(bool _enabled) public { + hooksEnabled = _enabled; + } + + /// @notice set fixed owner returned by the hook. + function setFixedOwner(address _fixedOwner) public { + fixedOwner = _fixedOwner; + } + + /// @notice Check if the ownerOf function should use hook. + /// @dev Returns whether or not to use the hook for ownerOf function + function useOwnerOfHook( + uint256 + ) external view override returns (bool) { + return hooksEnabled; + } + + /// @notice custom implementation for ownerOf Hook. + function ownerOfOverrideHook( + uint256 + ) external view override returns (address) { + revert OwnerOfHook_Executed(); + } +}