From 4173cb41f370bc2c068e4117327f943654594880 Mon Sep 17 00:00:00 2001 From: stadolf Date: Thu, 4 Jul 2024 18:11:33 +0200 Subject: [PATCH] IPSeed: the tokenizer is the only account that controls token issuance Tokenizer ensures that the sender is the IPNFT holder loosens SalesShareDistributor's cap rules since there's only 1 ipt instance updates test cases where necessary Signed-off-by: stadolf --- src/IPToken.sol | 42 ++--- src/SalesShareDistributor.sol | 28 ++-- src/Tokenizer.sol | 63 +++++++- test/CrowdSalePermissioned.t.sol | 24 ++- test/Forking/TokenizerFork.t.sol | 5 +- test/SalesShareDistributor.t.sol | 69 ++------ test/SynthesizerUpgrade.t.sol | 260 ------------------------------- test/Tokenizer.t.sol | 32 ++-- 8 files changed, 136 insertions(+), 387 deletions(-) delete mode 100644 test/SynthesizerUpgrade.t.sol diff --git a/src/IPToken.sol b/src/IPToken.sol index 9dcc984d..96a9df3e 100644 --- a/src/IPToken.sol +++ b/src/IPToken.sol @@ -13,25 +13,25 @@ struct Metadata { } error TokenCapped(); -error OnlyIssuerOrOwner(); +error OnlyIssuer(); /** * @title IPToken * @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. + * @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); - //this will only go up. + /// @notice the amount of tokens that ever have been issued (not necessarily == supply) uint256 public totalIssued; - /** - * @notice when true, no one can ever mint tokens again. - */ + + /// @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 { @@ -44,36 +44,28 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { _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`] + * @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.originalOwner, _metadata.ipnftId))); + return uint256(keccak256(abi.encodePacked(_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 + * @notice the supply of IP Tokens is controlled by the tokenizer contract. * @param receiver address * @param amount uint256 */ - function issue(address receiver, uint256 amount) external onlyIssuerOrOwner { - if (capped) revert TokenCapped(); + function issue(address receiver, uint256 amount) external onlyOwner { + if (capped) { + revert TokenCapped(); + } totalIssued += amount; _mint(receiver, amount); } @@ -81,7 +73,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 onlyIssuerOrOwner { + function cap() external onlyOwner { capped = true; emit Capped(totalIssued); } diff --git a/src/SalesShareDistributor.sol b/src/SalesShareDistributor.sol index 1cd3c1df..4377d8bf 100644 --- a/src/SalesShareDistributor.sol +++ b/src/SalesShareDistributor.sol @@ -26,6 +26,12 @@ error OnlyIssuer(); error NotClaimingYet(); +/** + * @title SalesShareDistributor + * @author molecule.xyz + * @notice THIS IS NOT SAFE TO BE USED IN PRODUCTION!! + * This is a one time sell out contract for a "final" IPT sale and requires the IP token to be capped. + */ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; @@ -41,7 +47,7 @@ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, Reentranc } /** - * @notice returns the `amount` of `paymentToken` that `tokenHolder` can claim by burning their molecules + * @notice returns the `amount` of `paymentToken` that `tokenHolder` can claim by burning their IP Tokens * * @param tokenContract address * @param holder address @@ -85,7 +91,7 @@ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, Reentranc /** * @notice anyone should be able to call this function after having observed the sale - * rn we restrict it to the token issuer since they must provide a permissioner that controls the claiming rules + * rn we restrict it to the original owner since they must provide a permissioner that controls the claiming rules * this is a deep dependency on our own sales contract * * @param tokenContract IPToken the tokenContract of the IPToken @@ -93,11 +99,12 @@ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, Reentranc * @param permissioner IPermissioner the permissioner that permits claims */ function afterSale(IPToken tokenContract, uint256 listingId, IPermissioner permissioner) external { - if (_msgSender() != tokenContract.issuer()) { + Metadata memory metadata = tokenContract.metadata(); + //todo: this should be the *former* holder of the IPNFT, not the 1st owner :) + if (_msgSender() != metadata.originalOwner) { revert OnlyIssuer(); } - Metadata memory metadata = tokenContract.metadata(); (, uint256 ipnftId,, IERC20 _paymentToken, uint256 askPrice, address beneficiary, ListingState listingState) = schmackoSwap.listings(listingId); @@ -130,12 +137,12 @@ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, Reentranc * @param permissioner IPermissioner the permissioner that permits claims */ function afterSale(IPToken tokenContract, IERC20 paymentToken, uint256 paidPrice, IPermissioner permissioner) external nonReentrant { - if (_msgSender() != tokenContract.issuer()) { + Metadata memory metadata = tokenContract.metadata(); + //todo: this should be the *former* holder of the IPNFT, not the 1st owner :) + if (_msgSender() != metadata.originalOwner) { revert OnlyIssuer(); } - Metadata memory metadata = tokenContract.metadata(); - //create a fake (but valid) schmackoswap listing id uint256 fulfilledListingId = uint256( keccak256( @@ -160,9 +167,10 @@ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, Reentranc function _startClaimingPhase(IPToken tokenContract, uint256 fulfilledListingId, IERC20 _paymentToken, uint256 price, IPermissioner permissioner) internal { - if (!tokenContract.capped()) { - revert UncappedToken(); - } + //todo: this actually must be enforced before a sale starts + // if (!tokenContract.capped()) { + // revert UncappedToken(); + // } sales[address(tokenContract)] = Sales(fulfilledListingId, _paymentToken, price, permissioner); emit SalesActivated(address(tokenContract), address(_paymentToken), price); } diff --git a/src/Tokenizer.sol b/src/Tokenizer.sol index 56bb70b1..c9165bea 100644 --- a/src/Tokenizer.sol +++ b/src/Tokenizer.sol @@ -12,8 +12,9 @@ import { IPNFT } from "./IPNFT.sol"; error MustOwnIpnft(); error AlreadyTokenized(); error ZeroAddress(); +error IPTNotControlledByTokenizer(); -/// @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 { @@ -63,6 +64,27 @@ 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)) { + revert IPTNotControlledByTokenizer(); + } + + TokenMetadata memory metadata = token.metadata(); + if (_msgSender() != ipnft.ownerOf(metadata.ipnftId)) { + revert MustOwnIpnft(); + } + _; + } + /** * @notice sets the new implementation address of the IPToken * @param _ipTokenImplementation address pointing to the new implementation @@ -103,19 +125,15 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { string memory tokenSymbol, string memory agreementCid, bytes calldata signedAgreement - ) external returns (IPToken token) { - if (ipnft.ownerOf(ipnftId) != _msgSender()) { - revert MustOwnIpnft(); - } - + ) external onlyIPNFTHolder(ipnftId) returns (IPToken token) { // 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)); uint256 tokenHash = token.hash(); - // ensure we can only call this once per sales cycle - if (address(synthesized[tokenHash]) != address(0)) { + + if (address(synthesized[tokenHash]) != address(0) || address(synthesized[legacyHash(ipnftId, _msgSender())]) != address(0)) { revert AlreadyTokenized(); } @@ -127,6 +145,35 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { 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)))` + * @param amount the amount of tokens to issue + * @param receiver the address that receives the tokens + */ + function issue(IPToken ipToken, uint256 amount, address receiver) external onlyControlledIPTs(ipToken) { + ipToken.issue(receiver, amount); + } + + /** + * @notice caps the supply of an IPT. After calling this, no new tokens can be `issue`d + * @dev you must compute the ipt hash externally. + * @param ipToken the IPToken to cap. + */ + function cap(IPToken ipToken) external onlyControlledIPTs(ipToken) { + 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))); + } + /// @notice upgrade authorization logic function _authorizeUpgrade(address /*newImplementation*/ ) internal diff --git a/test/CrowdSalePermissioned.t.sol b/test/CrowdSalePermissioned.t.sol index 4a6959cf..063c512b 100644 --- a/test/CrowdSalePermissioned.t.sol +++ b/test/CrowdSalePermissioned.t.sol @@ -52,8 +52,15 @@ contract CrowdSalePermissionedTest is Test { ipnft.initialize(); ipnft.setAuthorizer(new AcceptAllAuthorizer()); - Tokenizer tokenizer = Tokenizer(address(new ERC1967Proxy(address(new Tokenizer()), ""))); - tokenizer.initialize(ipnft, new BlindPermissioner()); + Tokenizer tokenizer = Tokenizer( + address( + new ERC1967Proxy( + address(new Tokenizer()), + abi.encodeWithSelector(Tokenizer.initialize.selector, [address(ipnft), address(new BlindPermissioner())]) + ) + ) + ); + tokenizer.setIPTokenImplementation(new IPToken()); biddingToken = new FakeERC20("USD token", "USDC"); @@ -61,11 +68,7 @@ contract CrowdSalePermissionedTest is Test { crowdSale = new StakedLockingCrowdSale(); - vestedDao = new TokenVesting( - daoToken, - string(abi.encodePacked("Vested ", daoToken.name())), - string(abi.encodePacked("v", daoToken.symbol())) - ); + vestedDao = new TokenVesting(daoToken, string(abi.encodePacked("Vested ", daoToken.name())), string(abi.encodePacked("v", daoToken.symbol()))); vestedDao.grantRole(vestedDao.ROLE_CREATE_SCHEDULE(), address(crowdSale)); crowdSale.trustVestingContract(vestedDao); vm.stopPrank(); @@ -76,10 +79,15 @@ contract CrowdSalePermissionedTest is Test { ipnft.mintReservation{ value: MINTING_FEE }(emitter, reservationId, "", "", ""); auctionToken = tokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); - auctionToken.issue(emitter, 500_000 ether); + tokenizer.issue(auctionToken, 500_000 ether, emitter); 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(); diff --git a/test/Forking/TokenizerFork.t.sol b/test/Forking/TokenizerFork.t.sol index b9e58d9a..c1b173cc 100644 --- a/test/Forking/TokenizerFork.t.sol +++ b/test/Forking/TokenizerFork.t.sol @@ -11,10 +11,11 @@ import { IPNFT } from "../../src/IPNFT.sol"; import { MustOwnIpnft, AlreadyTokenized, Tokenizer } from "../../src/Tokenizer.sol"; import { Tokenizer11 } from "../../src/helpers/test-upgrades/Tokenizer11.sol"; -import { IPToken, OnlyIssuerOrOwner, TokenCapped, Metadata } from "../../src/IPToken.sol"; +import { IPToken, TokenCapped, Metadata } from "../../src/IPToken.sol"; import { IPermissioner, BlindPermissioner } from "../../src/Permissioner.sol"; -//import { SchmackoSwap, ListingState } from "../../src/SchmackoSwap.sol"; +// an error thrown by IPToken before 1.3 +//error OnlyIssuerOrOwner(); contract TokenizerForkTest is Test { using SafeERC20Upgradeable for IPToken; diff --git a/test/SalesShareDistributor.t.sol b/test/SalesShareDistributor.t.sol index ca277e44..25aa6e13 100644 --- a/test/SalesShareDistributor.t.sol +++ b/test/SalesShareDistributor.t.sol @@ -28,7 +28,7 @@ import { InsufficientBalance } from "../src/SalesShareDistributor.sol"; -import { IPToken, OnlyIssuerOrOwner } from "../src/IPToken.sol"; +import { IPToken } from "../src/IPToken.sol"; import { SchmackoSwap, ListingState } from "../src/SchmackoSwap.sol"; import { FakeERC20 } from "../src/helpers/FakeERC20.sol"; @@ -77,29 +77,11 @@ contract SalesShareDistributorTest is Test { IPToken ipTokenImplementation = new IPToken(); - tokenizer = Tokenizer( - address( - new ERC1967Proxy( - address( - new Tokenizer() - ), - "" - ) - ) - ); + tokenizer = Tokenizer(address(new ERC1967Proxy(address(new Tokenizer()), ""))); tokenizer.initialize(ipnft, blindPermissioner); tokenizer.setIPTokenImplementation(ipTokenImplementation); - distributor = SalesShareDistributor( - address( - new ERC1967Proxy( - address( - new SalesShareDistributor() - ), - "" - ) - ) - ); + distributor = SalesShareDistributor(address(new ERC1967Proxy(address(new SalesShareDistributor()), ""))); distributor.initialize(schmackoSwap); vm.stopPrank(); @@ -145,14 +127,12 @@ contract SalesShareDistributorTest is Test { function testStartClaimingPhase() public { vm.startPrank(originalOwner); - // ipnft.setApprovalForAll(address(tokenizer), true); IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); - uint256 listingId = helpCreateListing(1_000_000 ether, address(distributor)); - vm.stopPrank(); + // 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); - assertEq(tokenContract.issuer(), originalOwner); - vm.startPrank(originalOwner); + uint256 listingId = helpCreateListing(1_000_000 ether, address(distributor)); vm.expectRevert(ListingNotFulfilled.selector); distributor.afterSale(tokenContract, listingId, blindPermissioner); vm.stopPrank(); @@ -160,15 +140,10 @@ contract SalesShareDistributorTest is Test { vm.startPrank(ipnftBuyer); erc20.approve(address(schmackoSwap), 1_000_000 ether); schmackoSwap.fulfill(listingId); - vm.stopPrank(); assertEq(erc20.balanceOf(address(distributor)), 1_000_000 ether); vm.startPrank(originalOwner); - vm.expectRevert(UncappedToken.selector); - distributor.afterSale(tokenContract, listingId, blindPermissioner); - - tokenContract.cap(); distributor.afterSale(tokenContract, listingId, blindPermissioner); vm.stopPrank(); @@ -179,20 +154,16 @@ contract SalesShareDistributorTest is Test { function testManuallyStartClaimingPhase() public { vm.startPrank(originalOwner); IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); + tokenizer.cap(tokenContract); ipnft.safeTransferFrom(originalOwner, ipnftBuyer, 1); - assertEq(tokenContract.issuer(), originalOwner); - tokenContract.cap(); - vm.stopPrank(); vm.startPrank(ipnftBuyer); erc20.transfer(originalOwner, 1_000_000 ether); - vm.stopPrank(); // only the owner can manually start the claiming phase. vm.startPrank(bob); vm.expectRevert(OnlyIssuer.selector); distributor.afterSale(tokenContract, erc20, 1_000_000 ether, blindPermissioner); - vm.stopPrank(); vm.startPrank(originalOwner); vm.expectRevert(); // not approved @@ -200,7 +171,6 @@ contract SalesShareDistributorTest is Test { erc20.approve(address(distributor), 1_000_000 ether); distributor.afterSale(tokenContract, erc20, 1_000_000 ether, blindPermissioner); - vm.stopPrank(); assertEq(erc20.balanceOf(address(originalOwner)), 0); (uint256 fulfilledListingId,,,) = distributor.sales(address(tokenContract)); @@ -210,7 +180,7 @@ contract SalesShareDistributorTest is Test { function testClaimBuyoutShares() public { vm.startPrank(originalOwner); IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); - tokenContract.cap(); + tokenizer.cap(tokenContract); TermsAcceptedPermissioner permissioner = new TermsAcceptedPermissioner(); @@ -220,11 +190,9 @@ contract SalesShareDistributorTest is Test { vm.startPrank(ipnftBuyer); erc20.transfer(originalOwner, 1_000_000 ether); - vm.stopPrank(); vm.startPrank(originalOwner); ipnft.safeTransferFrom(originalOwner, ipnftBuyer, 1); - vm.stopPrank(); (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePk, ECDSA.toEthSignedMessageHash(abi.encodePacked(permissioner.specificTermsV1(tokenContract)))); @@ -233,12 +201,10 @@ contract SalesShareDistributorTest is Test { //someone must start the claiming phase first vm.expectRevert(NotClaimingYet.selector); distributor.claim(tokenContract, abi.encodePacked(r, s, v)); - vm.stopPrank(); vm.startPrank(originalOwner); erc20.approve(address(distributor), 1_000_000 ether); distributor.afterSale(tokenContract, erc20, 1_000_000 ether, permissioner); - vm.stopPrank(); vm.startPrank(alice); (, uint256 amount) = distributor.claimableTokens(tokenContract, alice); @@ -279,7 +245,7 @@ contract SalesShareDistributorTest is Test { function testClaimBuyoutSharesAfterSwap() public { vm.startPrank(originalOwner); IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); - tokenContract.cap(); + tokenizer.cap(tokenContract); uint256 listingId = helpCreateListing(1_000_000 ether, address(distributor)); tokenContract.safeTransfer(alice, 25_000); @@ -324,7 +290,7 @@ contract SalesShareDistributorTest is Test { vm.startPrank(originalOwner); IPToken tokenContract = tokenizer.tokenizeIpnft(1, iptokenAmount, "MOLE", agreementCid, ""); - tokenContract.cap(); + tokenizer.cap(tokenContract); assertEq(tokenContract.balanceOf(originalOwner), iptokenAmount); tokenContract.safeTransfer(alice, iptokenAmount); @@ -344,23 +310,19 @@ contract SalesShareDistributorTest is Test { function testClaimingFraud() public { vm.startPrank(originalOwner); IPToken tokenContract1 = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); - tokenContract1.cap(); + tokenizer.cap(tokenContract1); + ipnft.setApprovalForAll(address(schmackoSwap), true); uint256 listingId1 = schmackoSwap.list(IERC721(address(ipnft)), 1, erc20, 1000 ether, address(distributor)); schmackoSwap.changeBuyerAllowance(listingId1, ipnftBuyer, true); - vm.stopPrank(); - - vm.deal(bob, MINTING_FEE); - vm.startPrank(deployer); - vm.stopPrank(); - vm.startPrank(bob); + vm.deal(bob, MINTING_FEE); uint256 reservationId = ipnft.reserve(); ipnft.mintReservation{ value: MINTING_FEE }(bob, reservationId, ipfsUri, DEFAULT_SYMBOL, ""); ipnft.setApprovalForAll(address(schmackoSwap), true); IPToken tokenContract2 = tokenizer.tokenizeIpnft(2, 70_000, "MOLE", agreementCid, ""); - tokenContract2.cap(); + tokenizer.cap(tokenContract2); uint256 listingId2 = schmackoSwap.list(IERC721(address(ipnft)), 2, erc20, 700 ether, address(originalOwner)); schmackoSwap.changeBuyerAllowance(listingId2, ipnftBuyer, true); vm.stopPrank(); @@ -369,12 +331,10 @@ contract SalesShareDistributorTest is Test { erc20.approve(address(schmackoSwap), 1_000_000 ether); schmackoSwap.fulfill(listingId1); schmackoSwap.fulfill(listingId2); - vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(InsufficientBalance.selector); distributor.afterSale(tokenContract2, listingId2, blindPermissioner); - vm.stopPrank(); vm.startPrank(originalOwner); vm.expectRevert(ListingMismatch.selector); @@ -384,7 +344,6 @@ contract SalesShareDistributorTest is Test { distributor.afterSale(tokenContract2, listingId2, blindPermissioner); distributor.afterSale(tokenContract1, listingId1, blindPermissioner); - vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(InsufficientBalance.selector); diff --git a/test/SynthesizerUpgrade.t.sol b/test/SynthesizerUpgrade.t.sol deleted file mode 100644 index 0ed9726a..00000000 --- a/test/SynthesizerUpgrade.t.sol +++ /dev/null @@ -1,260 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; - -import "forge-std/Test.sol"; -import { console } from "forge-std/console.sol"; -import { stdJson } from "forge-std/StdJson.sol"; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; - -import { Base64Url } from "base64/Base64Url.sol"; - -import { Strings } from "./helpers/Strings.sol"; - -import { IPNFT } from "../src/IPNFT.sol"; -import { AcceptAllAuthorizer } from "./helpers/AcceptAllAuthorizer.sol"; -import { TimelockedToken, StillLocked } from "../src/TimelockedToken.sol"; - -import { FakeERC20 } from "../src/helpers/FakeERC20.sol"; -import { MustOwnIpnft, AlreadyTokenized, Tokenizer11 } from "../src/helpers/test-upgrades/Tokenizer11.sol"; - -import { IPToken, Metadata as IPTMetadata, OnlyIssuerOrOwner, TokenCapped } from "../src/IPToken.sol"; -import { Molecules, Metadata as MoleculeMetadata } from "../src/helpers/test-upgrades/Molecules.sol"; -import { Synthesizer } from "../src/helpers/test-upgrades/Synthesizer.sol"; -import { IPermissioner, TermsAcceptedPermissioner, InvalidSignature } from "../src/Permissioner.sol"; -import { - IPermissioner as OldIPermissioner, - TermsAcceptedPermissioner as OldTermsAcceptedPermissioner -} from "../src/helpers/test-upgrades/SynthPermissioner.sol"; - -import { CrowdSale, SaleState, Sale, SaleInfo } from "../src/crowdsale/CrowdSale.sol"; -import { LockingCrowdSale, InvalidDuration } from "../src/crowdsale/LockingCrowdSale.sol"; -import { CrowdSaleHelpers } from "./helpers/CrowdSaleHelpers.sol"; - -struct IPTMetadataProps { - string agreement_content; - address erc20_contract; - uint256 ipnft_id; - address original_owner; - string supply; -} - -struct IPTUriMetadata { - uint256 decimals; - string description; - string external_url; - string image; - string name; - IPTMetadataProps properties; -} - -contract SynthesizerUpgradeTest is Test { - using SafeERC20Upgradeable for IPToken; - - string ipfsUri = "ipfs://bafkreiankqd3jvpzso6khstnaoxovtyezyatxdy7t2qzjoolqhltmasqki"; - string agreementCid = "bafkreigk5dvqblnkdniges6ft5kmuly47ebw4vho6siikzmkaovq6sjstq"; - uint256 MINTING_FEE = 0.001 ether; - string DEFAULT_SYMBOL = "MOL-0001"; - - address deployer = makeAddr("chucknorris"); - address protocolOwner = makeAddr("protocolOwner"); - address originalOwner = makeAddr("daoMultisig"); - uint256 originalOwnerPk; - - //Alice, Bob will be token holders - address alice = makeAddr("alice"); - uint256 alicePk; - - address bob = makeAddr("bob"); - uint256 bobPk; - - IPNFT internal ipnft; - Synthesizer internal synthesizer; - OldTermsAcceptedPermissioner internal oldTermsPermissioner; - - FakeERC20 internal erc20; - - function setUp() public { - (alice, alicePk) = makeAddrAndKey("alice"); - (bob, bobPk) = makeAddrAndKey("bob"); - (originalOwner, originalOwnerPk) = makeAddrAndKey("daoMultisig"); - - vm.startPrank(deployer); - - ipnft = IPNFT(address(new ERC1967Proxy(address(new IPNFT()), ""))); - ipnft.initialize(); - ipnft.setAuthorizer(new AcceptAllAuthorizer()); - - erc20 = new FakeERC20('Fake ERC20', 'FERC'); - erc20.mint(alice, 500_000 ether); - erc20.mint(bob, 500_000 ether); - - oldTermsPermissioner = new OldTermsAcceptedPermissioner(); - - synthesizer = Synthesizer(address(new ERC1967Proxy(address(new Synthesizer()), ""))); - synthesizer.initialize(ipnft, oldTermsPermissioner); - - vm.stopPrank(); - - vm.deal(originalOwner, MINTING_FEE); - vm.startPrank(originalOwner); - uint256 reservationId = ipnft.reserve(); - ipnft.mintReservation{ value: MINTING_FEE }(originalOwner, reservationId, ipfsUri, DEFAULT_SYMBOL, ""); - vm.stopPrank(); - } - - function testCanUpgradeErc20TokenImplementation() public { - vm.startPrank(originalOwner); - MoleculeMetadata memory oldMetadata = MoleculeMetadata(1, originalOwner, agreementCid); - string memory terms = oldTermsPermissioner.specificTermsV1(oldMetadata); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(originalOwnerPk, ECDSA.toEthSignedMessageHash(abi.encodePacked(terms))); - bytes memory xsignature = abi.encodePacked(r, s, v); - - Molecules tokenContractOld = synthesizer.synthesizeIpnft(1, 100_000, "MOLE", agreementCid, xsignature); - - vm.startPrank(deployer); - Tokenizer11 tokenizerImpl = new Tokenizer11(); - - synthesizer.upgradeTo(address(tokenizerImpl)); - - Tokenizer11 tokenizer = Tokenizer11(address(synthesizer)); - - TermsAcceptedPermissioner termsPermissioner = new TermsAcceptedPermissioner(); - tokenizer.reinit(termsPermissioner); - - vm.expectRevert("Initializable: contract is already initialized"); - tokenizer.reinit(termsPermissioner); - - vm.stopPrank(); - - assertEq(tokenContractOld.balanceOf(originalOwner), 100_000); - - vm.deal(originalOwner, MINTING_FEE); - vm.startPrank(originalOwner); - uint256 reservationId = ipnft.reserve(); - ipnft.mintReservation{ value: MINTING_FEE }(originalOwner, reservationId, ipfsUri, DEFAULT_SYMBOL, ""); - IPTMetadata memory newMetadata = IPTMetadata(2, originalOwner, agreementCid); - - terms = termsPermissioner.specificTermsV1(newMetadata); - (v, r, s) = vm.sign(originalOwnerPk, ECDSA.toEthSignedMessageHash(abi.encodePacked(terms))); - xsignature = abi.encodePacked(r, s, v); - IPToken tokenContractNew = tokenizer.tokenizeIpnft(2, 70_000, "FAST", agreementCid, xsignature); - vm.stopPrank(); - - assertEq(tokenContractNew.balanceOf(originalOwner), 70_000); - - vm.startPrank(originalOwner); - tokenContractOld.issue(originalOwner, 50_000); - assertEq(tokenContractOld.balanceOf(originalOwner), 150_000); - - tokenContractNew.issue(originalOwner, 30_000); - assertEq(tokenContractNew.balanceOf(originalOwner), 100_000); - vm.stopPrank(); - - string memory encodedUri = tokenContractOld.uri(); - IPTUriMetadata memory parsedMetadata = - abi.decode(vm.parseJson(string(Base64Url.decode(Strings.substring(encodedUri, 29, bytes(encodedUri).length)))), (IPTUriMetadata)); - assertEq(parsedMetadata.name, "Molecules of IPNFT #1"); - - encodedUri = tokenContractNew.uri(); - parsedMetadata = - abi.decode(vm.parseJson(string(Base64Url.decode(Strings.substring(encodedUri, 29, bytes(encodedUri).length)))), (IPTUriMetadata)); - assertEq(parsedMetadata.name, "IP Tokens of IPNFT #2"); - } - - function helpSignMessage(uint256 pk, string memory terms) internal returns (bytes memory signature) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ECDSA.toEthSignedMessageHash(abi.encodePacked(terms))); - signature = abi.encodePacked(r, s, v); - } - /** - * this demonstrates that upgrading the VITA-FAST sale works - */ - - function testCanInteractWithUpgradedERC20sAfterCrowdsale() public { - vm.startPrank(originalOwner); - MoleculeMetadata memory oldMetadata = MoleculeMetadata(1, originalOwner, agreementCid); - bytes memory xsignature = helpSignMessage(originalOwnerPk, oldTermsPermissioner.specificTermsV1(oldMetadata)); - - Molecules tokenContractOld = synthesizer.synthesizeIpnft(1, 500_000 ether, "MOLE", agreementCid, xsignature); - - LockingCrowdSale crowdSale = new LockingCrowdSale(); - Sale memory _sale = CrowdSaleHelpers.makeSale(originalOwner, IERC20Metadata(address(tokenContractOld)), erc20); - //todo: in reality the sale has been initialized with the old interface that used the `Molecule` type - _sale.permissioner = IPermissioner(address(oldTermsPermissioner)); - - tokenContractOld.approve(address(crowdSale), 400_000 ether); - uint256 saleId = crowdSale.startSale(_sale, 60 days); - vm.stopPrank(); - - vm.startPrank(alice); - xsignature = helpSignMessage(alicePk, oldTermsPermissioner.specificTermsV1(oldMetadata)); - erc20.approve(address(crowdSale), 100_000 ether); - crowdSale.placeBid(saleId, 100_000 ether, xsignature); - vm.stopPrank(); - - vm.startPrank(bob); - xsignature = helpSignMessage(bobPk, oldTermsPermissioner.specificTermsV1(oldMetadata)); - erc20.approve(address(crowdSale), 100_000 ether); - crowdSale.placeBid(saleId, 100_000 ether, xsignature); - vm.stopPrank(); - - vm.warp(block.timestamp + 3 hours); - TimelockedToken lockedAuctionToken = crowdSale.lockingContracts(address(tokenContractOld)); - - //bob settles & claims before the upgrade - vm.startPrank(bob); - crowdSale.settle(saleId); - - vm.recordLogs(); - //bob remembered his signature: - crowdSale.claim(saleId, xsignature); - Vm.Log[] memory entries = vm.getRecordedLogs(); - //0: TermsAccepted, 1: Claimed, 2: ScheduleCreated - assertEq(entries[2].topics[0], keccak256("ScheduleCreated(bytes32,address,address,uint256,uint64)")); - bytes32 bobScheduleId = entries[2].topics[1]; - vm.stopPrank(); - - assertEq(lockedAuctionToken.balanceOf(bob), 200_000 ether); - - //upgrade after sale concluded - vm.startPrank(deployer); - Tokenizer11 tokenizerImpl = new Tokenizer11(); - synthesizer.upgradeTo(address(tokenizerImpl)); - - Tokenizer11 tokenizer = Tokenizer11(address(synthesizer)); - TermsAcceptedPermissioner termsPermissioner = new TermsAcceptedPermissioner(); - tokenizer.reinit(termsPermissioner); - vm.stopPrank(); - - vm.startPrank(alice); - - //alice crafts signatures with the **new** permissioner... - xsignature = helpSignMessage(alicePk, termsPermissioner.specificTermsV1(IPTMetadata(1, originalOwner, agreementCid))); - vm.expectRevert(InvalidSignature.selector); - crowdSale.claim(saleId, xsignature); - - //instead, alice *must remember* to use the old permissioner's terms: - xsignature = helpSignMessage(alicePk, oldTermsPermissioner.specificTermsV1(MoleculeMetadata(1, originalOwner, agreementCid))); - vm.recordLogs(); - crowdSale.claim(saleId, xsignature); - entries = vm.getRecordedLogs(); - - assertEq(entries[2].topics[0], keccak256("ScheduleCreated(bytes32,address,address,uint256,uint64)")); - bytes32 aliceScheduleId = entries[2].topics[1]; - vm.stopPrank(); - - //finally get the tokens out - vm.warp(block.timestamp + 60 days); - vm.startPrank(alice); - lockedAuctionToken.release(aliceScheduleId); - lockedAuctionToken.release(bobScheduleId); - vm.stopPrank(); - - assertEq(tokenContractOld.balanceOf(alice), 200_000 ether); - assertEq(tokenContractOld.balanceOf(bob), 200_000 ether); - } -} diff --git a/test/Tokenizer.t.sol b/test/Tokenizer.t.sol index 10688fea..85a6409d 100644 --- a/test/Tokenizer.t.sol +++ b/test/Tokenizer.t.sol @@ -19,13 +19,11 @@ import { AcceptAllAuthorizer } from "./helpers/AcceptAllAuthorizer.sol"; import { FakeERC20 } from "../src/helpers/FakeERC20.sol"; import { MustOwnIpnft, AlreadyTokenized, Tokenizer, ZeroAddress } from "../src/Tokenizer.sol"; -import { IPToken, OnlyIssuerOrOwner, TokenCapped } from "../src/IPToken.sol"; +import { IPToken, TokenCapped } from "../src/IPToken.sol"; import { Molecules } from "../src/helpers/test-upgrades/Molecules.sol"; import { Synthesizer } from "../src/helpers/test-upgrades/Synthesizer.sol"; import { IPermissioner, BlindPermissioner } from "../src/Permissioner.sol"; -import { SchmackoSwap, ListingState } from "../src/SchmackoSwap.sol"; - contract TokenizerTest is Test { using SafeERC20Upgradeable for IPToken; @@ -50,7 +48,7 @@ contract TokenizerTest is Test { IPNFT internal ipnft; Tokenizer internal tokenizer; - SchmackoSwap internal schmackoSwap; + IPermissioner internal blindPermissioner; FakeERC20 internal erc20; @@ -63,8 +61,7 @@ contract TokenizerTest is Test { ipnft.initialize(); 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(); @@ -79,7 +76,6 @@ contract TokenizerTest is Test { vm.startPrank(originalOwner); uint256 reservationId = ipnft.reserve(); ipnft.mintReservation{ value: MINTING_FEE }(originalOwner, reservationId, ipfsUri, DEFAULT_SYMBOL, ""); - vm.stopPrank(); } function testSetIPTokenImplementation() public { @@ -95,7 +91,6 @@ contract TokenizerTest is Test { vm.startPrank(originalOwner); vm.expectRevert("Ownable: caller is not the owner"); tokenizer.setIPTokenImplementation(newIPTokenImplementation); - vm.stopPrank(); } function testUrl() public { @@ -103,7 +98,6 @@ contract TokenizerTest is Test { IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); string memory uri = tokenContract.uri(); assertGt(bytes(uri).length, 200); - vm.stopPrank(); } function testIssueIPToken() public { @@ -121,27 +115,27 @@ contract TokenizerTest is Test { vm.startPrank(originalOwner); tokenContract.transfer(alice, 10_000); - vm.stopPrank(); assertEq(tokenContract.balanceOf(alice), 10_000); assertEq(tokenContract.balanceOf(originalOwner), 90_000); assertEq(tokenContract.totalSupply(), 100_000); } - function testIncreaseIPToken() public { + function testIncreaseIPTokenSupply() public { vm.startPrank(originalOwner); IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); tokenContract.transfer(alice, 25_000); tokenContract.transfer(bob, 25_000); + vm.expectRevert("Ownable: caller is not the owner"); tokenContract.issue(originalOwner, 100_000); - vm.stopPrank(); + + tokenizer.issue(tokenContract, 100_000, originalOwner); vm.startPrank(bob); - vm.expectRevert(OnlyIssuerOrOwner.selector); + vm.expectRevert("Ownable: caller is not the owner"); tokenContract.issue(bob, 12345); - vm.stopPrank(); assertEq(tokenContract.balanceOf(alice), 25_000); assertEq(tokenContract.balanceOf(bob), 25_000); @@ -150,15 +144,16 @@ contract TokenizerTest is Test { assertEq(tokenContract.totalIssued(), 200_000); vm.startPrank(bob); - vm.expectRevert(OnlyIssuerOrOwner.selector); + vm.expectRevert("Ownable: caller is not the owner"); tokenContract.cap(); - vm.stopPrank(); vm.startPrank(originalOwner); + vm.expectRevert("Ownable: caller is not the owner"); tokenContract.cap(); + + tokenizer.cap(tokenContract); vm.expectRevert(TokenCapped.selector); - tokenContract.issue(bob, 12345); - vm.stopPrank(); + tokenizer.issue(tokenContract, 12345, bob); } function testCanBeTokenizedOnlyOnce() public { @@ -209,7 +204,6 @@ contract TokenizerTest is Test { wallet.execTransaction( address(tokenContract), 0, transferCall, Enum.Operation.Call, 80_000, 1 gwei, 20 gwei, address(0x0), payable(0x0), xsignatures ); - vm.stopPrank(); assertEq(tokenContract.balanceOf(bob), 10_000); }