Skip to content

Commit 43f8c86

Browse files
Merge pull request #169 from moleculeprotocol/feature/hubs-145-mint-ipnft-with-poi
add poi verification before minting function
2 parents 20c5824 + cf62367 commit 43f8c86

File tree

3 files changed

+76
-33
lines changed

3 files changed

+76
-33
lines changed

src/IPNFT.sol

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import { IReservable } from "./IReservable.sol";
2121
_| ▓▓_| ▓▓ | ▓▓ \▓▓▓▓ ▓▓ | ▓▓
2222
| ▓▓ \ ▓▓ | ▓▓ \▓▓▓ ▓▓ | ▓▓
2323
\▓▓▓▓▓▓\▓▓ \▓▓ \▓▓\▓▓ \▓▓
24-
*/
24+
*/
2525

26-
/// @title IPNFT V2.5
26+
/// @title IPNFT V2.5.1
2727
/// @author molecule.to
2828
/// @notice IP-NFTs capture intellectual property to be traded and synthesized
2929
contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReservable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable {
@@ -44,6 +44,9 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser
4444
/// @notice an IPNFT's base symbol, to be determined by the minter / owner, e.g. BIO-00001
4545
mapping(uint256 => string) public symbol;
4646

47+
/// @dev the highest possible reservation id
48+
uint256 private constant MAX_RESERVATION_ID = type(uint128).max;
49+
4750
event Reserved(address indexed reserver, uint256 indexed reservationId);
4851
event IPNFTMinted(address indexed owner, uint256 indexed tokenId, string tokenURI, string symbol);
4952
event ReadAccessGranted(uint256 indexed tokenId, address indexed reader, uint256 until);
@@ -103,15 +106,17 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser
103106
}
104107

105108
/**
106-
* @notice mints an IPNFT with `tokenURI` as source of metadata. Invalidates the reservation. Redeems `mintpassId` on the authorizer contract
107-
* @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.
109+
* @notice mints an IPNFT with `tokenURI` as source of metadata.
110+
* Minting the IPNFT can happen either with a reservation id or poi hash (Proof of Idea).
111+
* if the tokenId is a reservationId then it invalidates the reservation.
112+
* @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.
108113
*
109114
* @param to the recipient of the NFT
110-
* @param reservationId the reserved token id that has been reserved with `reserve()`
115+
* @param reservationId the reserved token id that has been reserved with `reserve()` / or the poi hash
111116
* @param _tokenURI a location that resolves to a valid IP-NFT metadata structure
112117
* @param _symbol a symbol that represents the IPNFT's derivatives. Can be changed by the owner
113118
* @param authorization a bytes encoded parameter that's handed to the current authorizer
114-
* @return the `reservationId`
119+
* @return the `tokenId`
115120
*/
116121
function mintReservation(address to, uint256 reservationId, string calldata _tokenURI, string calldata _symbol, bytes calldata authorization)
117122
external
@@ -120,7 +125,8 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser
120125
whenNotPaused
121126
returns (uint256)
122127
{
123-
if (reservations[reservationId] != _msgSender()) {
128+
bool _isPoi = isPoi(reservationId);
129+
if (!_isPoi && reservations[reservationId] != _msgSender()) {
124130
revert NotOwningReservation(reservationId);
125131
}
126132

@@ -131,8 +137,10 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser
131137
if (!mintAuthorizer.authorizeMint(_msgSender(), to, abi.encode(SignedMintAuthorization(reservationId, _tokenURI, authorization)))) {
132138
revert Unauthorized();
133139
}
140+
if (!_isPoi) {
141+
delete reservations[reservationId];
142+
}
134143

135-
delete reservations[reservationId];
136144
symbol[reservationId] = _symbol;
137145
mintAuthorizer.redeem(authorization);
138146

@@ -188,7 +196,7 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser
188196
(bool success,) = _msgSender().call{ value: address(this).balance }("");
189197
require(success, "transfer failed");
190198
}
191-
199+
192200
/// @inheritdoc UUPSUpgradeable
193201
function _authorizeUpgrade(address /*newImplementation*/ )
194202
internal
@@ -201,6 +209,12 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser
201209
super._burn(tokenId);
202210
}
203211

