Skip to content

Commit

Permalink
reintroduced IPNFT default minting functionality
Browse files Browse the repository at this point in the history
Tokenizer does not need to know the market

Signed-off-by: stadolf <[email protected]>
  • Loading branch information
elmariachi111 committed Feb 5, 2024
1 parent 4ad72d7 commit b00d02b
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 57 deletions.
89 changes: 68 additions & 21 deletions src/IPNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IAuthorizeMints, SignedMintAuthorization } from "./IAuthorizeMints.sol"
import { IReservable } from "./IReservable.sol";
import { IPToken } from "./IPToken.sol";
import { Tokenizer } from "./Tokenizer.sol";
import { MarketData } from "./IPSeedMarket.sol";
import { MarketData, IPSeedMarket } from "./IPSeedMarket.sol";

/*
______ _______ __ __ ________ ________
Expand All @@ -26,7 +26,7 @@ import { MarketData } from "./IPSeedMarket.sol";
\▓▓▓▓▓▓\▓▓ \▓▓ \▓▓\▓▓ \▓▓
*/

/// @title IPNFT V2.4
/// @title IPNFT V2.5
/// @author molecule.to
/// @notice IP-NFTs capture intellectual property to be traded and synthesized

Expand Down Expand Up @@ -64,14 +64,17 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, UUPSUp
mapping(string => uint256) public streams;

/// @notice the protocol signer who will automatically become a signer on seed multisigs
address public protocolSigner;
//todo: required when we want to setup control wallets automatically: address public protocolSigner;

IPSeedMarket public seedMarket;
Tokenizer public tokenizer;

event Reserved(address indexed reserver, uint256 indexed reservationId);
event IPNFTMinted(address indexed owner, uint256 indexed tokenId, string tokenURI, string symbol);
event ReadAccessGranted(uint256 indexed tokenId, address indexed reader, uint256 until);
event AuthorizerUpdated(address authorizer);

error NotOwningReservation(uint256 id);
error InvalidTokenId();
error ToZeroAddress();
error Unauthorized();
Expand All @@ -85,13 +88,13 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, UUPSUp
}

