From b00d02be69327f94e0cd4d3213290fb568c23134 Mon Sep 17 00:00:00 2001 From: stadolf Date: Mon, 5 Feb 2024 10:47:18 +0100 Subject: [PATCH] reintroduced IPNFT default minting functionality Tokenizer does not need to know the market Signed-off-by: stadolf --- src/IPNFT.sol | 89 +++++++++++++++++++++------- src/IPSeedMarket.sol | 53 +++++++++-------- src/Tokenizer.sol | 8 +-- src/curves/AlgebraicSigmoidCurve.sol | 5 ++ test/Forking/TokenizerFork.t.sol | 1 + test/IPNFTMintHelper.sol | 8 +-- test/Tokenizer.t.sol | 2 +- 7 files changed, 109 insertions(+), 57 deletions(-) diff --git a/src/IPNFT.sol b/src/IPNFT.sol index 3a839d7b..e88a0e64 100644 --- a/src/IPNFT.sol +++ b/src/IPNFT.sol @@ -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"; /* ______ _______ __ __ ________ ________ @@ -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 @@ -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(); @@ -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 { @@ -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(); } @@ -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, ""); diff --git a/src/IPSeedMarket.sol b/src/IPSeedMarket.sol index 36d1ff97..e7b89202 100644 --- a/src/IPSeedMarket.sol +++ b/src/IPSeedMarket.sol @@ -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 @@ -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; @@ -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(); } _; @@ -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) { @@ -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 { @@ -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) { @@ -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(); @@ -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 { } @@ -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; @@ -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); } /** diff --git a/src/Tokenizer.sol b/src/Tokenizer.sol index 5259180a..be8ebdfd 100644 --- a/src/Tokenizer.sol +++ b/src/Tokenizer.sol @@ -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 { @@ -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 diff --git a/src/curves/AlgebraicSigmoidCurve.sol b/src/curves/AlgebraicSigmoidCurve.sol index 7a67947b..c8716f9b 100644 --- a/src/curves/AlgebraicSigmoidCurve.sol +++ b/src/curves/AlgebraicSigmoidCurve.sol @@ -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] diff --git a/test/Forking/TokenizerFork.t.sol b/test/Forking/TokenizerFork.t.sol index b9e58d9a..18125835 100644 --- a/test/Forking/TokenizerFork.t.sol +++ b/test/Forking/TokenizerFork.t.sol @@ -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"; diff --git a/test/IPNFTMintHelper.sol b/test/IPNFTMintHelper.sol index f9732ebe..c5f2addf 100644 --- a/test/IPNFTMintHelper.sol +++ b/test/IPNFTMintHelper.sol @@ -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 { @@ -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(); diff --git a/test/Tokenizer.t.sol b/test/Tokenizer.t.sol index 10688fea..5ff36a5e 100644 --- a/test/Tokenizer.t.sol +++ b/test/Tokenizer.t.sol @@ -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();