Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IPN-54 tokenizer adds function to reserve and tokenize at once #164

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions src/IPNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}

/**
Expand Down
15 changes: 14 additions & 1 deletion src/Tokenizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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
Expand All @@ -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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this correct:
return ipnft.reservations(ipnftId) || ipnft.ownerOf(ipnftId)

if the ipnft is minted it will return ipnft.ownerOf(ipnftId) as ipnft.reservations(ipnftId) would be address(0) abd vice versa.

Copy link
Member Author

@elmariachi111 elmariachi111 Aug 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would never assume by default whether a language besides Javascript would automatically cast an (address(0)) to a boolean (true). That code is pretty concise imo. If there's a reservation it returns its initiator, otherwise the holder.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes yes the code is clear, I just wanted to know whether the other optiion is also correct or not

return ipnft.reservations(ipnftId);
}
return ipnft.ownerOf(ipnftId);
}

Expand Down
88 changes: 88 additions & 0 deletions test/PreliminaryIPTs.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}