/// @notice Contract initialization logic
function initialize(address protocolSigner_) external initializer {
function initialize( /*address protocolSigner_*/ ) external initializer {
__UUPSUpgradeable_init();
__Ownable_init();
__Pausable_init();
__ERC721_init("IPNFT", "IPNFT");

protocolSigner = protocolSigner_;
//protocolSigner = protocolSigner_;
}

function pause() external onlyOwner {
Expand Down Expand Up @@ -133,30 +136,74 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, UUPSUp
* @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())) {
// revert Unauthorized();
// }
// reservationId = _reservationCounter.current();
// _reservationCounter.increment();
// reservations[reservationId] = _msgSender();
// emit Reserved(_msgSender(), reservationId);
// }
function reserve() external whenNotPaused returns (uint256 reservationId) {
if (!mintAuthorizer.authorizeReservation(_msgSender())) {
revert Unauthorized();
}
reservationId = _reservationCounter.current();
_reservationCounter.increment();
reservations[reservationId] = _msgSender();
emit Reserved(_msgSender(), reservationId);
}

/**
* @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.
*
* @param tokenId the precomputed token id, contains the sourcer's address as its hash
* @param streamId the orbis project id (usually a base36 encoded ceramic stream id)
* @param to the first owner of the ipnft (should be a multisig)
* @param to the recipient of the NFT
* @param reservationId the reserved token id that has been reserved with `reserve()`
* @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's handed to the current authorizer
* @return the `reservationId`
*/
function seed(uint256 tokenId, string calldata streamId, address to, MarketData calldata marketData, bytes calldata authorization)
function mintReservation(address to, uint256 reservationId, string calldata _tokenURI, string calldata _symbol, bytes calldata authorization)
external
payable
whenNotPaused
returns (uint256)
{
if (reservations[reservationId] != _msgSender()) {
revert NotOwningReservation(reservationId);
}

if (msg.value < SYMBOLIC_MINT_FEE) {
revert MintingFeeTooLow();
}

if (!mintAuthorizer.authorizeMint(_msgSender(), to, abi.encode(SignedMintAuthorization(reservationId, _tokenURI, authorization)))) {
revert Unauthorized();
}

delete reservations[reservationId];
symbol[reservationId] = _symbol;
mintAuthorizer.redeem(authorization);

_mint(to, reservationId);
_setTokenURI(reservationId, _tokenURI);
emit IPNFTMinted(to, reservationId, _tokenURI, _symbol);
return reservationId;
}

/**
* @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.
*
* @param tokenId the precomputed token id, contains the sourcer's address as its hash
* @param streamId the orbis project id (usually a base36 encoded ceramic stream id)
* @param to the first owner of the ipnft (should be a multisig)
* @param _symbol the ipt's ticker symbol
* @param marketData the market data for the seed market
* @param authorization a bytes encoded parameter that's handed to the current authorizer
*/
function seed(
uint256 tokenId,
string calldata streamId,
address to,
string calldata _symbol,
MarketData memory marketData,
bytes calldata authorization
) external payable whenNotPaused {
if (bytes(streamId).length < 10 || tokenId != computeTokenId(_msgSender(), streamId)) {
revert InvalidTokenId();
}
Expand All @@ -179,12 +226,12 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, UUPSUp
_mint(address(this), tokenId);

//todo: provide an "original" owner for the ipt contract
IPToken ipToken = this.tokenizer.tokenizeIpnft(tokenId, marketData.sourcerSupply, marketData.symbol, "", bytes(string("")));
ipToken.safeTransferFrom(address(this), _msgSender(), marketData.sourcerSupply);
IPToken ipToken = tokenizer.tokenizeIpnft(tokenId, marketData.sourcerSupply, _symbol, "", bytes(string("")));
ipToken.transfer(_msgSender(), marketData.sourcerSupply);

ipToken.transferOwnership(address(seedMarket));
marketData.beneficiary = to;
this.seedMarket.start(ipToken, marketData);
seedMarket.start(ipToken, marketData);
safeTransferFrom(address(this), to, tokenId);

emit IPNFTMinted(to, tokenId, streamId, "");
Expand Down
53 changes: 28 additions & 25 deletions src/IPSeedMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
import { isContract } from "./helpers/IsContract.sol";
import { IIPSeedCurve, TradeType } from "./curves/IIPSeedCurve.sol";
import { IPToken } from "./IPToken.sol";
import { Tokenizer } from "./Tokenizer.sol";

struct MarketData {
/// the curve that this token is traded on
Expand Down Expand Up @@ -92,7 +93,7 @@ contract IPSeedMarket is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpg

function initialize(address payable _protocolFeeBeneficiary) public initializer {
__UUPSUpgradeable_init();
__Ownable_init(_msgSender());
__Ownable_init();
__ReentrancyGuard_init();
protocolFeeBeneficiary = _protocolFeeBeneficiary;

Expand All @@ -102,11 +103,12 @@ contract IPSeedMarket is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpg
emit FeesUpdated(fees);

// note: address(this) indeed refers to the proxy contract here.
feeEscrow.initialize(address(this));
feeEscrow.initialize();
}

modifier onlySourcer(uint256 tokenId) {
if (_msgSender() != tokenMeta[tokenId].sourcer) {
modifier onlySourcer(IPToken ipToken) {
//todo likely not the beneficiary but the sourcer
if (_msgSender() != marketData[ipToken].beneficiary) {
revert UnauthorizedAccess();
}
_;
Expand Down Expand Up @@ -141,22 +143,20 @@ contract IPSeedMarket is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpg
* @dev in the future we might consider nailing down some curve deployments / parameter sets that we control so no one can use custom ones without forking
*
* @param ipToken ipt token to start seeding
* @param projectId the orbis project id (usually a base36 encoded ceramic stream id)
* @param curve an IIPSeedCurve implementation address
* @param curveParameters the fixed parameters that define the curve's shape, encoded as bytes32
* @param _marketData the market information including curve parameters & funding goals
*/
function start(IPToken ipToken, MarketData calldata _marketData) public {
// ERC1155's `exists` function checks for totalSupply > 0, which is not what we want here
if (address(marketData[ipToken].curve) != address(0)) {
if (address(marketData[ipToken].priceCurve) != address(0)) {
//todo: MarketAlreadyActive
revert TokenAlreadyExists();
}

if (!trustedCurves[address(curve)]) {
if (!trustedCurves[address(marketData[ipToken].priceCurve)]) {
revert UntrustedCurve();
}

if (!curve.areParametersInRange(curveParameters)) {
if (!marketData[ipToken].priceCurve.areParametersInRange(marketData[ipToken].curveParameters)) {
revert CurveParametersOutOfBounds();
}
if (ipToken.totalSupply() > _marketData.sourcerSupply) {
Expand All @@ -166,13 +166,13 @@ contract IPSeedMarket is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpg

//MarketData memory _marketData = MarketData(sourcer, sourcer, curve, curveParameters);
marketData[ipToken] = _marketData;
emit SaleStarted(ipToken, ipToken.metadata().ipnftId, sourcer, _marketData);
emit SaleStarted(ipToken, ipToken.metadata().ipnftId, _msgSender(), _marketData);
}

/**
* @notice buy tokens on the token's bonding curve. This requires the user to send the exact amount of ETH that can be queried by calling `getBuyPrice` -> `gross`
* users can send more ETH than required, the surplus will be refunded
* @param tokenId token id
* @param ipToken the traded token
* @param amount the amount of tokens to buy
*/
function mint(IPToken ipToken, uint256 amount) external payable nonReentrant {
Expand All @@ -191,11 +191,11 @@ contract IPSeedMarket is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpg
revert InsufficientPayment();
}

escrowFees(protocolFee, tokenMeta[tokenId].beneficiary, sourcerFee);
escrowFees(protocolFee, marketData[ipToken].beneficiary, sourcerFee);

emit Traded(_msgSender(), tokenId, TradeType.Buy, amount, gross, totalSupply(tokenId) + amount, sourcerFee, protocolFee);
emit Traded(_msgSender(), ipToken, TradeType.Buy, amount, gross, ipToken.totalSupply() + amount, sourcerFee, protocolFee);
ipToken.issue(_msgSender(), amount);
contributions[tokenId][_msgSender()] += msg.value;
contributions[ipToken][_msgSender()] += msg.value;

//refund surplus; that might help against frontrunners blocking trades by pushing the price up only by a tiny bit
if (msg.value > gross) {
Expand All @@ -204,12 +204,12 @@ contract IPSeedMarket is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpg
}

/**
* @notice sell tokens on the token's bonding curve by burning them. Fees are deducted and escrowed from the returned value
* @param tokenId token id
* @param amount the amount of tokens to burn
* @notice redeem an user's contribution on the bonding curve and burn / put away some of them. Fees are deducted and escrowed from the returned value
* @param ipToken token
*/
function exit(IPToken ipToken) external virtual nonReentrant {
uint256 currentBalance = ipToken.balanceOf(_msgSender());
uint256 redeemableEth = contributions[ipToken][_msgSender()];

// if (currentBalance < amount) {
// revert BalanceTooLow();
Expand All @@ -230,12 +230,15 @@ contract IPSeedMarket is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpg
// revert PriceDriftTooHigh(minOutNetAmount, net);
// }

emit Traded(_msgSender(), TradeType.Sell, amount, gross, ipToken.totalSupply() - amount, sourcerFee, protocolFee);
//emit Traded(_msgSender(), TradeType.Sell, amount, gross, ipToken.totalSupply() - amount, sourcerFee, protocolFee);

ipToken.burnFrom(_msgSender(), amount);
//that's computed on the curve
uint256 toBurn = currentBalance / 2;

escrowFees(protocolFee, tokenMeta[tokenId].beneficiary, sourcerFee);
Address.sendValue(payable(_msgSender()), net);
ipToken.burnFrom(_msgSender(), toBurn);

//escrowFees(protocolFee, tokenMeta[tokenId].beneficiary, sourcerFee);
Address.sendValue(payable(_msgSender()), redeemableEth);
}

function endSeeding() public { }
Expand All @@ -254,7 +257,7 @@ contract IPSeedMarket is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpg
* @return net the amount of ETH that's actually collateralized
*/
function getBuyPrice(IPToken ipToken, uint256 want) public view returns (uint256 gross, uint256 net, uint256 protocolFee, uint256 sourcerFee) {
net = marketData[iptToken].priceCurve.getBuyPrice(ipToken.totalSupply(), want, marketData[ipToken].curveParameters);
net = marketData[ipToken].priceCurve.getBuyPrice(ipToken.totalSupply(), want, marketData[ipToken].curveParameters);

(protocolFee, sourcerFee) = computeFees(net, TradeType.Buy);
gross = net + protocolFee + sourcerFee;
Expand All @@ -274,8 +277,8 @@ contract IPSeedMarket is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpg
/**
* @dev the total amount of collateral that's currently locked on token id's bonding curve
*/
function collateral(address ipToken) external view returns (uint256) {
return marketData[tokenId].priceCurve.getBuyPrice(0, tokenId.totalSupply(), marketData[ipToken].curveParameters);
function collateral(IPToken ipToken) external view returns (uint256) {
return marketData[ipToken].priceCurve.getBuyPrice(0, ipToken.totalSupply(), marketData[ipToken].curveParameters);
}

/**
Expand Down
8 changes: 2 additions & 6 deletions src/Tokenizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { IPToken, Metadata as TokenMetadata } from "./IPToken.sol";
import { IPermissioner } from "./Permissioner.sol";
import { IPNFT } from "./IPNFT.sol";
import { IPSeedMarket } from "./IPSeedMarket.sol";

error MustOwnIpnft();
error AlreadyTokenized();
error ZeroAddress();

/// @title Tokenizer 1.2
/// @title Tokenizer 1.3
/// @author molecule.to
/// @notice tokenizes an IPNFT to an ERC20 token (called IPToken or IPT) and controls its supply.
contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
Expand Down Expand Up @@ -47,18 +46,15 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
/// @notice the IPToken implementation this Tokenizer spawns
IPToken public ipTokenImplementation;

IPSeedMarket public seedMarket;

/**
* @param _ipnft the IPNFT contract
* @param _permissioner a permissioning contract that checks if callers have agreed to the tokenized token's legal agreements
*/
function initialize(IPNFT _ipnft, IPermissioner _permissioner, IPSeedMarket _market) external initializer {
function initialize(IPNFT _ipnft, IPermissioner _permissioner) external initializer {
__UUPSUpgradeable_init();
__Ownable_init();
ipnft = _ipnft;
permissioner = _permissioner;
seedMarket = _seedMarket;
}

/// @custom:oz-upgrades-unsafe-allow constructor
Expand Down
5 changes: 5 additions & 0 deletions src/curves/AlgebraicSigmoidCurve.sol
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ contract AlgebraicSigmoidCurve is IIPSeedCurve {
return getBuyPrice(supply - sell, sell, curveParameters);
}

function getTokensForValue(uint256 supply, uint256 amount, bytes32 curveParameters) external pure returns (uint256) {
//todo implement or drop
return 0;
}

/**
* @notice ranges are (after decoding):
* a: [0.00001 ether - 1_000_000_000_000 ether]
Expand Down
1 change: 1 addition & 0 deletions test/Forking/TokenizerFork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { MustOwnIpnft, AlreadyTokenized, Tokenizer } from "../../src/Tokenizer.s
import { Tokenizer11 } from "../../src/helpers/test-upgrades/Tokenizer11.sol";
import { IPToken, OnlyIssuerOrOwner, TokenCapped, Metadata } from "../../src/IPToken.sol";
import { IPermissioner, BlindPermissioner } from "../../src/Permissioner.sol";
import { IPSeedMarket } from "../../src/IPSeedMarket.sol";

//import { SchmackoSwap, ListingState } from "../../src/SchmackoSwap.sol";

Expand Down
8 changes: 4 additions & 4 deletions test/IPNFTMintHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.18;
import "forge-std/Test.sol";
import "forge-std/console.sol";

import { IReservable } from "../src/IReservable.sol";
import { IPNFT } from "../src/IPNFT.sol";
import { IAuthorizeMints } from "../src/IAuthorizeMints.sol";

abstract contract IPNFTMintHelper is Test {
Expand All @@ -17,18 +17,18 @@ abstract contract IPNFTMintHelper is Test {
uint256 constant MINTING_FEE = 0.001 ether;
string DEFAULT_SYMBOL = "IPT-0001";

function reserveAToken(IReservable ipnft, address to) internal returns (uint256) {
function reserveAToken(IPNFT ipnft, address to) internal returns (uint256) {
vm.startPrank(to);
uint256 reservationId = ipnft.reserve();
vm.stopPrank();
return reservationId;
}

function mintAToken(IReservable ipnft, address to) internal returns (uint256) {
function mintAToken(IPNFT ipnft, address to) internal returns (uint256) {
return mintAToken(ipnft, to, "");
}

function mintAToken(IReservable ipnft, address to, bytes memory authorization) internal returns (uint256) {
function mintAToken(IPNFT ipnft, address to, bytes memory authorization) internal returns (uint256) {
vm.startPrank(to);
uint256 reservationId = ipnft.reserve();

Expand Down
2 changes: 1 addition & 1 deletion test/Tokenizer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ contract TokenizerTest is Test {
ipnft.setAuthorizer(new AcceptAllAuthorizer());

schmackoSwap = new SchmackoSwap();
erc20 = new FakeERC20('Fake ERC20', 'FERC');
erc20 = new FakeERC20("Fake ERC20", "FERC");
erc20.mint(ipnftBuyer, 1_000_000 ether);

blindPermissioner = new BlindPermissioner();
Expand Down

0 comments on commit b00d02b

Please sign in to comment.