diff --git a/src/IPNFT.sol b/src/IPNFT.sol index 597869e3..68ed2f78 100644 --- a/src/IPNFT.sol +++ b/src/IPNFT.sol @@ -23,7 +23,7 @@ import { IReservable } from "./IReservable.sol"; \▓▓▓▓▓▓\▓▓ \▓▓ \▓▓\▓▓ \▓▓ */ -/// @title IPNFT V2.4 +/// @title IPNFT V2.5 /// @author molecule.to /// @notice IP-NFTs capture intellectual property to be traded and synthesized contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReservable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable { @@ -92,14 +92,23 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser * @notice reserves a new token id. Checks that the caller is authorized, according to the current implementation of IAuthorizeMints. * @return reservationId a new reservation id */ - function reserve() external whenNotPaused returns (uint256 reservationId) { - if (!mintAuthorizer.authorizeReservation(_msgSender())) { + function reserve() external returns (uint256 reservationId) { + return reserveFor(_msgSender()); + } + + /** + * @notice reserves a new token id for an account. Checks that the caller is authorized, according to the current implementation of IAuthorizeMints. + * @param _for the address that will own the reserved id + * @return reservationId a new reservation id + */ + function reserveFor(address _for) public whenNotPaused returns (uint256 reservationId) { + if (!mintAuthorizer.authorizeReservation(_for)) { revert Unauthorized(); } reservationId = _reservationCounter.current(); _reservationCounter.increment(); - reservations[reservationId] = _msgSender(); - emit Reserved(_msgSender(), reservationId); + reservations[reservationId] = _for; + emit Reserved(_for, reservationId); } /** diff --git a/src/Tokenizer.sol b/src/Tokenizer.sol index a6d9b38d..45fb7778 100644 --- a/src/Tokenizer.sol +++ b/src/Tokenizer.sol @@ -124,7 +124,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable, IControlIPTs { string memory tokenSymbol, string memory agreementCid, bytes calldata signedAgreement - ) external returns (IPToken token) { + ) public returns (IPToken token) { if (_msgSender() != controllerOf(ipnftId)) { revert MustControlIpnft(); } @@ -155,6 +155,14 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable, IControlIPTs { token.issue(_msgSender(), tokenAmount); } + function reserveNewIpnftIdAndTokenize(uint256 amount, string memory tokenSymbol, string memory agreementCid, bytes calldata signedAgreement) + external + returns (uint256 reservationId, IPToken ipToken) + { + reservationId = ipnft.reserveFor(_msgSender()); + ipToken = tokenizeIpnft(reservationId, amount, tokenSymbol, agreementCid, signedAgreement); + } + /** * @notice issues more IPTs when not capped. This can be used for new owners of legacy IPTs that otherwise wouldn't be able to pass their `onlyIssuerOrOwner` gate * @param ipToken The ip token to control @@ -176,6 +184,11 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable, IControlIPTs { /// @dev this will be called by IPTs. Right now the controller is the IPNFT's current owner, it can be a Governor in the future. function controllerOf(uint256 ipnftId) public view override returns (address) { + //todo: check whether this is safe (or if I can trick myself to be the controller somehow) + //reservations are deleted upon mints, so this imo should be good + if (ipnft.reservations(ipnftId) != address(0)) { + return ipnft.reservations(ipnftId); + } return ipnft.ownerOf(ipnftId); } diff --git a/test/PreliminaryIPTs.t.sol b/test/PreliminaryIPTs.t.sol new file mode 100644 index 00000000..fa6b9aa4 --- /dev/null +++ b/test/PreliminaryIPTs.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; + +import { Safe } from "safe-global/safe-contracts/Safe.sol"; +import { SafeProxyFactory } from "safe-global/safe-contracts/proxies/SafeProxyFactory.sol"; +import { Enum } from "safe-global/safe-contracts/common/Enum.sol"; +import "./helpers/MakeGnosisWallet.sol"; +import { IPNFT } from "../src/IPNFT.sol"; +import { AcceptAllAuthorizer } from "./helpers/AcceptAllAuthorizer.sol"; + +import { FakeERC20 } from "../src/helpers/FakeERC20.sol"; +import { MustControlIpnft, AlreadyTokenized, Tokenizer, ZeroAddress, IPTNotControlledByTokenizer } from "../src/Tokenizer.sol"; + +import { IPToken, TokenCapped, Metadata as TokenMetadata } from "../src/IPToken.sol"; +import { IControlIPTs } from "../src/IControlIPTs.sol"; +import { Molecules } from "../src/helpers/test-upgrades/Molecules.sol"; +import { Synthesizer } from "../src/helpers/test-upgrades/Synthesizer.sol"; +import { IPermissioner, BlindPermissioner } from "../src/Permissioner.sol"; + +contract PreliminaryIPTsTest is Test { + using SafeERC20Upgradeable for IPToken; + + string ipfsUri = "ipfs://bafkreiankqd3jvpzso6khstnaoxovtyezyatxdy7t2qzjoolqhltmasqki"; + string agreementCid = "bafkreigk5dvqblnkdniges6ft5kmuly47ebw4vho6siikzmkaovq6sjstq"; + uint256 MINTING_FEE = 0.001 ether; + string DEFAULT_SYMBOL = "IPT-0001"; + + address deployer = makeAddr("chucknorris"); + address originalOwner = makeAddr("brucelee"); + address bob = makeAddr("bob"); + address alice = makeAddr("alice"); + + IPNFT internal ipnft; + Tokenizer internal tokenizer; + + IPermissioner internal blindPermissioner; + FakeERC20 internal erc20; + + function setUp() public { + vm.startPrank(deployer); + ipnft = IPNFT(address(new ERC1967Proxy(address(new IPNFT()), ""))); + ipnft.initialize(); + ipnft.setAuthorizer(new AcceptAllAuthorizer()); + blindPermissioner = new BlindPermissioner(); + + tokenizer = Tokenizer(address(new ERC1967Proxy(address(new Tokenizer()), ""))); + tokenizer.initialize(ipnft, blindPermissioner); + tokenizer.setIPTokenImplementation(new IPToken()); + } + + function testReserveAndIssue() public { + vm.startPrank(originalOwner); + (uint256 reservationId, IPToken ipToken) = tokenizer.reserveNewIpnftIdAndTokenize(1_000_000 ether, "IPT-SOL-FOO", "QmAgreeToThat", ""); + + vm.expectRevert("ERC721: invalid token ID"); + ipnft.ownerOf(reservationId); + + assertEq(ipToken.balanceOf(originalOwner), 1_000_000 ether); + + //even direct minting works now ... //todo: check if this is intended or if we must prevent this + ipToken.issue(bob, 42 ether); + assertEq(ipToken.balanceOf(bob), 42 ether); + + // ... do anything with the ip token ... + + vm.startPrank(bob); //bob didn't reserve this. + vm.expectRevert(abi.encodeWithSelector(IPNFT.NotOwningReservation.selector, 1)); + ipnft.mintReservation(alice, reservationId, ipfsUri, "A-TOTALLY-DIFFERENT-SYMBOL", ""); + + vm.startPrank(originalOwner); + vm.deal(originalOwner, 0.1 ether); + ipnft.mintReservation{ value: 0.1 ether }(alice, reservationId, ipfsUri, "A-TOTALLY-DIFFERENT-SYMBOL", ""); + + assertEq(ipnft.ownerOf(reservationId), alice); + + vm.startPrank(alice); + ipToken.issue(bob, 58 ether); + assertEq(ipToken.balanceOf(bob), 100 ether); + } +}