diff --git a/src/example/NFTPortal.sol b/src/example/NFTPortal.sol new file mode 100644 index 00000000..38315d34 --- /dev/null +++ b/src/example/NFTPortal.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { IERC721, ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; +import { AbstractPortal } from "../interface/AbstractPortal.sol"; +import { Attestation, AttestationPayload } from "../types/Structs.sol"; + +/** + * @title NFT Portal + * @author Consensys + * @notice This contract aims to provide ERC 721 compatibility + * @dev This Portal implements parts of ERC 721 - balanceOf and ownerOf functions + */ +contract NFTPortal is AbstractPortal, ERC721 { + mapping(bytes owner => uint256 numberOfAttestations) private numberOfAttestationsPerOwner; + + constructor( + address[] memory modules, + address router + ) AbstractPortal(modules, router) ERC721("NFTPortal", "NFTPortal") {} + + /** + * @notice Count all attestations assigned to an owner + * @param owner An address for whom to query the balance + * @return The number of attestations owned by `owner`, possibly zero + */ + function balanceOf(address owner) public view virtual override returns (uint256) { + return numberOfAttestationsPerOwner[abi.encode(owner)]; + } + + /** + * @notice Find the owner of an attestation + * @param tokenId The identifier for an attestation + * @return The address of the owner of the attestation + */ + function ownerOf(uint256 tokenId) public view virtual override returns (address) { + bytes32 attestationId = bytes32(tokenId); + Attestation memory attestation = attestationRegistry.getAttestation(attestationId); + return abi.decode(attestation.subject, (address)); + } + + /** + * @notice Method run before a payload is attested + * @param attestationPayload the attestation payload supposed to be attested + */ + function _onAttest(AttestationPayload memory attestationPayload) internal override { + numberOfAttestationsPerOwner[attestationPayload.subject]++; + } + + function withdraw(address payable to, uint256 amount) external override {} + + /** + * @notice Verifies that a specific interface is implemented by the Portal, following ERC-165 specification + * @param interfaceID the interface identifier checked in this call + * @return The list of modules addresses linked to the Portal + */ + function supportsInterface(bytes4 interfaceID) public pure virtual override(AbstractPortal, ERC721) returns (bool) { + return + interfaceID == type(AbstractPortal).interfaceId || + interfaceID == type(IERC165).interfaceId || + interfaceID == type(IERC721).interfaceId; + } +} diff --git a/test/example/NFTPortal.t.sol b/test/example/NFTPortal.t.sol new file mode 100644 index 00000000..6139c940 --- /dev/null +++ b/test/example/NFTPortal.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { Test } from "forge-std/Test.sol"; +import { NFTPortal } from "../../src/example/NFTPortal.sol"; +import { Router } from "../../src/Router.sol"; +import { AbstractPortal } from "../../src/interface/AbstractPortal.sol"; +import { AttestationPayload } from "../../src/types/Structs.sol"; +import { AttestationRegistryMock } from "../mocks/AttestationRegistryMock.sol"; +import { ModuleRegistryMock } from "../mocks/ModuleRegistryMock.sol"; +import { IERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; + +contract NFTPortalTest is Test { + address public attester = makeAddr("attester"); + NFTPortal public nftPortal; + address[] public modules = new address[](0); + ModuleRegistryMock public moduleRegistryMock = new ModuleRegistryMock(); + AttestationRegistryMock public attestationRegistryMock = new AttestationRegistryMock(); + Router public router = new Router(); + + event Initialized(uint8 version); + event AttestationRegistered(); + event BulkAttestationsRegistered(); + + function setUp() public { + router.initialize(); + router.updateModuleRegistry(address(moduleRegistryMock)); + router.updateAttestationRegistry(address(attestationRegistryMock)); + + nftPortal = new NFTPortal(modules, address(router)); + + // Create attestation payload + AttestationPayload memory attestationPayload = AttestationPayload( + bytes32(uint256(1)), + uint64(block.timestamp + 1 days), + abi.encode(address(1)), // Convert address(1) to bytes and use as subject + new bytes(1) + ); + // Create validation payload + bytes[] memory validationPayload = new bytes[](0); + // Create 2 attestations + vm.expectEmit(true, true, true, true); + emit AttestationRegistered(); + nftPortal.attest(attestationPayload, validationPayload); + vm.expectEmit(true, true, true, true); + emit AttestationRegistered(); + nftPortal.attest(attestationPayload, validationPayload); + } + + function test_balanceOf() public { + uint256 balance = nftPortal.balanceOf(address(1)); + assertEq(balance, 2); + } + + function test_ownerOf() public { + address ownerOfFirstAttestation = nftPortal.ownerOf(1); + address ownerOfSecondAttestation = nftPortal.ownerOf(2); + assertEq(ownerOfFirstAttestation, address(1)); + assertEq(ownerOfSecondAttestation, address(1)); + } + + function testSupportsInterface() public { + bool isIERC165Supported = nftPortal.supportsInterface(type(IERC165).interfaceId); + assertTrue(isIERC165Supported); + bool isIERC721Supported = nftPortal.supportsInterface(type(IERC721).interfaceId); + assertTrue(isIERC721Supported); + bool isEASAbstractPortalSupported = nftPortal.supportsInterface(type(AbstractPortal).interfaceId); + assertTrue(isEASAbstractPortalSupported); + } +}