diff --git a/src/IAuthorizeMints.sol b/src/IAuthorizeMints.sol index 5d8de728..26038a9b 100644 --- a/src/IAuthorizeMints.sol +++ b/src/IAuthorizeMints.sol @@ -8,6 +8,13 @@ struct SignedMintAuthorization { bytes authorization; } +struct SignedPoiAuthorization { + string poi; + string tokenURI; + address to; + bytes authorization; +} + /// @title IAuthorizeMints /// @author molecule.to /// @notice a flexible interface to gate token mint calls on another contract, built for IP-NFTs @@ -22,4 +29,7 @@ interface IAuthorizeMints { /// @notice called by the gated token contract to signal that a token has been minted and an authorization can be invalidated /// @param data implementation specific data function redeem(bytes memory data) external; + + /// @notice checks whether the poi is owned by address to by verifying the trustee's signature + function verifyPoi(bytes memory signedAuthorization) external view returns (bool); } diff --git a/src/IPNFT.sol b/src/IPNFT.sol index 76ef64a0..4c0e5967 100644 --- a/src/IPNFT.sol +++ b/src/IPNFT.sol @@ -8,7 +8,7 @@ import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/O import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import { CountersUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { IAuthorizeMints, SignedMintAuthorization } from "./IAuthorizeMints.sol"; +import { IAuthorizeMints, SignedMintAuthorization, SignedPoiAuthorization } from "./IAuthorizeMints.sol"; import { IReservable } from "./IReservable.sol"; /* @@ -104,7 +104,7 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser /** * @notice mints an IPNFT with `tokenURI` as source of metadata. Invalidates the reservation. Redeems `mintpassId` on the authorizer contract - * @notice We are charging a nominal fee to symbolically represent the transfer of ownership rights, for a price of .001 ETH (<$2USD at current prices). This helps the ensure the protocol is affordable to almost all projects, but discourages frivolous IP-NFT minting. + * @notice We are charging a nominal fee to symbolically represent the transfer of ownership rights, for a price of .001 ETH (<$2USD at current prices). This helps ensure the protocol is affordable to almost all projects, but discourages frivolous IP-NFT minting. * * @param to the recipient of the NFT * @param reservationId the reserved token id that has been reserved with `reserve()` @@ -142,6 +142,31 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser return reservationId; } + /** + * @notice mints an IPNFT with `tokenURI` as source of metadata. This IPNFT is linked a proof of idea (POI) which is a hash of any collection of files that represents an idea, anchored on any chain. + * @notice We are charging a nominal fee to symbolically represent the transfer of ownership rights, for a price of .001 ETH (<$2USD at current prices). This helps ensure the protocol is affordable to almost all projects, but discourages frivolous IP-NFT minting. + * + * @param to the recipient of the NFT + * @param _tokenURI a location that resolves to a valid IP-NFT metadata structure + * @param _symbol a symbol that represents the IPNFT's derivatives. Can be changed by the owner + * @param authorization a bytes encoded parameter that ensures that the poi is owned by the owner (to param) + * @param poi the hash of the poi that will be the tokenId + * @return reservationId the tokenId + */ + function mintWithPOI( + address to, + uint256 reservationId, + string calldata _tokenURI, + string calldata _symbol, + bytes calldata authorization, + string memory poi + ) external payable whenNotPaused returns (uint256) { + if (!mintAuthorizer.verifyPoi(abi.encode(SignedPoiAuthorization(poi, _tokenURI, to, authorization)))) { + revert Unauthorized(); + } + return this.mintReservation(to, reservationId, _tokenURI, _symbol, authorization); + } + /** * @notice grants time limited "read" access to gated resources * @param reader the address that should be able to access gated content @@ -188,7 +213,7 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser (bool success,) = _msgSender().call{ value: address(this).balance }(""); require(success, "transfer failed"); } - + /// @inheritdoc UUPSUpgradeable function _authorizeUpgrade(address /*newImplementation*/ ) internal diff --git a/src/SignedMintAuthorizer.sol b/src/SignedMintAuthorizer.sol index 51e04b40..fe083447 100644 --- a/src/SignedMintAuthorizer.sol +++ b/src/SignedMintAuthorizer.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.18; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { IAuthorizeMints, SignedMintAuthorization } from "./IAuthorizeMints.sol"; +import { IAuthorizeMints, SignedMintAuthorization, SignedPoiAuthorization } from "./IAuthorizeMints.sol"; /// @title SignedMintAuthorizer /// @author molecule.to @@ -36,6 +36,16 @@ contract SignedMintAuthorizer is IAuthorizeMints, Ownable { return trustedSigners[signer]; } + function verifyPoi(bytes memory signedPoiAuthorization) external view override returns (bool) { + SignedPoiAuthorization memory auth = abi.decode(signedPoiAuthorization, (SignedPoiAuthorization)); + + bytes32 signedHash = ECDSA.toEthSignedMessageHash(keccak256(abi.encodePacked(auth.poi, auth.tokenURI, auth.to))); + + (address signer,) = ECDSA.tryRecover(signedHash, auth.authorization); + + return trustedSigners[signer]; + } + /// @inheritdoc IAuthorizeMints /// @dev this authorizer does not restrict reservations function authorizeReservation(address) external pure override returns (bool) {