From cdda55fbd8c0aebd834333bc1c3b6315cc185b76 Mon Sep 17 00:00:00 2001 From: stadolf Date: Thu, 4 Jul 2024 21:11:03 +0200 Subject: [PATCH] follows the original concept of self sufficient IPToken contracts Tokenizer indexes IPTs by IPNFT ids drops "hashes" completely reinit adds existing IPTs to the known ipt mapping IPToken asks its owner (Tokenizer) if senders hold the underlying IPNFT even works on top of the previous 1.1 -> 1.2 chain state Signed-off-by: stadolf --- script/dev/Synthesizer.s.sol | 10 +- script/dev/Tokenizer.s.sol | 1 - src/IPToken.sol | 35 ++--- src/Tokenizer.sol | 74 ++++------ src/helpers/test-upgrades/IPToken12.sol | 126 ++++++++++++++++++ .../{Tokenizer11.sol => Tokenizer12.sol} | 47 +++++-- test/CrowdSalePermissioned.t.sol | 9 +- test/Forking/TokenizerFork.t.sol | 14 +- test/Permissioner.t.sol | 2 +- test/SalesShareDistributor.t.sol | 14 +- test/Tokenizer.t.sol | 20 ++- 11 files changed, 241 insertions(+), 111 deletions(-) create mode 100644 src/helpers/test-upgrades/IPToken12.sol rename src/helpers/test-upgrades/{Tokenizer11.sol => Tokenizer12.sol} (68%) diff --git a/script/dev/Synthesizer.s.sol b/script/dev/Synthesizer.s.sol index 0d57b939..eb8e8627 100644 --- a/script/dev/Synthesizer.s.sol +++ b/script/dev/Synthesizer.s.sol @@ -34,13 +34,7 @@ contract DeploySynthesizer is CommonScript { function run() public { prepareAddresses(); vm.startBroadcast(deployer); - Synthesizer synthesizer = Synthesizer( - address( - new ERC1967Proxy( - address(new Synthesizer()), "" - ) - ) - ); + Synthesizer synthesizer = Synthesizer(address(new ERC1967Proxy(address(new Synthesizer()), ""))); MolTermsAcceptedPermissioner oldPermissioner = new MolTermsAcceptedPermissioner(); synthesizer.initialize(IPNFT(vm.envAddress("IPNFT_ADDRESS")), oldPermissioner); @@ -97,7 +91,7 @@ contract UpgradeSynthesizerToTokenizer is CommonScript { Tokenizer tokenizer = Tokenizer(address(synthesizer)); TermsAcceptedPermissioner newTermsPermissioner = new TermsAcceptedPermissioner(); - tokenizer.reinit(newTermsPermissioner); + //todo tokenizer.reinit(newTermsPermissioner); vm.stopBroadcast(); console.log("TOKENIZER_ADDRESS=%s", address(tokenizer)); //should equal synthesizer diff --git a/script/dev/Tokenizer.s.sol b/script/dev/Tokenizer.s.sol index 98630a55..96684539 100644 --- a/script/dev/Tokenizer.s.sol +++ b/script/dev/Tokenizer.s.sol @@ -58,6 +58,5 @@ contract FixtureTokenizer is CommonScript { vm.stopBroadcast(); console.log("IPTS_ADDRESS=%s", address(tokenContract)); - console.log("IPT round hash: %s", tokenContract.hash()); } } diff --git a/src/IPToken.sol b/src/IPToken.sol index 96a9df3e..cc6f9c01 100644 --- a/src/IPToken.sol +++ b/src/IPToken.sol @@ -5,6 +5,7 @@ import { ERC20BurnableUpgradeable } from "@openzeppelin/contracts-upgradeable/to import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { Tokenizer, MustOwnIpnft } from "./Tokenizer.sol"; struct Metadata { uint256 ipnftId; @@ -14,15 +15,15 @@ struct Metadata { error TokenCapped(); error OnlyIssuer(); - /** * @title IPToken - * @author molecule.to + * @author molecule.xyz * @notice this is a template contract that's spawned by the Tokenizer * @notice the owner of this contract is always the Tokenizer contract which enforces IPNFT holdership rules. * The owner can increase the token supply as long as it's not explicitly capped. * @dev formerly known as "molecules" */ + contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { event Capped(uint256 atSupply); @@ -34,27 +35,31 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { Metadata internal _metadata; - function initialize(string calldata name, string calldata symbol, Metadata calldata metadata_) external initializer { + Tokenizer internal tokenizer; + + function initialize(uint256 ipnftId, string calldata name, string calldata symbol, address originalOwner, string memory agreementCid) + external + initializer + { __Ownable_init(); __ERC20_init(name, symbol); - _metadata = metadata_; + _metadata = Metadata({ ipnftId: ipnftId, originalOwner: originalOwner, agreementCid: agreementCid }); + tokenizer = Tokenizer(owner()); } constructor() { _disableInitializers(); } - function metadata() external view returns (Metadata memory) { - return _metadata; + modifier onlyTokenizerOrIPNFTHolder() { + if (_msgSender() != owner() && _msgSender() != tokenizer.ownerOf(_metadata.ipnftId)) { + revert MustOwnIpnft(); + } + _; } - /** - * @notice ip tokens are identified by underlying ipnft token id - * @dev this once also included the current IPNFT owner address. We're leaving it a hash to stay downward compatible. - * @return uint256 a token hash that's unique for this token instance's `ipnftId` - */ - function hash() external view returns (uint256) { - return uint256(keccak256(abi.encodePacked(_metadata.ipnftId))); + function metadata() external view returns (Metadata memory) { + return _metadata; } /** @@ -62,7 +67,7 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { * @param receiver address * @param amount uint256 */ - function issue(address receiver, uint256 amount) external onlyOwner { + function issue(address receiver, uint256 amount) external onlyTokenizerOrIPNFTHolder { if (capped) { revert TokenCapped(); } @@ -73,7 +78,7 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { /** * @notice mark this token as capped. After calling this, no new tokens can be `issue`d */ - function cap() external onlyOwner { + function cap() external onlyTokenizerOrIPNFTHolder { capped = true; emit Capped(totalIssued); } diff --git a/src/Tokenizer.sol b/src/Tokenizer.sol index c9165bea..36c01116 100644 --- a/src/Tokenizer.sol +++ b/src/Tokenizer.sol @@ -19,14 +19,7 @@ error IPTNotControlledByTokenizer(); /// @notice tokenizes an IPNFT to an ERC20 token (called IPToken or IPT) and controls its supply. contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { event TokensCreated( - uint256 indexed moleculesId, - uint256 indexed ipnftId, - address indexed tokenContract, - address emitter, - uint256 amount, - string agreementCid, - string name, - string symbol + uint256 indexed ipnftId, address indexed tokenContract, address emitter, uint256 amount, string agreementCid, string name, string symbol ); event IPTokenImplementationUpdated(IPToken indexed old, IPToken indexed _new); @@ -64,21 +57,14 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { _disableInitializers(); } - modifier onlyIPNFTHolder(uint256 ipnftId) { - if (ipnft.ownerOf(ipnftId) != _msgSender()) { - revert MustOwnIpnft(); - } - _; - } - //todo: try breaking this with a faked IPToken modifier onlyControlledIPTs(IPToken ipToken) { - IPToken token = synthesized[ipToken.hash()]; - if (address(token) != address(ipToken)) { + TokenMetadata memory metadata = ipToken.metadata(); + + if (address(synthesized[metadata.ipnftId]) != address(ipToken)) { revert IPTNotControlledByTokenizer(); } - TokenMetadata memory metadata = token.metadata(); if (_msgSender() != ipnft.ownerOf(metadata.ipnftId)) { revert MustOwnIpnft(); } @@ -89,7 +75,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { * @notice sets the new implementation address of the IPToken * @param _ipTokenImplementation address pointing to the new implementation */ - function setIPTokenImplementation(IPToken _ipTokenImplementation) external onlyOwner { + function setIPTokenImplementation(IPToken _ipTokenImplementation) public onlyOwner { /* could call some functions on old contract to make sure its tokenizer not another contract behind a proxy for safety */ @@ -102,16 +88,18 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { } /** - * @dev called after an upgrade to reinitialize a new permissioner impl. - * @param _permissioner the new TermsPermissioner + * @dev sets legacy IPTs on the tokenized mapping */ - function reinit(IPermissioner _permissioner) public onlyOwner reinitializer(4) { - permissioner = _permissioner; + function reinit(IPToken _ipTokenImplementation) public onlyOwner reinitializer(5) { + synthesized[2] = IPToken(0x6034e0d6999741f07cb6Fb1162cBAA46a1D33d36); + synthesized[28] = IPToken(0x7b66E84Be78772a3afAF5ba8c1993a1B5D05F9C2); + synthesized[37] = IPToken(0xBcE56276591128047313e64744b3EBE03998783f); + + setIPTokenImplementation(_ipTokenImplementation); } /** - * @notice initializes synthesis on ipnft#id for the current asset holder. - * IPTokens are identified by the original token holder and the token id + * @notice tokenizes ipnft#id for the current asset holder. * @param ipnftId the token id on the underlying nft collection * @param tokenAmount the initially issued supply of IP tokens * @param tokenSymbol the ip token's ticker symbol @@ -125,30 +113,30 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { string memory tokenSymbol, string memory agreementCid, bytes calldata signedAgreement - ) external onlyIPNFTHolder(ipnftId) returns (IPToken token) { + ) external returns (IPToken token) { + if (ipnft.ownerOf(ipnftId) != _msgSender()) { + revert MustOwnIpnft(); + } + if (address(synthesized[ipnftId]) != address(0)) { + revert AlreadyTokenized(); + } + // https://github.com/OpenZeppelin/workshops/tree/master/02-contracts-clone token = IPToken(Clones.clone(address(ipTokenImplementation))); string memory name = string.concat("IP Tokens of IPNFT #", Strings.toString(ipnftId)); - token.initialize(name, tokenSymbol, TokenMetadata(ipnftId, _msgSender(), agreementCid)); + token.initialize(ipnftId, name, tokenSymbol, _msgSender(), agreementCid); - uint256 tokenHash = token.hash(); - - if (address(synthesized[tokenHash]) != address(0) || address(synthesized[legacyHash(ipnftId, _msgSender())]) != address(0)) { - revert AlreadyTokenized(); - } - - synthesized[tokenHash] = token; + synthesized[ipnftId] = token; //this has been called MoleculesCreated before - emit TokensCreated(tokenHash, ipnftId, address(token), _msgSender(), tokenAmount, agreementCid, name, tokenSymbol); + emit TokensCreated(ipnftId, address(token), _msgSender(), tokenAmount, agreementCid, name, tokenSymbol); permissioner.accept(token, _msgSender(), signedAgreement); token.issue(_msgSender(), tokenAmount); } /** - * @notice issues more IPTs for a given IPNFT - * @dev you must compute the ipt hash externally. - * @param ipToken the hash of the IPToken. See `IPToken.hash()` and `legacyHash()`. Some older IPT implementations required to compute the has as `uint256(keccak256(abi.encodePacked(owner,ipnftId)))` + * @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 * @param amount the amount of tokens to issue * @param receiver the address that receives the tokens */ @@ -165,13 +153,9 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { ipToken.cap(); } - /** - * @dev computes the legacy hash for an IPToken. It depended on the IPNFT owner before. Helps ensuring that the current holder cannot refractionalize an IPT - * @param ipnftId IPNFT token id - * @param owner the owner for the current IPT sales cycle (old concept) - */ - function legacyHash(uint256 ipnftId, address owner) public pure returns (uint256) { - return uint256(keccak256(abi.encodePacked(owner, ipnftId))); + /// @dev this will be called by IPTs to avoid handing over yet another IPNFT address (they already know this Tokenizer contract as their owner) + function ownerOf(uint256 ipnftId) external view returns (address) { + return ipnft.ownerOf(ipnftId); } /// @notice upgrade authorization logic diff --git a/src/helpers/test-upgrades/IPToken12.sol b/src/helpers/test-upgrades/IPToken12.sol new file mode 100644 index 00000000..d87d779d --- /dev/null +++ b/src/helpers/test-upgrades/IPToken12.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { ERC20BurnableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; + +struct Metadata { + uint256 ipnftId; + address originalOwner; + string agreementCid; +} + +error TokenCapped(); +error OnlyIssuerOrOwner(); + +/** + * @title IPToken 1.2 + * @author molecule.to + * @notice this is a template contract that's spawned by the Tokenizer + * @notice the owner of this contract is always the Tokenizer contract. + * the issuer of a token bears the right to increase the supply as long as the token is not capped. + * @dev formerly known as "molecules" + */ +contract IPToken12 is ERC20BurnableUpgradeable, OwnableUpgradeable { + event Capped(uint256 atSupply); + + //this will only go up. + uint256 public totalIssued; + /** + * @notice when true, no one can ever mint tokens again. + */ + bool public capped; + Metadata internal _metadata; + + function initialize(string calldata name, string calldata symbol, Metadata calldata metadata_) external initializer { + __Ownable_init(); + __ERC20_init(name, symbol); + _metadata = metadata_; + } + + constructor() { + _disableInitializers(); + } + + modifier onlyIssuerOrOwner() { + if (_msgSender() != _metadata.originalOwner && _msgSender() != owner()) { + revert OnlyIssuerOrOwner(); + } + _; + } + + function issuer() external view returns (address) { + return _metadata.originalOwner; + } + + function metadata() external view returns (Metadata memory) { + return _metadata; + } + /** + * @notice ip tokens are identified by the original ipnft token holder and the underlying ip token id + * @return uint256 a token hash that's unique for [`originaOwner`,`ipnftid`] + */ + + function hash() external view returns (uint256) { + return uint256(keccak256(abi.encodePacked(_metadata.originalOwner, _metadata.ipnftId))); + } + + /** + * @notice we deliberately allow the synthesis initializer to increase the supply of IP Tokens at will as long as the underlying asset has not been sold yet + * @param receiver address + * @param amount uint256 + */ + function issue(address receiver, uint256 amount) external onlyIssuerOrOwner { + if (capped) revert TokenCapped(); + totalIssued += amount; + _mint(receiver, amount); + } + + /** + * @notice mark this token as capped. After calling this, no new tokens can be `issue`d + */ + function cap() external onlyIssuerOrOwner { + capped = true; + emit Capped(totalIssued); + } + + /** + * @notice contract metadata, compatible to ERC1155 + * @return string base64 encoded data url + */ + function uri() external view returns (string memory) { + string memory tokenId = Strings.toString(_metadata.ipnftId); + + string memory props = string.concat( + '"properties": {', + '"ipnft_id": ', + tokenId, + ',"agreement_content": "ipfs://', + _metadata.agreementCid, + '","original_owner": "', + Strings.toHexString(_metadata.originalOwner), + '","erc20_contract": "', + Strings.toHexString(address(this)), + '","supply": "', + Strings.toString(totalIssued), + '"}' + ); + + return string.concat( + "data:application/json;base64,", + Base64.encode( + bytes( + string.concat( + '{"name": "IP Tokens of IPNFT #', + tokenId, + '","description": "IP Tokens, derived from IP-NFTs, are ERC-20 tokens governing IP pools.","decimals": 18,"external_url": "https://molecule.to","image": "",', + props, + "}" + ) + ) + ) + ); + } +} diff --git a/src/helpers/test-upgrades/Tokenizer11.sol b/src/helpers/test-upgrades/Tokenizer12.sol similarity index 68% rename from src/helpers/test-upgrades/Tokenizer11.sol rename to src/helpers/test-upgrades/Tokenizer12.sol index e613e30e..bd717787 100644 --- a/src/helpers/test-upgrades/Tokenizer11.sol +++ b/src/helpers/test-upgrades/Tokenizer12.sol @@ -5,17 +5,20 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { IPToken, Metadata as TokenMetadata } from "../../IPToken.sol"; +import { IPToken12 as IPToken, Metadata as TokenMetadata } from "./IPToken12.sol"; import { IPermissioner } from "../../Permissioner.sol"; import { IPNFT } from "../../IPNFT.sol"; +import { IPToken as NewIPtoken } from "../../IPToken.sol"; + error MustOwnIpnft(); error AlreadyTokenized(); +error ZeroAddress(); -/// @title Tokenizer 1.1 +/// @title Tokenizer 1.2 /// @author molecule.to -/// @notice tokenizes an IPNFT to an ERC20 token (called IPT) and controls its supply. -contract Tokenizer11 is UUPSUpgradeable, OwnableUpgradeable { +/// @notice tokenizes an IPNFT to an ERC20 token (called IPToken or IPT) and controls its supply. +contract Tokenizer12 is UUPSUpgradeable, OwnableUpgradeable { event TokensCreated( uint256 indexed moleculesId, uint256 indexed ipnftId, @@ -27,15 +30,23 @@ contract Tokenizer11 is UUPSUpgradeable, OwnableUpgradeable { string symbol ); + event IPTokenImplementationUpdated(IPToken indexed old, IPToken indexed _new); + event PermissionerUpdated(IPermissioner indexed old, IPermissioner indexed _new); + IPNFT internal ipnft; - //this is the old term to keep the storage layout intact + /// @dev a map of all IPTs. We're staying with the the initial term "synthesized" to keep the storage layout intact mapping(uint256 => IPToken) public synthesized; + + /// @dev not used, needed to ensure that storage slots are still in order after 1.1 -> 1.2, use ipTokenImplementation /// @custom:oz-upgrades-unsafe-allow state-variable-immutable address immutable tokenImplementation; /// @dev the permissioner checks if senders have agreed to legal requirements - IPermissioner permissioner; + IPermissioner public permissioner; + + /// @notice the IPToken implementation this Tokenizer spawns + IPToken public ipTokenImplementation; /** * @param _ipnft the IPNFT contract @@ -50,12 +61,28 @@ contract Tokenizer11 is UUPSUpgradeable, OwnableUpgradeable { /// @custom:oz-upgrades-unsafe-allow constructor constructor() { - tokenImplementation = address(new IPToken()); + tokenImplementation = address(0); _disableInitializers(); } /** - * @dev called after an upgrade to reinitialize a new permissioner impl. This is 4 for görli compatibility + * @notice sets the new implementation address of the IPToken + * @param _ipTokenImplementation address pointing to the new implementation + */ + function setIPTokenImplementation(IPToken _ipTokenImplementation) external onlyOwner { + /* + could call some functions on old contract to make sure its tokenizer not another contract behind a proxy for safety + */ + if (address(_ipTokenImplementation) == address(0)) { + revert ZeroAddress(); + } + + emit IPTokenImplementationUpdated(ipTokenImplementation, _ipTokenImplementation); + ipTokenImplementation = _ipTokenImplementation; + } + + /** + * @dev called after an upgrade to reinitialize a new permissioner impl. * @param _permissioner the new TermsPermissioner */ function reinit(IPermissioner _permissioner) public onlyOwner reinitializer(4) { @@ -84,7 +111,7 @@ contract Tokenizer11 is UUPSUpgradeable, OwnableUpgradeable { } // https://github.com/OpenZeppelin/workshops/tree/master/02-contracts-clone - token = IPToken(Clones.clone(tokenImplementation)); + token = IPToken(Clones.clone(address(ipTokenImplementation))); string memory name = string.concat("IP Tokens of IPNFT #", Strings.toString(ipnftId)); token.initialize(name, tokenSymbol, TokenMetadata(ipnftId, _msgSender(), agreementCid)); @@ -98,7 +125,7 @@ contract Tokenizer11 is UUPSUpgradeable, OwnableUpgradeable { //this has been called MoleculesCreated before emit TokensCreated(tokenHash, ipnftId, address(token), _msgSender(), tokenAmount, agreementCid, name, tokenSymbol); - permissioner.accept(token, _msgSender(), signedAgreement); + permissioner.accept(NewIPtoken(address(token)), _msgSender(), signedAgreement); token.issue(_msgSender(), tokenAmount); } diff --git a/test/CrowdSalePermissioned.t.sol b/test/CrowdSalePermissioned.t.sol index 063c512b..05c96484 100644 --- a/test/CrowdSalePermissioned.t.sol +++ b/test/CrowdSalePermissioned.t.sol @@ -79,18 +79,15 @@ contract CrowdSalePermissionedTest is Test { ipnft.mintReservation{ value: MINTING_FEE }(emitter, reservationId, "", "", ""); auctionToken = tokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); - tokenizer.issue(auctionToken, 500_000 ether, emitter); + auctionToken.issue(emitter, 500_000 ether); + vm.stopPrank(); - vm.startPrank(deployer); // here's a funny hack we're utilizing only for this test: // to make the tokenization easier, we're using a BlindPermissioner above // from now on, we're switching to a TermsAcceptedPermissioner - // we utilize the reinit function with it's code based version id here to easily update it once after initialization. - // this oc is not how it'd work in real life: the permissioner is fixed during the contract deployment and can *only* be updated during a full contract implementation upgrade permissioner = new TermsAcceptedPermissioner(); - tokenizer.reinit(permissioner); - vm.stopPrank(); + vm.store(address(tokenizer), bytes32(uint256(3)), bytes32(uint256(uint160(address(permissioner))))); vm.startPrank(bidder); biddingToken.mint(bidder, 1_000_000 ether); diff --git a/test/Forking/TokenizerFork.t.sol b/test/Forking/TokenizerFork.t.sol index c1b173cc..53a904b3 100644 --- a/test/Forking/TokenizerFork.t.sol +++ b/test/Forking/TokenizerFork.t.sol @@ -10,7 +10,7 @@ import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ import { IPNFT } from "../../src/IPNFT.sol"; import { MustOwnIpnft, AlreadyTokenized, Tokenizer } from "../../src/Tokenizer.sol"; -import { Tokenizer11 } from "../../src/helpers/test-upgrades/Tokenizer11.sol"; +import { Tokenizer12 } from "../../src/helpers/test-upgrades/Tokenizer12.sol"; import { IPToken, TokenCapped, Metadata } from "../../src/IPToken.sol"; import { IPermissioner, BlindPermissioner } from "../../src/Permissioner.sol"; @@ -54,9 +54,9 @@ contract TokenizerForkTest is Test { vm.selectFork(mainnetFork); } - function testCanUpgradeErc20TokenImplementation() public { + function testCanUpgradeToV13() public { IPNFT ipnftMainnetInstance = IPNFT(mainnetIPNFT); - Tokenizer11 tokenizer11 = Tokenizer11(mainnetTokenizer); + Tokenizer12 tokenizer12 = Tokenizer12(mainnetTokenizer); vm.startPrank(mainnetDeployer); Tokenizer newTokenizerImplementation = new Tokenizer(); @@ -64,8 +64,8 @@ contract TokenizerForkTest is Test { vm.stopPrank(); vm.startPrank(mainnetOwner); - bytes memory upgradeCallData = abi.encodeWithSelector(Tokenizer.setIPTokenImplementation.selector, address(newIPTokenImplementation)); - tokenizer11.upgradeToAndCall(address(newTokenizerImplementation), upgradeCallData); + bytes memory upgradeCallData = abi.encodeWithSelector(Tokenizer.reinit.selector, address(newIPTokenImplementation)); + tokenizer12.upgradeToAndCall(address(newTokenizerImplementation), upgradeCallData); Tokenizer upgradedTokenizer = Tokenizer(mainnetTokenizer); assertEq(address(upgradedTokenizer.ipTokenImplementation()), address(newIPTokenImplementation)); @@ -77,14 +77,14 @@ contract TokenizerForkTest is Test { vm.expectRevert("Initializable: contract is already initialized"); newTokenizerImplementation.initialize(IPNFT(address(0)), BlindPermissioner(address(0))); vm.expectRevert("Initializable: contract is already initialized"); - newIPTokenImplementation.initialize("Foo", "Bar", Metadata(2, alice, "abcde")); + newIPTokenImplementation.initialize(2, "Foo", "Bar", alice, "abcde"); vm.stopPrank(); vm.startPrank(mainnetOwner); vm.expectRevert("Initializable: contract is already initialized"); upgradedTokenizer.initialize(IPNFT(address(0)), BlindPermissioner(address(0))); vm.expectRevert("Initializable: contract is already initialized"); - upgradedTokenizer.reinit(BlindPermissioner(address(0))); + upgradedTokenizer.reinit(IPToken(address(0))); vm.stopPrank(); assertEq(ipnftMainnetInstance.ownerOf(valleyDaoIpnftId), valleyDaoMultisig); diff --git a/test/Permissioner.t.sol b/test/Permissioner.t.sol index 0b8be9d3..ab26782b 100644 --- a/test/Permissioner.t.sol +++ b/test/Permissioner.t.sol @@ -55,7 +55,7 @@ contract PermissionerTest is Test { vm.startPrank(deployer); permissioner = new TermsAcceptedPermissioner(); - tokenizer.reinit(permissioner); + vm.store(address(tokenizer), bytes32(uint256(3)), bytes32(uint256(uint160(address(permissioner))))); vm.stopPrank(); } diff --git a/test/SalesShareDistributor.t.sol b/test/SalesShareDistributor.t.sol index 25aa6e13..c645b007 100644 --- a/test/SalesShareDistributor.t.sol +++ b/test/SalesShareDistributor.t.sol @@ -130,7 +130,7 @@ contract SalesShareDistributorTest is Test { IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); // This is maximally important to do upfront. Otherwise the new buyer could simply issue new tokens and claim it back for themselves - tokenizer.cap(tokenContract); + tokenContract.cap(); uint256 listingId = helpCreateListing(1_000_000 ether, address(distributor)); vm.expectRevert(ListingNotFulfilled.selector); @@ -154,7 +154,7 @@ contract SalesShareDistributorTest is Test { function testManuallyStartClaimingPhase() public { vm.startPrank(originalOwner); IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); - tokenizer.cap(tokenContract); + tokenContract.cap(); ipnft.safeTransferFrom(originalOwner, ipnftBuyer, 1); vm.startPrank(ipnftBuyer); @@ -180,7 +180,7 @@ contract SalesShareDistributorTest is Test { function testClaimBuyoutShares() public { vm.startPrank(originalOwner); IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); - tokenizer.cap(tokenContract); + tokenContract.cap(); TermsAcceptedPermissioner permissioner = new TermsAcceptedPermissioner(); @@ -245,7 +245,7 @@ contract SalesShareDistributorTest is Test { function testClaimBuyoutSharesAfterSwap() public { vm.startPrank(originalOwner); IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); - tokenizer.cap(tokenContract); + tokenContract.cap(); uint256 listingId = helpCreateListing(1_000_000 ether, address(distributor)); tokenContract.safeTransfer(alice, 25_000); @@ -290,7 +290,7 @@ contract SalesShareDistributorTest is Test { vm.startPrank(originalOwner); IPToken tokenContract = tokenizer.tokenizeIpnft(1, iptokenAmount, "MOLE", agreementCid, ""); - tokenizer.cap(tokenContract); + tokenContract.cap(); assertEq(tokenContract.balanceOf(originalOwner), iptokenAmount); tokenContract.safeTransfer(alice, iptokenAmount); @@ -310,7 +310,7 @@ contract SalesShareDistributorTest is Test { function testClaimingFraud() public { vm.startPrank(originalOwner); IPToken tokenContract1 = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); - tokenizer.cap(tokenContract1); + tokenContract1.cap(); ipnft.setApprovalForAll(address(schmackoSwap), true); uint256 listingId1 = schmackoSwap.list(IERC721(address(ipnft)), 1, erc20, 1000 ether, address(distributor)); @@ -322,7 +322,7 @@ contract SalesShareDistributorTest is Test { ipnft.mintReservation{ value: MINTING_FEE }(bob, reservationId, ipfsUri, DEFAULT_SYMBOL, ""); ipnft.setApprovalForAll(address(schmackoSwap), true); IPToken tokenContract2 = tokenizer.tokenizeIpnft(2, 70_000, "MOLE", agreementCid, ""); - tokenizer.cap(tokenContract2); + tokenContract2.cap(); uint256 listingId2 = schmackoSwap.list(IERC721(address(ipnft)), 2, erc20, 700 ether, address(originalOwner)); schmackoSwap.changeBuyerAllowance(listingId2, ipnftBuyer, true); vm.stopPrank(); diff --git a/test/Tokenizer.t.sol b/test/Tokenizer.t.sol index 85a6409d..1ab69824 100644 --- a/test/Tokenizer.t.sol +++ b/test/Tokenizer.t.sol @@ -128,30 +128,28 @@ contract TokenizerTest is Test { tokenContract.transfer(alice, 25_000); tokenContract.transfer(bob, 25_000); - vm.expectRevert("Ownable: caller is not the owner"); - tokenContract.issue(originalOwner, 100_000); - - tokenizer.issue(tokenContract, 100_000, originalOwner); + tokenContract.issue(originalOwner, 50_000); + // this allows new owners of legacy IPTs to increase their supply: + tokenizer.issue(tokenContract, 50_000, originalOwner); vm.startPrank(bob); - vm.expectRevert("Ownable: caller is not the owner"); + vm.expectRevert(MustOwnIpnft.selector); tokenContract.issue(bob, 12345); + vm.expectRevert(MustOwnIpnft.selector); + tokenContract.cap(); + assertEq(tokenContract.balanceOf(alice), 25_000); assertEq(tokenContract.balanceOf(bob), 25_000); assertEq(tokenContract.balanceOf(originalOwner), 150_000); assertEq(tokenContract.totalSupply(), 200_000); assertEq(tokenContract.totalIssued(), 200_000); - vm.startPrank(bob); - vm.expectRevert("Ownable: caller is not the owner"); - tokenContract.cap(); - vm.startPrank(originalOwner); - vm.expectRevert("Ownable: caller is not the owner"); + // both work and cap can be called multiple times without reverting tokenContract.cap(); - tokenizer.cap(tokenContract); + vm.expectRevert(TokenCapped.selector); tokenizer.issue(tokenContract, 12345, bob); }