212+
/// @notice checks whether a token id is a Proof of Idea
213+
/// @param tokenId the token id, either a reserved id or a poi hash
214+
function isPoi(uint256 tokenId) public pure returns (bool) {
215+
return tokenId > MAX_RESERVATION_ID;
216+
}
217+
204218
/// @inheritdoc ERC721Upgradeable
205219
function tokenURI(uint256 tokenId) public view virtual override(ERC721URIStorageUpgradeable, ERC721Upgradeable) returns (string memory) {
206220
return super.tokenURI(tokenId);

subgraph/src/ipnftMapping.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,21 @@ export function handleReservation(event: ReservedEvent): void {
7575
reservation.save()
7676
}
7777

78-
function updateIpnftMetadata(ipnft: Ipnft, uri: string, timestamp: BigInt): void {
79-
let ipfsLocation = uri.replace('ipfs://', '');
80-
if (!ipfsLocation || ipfsLocation == uri) {
81-
log.error("Invalid URI format for tokenId {}: {}", [ipnft.id, uri])
82-
return
83-
}
78+
function updateIpnftMetadata(
79+
ipnft: Ipnft,
80+
uri: string,
81+
timestamp: BigInt
82+
): void {
83+
let ipfsLocation = uri.replace('ipfs://', '')
84+
if (!ipfsLocation || ipfsLocation == uri) {
85+
log.error('Invalid URI format for tokenId {}: {}', [ipnft.id, uri])
86+
return
87+
}
8488

85-
ipnft.tokenURI = uri
86-
ipnft.metadata = ipfsLocation
87-
ipnft.updatedAtTimestamp = timestamp
88-
IpnftMetadataTemplate.create(ipfsLocation)
89+
ipnft.tokenURI = uri
90+
ipnft.metadata = ipfsLocation
91+
ipnft.updatedAtTimestamp = timestamp
92+
IpnftMetadataTemplate.create(ipfsLocation)
8993
}
9094

9195
//the underlying parameter arrays are misaligned, hence we cannot cast or unify both events
@@ -97,7 +101,6 @@ export function handleMint(event: IPNFTMintedEvent): void {
97101
updateIpnftMetadata(ipnft, event.params.tokenURI, event.block.timestamp)
98102
store.remove('Reservation', event.params.tokenId.toString())
99103
ipnft.save()
100-
101104
}
102105

103106
export function handleMetadataUpdated(event: MetadataUpdateEvent): void {
@@ -108,13 +111,14 @@ export function handleMetadataUpdated(event: MetadataUpdateEvent): void {
108111
}
109112

110113
//erc4906 is not emitting the new url, we must query it ourselves
111-
let _ipnftContract = IPNFTContract.bind(event.params._event.address);
114+
let _ipnftContract = IPNFTContract.bind(event.params._event.address)
112115
let newUri = _ipnftContract.tokenURI(event.params._tokenId)
113-
if (!newUri || newUri == "") {
114-
log.debug("no new uri found for token, likely just minted {}", [event.params._tokenId.toString()])
115-
return
116+
if (!newUri || newUri == '') {
117+
log.debug('no new uri found for token, likely just minted {}', [
118+
event.params._tokenId.toString()
119+
])
120+
return
116121
}
117-
updateIpnftMetadata(ipnft, newUri, event.block.timestamp)
122+
updateIpnftMetadata(ipnft, newUri, event.block.timestamp)
118123
ipnft.save()
119124
}
120-

test/IPNFT.t.sol

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,36 @@ contract IPNFTTest is IPNFTMintHelper {
7272
assertEq(ipnft.reservations(2), bob);
7373
}
7474

75+
function testVerifyPoi() public {
76+
uint256 tokenId = uint256(0x073cb54264ef688e56531a2d09ab47b14086b5c7813e3a23a2bd7b1bb6458a52);
77+
bool isPoi = ipnft.isPoi(tokenId);
78+
assertEq(isPoi, true);
79+
}
80+
81+
function testMintWithPoi() public {
82+
bytes32 poiHash = 0x073cb54264ef688e56531a2d09ab47b14086b5c7813e3a23a2bd7b1bb6458a52;
83+
uint256 tokenId = uint256(poiHash);
84+
bytes32 authMessageHash = ECDSA.toEthSignedMessageHash(keccak256(abi.encodePacked(alice, alice, tokenId, ipfsUri)));
85+
86+
vm.startPrank(deployer);
87+
ipnft.setAuthorizer(new SignedMintAuthorizer(deployer));
88+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(deployerPk, authMessageHash);
89+
bytes memory authorization = abi.encodePacked(r, s, v);
90+
91+
vm.startPrank(alice);
92+
vm.expectRevert(IPNFT.Unauthorized.selector);
93+
ipnft.mintReservation{ value: MINTING_FEE }(alice, tokenId, ipfsUri, DEFAULT_SYMBOL, bytes("abcde"));
94+
95+
vm.expectEmit(true, true, false, true);
96+
emit IPNFTMinted(alice, tokenId, ipfsUri, DEFAULT_SYMBOL);
97+
ipnft.mintReservation{ value: MINTING_FEE }(alice, tokenId, ipfsUri, DEFAULT_SYMBOL, authorization);
98+
assertEq(ipnft.ownerOf(tokenId), alice);
99+
assertEq(ipnft.tokenURI(tokenId), ipfsUri);
100+
assertEq(ipnft.symbol(tokenId), DEFAULT_SYMBOL);
101+
102+
vm.stopPrank();
103+
}
104+
75105
function testMintFromReservation() public {
76106
vm.startPrank(deployer);
77107
ipnft.setAuthorizer(new SignedMintAuthorizer(deployer));
@@ -101,8 +131,6 @@ contract IPNFTTest is IPNFTMintHelper {
101131
assertEq(ipnft.tokenURI(1), ipfsUri);
102132
assertEq(ipnft.symbol(reservationId), DEFAULT_SYMBOL);
103133

104-
assertEq(ipnft.reservations(1), address(0));
105-
106134
vm.stopPrank();
107135
}
108136

@@ -145,7 +173,6 @@ contract IPNFTTest is IPNFTMintHelper {
145173
/**
146174
* ... but when set as heir of a self destruct operation the contract accepts the money.
147175
*/
148-
149176
function testOwnerCanWithdrawEthFunds() public {
150177
vm.deal(address(bob), 10 ether);
151178
vm.startPrank(bob);
@@ -232,15 +259,13 @@ contract IPNFTTest is IPNFTMintHelper {
232259
//the signoff only allows alice to call this
233260
vm.startPrank(charlie);
234261
vm.expectRevert(IPNFT.Unauthorized.selector);
235-
ipnft.amendMetadata(1, "ipfs://QmNewUri", authorization);
262+
ipnft.amendMetadata(1, "ipfs://QmNewUri", authorization);
236263

237264
vm.startPrank(alice);
238265
vm.expectEmit(true, true, false, false);
239266
emit MetadataUpdate(1);
240-
ipnft.amendMetadata(1, "ipfs://QmNewUri", authorization);
267+
ipnft.amendMetadata(1, "ipfs://QmNewUri", authorization);
241268
assertEq(ipnft.tokenURI(1), "ipfs://QmNewUri");
242269
vm.stopPrank();
243270
}
244-
245-
246271
}

0 commit comments

Comments
 (0)