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); }