From 4173cb41f370bc2c068e4117327f943654594880 Mon Sep 17 00:00:00 2001 From: stadolf Date: Thu, 4 Jul 2024 18:11:33 +0200 Subject: [PATCH 01/10] 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); } From c4c15c0a6125539317ef40be4bcded12f9e60ab5 Mon Sep 17 00:00:00 2001 From: stadolf Date: Thu, 4 Jul 2024 21:11:03 +0200 Subject: [PATCH 02/10] 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 | 36 ++--- 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 | 15 ++- test/Permissioner.t.sol | 2 +- test/SalesShareDistributor.t.sol | 14 +- test/Tokenizer.t.sol | 20 ++- 11 files changed, 242 insertions(+), 112 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..5a7cccea 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; @@ -13,11 +14,10 @@ struct Metadata { } error TokenCapped(); -error OnlyIssuer(); /** - * @title IPToken - * @author molecule.to + * @title IPToken 1.3 + * @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. @@ -34,27 +34,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 +66,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 +77,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..1a5c22de 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,9 @@ 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)); + //todo: make sure that the legacy IPTs are indexed now + tokenizer12.upgradeToAndCall(address(newTokenizerImplementation), upgradeCallData); Tokenizer upgradedTokenizer = Tokenizer(mainnetTokenizer); assertEq(address(upgradedTokenizer.ipTokenImplementation()), address(newIPTokenImplementation)); @@ -77,14 +78,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); } From b58a912fee162ce6bb58afba79f3c2656b7b6bff Mon Sep 17 00:00:00 2001 From: stadolf Date: Fri, 5 Jul 2024 12:20:20 +0200 Subject: [PATCH 03/10] IPTs: save a storage slot for not storing the tokenizer explicitly tests IPTs control after IPNFT transfers Signed-off-by: stadolf --- src/IPToken.sol | 7 ++--- src/Tokenizer.sol | 2 +- ...t.sol => Tokenizer13UpgradeForkTest.t.sol} | 2 +- test/Tokenizer.t.sol | 27 +++++++++++++++++-- 4 files changed, 29 insertions(+), 9 deletions(-) rename test/Forking/{TokenizerFork.t.sol => Tokenizer13UpgradeForkTest.t.sol} (99%) diff --git a/src/IPToken.sol b/src/IPToken.sol index 5a7cccea..ecab5dd9 100644 --- a/src/IPToken.sol +++ b/src/IPToken.sol @@ -18,7 +18,7 @@ error TokenCapped(); /** * @title IPToken 1.3 * @author molecule.xyz - * @notice this is a template contract that's spawned by the Tokenizer + * @notice this is a template contract that's cloned 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" @@ -34,8 +34,6 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { Metadata internal _metadata; - Tokenizer internal tokenizer; - function initialize(uint256 ipnftId, string calldata name, string calldata symbol, address originalOwner, string memory agreementCid) external initializer @@ -43,7 +41,6 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { __Ownable_init(); __ERC20_init(name, symbol); _metadata = Metadata({ ipnftId: ipnftId, originalOwner: originalOwner, agreementCid: agreementCid }); - tokenizer = Tokenizer(owner()); } constructor() { @@ -51,7 +48,7 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { } modifier onlyTokenizerOrIPNFTHolder() { - if (_msgSender() != owner() && _msgSender() != tokenizer.ownerOf(_metadata.ipnftId)) { + if (_msgSender() != owner() && _msgSender() != Tokenizer(owner()).ownerOf(_metadata.ipnftId)) { revert MustOwnIpnft(); } _; diff --git a/src/Tokenizer.sol b/src/Tokenizer.sol index 36c01116..3f33690d 100644 --- a/src/Tokenizer.sol +++ b/src/Tokenizer.sol @@ -37,7 +37,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { /// @dev the permissioner checks if senders have agreed to legal requirements IPermissioner public permissioner; - /// @notice the IPToken implementation this Tokenizer spawns + /// @notice the IPToken implementation this Tokenizer clones from IPToken public ipTokenImplementation; /** diff --git a/test/Forking/TokenizerFork.t.sol b/test/Forking/Tokenizer13UpgradeForkTest.t.sol similarity index 99% rename from test/Forking/TokenizerFork.t.sol rename to test/Forking/Tokenizer13UpgradeForkTest.t.sol index 1a5c22de..279afd3b 100644 --- a/test/Forking/TokenizerFork.t.sol +++ b/test/Forking/Tokenizer13UpgradeForkTest.t.sol @@ -17,7 +17,7 @@ import { IPermissioner, BlindPermissioner } from "../../src/Permissioner.sol"; // an error thrown by IPToken before 1.3 //error OnlyIssuerOrOwner(); -contract TokenizerForkTest is Test { +contract Tokenizer13UpgradeForkTest is Test { using SafeERC20Upgradeable for IPToken; uint256 mainnetFork; diff --git a/test/Tokenizer.t.sol b/test/Tokenizer.t.sol index 1ab69824..faec0a39 100644 --- a/test/Tokenizer.t.sol +++ b/test/Tokenizer.t.sol @@ -129,15 +129,18 @@ contract TokenizerTest is Test { tokenContract.transfer(bob, 25_000); 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(MustOwnIpnft.selector); tokenContract.issue(bob, 12345); + vm.expectRevert(MustOwnIpnft.selector); + tokenizer.issue(tokenContract, 12345, bob); vm.expectRevert(MustOwnIpnft.selector); tokenContract.cap(); + vm.expectRevert(MustOwnIpnft.selector); + tokenizer.cap(tokenContract); assertEq(tokenContract.balanceOf(alice), 25_000); assertEq(tokenContract.balanceOf(bob), 25_000); @@ -154,6 +157,26 @@ contract TokenizerTest is Test { tokenizer.issue(tokenContract, 12345, bob); } + function testIPNFTHolderControlsIPT() public { + vm.startPrank(originalOwner); + IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); + tokenContract.issue(bob, 50_000); + ipnft.transferFrom(originalOwner, alice, 1); + + vm.startPrank(alice); + tokenContract.issue(alice, 50_000); + assertEq(tokenContract.balanceOf(alice), 50_000); + + //the original owner *cannot* issue tokens anymore + //this actually worked before 1.3 since IPTs were bound to their original owner + vm.startPrank(originalOwner); + vm.expectRevert(MustOwnIpnft.selector); + tokenContract.issue(alice, 50_000); + + vm.expectRevert(MustOwnIpnft.selector); + tokenizer.issue(tokenContract, 50_000, bob); + } + function testCanBeTokenizedOnlyOnce() public { vm.startPrank(originalOwner); tokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); @@ -189,7 +212,7 @@ contract TokenizerTest is Test { assertEq(tokenContract.balanceOf(address(wallet)), 100_000); - //test the SAFE can send molecules to another account + //test the SAFE can send IPTs to another account bytes memory transferCall = abi.encodeCall(tokenContract.transfer, (bob, 10_000)); bytes32 encodedTxDataHash = wallet.getTransactionHash( address(tokenContract), 0, transferCall, Enum.Operation.Call, 80_000, 1 gwei, 20 gwei, address(0x0), payable(0x0), 0 From 16a696be341fe75a520ec1fa69e2828d33bbe26a Mon Sep 17 00:00:00 2001 From: stadolf Date: Fri, 5 Jul 2024 14:48:53 +0200 Subject: [PATCH 04/10] happy first throw at subgraphs regnerated ABIs & tested this with a local setup Signed-off-by: stadolf --- subgraph/abis/IPToken.json | 61 +++-------- subgraph/abis/SharedSalesDistributor.json | 5 - subgraph/abis/Tokenizer.json | 125 ++++++++++++++++++++-- subgraph/makeAbis.sh | 56 +++++++++- subgraph/src/tokenizerMapping.ts | 36 ++----- subgraph/subgraph.template.yaml | 6 +- 6 files changed, 200 insertions(+), 89 deletions(-) diff --git a/subgraph/abis/IPToken.json b/subgraph/abis/IPToken.json index a3300de1..17730b81 100644 --- a/subgraph/abis/IPToken.json +++ b/subgraph/abis/IPToken.json @@ -159,19 +159,6 @@ ], "stateMutability": "nonpayable" }, - { - "type": "function", - "name": "hash", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "increaseAllowance", @@ -200,6 +187,11 @@ "type": "function", "name": "initialize", "inputs": [ + { + "name": "ipnftId", + "type": "uint256", + "internalType": "uint256" + }, { "name": "name", "type": "string", @@ -211,26 +203,14 @@ "internalType": "string" }, { - "name": "metadata_", - "type": "tuple", - "internalType": "struct Metadata", - "components": [ - { - "name": "ipnftId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "originalOwner", - "type": "address", - "internalType": "address" - }, - { - "name": "agreementCid", - "type": "string", - "internalType": "string" - } - ] + "name": "originalOwner", + "type": "address", + "internalType": "address" + }, + { + "name": "agreementCid", + "type": "string", + "internalType": "string" } ], "outputs": [], @@ -254,19 +234,6 @@ "outputs": [], "stateMutability": "nonpayable" }, - { - "type": "function", - "name": "issuer", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "metadata", @@ -545,7 +512,7 @@ }, { "type": "error", - "name": "OnlyIssuerOrOwner", + "name": "MustOwnIpnft", "inputs": [] }, { diff --git a/subgraph/abis/SharedSalesDistributor.json b/subgraph/abis/SharedSalesDistributor.json index 82976d74..8291dfd0 100644 --- a/subgraph/abis/SharedSalesDistributor.json +++ b/subgraph/abis/SharedSalesDistributor.json @@ -372,10 +372,5 @@ "type": "error", "name": "OnlyIssuer", "inputs": [] - }, - { - "type": "error", - "name": "UncappedToken", - "inputs": [] } ] diff --git a/subgraph/abis/Tokenizer.json b/subgraph/abis/Tokenizer.json index 47b09165..ec86c282 100644 --- a/subgraph/abis/Tokenizer.json +++ b/subgraph/abis/Tokenizer.json @@ -4,6 +4,19 @@ "inputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "cap", + "inputs": [ + { + "name": "ipToken", + "type": "address", + "internalType": "contract IPToken" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "initialize", @@ -35,6 +48,29 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "issue", + "inputs": [ + { + "name": "ipToken", + "type": "address", + "internalType": "contract IPToken" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "owner", @@ -48,6 +84,25 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "ownerOf", + "inputs": [ + { + "name": "ipnftId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "permissioner", @@ -79,9 +134,9 @@ "name": "reinit", "inputs": [ { - "name": "_permissioner", + "name": "_ipTokenImplementation", "type": "address", - "internalType": "contract IPermissioner" + "internalType": "contract IPToken" } ], "outputs": [], @@ -315,12 +370,6 @@ "type": "event", "name": "TokensCreated", "inputs": [ - { - "name": "moleculesId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, { "name": "ipnftId", "type": "uint256", @@ -384,6 +433,11 @@ "name": "AlreadyTokenized", "inputs": [] }, + { + "type": "error", + "name": "IPTNotControlledByTokenizer", + "inputs": [] + }, { "type": "error", "name": "MustOwnIpnft", @@ -448,5 +502,60 @@ ], "name": "MoleculesCreated", "type": "event" + }, + { + "type": "event", + "name": "TokensCreated", + "inputs": [ + { + "name": "moleculesId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "ipnftId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "tokenContract", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "emitter", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "agreementCid", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "name", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "symbol", + "type": "string", + "indexed": false, + "internalType": "string" + } + ], + "anonymous": false } ] diff --git a/subgraph/makeAbis.sh b/subgraph/makeAbis.sh index f701bb67..c46e627d 100755 --- a/subgraph/makeAbis.sh +++ b/subgraph/makeAbis.sh @@ -13,7 +13,7 @@ cat ../out/CrowdSale.sol/CrowdSale.json | jq .abi > ./abis/CrowdSale.json cat ../out/Tokenizer.sol/Tokenizer.json | jq .abi > ./abis/_Tokenizer.json -# add the old Synthesizer's `MoleculesCreated` event to the Tokenizer abi so the subgraph can index them +# add the old Synthesizer's `MoleculesCreated` and `TokensCreated` event to the Tokenizer abi so the subgraph can index them jq '. += [{ "anonymous": false, "inputs": [ @@ -68,6 +68,60 @@ jq '. += [{ ], "name": "MoleculesCreated", "type": "event" + },{ + "type": "event", + "name": "TokensCreated", + "inputs": [ + { + "name": "moleculesId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "ipnftId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "tokenContract", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "emitter", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "agreementCid", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "name", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "symbol", + "type": "string", + "indexed": false, + "internalType": "string" + } + ], + "anonymous": false }]' ./abis/_Tokenizer.json > ./abis/Tokenizer.json rm ./abis/_Tokenizer.json diff --git a/subgraph/src/tokenizerMapping.ts b/subgraph/src/tokenizerMapping.ts index 91fb5c87..bb8c2223 100644 --- a/subgraph/src/tokenizerMapping.ts +++ b/subgraph/src/tokenizerMapping.ts @@ -6,34 +6,20 @@ import { IPToken } from '../generated/templates' import { IPT } from '../generated/schema' export function handleIPTsCreated(event: TokensCreatedEvent): void { - let reacted = new IPT(event.params.tokenContract.toHexString()) + let ipt = new IPT(event.params.tokenContract.toHexString()) - reacted.createdAt = event.block.timestamp - reacted.ipnft = event.params.ipnftId.toString() - reacted.agreementCid = event.params.agreementCid - reacted.originalOwner = event.params.emitter - reacted.symbol = event.params.symbol - reacted.name = event.params.name - reacted.decimals = BigInt.fromU32(18) + ipt.createdAt = event.block.timestamp + ipt.ipnft = event.params.ipnftId.toString() + ipt.agreementCid = event.params.agreementCid + ipt.originalOwner = event.params.emitter + ipt.symbol = event.params.symbol + ipt.name = event.params.name + ipt.decimals = BigInt.fromU32(18) //these will be updated by the underlying IPT subgraph template - reacted.totalIssued = BigInt.fromU32(0) - reacted.circulatingSupply = BigInt.fromU32(0) + ipt.totalIssued = BigInt.fromU32(0) + ipt.circulatingSupply = BigInt.fromU32(0) IPToken.create(event.params.tokenContract) - reacted.save() + ipt.save() } - -// export function handleSalesActivated(event: SalesActivatedEvent): void { -// let reacted = ReactedIpnft.load(event.params.moleculesId.toString()); -// if (!reacted) { -// log.error('ReactedIpnft not found for id: {}', [ -// event.params.moleculesId.toString() -// ]); -// return; -// } -// reacted.paymentToken = event.params.paymentToken; -// reacted.paidPrice = event.params.paidPrice; -// reacted.claimedShares = BigInt.fromI32(0); -// reacted.save(); -// } diff --git a/subgraph/subgraph.template.yaml b/subgraph/subgraph.template.yaml index 5636c0d8..7fb7ca67 100644 --- a/subgraph/subgraph.template.yaml +++ b/subgraph/subgraph.template.yaml @@ -72,13 +72,13 @@ dataSources: - name: Tokenizer file: ./abis/Tokenizer.json eventHandlers: + - event: TokensCreated(indexed uint256,indexed address,address,uint256,string,string,string) + handler: handleIPTsCreated + # legacy events, used to index mainnet - event: TokensCreated(indexed uint256,indexed uint256,indexed address,address,uint256,string,string,string) handler: handleIPTsCreated - # the legacy event - event: MoleculesCreated(indexed uint256,indexed uint256,indexed address,address,uint256,string,string,string) handler: handleIPTsCreated - # - event: SalesActivated(uint256,address,uint256) - # handler: handleSalesActivated # - event: TermsAccepted(indexed uint256,indexed address,bytes) # handler: handleTermsAccepted file: ./src/tokenizerMapping.ts From 653474af8f2e188e17ab3166b248160908846fd7 Mon Sep 17 00:00:00 2001 From: stadolf Date: Fri, 5 Jul 2024 16:40:26 +0200 Subject: [PATCH 05/10] full mainnet fork test case for new Tokenizer Signed-off-by: stadolf --- test/Forking/Tokenizer13UpgradeForkTest.t.sol | 181 ++++++++++++++---- 1 file changed, 144 insertions(+), 37 deletions(-) diff --git a/test/Forking/Tokenizer13UpgradeForkTest.t.sol b/test/Forking/Tokenizer13UpgradeForkTest.t.sol index 279afd3b..e257910a 100644 --- a/test/Forking/Tokenizer13UpgradeForkTest.t.sol +++ b/test/Forking/Tokenizer13UpgradeForkTest.t.sol @@ -4,19 +4,16 @@ pragma solidity ^0.8.18; import "forge-std/Test.sol"; import { console } from "forge-std/console.sol"; -//import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import { IPNFT } from "../../src/IPNFT.sol"; import { MustOwnIpnft, AlreadyTokenized, Tokenizer } from "../../src/Tokenizer.sol"; import { Tokenizer12 } from "../../src/helpers/test-upgrades/Tokenizer12.sol"; +import { IPToken12, OnlyIssuerOrOwner } from "../../src/helpers/test-upgrades/IPToken12.sol"; import { IPToken, TokenCapped, Metadata } from "../../src/IPToken.sol"; import { IPermissioner, BlindPermissioner } from "../../src/Permissioner.sol"; -// an error thrown by IPToken before 1.3 -//error OnlyIssuerOrOwner(); - contract Tokenizer13UpgradeForkTest is Test { using SafeERC20Upgradeable for IPToken; @@ -32,71 +29,93 @@ contract Tokenizer13UpgradeForkTest is Test { address mainnetTokenizer = 0x58EB89C69CB389DBef0c130C6296ee271b82f436; address mainnetIPNFT = 0xcaD88677CA87a7815728C72D74B4ff4982d54Fc1; - // old IP Token implementation - address vitaFASTAddress = 0x6034e0d6999741f07cb6Fb1162cBAA46a1D33d36; - - // https://app.safe.global/home?safe=eth:0xf7990CD398daFB4fe5Fd6B9228B8e6f72b296555 - address vitaFASTMultisig = 0xf7990CD398daFB4fe5Fd6B9228B8e6f72b296555; + address vitaDaoTreasury = 0xF5307a74d1550739ef81c6488DC5C7a6a53e5Ac2; // paulhaas.eth address paulhaas = 0x45602BFBA960277bF917C1b2007D1f03d7bd29e4; - // ValleyDAO multisig - address valleyDaoMultisig = 0xD920E60b798A2F5a8332799d8a23075c9E77d5F8; - - // ValleyDAO IPNFT - uint256 valleyDaoIpnftId = 3; + IPNFT ipnft = IPNFT(mainnetIPNFT); + Tokenizer tokenizer13; + IPToken newIPTokenImplementation; address alice = makeAddr("alice"); function setUp() public { - mainnetFork = vm.createFork(vm.envString("MAINNET_RPC_URL"), 18968463); + mainnetFork = vm.createFork(vm.envString("MAINNET_RPC_URL"), 20240430); vm.selectFork(mainnetFork); } - function testCanUpgradeToV13() public { - IPNFT ipnftMainnetInstance = IPNFT(mainnetIPNFT); - Tokenizer12 tokenizer12 = Tokenizer12(mainnetTokenizer); - + function upgradeToTokenizer13() public { vm.startPrank(mainnetDeployer); Tokenizer newTokenizerImplementation = new Tokenizer(); - IPToken newIPTokenImplementation = new IPToken(); + newIPTokenImplementation = new IPToken(); vm.stopPrank(); vm.startPrank(mainnetOwner); - bytes memory upgradeCallData = abi.encodeWithSelector(Tokenizer.reinit.selector, address(newIPTokenImplementation)); + Tokenizer12 tokenizer12 = Tokenizer12(mainnetTokenizer); //todo: make sure that the legacy IPTs are indexed now + 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)); + tokenizer13 = Tokenizer(mainnetTokenizer); + } - deployCodeTo("Permissioner.sol:BlindPermissioner", "", address(upgradedTokenizer.permissioner())); - vm.stopPrank(); + function testCanUpgradeToV13() public { + upgradeToTokenizer13(); + assertEq(address(tokenizer13.ipTokenImplementation()), address(newIPTokenImplementation)); + assertEq(address(tokenizer13.permissioner()), 0xC837E02982992B701A1B5e4E21fA01cEB0a628fA); vm.startPrank(alice); vm.expectRevert("Initializable: contract is already initialized"); - newTokenizerImplementation.initialize(IPNFT(address(0)), BlindPermissioner(address(0))); + tokenizer13.initialize(IPNFT(address(0)), BlindPermissioner(address(0))); + vm.expectRevert("Initializable: contract is already initialized"); 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))); + tokenizer13.initialize(IPNFT(address(0)), BlindPermissioner(address(0))); + vm.expectRevert("Initializable: contract is already initialized"); - upgradedTokenizer.reinit(IPToken(address(0))); - vm.stopPrank(); + tokenizer13.reinit(newIPTokenImplementation); + } + + function testOldIPTsAreMigratedAndCantBeReminted() public { + upgradeToTokenizer13(); + + assertEq(address(tokenizer13.synthesized(2)), 0x6034e0d6999741f07cb6Fb1162cBAA46a1D33d36); + assertEq(address(tokenizer13.synthesized(28)), 0x7b66E84Be78772a3afAF5ba8c1993a1B5D05F9C2); + assertEq(address(tokenizer13.synthesized(37)), 0xBcE56276591128047313e64744b3EBE03998783f); + assertEq(address(tokenizer13.synthesized(31415269)), address(0)); + + deployCodeTo("Permissioner.sol:BlindPermissioner", "", address(tokenizer13.permissioner())); + + address vitaFASTMultisig = 0xf7990CD398daFB4fe5Fd6B9228B8e6f72b296555; - assertEq(ipnftMainnetInstance.ownerOf(valleyDaoIpnftId), valleyDaoMultisig); + vm.startPrank(vitaFASTMultisig); + vm.expectRevert(AlreadyTokenized.selector); + tokenizer13.tokenizeIpnft(2, 1_000_000 ether, "VITA-FAST", "bafkreig274nfj7srmtnb5wd5wlwm3ig2s63wovlz7i3noodjlfz2tm3n5q", bytes("")); + + vm.startPrank(alice); + vm.expectRevert(MustOwnIpnft.selector); + tokenizer13.tokenizeIpnft(2, 1_000_000 ether, "VITA-FAST", "bafkreig274nfj7srmtnb5wd5wlwm3ig2s63wovlz7i3noodjlfz2tm3n5q", bytes("")); + } + + function testTokenizeNewIPTs() public { + upgradeToTokenizer13(); + address valleyDaoMultisig = 0xD920E60b798A2F5a8332799d8a23075c9E77d5F8; + uint256 valleyDaoIpnftId = 3; //hasnt been tokenized yet + deployCodeTo("Permissioner.sol:BlindPermissioner", "", address(tokenizer13.permissioner())); + + assertEq(ipnft.ownerOf(valleyDaoIpnftId), valleyDaoMultisig); vm.startPrank(valleyDaoMultisig); - IPToken ipt = upgradedTokenizer.tokenizeIpnft(valleyDaoIpnftId, 1_000_000 ether, "IPT", agreementCid, ""); + IPToken ipt = tokenizer13.tokenizeIpnft(valleyDaoIpnftId, 1_000_000 ether, "VALLEY", agreementCid, ""); assertEq(ipt.balanceOf(valleyDaoMultisig), 1_000_000 ether); ipt.transfer(alice, 100_000 ether); - assertEq(ipt.balanceOf(valleyDaoMultisig), 900_000 ether); + assertEq(ipt.balanceOf(alice), 100_000 ether); + //controlling the IPT from its own interface ipt.issue(valleyDaoMultisig, 1_000_000 ether); assertEq(ipt.totalSupply(), 2_000_000 ether); assertEq(ipt.balanceOf(valleyDaoMultisig), 1_900_000 ether); @@ -107,6 +126,12 @@ contract Tokenizer13UpgradeForkTest is Test { ipt.issue(valleyDaoMultisig, 100); vm.stopPrank(); + } + + function testOldTokensAreStillControllable() public { + upgradeToTokenizer13(); + address vitaFASTAddress = 0x6034e0d6999741f07cb6Fb1162cBAA46a1D33d36; + address vitaFASTMultisig = 0xf7990CD398daFB4fe5Fd6B9228B8e6f72b296555; IPToken vitaFast = IPToken(vitaFASTAddress); @@ -120,12 +145,94 @@ contract Tokenizer13UpgradeForkTest is Test { vm.stopPrank(); vm.startPrank(vitaFASTMultisig); - assertEq(vitaFast.balanceOf(vitaFASTMultisig), 16940676213630533216614); + assertEq(vitaFast.totalSupply(), 1_029_555 ether); + assertEq(vitaFast.balanceOf(vitaFASTMultisig), 13390539642731621592709); + //the old IPTs allow their original owner to issue more tokens vitaFast.issue(vitaFASTMultisig, 100_000 ether); - assertEq(vitaFast.totalSupply(), 1129555 ether); - assertEq(vitaFast.balanceOf(vitaFASTMultisig), 116940676213630533216614); + assertEq(vitaFast.totalSupply(), 1_129_555 ether); + assertEq(vitaFast.balanceOf(vitaFASTMultisig), 113390539642731621592709); + vitaFast.cap(); vm.expectRevert(TokenCapped.selector); vitaFast.issue(vitaFASTMultisig, 100_000 ether); + + /// --- same for VitaRNA, better safe than sorry. + address vitaRNAAddress = 0x7b66E84Be78772a3afAF5ba8c1993a1B5D05F9C2; + address vitaRNAMultisig = 0x452f3b60129FdB3cdc78178848c63eC23f38C80d; + IPToken vitaRna = IPToken(vitaRNAAddress); + + assertEq(vitaRna.balanceOf(paulhaas), 514.411456805927582924 ether); + assertEq(vitaRna.balanceOf(alice), 0); + + vm.startPrank(paulhaas); + vitaRna.transfer(alice, 100 ether); + assertEq(vitaRna.balanceOf(paulhaas), 414.411456805927582924 ether); + assertEq(vitaRna.balanceOf(alice), 100 ether); + vm.stopPrank(); + + vm.startPrank(vitaRNAMultisig); + assertEq(vitaRna.totalSupply(), 5_000_000 ether); + assertEq(vitaRna.balanceOf(vitaRNAMultisig), 200_000 ether); + vitaRna.issue(vitaRNAMultisig, 100_000 ether); + assertEq(vitaRna.totalSupply(), 5_100_000 ether); + assertEq(vitaRna.balanceOf(vitaRNAMultisig), 300_000 ether); + + vitaRna.cap(); + vm.expectRevert(TokenCapped.selector); + vitaRna.issue(vitaRNAMultisig, 100_000 ether); + } + + // IPN-21: and the main reason why we're doing all the above + function testOldTokensCanBeIssuedByNewIPNFTHolder() public { + upgradeToTokenizer13(); + + deployCodeTo("Permissioner.sol:BlindPermissioner", "", address(tokenizer13.permissioner())); + + address bob = makeAddr("bob"); + address vitaFASTMultisig = 0xf7990CD398daFB4fe5Fd6B9228B8e6f72b296555; + //we're using vita fast's original abi here. It actually is call-compatible to IPToken, but this is the ultimate legacy test + IPToken12 vitaFAST12 = IPToken12(0x6034e0d6999741f07cb6Fb1162cBAA46a1D33d36); + IPToken vitaFAST13 = IPToken(0x6034e0d6999741f07cb6Fb1162cBAA46a1D33d36); + + vm.startPrank(vitaFASTMultisig); + ipnft.transferFrom(vitaFASTMultisig, alice, 2); + assertEq(ipnft.ownerOf(2), alice); + + vm.startPrank(alice); + // This is new: originally Alice *would* indeed have been able to do this: + vm.expectRevert(AlreadyTokenized.selector); + tokenizer13.tokenizeIpnft(2, 1_000_000 ether, "VITA-FAST", "imfeelingfunny", bytes("")); + + assertEq(vitaFAST12.balanceOf(alice), 0); + + //this *should* be possible but can't work due to the old implementation + vm.expectRevert(OnlyIssuerOrOwner.selector); + vitaFAST12.issue(alice, 1_000_000 ether); + //the selector of course doesnt exist on the new interface, but the implementation reverts with it: + vm.expectRevert(OnlyIssuerOrOwner.selector); + vitaFAST13.issue(alice, 1_000_000 ether); + + //to issue new tokens, alice uses the Tokenizer instead: + tokenizer13.issue(vitaFAST13, 1_000_000 ether, alice); + assertEq(vitaFAST12.balanceOf(alice), 1_000_000 ether); + + //due to the original implementation, the original owner can still issue tokens and we cannot do anything about it: + vm.startPrank(vitaFASTMultisig); + vitaFAST12.issue(bob, 1_000_000 ether); + assertEq(vitaFAST12.balanceOf(bob), 1_000_000 ether); + assertEq(vitaFAST13.balanceOf(bob), 1_000_000 ether); + + // but they cannot do that using the tokenizer: + vm.expectRevert(MustOwnIpnft.selector); + tokenizer13.issue(vitaFAST13, 1_000_000 ether, alice); + vm.expectRevert(MustOwnIpnft.selector); + tokenizer13.cap(vitaFAST13); + + //but they unfortunately also can cap the token: + vitaFAST12.cap(); + + vm.startPrank(alice); + vm.expectRevert(TokenCapped.selector); + tokenizer13.issue(vitaFAST13, 1_000_000 ether, alice); } } From 78638e5bd4d78498245ae1976fe2da3be31ab72f Mon Sep 17 00:00:00 2001 From: stadolf Date: Mon, 8 Jul 2024 12:10:20 +0200 Subject: [PATCH 06/10] updates subgraph dependencies & config uses latest foundry image uses latest graph specs replaces graph templating with network feature adds network string replacements mainnet Signed-off-by: stadolf --- deploy/inittest.sh | 6 +- docker-compose.yml | 8 +- subgraph/.gitignore | 2 - subgraph/networks.json | 80 ++++ subgraph/package.json | 16 +- .../{subgraph.template.yaml => subgraph.yaml} | 89 ++-- subgraph/yarn.lock | 450 +++++------------- 7 files changed, 271 insertions(+), 380 deletions(-) create mode 100644 subgraph/networks.json rename subgraph/{subgraph.template.yaml => subgraph.yaml} (74%) diff --git a/deploy/inittest.sh b/deploy/inittest.sh index c2fec7b3..1cb4a98e 100755 --- a/deploy/inittest.sh +++ b/deploy/inittest.sh @@ -14,10 +14,14 @@ $DC ps ./setupLocal.sh -f cd subgraph -yarn prepare:local yarn codegen +yarn build:local yarn create:local +# that's a bad local config hack still required: the root's subgraph.yaml's network must be "mainnet" for foundry +sed -i '' -e 's/network\: foundry/network\: mainnet/g' build/subgraph.yaml +sed -i '' -e 's/network\: foundry/network\: mainnet/g' subgraph.yaml yarn deploy:local -l v0.0.1 +sed -i '' -e 's/network\: mainnet/network\: foundry/g' subgraph.yaml cd .. $DC exec -T postgres pg_dump -Fc -U graph-node -w graph-node -f after_setup.dump diff --git a/docker-compose.yml b/docker-compose.yml index 35937f55..259afdeb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,14 @@ #https://github.com/graphprotocol/graph-node/blob/master/docker/docker-compose.yml -version: '3' services: anvil: - image: ghcr.io/foundry-rs/foundry:nightly-a117fbfa41edbaa1618ed099d78d65727bff4790 + image: ghcr.io/foundry-rs/foundry:nightly-a470d635cfcdce68609e9dc5762a3584351bacc1 command: - 'anvil --host 0.0.0.0' ports: - '8545:8545' graph-node: - image: graphprotocol/graph-node + image: graphprotocol/graph-node:845f8fa ports: - '8000:8000' - '8001:8001' @@ -19,6 +18,7 @@ services: depends_on: - ipfs - postgres + - anvil extra_hosts: - host.docker.internal:host-gateway environment: @@ -28,7 +28,7 @@ services: postgres_db: graph-node ipfs: 'ipfs:5001' ethereum: 'mainnet:http://anvil:8545' - GRAPH_LOG: info + GRAPH_LOG: debug GRAPH_ALLOW_NON_DETERMINISTIC_IPFS: 1 ipfs: image: ipfs/kubo:v0.28.0 diff --git a/subgraph/.gitignore b/subgraph/.gitignore index 70a25975..08ba03fa 100644 --- a/subgraph/.gitignore +++ b/subgraph/.gitignore @@ -2,5 +2,3 @@ node_modules build/ generated - -subgraph.yaml diff --git a/subgraph/networks.json b/subgraph/networks.json new file mode 100644 index 00000000..f85ce339 --- /dev/null +++ b/subgraph/networks.json @@ -0,0 +1,80 @@ +{ + "sepolia": { + "IPNFT": { + "address": "0x152B444e60C526fe4434C721561a077269FcF61a", + "startBlock": 5300057 + }, + "SchmackoSwap": { + "address": "0x9e4c638e703d0Af3a3B9eb488dE79A16d402698f", + "startBlock": 5300057 + }, + "Tokenizer": { + "address": "0xca63411FF5187431028d003eD74B57531408d2F9", + "startBlock": 5300057 + }, + "CrowdSale": { + "address": "0x8cA737E2cdaE1Ceb332bEf7ba9eA711a3a2f8037", + "startBlock": 5300057 + }, + "StakedLockingCrowdSale": { + "address": "0xd1cE2EA7d3b0C9cAB025A4aD762FC00315141ad7", + "startBlock": 5300057 + }, + "TermsAcceptedPermissioner": { + "address": "0xC05D649368d8A5e2E98CAa205d47795de5fCB599", + "startBlock": 5300057 + } + }, + "mainnet": { + "IPNFT": { + "address": "0xcaD88677CA87a7815728C72D74B4ff4982d54Fc1", + "startBlock": 17463429 + }, + "SchmackoSwap": { + "address": "0xc09b8577c762b5e97a7d640f242e1d9bfaa7eb9d", + "startBlock": 17441953 + }, + "Tokenizer": { + "address": "0x58EB89C69CB389DBef0c130C6296ee271b82f436", + "startBlock": 17464363 + }, + "CrowdSale": { + "address": "0xf0a8d23f38e9cbbe01c4ed37f23bd519b65bc6c2", + "startBlock": 18490640 + }, + "StakedLockingCrowdSale": { + "address": "0x35Bce29F52f51f547998717CD598068Afa2B29B7", + "startBlock": 17481804 + }, + "TermsAcceptedPermissioner": { + "address": "0xC837E02982992B701A1B5e4E21fA01cEB0a628fA", + "startBlock": 17790450 + } + }, + "foundry": { + "IPNFT": { + "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "startBlock": 0 + }, + "SchmackoSwap": { + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "startBlock": 0 + }, + "Tokenizer": { + "address": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", + "startBlock": 0 + }, + "CrowdSale": { + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + "startBlock": 0 + }, + "StakedLockingCrowdSale": { + "address": "0x0B306BF915C4d645ff596e518fAf3F9669b97016", + "startBlock": 0 + }, + "TermsAcceptedPermissioner": { + "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "startBlock": 0 + } + } +} diff --git a/subgraph/package.json b/subgraph/package.json index 8d8b7c85..77af4e01 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -4,23 +4,21 @@ "scripts": { "abis": "./makeAbis.sh", "codegen": "graph codegen", - "build": "graph codegen && graph build", - "prepare:local": "mustache config/local.js subgraph.template.yaml > subgraph.yaml", - "prepare:sepolia": "mustache config/sepolia.js subgraph.template.yaml > subgraph.yaml", - "prepare:mainnet": "mustache config/mainnet.js subgraph.template.yaml > subgraph.yaml", + "build:local": "graph codegen && graph build --network foundry", + "build:sepolia": "graph codegen && graph build --network sepolia", + "build:mainnet": "graph codegen && graph build --network mainnet", + "deploy:local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 moleculeprotocol/ipnft-subgraph", "deploy:sepolia": "env-cmd -x -f ../.env graph deploy ip-nft-sepolia --version-label 1.0.0 --node https://subgraphs.alchemy.com/api/subgraphs/deploy --ipfs https://ipfs.satsuma.xyz --deploy-key \\$SATSUMA_DEPLOY_KEY", "deploy:mainnet": "env-cmd -x -f ../.env graph deploy ip-nft-mainnet --version-label 1.0.0 --node https://subgraphs.alchemy.com/api/subgraphs/deploy --ipfs https://ipfs.satsuma.xyz --deploy-key \\$SATSUMA_DEPLOY_KEY", "create:local": "graph create --node http://localhost:8020/ moleculeprotocol/ipnft-subgraph", "remove:local": "graph remove --node http://localhost:8020/ moleculeprotocol/ipnft-subgraph", - "deploy:local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 moleculeprotocol/ipnft-subgraph", "test": "graph test" }, "dependencies": { - "@graphprotocol/graph-cli": "^0.50.1", - "@graphprotocol/graph-ts": "^0.30.0", + "@graphprotocol/graph-cli": "^0.78.0", + "@graphprotocol/graph-ts": "^0.35.1", "dotenv": "^16.0.3", - "matchstick-as": "0.5.2", - "mustache": "^4.2.0" + "matchstick-as": "0.5.2" }, "devDependencies": { "env-cmd": "^10.1.0" diff --git a/subgraph/subgraph.template.yaml b/subgraph/subgraph.yaml similarity index 74% rename from subgraph/subgraph.template.yaml rename to subgraph/subgraph.yaml index 7fb7ca67..fe79cd2c 100644 --- a/subgraph/subgraph.template.yaml +++ b/subgraph/subgraph.yaml @@ -1,17 +1,17 @@ -specVersion: 0.0.4 +specVersion: 1.2.0 schema: file: ./schema.graphql dataSources: - kind: ethereum/contract name: IPNFT - network: {{network}} + network: foundry source: - address: '{{ipnft.address}}' abi: IPNFT - startBlock: {{ipnft.startBlock}} + address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + startBlock: 0 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - IPNFT @@ -30,14 +30,14 @@ dataSources: file: ./src/ipnftMapping.ts - kind: ethereum/contract name: SchmackoSwap - network: {{network}} + network: foundry source: - address: '{{schmackoSwap.address}}' abi: SchmackoSwap - startBlock: {{schmackoSwap.startBlock}} + address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + startBlock: 0 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - Listing @@ -49,21 +49,22 @@ dataSources: handler: handleListed - event: Unlisted(uint256,(address,uint256,address,address,uint256,address,uint8)) handler: handleUnlisted - - event: Purchased(uint256,indexed address,(address,uint256,address,address,uint256,address,uint8)) + - event: Purchased(uint256,indexed + address,(address,uint256,address,address,uint256,address,uint8)) handler: handlePurchased - event: AllowlistUpdated(uint256,indexed address,bool) handler: handleAllowlistUpdated file: ./src/swapMapping.ts - kind: ethereum/contract name: Tokenizer - network: {{network}} + network: foundry source: - address: '{{tokenizer.address}}' abi: Tokenizer - startBlock: {{tokenizer.startBlock}} + address: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" + startBlock: 0 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - IPT @@ -72,26 +73,26 @@ dataSources: - name: Tokenizer file: ./abis/Tokenizer.json eventHandlers: - - event: TokensCreated(indexed uint256,indexed address,address,uint256,string,string,string) + - event: TokensCreated(indexed uint256,indexed + address,address,uint256,string,string,string) handler: handleIPTsCreated - # legacy events, used to index mainnet - - event: TokensCreated(indexed uint256,indexed uint256,indexed address,address,uint256,string,string,string) + - event: TokensCreated(indexed uint256,indexed uint256,indexed + address,address,uint256,string,string,string) handler: handleIPTsCreated - - event: MoleculesCreated(indexed uint256,indexed uint256,indexed address,address,uint256,string,string,string) + - event: MoleculesCreated(indexed uint256,indexed uint256,indexed + address,address,uint256,string,string,string) handler: handleIPTsCreated - # - event: TermsAccepted(indexed uint256,indexed address,bytes) - # handler: handleTermsAccepted file: ./src/tokenizerMapping.ts - kind: ethereum/contract name: CrowdSale - network: {{network}} + network: foundry source: - address: '{{crowdSale.address}}' abi: CrowdSale - startBlock: {{crowdSale.startBlock}} + address: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + startBlock: 0 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - CrowdSale @@ -103,7 +104,8 @@ dataSources: - name: IERC20Metadata file: ./abis/IERC20Metadata.json eventHandlers: - - event: Started(indexed uint256,indexed address,(address,address,address,uint256,uint256,uint64,address),uint16) + - event: Started(indexed uint256,indexed + address,(address,address,address,uint256,uint256,uint64,address),uint16) handler: handleStarted - event: Settled(indexed uint256,uint256,uint256) handler: handleSettled @@ -120,14 +122,14 @@ dataSources: file: ./src/crowdSaleMapping.ts - kind: ethereum/contract name: StakedLockingCrowdSale - network: {{network}} + network: foundry source: - address: '{{stakedLockingCrowdSale.address}}' abi: StakedLockingCrowdSale - startBlock: {{stakedLockingCrowdSale.startBlock}} + address: "0x0B306BF915C4d645ff596e518fAf3F9669b97016" + startBlock: 0 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - CrowdSale @@ -139,10 +141,11 @@ dataSources: - name: IERC20Metadata file: ./abis/IERC20Metadata.json eventHandlers: - # the initial crowdsale contract didn't use any fees - - event: Started(indexed uint256,indexed address,(address,address,address,uint256,uint256,uint64,address),(address,address,uint256),address,uint256,uint256) + - event: Started(indexed uint256,indexed + address,(address,address,address,uint256,uint256,uint64,address),(address,address,uint256),address,uint256,uint256) handler: handleStartedLegacy - - event: Started(indexed uint256,indexed address,(address,address,address,uint256,uint256,uint64,address),(address,address,uint256),address,uint256,uint256,uint16) + - event: Started(indexed uint256,indexed + address,(address,address,address,uint256,uint256,uint64,address),(address,address,uint256),address,uint256,uint256,uint16) handler: handleStarted - event: Settled(indexed uint256,uint256,uint256) handler: handleSettled @@ -165,14 +168,14 @@ dataSources: file: ./src/stakedLockingCrowdSaleMapping.ts - kind: ethereum/contract name: TermsAcceptedPermissioner - network: {{network}} + network: foundry source: - address: '{{termsAcceptedPermissioner.address}}' abi: TermsAcceptedPermissioner - startBlock: {{termsAcceptedPermissioner.startBlock}} + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + startBlock: 0 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - IPTBalance @@ -183,16 +186,15 @@ dataSources: - event: TermsAccepted(indexed address,indexed address,bytes) handler: handleTermsAccepted file: ./src/termsAcceptedPermissionerMapping.ts - templates: - name: IPToken kind: ethereum/contract - network: {{network}} + network: foundry source: abi: IPToken mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript file: ./src/iptMapping.ts entities: @@ -205,16 +207,14 @@ templates: handler: handleTransfer - event: Capped(uint256) handler: handleCapped - # - event: SharesClaimed(indexed uint256,indexed address,uint256) - # handler: handleSharesClaimed - name: TimelockedToken kind: ethereum/contract - network: {{network}} + network: foundry source: abi: TimelockedToken mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript file: ./src/timelockedTokenMapping.ts entities: @@ -224,7 +224,8 @@ templates: - name: TimelockedToken file: ./abis/TimelockedToken.json eventHandlers: - - event: ScheduleCreated(indexed bytes32,indexed address,indexed address,uint256,uint64) + - event: ScheduleCreated(indexed bytes32,indexed address,indexed + address,uint256,uint64) handler: handleScheduled - event: ScheduleReleased(indexed bytes32,indexed address,uint256) handler: handleReleased diff --git a/subgraph/yarn.lock b/subgraph/yarn.lock index 4d264c0e..deeb563a 100644 --- a/subgraph/yarn.lock +++ b/subgraph/yarn.lock @@ -217,13 +217,15 @@ graphql-import-node "^0.0.5" js-yaml "^4.1.0" -"@graphprotocol/graph-cli@^0.50.1": - version "0.50.1" - resolved "https://registry.yarnpkg.com/@graphprotocol/graph-cli/-/graph-cli-0.50.1.tgz#ca44138761e4d111e27c3645dd6bb6ea6e07753e" - integrity sha512-tk3e5NYBwXRuRD1y5+UKTRDv+Hwf3a78qmcpGOpIMjMgIpvDnepApRbqMqxt3Ma/RCRkACp0Kmkt3O5Ey4xlkQ== +"@graphprotocol/graph-cli@^0.78.0": + version "0.78.0" + resolved "https://registry.yarnpkg.com/@graphprotocol/graph-cli/-/graph-cli-0.78.0.tgz#9e64bdeef4be66793f92d40db3cfafac2c42cf2e" + integrity sha512-Zw/6k7YYjVoHFgy1BbqpllPUSjzk1ubkIBbYp7HlD//mPUnUhWQLgId92yENw1DqbWOx1R+LhXjeKkwbJgjUxA== dependencies: "@float-capital/float-subgraph-uncrashable" "^0.0.0-alpha.4" - "@oclif/core" "2.8.4" + "@oclif/core" "2.8.6" + "@oclif/plugin-autocomplete" "^2.3.6" + "@oclif/plugin-not-found" "^2.4.0" "@whatwg-node/fetch" "^0.8.4" assemblyscript "0.19.23" binary-install-raw "0.0.13" @@ -234,14 +236,14 @@ dockerode "2.5.8" fs-extra "9.1.0" glob "9.3.5" - gluegun "5.1.2" + gluegun "5.1.6" graphql "15.5.0" immutable "4.2.1" ipfs-http-client "55.0.0" jayson "4.0.0" js-yaml "3.14.1" - prettier "1.19.1" - request "2.88.2" + open "8.4.2" + prettier "3.0.3" semver "7.4.0" sync-request "6.1.0" tmp-promise "3.0.3" @@ -249,10 +251,10 @@ which "2.0.2" yaml "1.10.2" -"@graphprotocol/graph-ts@^0.30.0": - version "0.30.0" - resolved "https://registry.yarnpkg.com/@graphprotocol/graph-ts/-/graph-ts-0.30.0.tgz#591dee3c7d9fc236ad57ce0712779e94aef9a50a" - integrity sha512-h5tJqlsZXglGYM0PcBsBOqof4PT0Fr4Z3QBTYN/IjMF3VvRX2A8/bdpqaAnva+2N0uAfXXwRcwcOcW5O35yzXw== +"@graphprotocol/graph-ts@^0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@graphprotocol/graph-ts/-/graph-ts-0.35.1.tgz#1e1ecc36d8f7a727ef3a6f1fed4c5ce16de378c2" + integrity sha512-74CfuQmf7JI76/XCC34FTkMMKeaf+3Pn0FIV3m9KNeaOJ+OI3CvjMIVRhOZdKcJxsFCBGaCCl0eQjh47xTjxKA== dependencies: assemblyscript "0.19.10" @@ -318,10 +320,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oclif/core@2.8.4": - version "2.8.4" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-2.8.4.tgz#7b453be6d4cd060ff4990bc8e31824a1de308354" - integrity sha512-VlFDhoAJ1RDwcpDF46wAlciWTIryapMUViACttY9GwX6Ci6Lud1awe/pC3k4jad5472XshnPQV4bHAl4a/yxpA== +"@oclif/core@2.8.6": + version "2.8.6" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-2.8.6.tgz#7eb6984108f471ad0d719d3c07cde14c47ab17c5" + integrity sha512-1QlPaHMhOORySCXkQyzjsIsy2GYTilOw3LkjeHkCgsPJQjAT4IclVytJusWktPbYNys9O+O4V23J44yomQvnBQ== dependencies: "@types/cli-progress" "^3.11.0" ansi-escapes "^4.3.2" @@ -353,6 +355,58 @@ wordwrap "^1.0.0" wrap-ansi "^7.0.0" +"@oclif/core@^2.15.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-2.16.0.tgz#e6f3c6c359d4313a15403d8652bbdd0e99ce4b3a" + integrity sha512-dL6atBH0zCZl1A1IXCKJgLPrM/wR7K+Wi401E/IvqsK8m2iCHW+0TEOGrans/cuN3oTW+uxIyJFHJ8Im0k4qBw== + dependencies: + "@types/cli-progress" "^3.11.0" + ansi-escapes "^4.3.2" + ansi-styles "^4.3.0" + cardinal "^2.1.1" + chalk "^4.1.2" + clean-stack "^3.0.1" + cli-progress "^3.12.0" + debug "^4.3.4" + ejs "^3.1.8" + get-package-type "^0.1.0" + globby "^11.1.0" + hyperlinker "^1.0.0" + indent-string "^4.0.0" + is-wsl "^2.2.0" + js-yaml "^3.14.1" + natural-orderby "^2.0.3" + object-treeify "^1.1.33" + password-prompt "^1.1.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + supports-color "^8.1.1" + supports-hyperlinks "^2.2.0" + ts-node "^10.9.1" + tslib "^2.5.0" + widest-line "^3.1.0" + wordwrap "^1.0.0" + wrap-ansi "^7.0.0" + +"@oclif/plugin-autocomplete@^2.3.6": + version "2.3.10" + resolved "https://registry.yarnpkg.com/@oclif/plugin-autocomplete/-/plugin-autocomplete-2.3.10.tgz#787f6208cdfe10ffc68ad89e9e7f1a7ad0e8987f" + integrity sha512-Ow1AR8WtjzlyCtiWWPgzMyT8SbcDJFr47009riLioHa+MHX2BCDtVn2DVnN/E6b9JlPV5ptQpjefoRSNWBesmg== + dependencies: + "@oclif/core" "^2.15.0" + chalk "^4.1.0" + debug "^4.3.4" + +"@oclif/plugin-not-found@^2.4.0": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@oclif/plugin-not-found/-/plugin-not-found-2.4.3.tgz#3d24095adb0f3876cb4bcfdfdcb775086cf6d4b5" + integrity sha512-nIyaR4y692frwh7wIHZ3fb+2L6XEecQwRDIb4zbEam0TvaVmBQWZoColQyWA84ljFBPZ8XWiQyTz+ixSwdRkqg== + dependencies: + "@oclif/core" "^2.15.0" + chalk "^4" + fast-levenshtein "^3.0.0" + "@peculiar/asn1-schema@^2.3.6": version "2.3.6" resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz#3dd3c2ade7f702a9a94dfb395c192f5fa5d6b922" @@ -619,16 +673,6 @@ acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== -ajv@^6.12.3: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - ansi-colors@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" @@ -735,13 +779,6 @@ asap@~2.0.6: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - asn1js@^3.0.1, asn1js@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" @@ -768,10 +805,10 @@ assemblyscript@0.19.23: long "^5.2.0" source-map-support "^0.5.20" -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async@^3.2.3: version "3.2.4" @@ -788,16 +825,6 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== - -aws4@^1.8.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" - integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== - axios@^0.21.1, axios@^0.21.4: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -822,13 +849,6 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -1039,7 +1059,7 @@ chalk@^2.0.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.2, chalk@^4.1.2: +chalk@^4, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1150,7 +1170,7 @@ colors@1.4.0, colors@^1.1.2: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -1182,11 +1202,6 @@ concat-stream@^1.6.0, concat-stream@^1.6.2, concat-stream@~1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== - core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -1251,13 +1266,6 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== - dependencies: - assert-plus "^1.0.0" - debug@4.3.4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -1279,6 +1287,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + delay@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" @@ -1341,20 +1354,12 @@ dotenv@^16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -ejs@3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" - integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== +ejs@3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" + integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== dependencies: - jake "^10.6.1" + jake "^10.8.5" ejs@^3.1.8: version "3.1.9" @@ -1531,21 +1536,6 @@ execa@5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - eyes@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" @@ -1556,11 +1546,6 @@ fast-decode-uri-component@^1.0.1: resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - fast-fifo@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.2.0.tgz#2ee038da2468e8623066dee96958b0c1763aa55a" @@ -1577,10 +1562,12 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-levenshtein@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz#37b899ae47e1090e40e3fd2318e4d5f0142ca912" + integrity sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ== + dependencies: + fastest-levenshtein "^1.0.7" fast-querystring@^1.1.1: version "1.1.1" @@ -1596,6 +1583,11 @@ fast-url-parser@^1.1.3: dependencies: punycode "^1.3.2" +fastest-levenshtein@^1.0.7: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + fastq@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" @@ -1603,7 +1595,7 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -filelist@^1.0.1, filelist@^1.0.4: +filelist@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== @@ -1622,11 +1614,6 @@ follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== - form-data@^2.2.0: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" @@ -1636,15 +1623,6 @@ form-data@^2.2.0: combined-stream "^1.0.6" mime-types "^2.1.12" -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -1719,13 +1697,6 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== - dependencies: - assert-plus "^1.0.0" - glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -1767,10 +1738,10 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -gluegun@5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/gluegun/-/gluegun-5.1.2.tgz#ffa0beda0fb6bbc089a867157b08602beae2c8cf" - integrity sha512-Cwx/8S8Z4YQg07a6AFsaGnnnmd8mN17414NcPS3OoDtZRwxgsvwRNJNg69niD6fDa8oNwslCG0xH7rEpRNNE/g== +gluegun@5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/gluegun/-/gluegun-5.1.6.tgz#74ec13193913dc610f5c1a4039972c70c96a7bad" + integrity sha512-9zbi4EQWIVvSOftJWquWzr9gLX2kaDgPkNR5dYWbM53eVvCI3iKuxLlnKoHC0v4uPoq+Kr/+F569tjoFbA4DSA== dependencies: apisauce "^2.1.5" app-module-path "^2.2.0" @@ -1778,7 +1749,7 @@ gluegun@5.1.2: colors "1.4.0" cosmiconfig "7.0.1" cross-spawn "7.0.3" - ejs "3.1.6" + ejs "3.1.8" enquirer "2.3.6" execa "5.1.1" fs-jetpack "4.3.1" @@ -1823,19 +1794,6 @@ graphql@^16.6.0: resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb" integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -1901,15 +1859,6 @@ http-response-object@^3.0.1: dependencies: "@types/node" "^10.0.3" -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -2089,7 +2038,7 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-docker@^2.0.0: +is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== @@ -2148,11 +2097,6 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" @@ -2185,11 +2129,6 @@ isomorphic-ws@^4.0.1: resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== - it-all@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/it-all/-/it-all-1.0.6.tgz#852557355367606295c4c3b7eff0136f07749335" @@ -2235,16 +2174,6 @@ it-to-stream@^1.0.0: p-fifo "^1.0.0" readable-stream "^3.6.0" -jake@^10.6.1: - version "10.8.6" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.6.tgz#227a96786a1e035214e0ba84b482d6223d41ef04" - integrity sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA== - dependencies: - async "^3.2.3" - chalk "^4.0.2" - filelist "^1.0.4" - minimatch "^3.1.2" - jake@^10.8.5: version "10.8.5" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" @@ -2298,27 +2227,12 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== - json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - -json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: +json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== @@ -2337,16 +2251,6 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - keccak@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.3.tgz#4bc35ad917be1ef54ff246f904c2bbbf9ac61276" @@ -2516,7 +2420,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -2538,7 +2442,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -2642,11 +2546,6 @@ multiformats@^9.4.13, multiformats@^9.4.2, multiformats@^9.4.5, multiformats@^9. resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== -mustache@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" - integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== - nanoid@^3.0.2, nanoid@^3.1.20, nanoid@^3.1.23: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" @@ -2709,11 +2608,6 @@ number-to-bn@1.7.0: bn.js "4.11.6" strip-hex-prefix "1.0.0" -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -2743,6 +2637,15 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open@8.4.2: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + ora@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.2.tgz#0e1e68fd45b135d28648b27cf08081fa6e8a297d" @@ -2843,11 +2746,6 @@ pbkdf2@^3.0.17: safe-buffer "^5.0.1" sha.js "^2.4.8" -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== - picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -2858,10 +2756,10 @@ pluralize@^8.0.0: resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== -prettier@1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" - integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +prettier@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" + integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== process-nextick-args@~2.0.0: version "2.0.1" @@ -2894,11 +2792,6 @@ protobufjs@^6.10.2: "@types/node" ">=13.7.0" long "^4.0.0" -psl@^1.1.28: - version "1.9.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== - pump@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" @@ -2912,11 +2805,6 @@ punycode@^1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== -punycode@^2.1.0, punycode@^2.1.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== - pvtsutils@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de" @@ -2936,11 +2824,6 @@ qs@^6.4.0: dependencies: side-channel "^1.0.4" -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -3013,32 +2896,6 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" -request@2.88.2: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -3108,7 +2965,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -3209,6 +3066,15 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + source-map-support@^0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -3232,21 +3098,6 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - stream-to-it@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/stream-to-it/-/stream-to-it-0.2.4.tgz#d2fd7bfbd4a899b4c0d6a7e6a533723af5749bd0" @@ -3450,14 +3301,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -3487,18 +3330,6 @@ tslib@^2.0.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -3521,13 +3352,6 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - urlpattern-polyfill@^8.0.0: version "8.0.2" resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz#99f096e35eff8bf4b5a2aa7d58a1523d6ebc7ce5" @@ -3543,11 +3367,6 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -3563,15 +3382,6 @@ varint@^6.0.0: resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - wabt@1.0.24: version "1.0.24" resolved "https://registry.yarnpkg.com/wabt/-/wabt-1.0.24.tgz#c02e0b5b4503b94feaf4a30a426ef01c1bea7c6c" From 8566a1914d4e0e579c6c682c43e07d02eb639271 Mon Sep 17 00:00:00 2001 From: stadolf Date: Tue, 9 Jul 2024 00:30:20 +0200 Subject: [PATCH 07/10] rolled back reduced event signature for subgraph compatibility new deployment preparation script Signed-off-by: stadolf --- ...zerV12.s.sol => RolloutTokenizerV13.s.sol} | 4 +- src/Tokenizer.sol | 21 ++++++- subgraph/abis/Tokenizer.json | 61 ++----------------- subgraph/makeAbis.sh | 54 ---------------- subgraph/subgraph.yaml | 3 - 5 files changed, 27 insertions(+), 116 deletions(-) rename script/prod/{RolloutTokenizerV12.s.sol => RolloutTokenizerV13.s.sol} (85%) diff --git a/script/prod/RolloutTokenizerV12.s.sol b/script/prod/RolloutTokenizerV13.s.sol similarity index 85% rename from script/prod/RolloutTokenizerV12.s.sol rename to script/prod/RolloutTokenizerV13.s.sol index 5bcbb1f9..35aa5f4e 100644 --- a/script/prod/RolloutTokenizerV12.s.sol +++ b/script/prod/RolloutTokenizerV13.s.sol @@ -6,14 +6,14 @@ import { Tokenizer } from "../../src/Tokenizer.sol"; import { IPToken } from "../../src/IPToken.sol"; import { console } from "forge-std/console.sol"; -contract RolloutTokenizerV12 is Script { +contract RolloutTokenizerV13 is Script { function run() public { vm.startBroadcast(); IPToken newIpTokenImplementation = new IPToken(); Tokenizer newTokenizerImplementation = new Tokenizer(); - bytes memory upgradeCallData = abi.encodeWithSelector(Tokenizer.setIPTokenImplementation.selector, address(newIpTokenImplementation)); + bytes memory upgradeCallData = abi.encodeWithSelector(Tokenizer.reinit.selector, address(newIpTokenImplementation)); console.log("NEWTOKENIMPLEMENTATION=%s", address(newIpTokenImplementation)); console.log("NEWTOKENIZER=%s", address(newTokenizerImplementation)); diff --git a/src/Tokenizer.sol b/src/Tokenizer.sol index 3f33690d..2c16c7e9 100644 --- a/src/Tokenizer.sol +++ b/src/Tokenizer.sol @@ -19,7 +19,14 @@ 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 ipnftId, address indexed tokenContract, address emitter, uint256 amount, string agreementCid, string name, string symbol + uint256 indexed moleculesId, + uint256 indexed ipnftId, + address indexed tokenContract, + address emitter, + uint256 amount, + string agreementCid, + string name, + string symbol ); event IPTokenImplementationUpdated(IPToken indexed old, IPToken indexed _new); @@ -129,7 +136,17 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { synthesized[ipnftId] = token; //this has been called MoleculesCreated before - emit TokensCreated(ipnftId, address(token), _msgSender(), tokenAmount, agreementCid, name, tokenSymbol); + emit TokensCreated( + //upwards compatibility: signaling an unique "Molecules ID" as first parameter ("sales cycle id"). This is unused and not interpreted. + uint256(keccak256(abi.encodePacked(ipnftId))), + ipnftId, + address(token), + _msgSender(), + tokenAmount, + agreementCid, + name, + tokenSymbol + ); permissioner.accept(token, _msgSender(), signedAgreement); token.issue(_msgSender(), tokenAmount); } diff --git a/subgraph/abis/Tokenizer.json b/subgraph/abis/Tokenizer.json index ec86c282..ac4e89c7 100644 --- a/subgraph/abis/Tokenizer.json +++ b/subgraph/abis/Tokenizer.json @@ -370,6 +370,12 @@ "type": "event", "name": "TokensCreated", "inputs": [ + { + "name": "moleculesId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, { "name": "ipnftId", "type": "uint256", @@ -502,60 +508,5 @@ ], "name": "MoleculesCreated", "type": "event" - }, - { - "type": "event", - "name": "TokensCreated", - "inputs": [ - { - "name": "moleculesId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "ipnftId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "tokenContract", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "emitter", - "type": "address", - "indexed": false, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "agreementCid", - "type": "string", - "indexed": false, - "internalType": "string" - }, - { - "name": "name", - "type": "string", - "indexed": false, - "internalType": "string" - }, - { - "name": "symbol", - "type": "string", - "indexed": false, - "internalType": "string" - } - ], - "anonymous": false } ] diff --git a/subgraph/makeAbis.sh b/subgraph/makeAbis.sh index c46e627d..9ad940ae 100755 --- a/subgraph/makeAbis.sh +++ b/subgraph/makeAbis.sh @@ -68,60 +68,6 @@ jq '. += [{ ], "name": "MoleculesCreated", "type": "event" - },{ - "type": "event", - "name": "TokensCreated", - "inputs": [ - { - "name": "moleculesId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "ipnftId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "tokenContract", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "emitter", - "type": "address", - "indexed": false, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "agreementCid", - "type": "string", - "indexed": false, - "internalType": "string" - }, - { - "name": "name", - "type": "string", - "indexed": false, - "internalType": "string" - }, - { - "name": "symbol", - "type": "string", - "indexed": false, - "internalType": "string" - } - ], - "anonymous": false }]' ./abis/_Tokenizer.json > ./abis/Tokenizer.json rm ./abis/_Tokenizer.json diff --git a/subgraph/subgraph.yaml b/subgraph/subgraph.yaml index fe79cd2c..94d7b019 100644 --- a/subgraph/subgraph.yaml +++ b/subgraph/subgraph.yaml @@ -73,9 +73,6 @@ dataSources: - name: Tokenizer file: ./abis/Tokenizer.json eventHandlers: - - event: TokensCreated(indexed uint256,indexed - address,address,uint256,string,string,string) - handler: handleIPTsCreated - event: TokensCreated(indexed uint256,indexed uint256,indexed address,address,uint256,string,string,string) handler: handleIPTsCreated From df9f67693d058da6ef11aea619490bde9f96b4bc Mon Sep 17 00:00:00 2001 From: stadolf Date: Tue, 9 Jul 2024 12:13:15 +0200 Subject: [PATCH 08/10] IPT/Tokenizer: "controller" terminology leaves room for interpretation updates sales distributor to acknowledge IPT controllers Signed-off-by: stadolf --- README.md | 34 ++-------- package.json | 3 +- src/IPToken.sol | 12 ++-- src/SalesShareDistributor.sol | 68 ++++++++++--------- src/Tokenizer.sol | 18 +++-- subgraph/abis/IPToken.json | 2 +- subgraph/abis/SharedSalesDistributor.json | 16 ++++- subgraph/abis/Tokenizer.json | 53 +++++++++------ test/CrowdSalePermissioned.t.sol | 2 +- test/Forking/Tokenizer13UpgradeForkTest.t.sol | 8 +-- test/Permissioner.t.sol | 2 +- test/SalesShareDistributor.t.sol | 41 ++++++----- test/Tokenizer.t.sol | 16 ++--- 13 files changed, 143 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 2ea98796..425fbbc2 100644 --- a/README.md +++ b/README.md @@ -87,38 +87,12 @@ VDAO_TOKEN_ADDRESS=0x19A3036b828bffB5E14da2659E950E76f8e6BAA2 --- -### ~~Deprecated Goerli~~ +### upgrading to Tokenizer 1.3 -| Contract | Address | Actions | -| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| IP-NFT | [0xaf7358576C9F7cD84696D28702fC5ADe33cce0e9](https://goerli.etherscan.io/address/0xaf7358576C9F7cD84696D28702fC5ADe33cce0e9#code>) | View contract | -| SchmackoSwap | [0x67D8ed102E2168A46FA342e39A5f7D16c103Bd0d](https://goerli.etherscan.io/address/0x67D8ed102E2168A46FA342e39A5f7D16c103Bd0d#code) | View contract | -| Tokenizer | [0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c](https://goerli.etherscan.io/address/0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c#code) | View contract | -| Permissioner | [0xd735d9504cce32F2cd665b779D699B4157686fcd](https://goerli.etherscan.io/address/0xd735d9504cce32F2cd665b779D699B4157686fcd#code) | View contract | -| Crowdsale | [0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373](https://goerli.etherscan.io/address/0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373#code>) | View contract | -| StakedLockingCrowdSale | [0x46c3369dece07176ad7164906d3593aa4c126d35](https://goerli.etherscan.io/address/0x46c3369dece07176ad7164906d3593aa4c126d35#code) | View contract | -| SignedMintAuthorizer | [0x5e555eE24DB66825171Ac63EA614864987CEf1Af](https://goerli.etherscan.io/address/0x5e555eE24DB66825171Ac63EA614864987CEf1Af#code) | View contract | -| IPToken Implementation | [0x38Ca0fEEc7cd48629f9388727bfA747859a6feE7](https://goerli.etherscan.io/address/0x38Ca0fEEc7cd48629f9388727bfA747859a6feE7#code) | View contract | +forge script --private-key=$PRIVATE_KEY --rpc-url=$RPC_URL script/prod/RolloutTokenizerV13.s.sol --broadcast -- Subgraph: https://api.thegraph.com/subgraphs/name/moleculeprotocol/ip-nft-goerli - -- Tokenizer Implementation 1.2: 0x18E5ae026CFC8020b2eDbA7050eA6144Fd313c02 (reinit 4) - -- Bio pricefeed: 0x8647dEFdEAAdF5448d021B364B2F17815aba4360 - - -- old ("molecule") permissioner: 0x0045723801561079d94f0Bb1B65f322078E52635 - - -- Blind Permissioner: 0xec68a1fc8d4c2834f8dfbdb56691f9f0a3d6be11 - - -#### ~~Tokens~~ - -| Token name | Symbol | address | -| ------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------- | -| BioDao Test token | BIODAO | [0x3110a768DC64f7aAB92F7Ae6E1371e5CE581F95F](https://goerli.etherscan.io/address/0x3110a768dc64f7aab92f7ae6e1371e5ce581f95f#code) | -| Vested BioDao Test token | vBIODAO | [0x6FFBd6325B2102F5f9AaB74d7418A27F9174c92f](https://goerli.etherscan.io/address/0x6FFBd6325B2102F5f9AaB74d7418A27F9174c92f) | +// 0xTokenizer 0xNewImpl 0xNewTokenImpl +cast send --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY 0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e "upgradeToAndCall(address,bytes)" 0x70e0bA845a1A0F2DA3359C97E0285013525FFC49 0x84646c1f000000000000000000000000998abeb3e57409262ae5b751f60747921b33613e ## Prerequisites diff --git a/package.json b/package.json index b4dc992b..fadfd480 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "license": "MIT", "scripts": { - "test": "hardhat test --network hardhat" + "test": "hardhat test --network hardhat", + "clean": "rm -rf out cache_forge" }, "devDependencies": { "@nomicfoundation/hardhat-foundry": "^1.0.0", diff --git a/src/IPToken.sol b/src/IPToken.sol index ecab5dd9..c4c6aed4 100644 --- a/src/IPToken.sol +++ b/src/IPToken.sol @@ -5,7 +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"; +import { Tokenizer, MustControlIpnft } from "./Tokenizer.sol"; struct Metadata { uint256 ipnftId; @@ -47,9 +47,9 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { _disableInitializers(); } - modifier onlyTokenizerOrIPNFTHolder() { - if (_msgSender() != owner() && _msgSender() != Tokenizer(owner()).ownerOf(_metadata.ipnftId)) { - revert MustOwnIpnft(); + modifier onlyTokenizerOrIPNFTController() { + if (_msgSender() != owner() && _msgSender() != Tokenizer(owner()).controllerOf(_metadata.ipnftId)) { + revert MustControlIpnft(); } _; } @@ -63,7 +63,7 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { * @param receiver address * @param amount uint256 */ - function issue(address receiver, uint256 amount) external onlyTokenizerOrIPNFTHolder { + function issue(address receiver, uint256 amount) external onlyTokenizerOrIPNFTController { if (capped) { revert TokenCapped(); } @@ -74,7 +74,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 onlyTokenizerOrIPNFTHolder { + function cap() external onlyTokenizerOrIPNFTController { capped = true; emit Capped(totalIssued); } diff --git a/src/SalesShareDistributor.sol b/src/SalesShareDistributor.sol index 4377d8bf..16208780 100644 --- a/src/SalesShareDistributor.sol +++ b/src/SalesShareDistributor.sol @@ -7,7 +7,9 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IPNFT } from "./IPNFT.sol"; import { IPToken, Metadata } from "./IPToken.sol"; +import { Tokenizer, MustControlIpnft } from "./Tokenizer.sol"; import { SchmackoSwap, ListingState } from "./SchmackoSwap.sol"; import { IPermissioner, TermsAcceptedPermissioner } from "./Permissioner.sol"; @@ -21,9 +23,9 @@ struct Sales { error ListingNotFulfilled(); error ListingMismatch(); error InsufficientBalance(); +error NotSalesBeneficiary(); error UncappedToken(); -error OnlyIssuer(); - +error OnlySeller(); error NotClaimingYet(); /** @@ -90,57 +92,61 @@ 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 original owner since they must provide a permissioner that controls the claiming rules - * this is a deep dependency on our own sales contract + * @notice release sales shares for a Schmackoswap transaction + * @dev todo: *anyone* should be able to call this function after having observed the sale; right now we restrict it to the creator of the trade since they were in control of the IPNFT before + * @dev this has a deep dependency on our own swap contract * - * @param tokenContract IPToken the tokenContract of the IPToken + * @param ipt IPToken the tokenContract of the IPToken * @param listingId uint256 the listing id on Schmackoswap * @param permissioner IPermissioner the permissioner that permits claims */ - function afterSale(IPToken tokenContract, uint256 listingId, IPermissioner permissioner) external { - 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(); - } - - (, uint256 ipnftId,, IERC20 _paymentToken, uint256 askPrice, address beneficiary, ListingState listingState) = + function afterSale(IPToken ipt, uint256 listingId, IPermissioner permissioner) external { + (, uint256 ipnftId, address seller, IERC20 _paymentToken, uint256 askPrice, address beneficiary, ListingState listingState) = schmackoSwap.listings(listingId); + if (_msgSender() != seller) { + revert OnlySeller(); + } + if (listingState != ListingState.FULFILLED) { revert ListingNotFulfilled(); } + Metadata memory metadata = ipt.metadata(); if (ipnftId != metadata.ipnftId) { revert ListingMismatch(); } if (beneficiary != address(this)) { - revert InsufficientBalance(); + revert NotSalesBeneficiary(); } - _startClaimingPhase(tokenContract, listingId, _paymentToken, askPrice, permissioner); + _startClaimingPhase(ipt, listingId, _paymentToken, askPrice, permissioner); } //audit: ensure that no one can withdraw arbitrary amounts here //by simply creating a new IPToken instance and claim an arbitrary value - + //todo: try breaking this by providing a fake IPT with a fake Tokenizer owner + //todo: this must be called by the beneficiary of a sale we don't control. /** * @notice When the sales beneficiary has not been set to the underlying erc20 token address but to the original owner's wallet instead, - * they can invoke this method to start the claiming phase manually. This e.g. allows sales off the record. + * they can invoke this method to start the claiming phase manually. This e.g. allows sales off the record ("OpenSea"). * - * Requires the originalOwner to behave honestly / in favor of the molecules holders - * Requires the caller to have approved `price` of `paymentToken` to this contract + * Requires the originalOwner to behave honestly / in favor of the IPT holders + * Requires the caller to have approved `paidPrice` of `paymentToken` to this contract * * @param tokenContract IPToken the IPToken token contract * @param paymentToken IERC20 the payment token contract address - * @param paidPrice uint256 the price the NFT has been sold for - * @param permissioner IPermissioner the permissioner that permits claims + * @param paidPrice uint256 the price the NFT has been sold for + * @param permissioner IPermissioner the permissioner that permits claims */ - function afterSale(IPToken tokenContract, IERC20 paymentToken, uint256 paidPrice, IPermissioner permissioner) external nonReentrant { + function UNSAFE_afterSale(IPToken tokenContract, IERC20 paymentToken, uint256 paidPrice, IPermissioner permissioner) external nonReentrant { Metadata memory metadata = tokenContract.metadata(); - //todo: this should be the *former* holder of the IPNFT, not the 1st owner :) + + Tokenizer tokenizer = Tokenizer(tokenContract.owner()); + + //todo: this should be a selected beneficiary of the IPNFT's sales proceeds, and not the original owner :) + //idea is to allow *several* sales proceeds to be notified here, create unique sales ids for each and let users claim the all of them at once if (_msgSender() != metadata.originalOwner) { - revert OnlyIssuer(); + revert MustControlIpnft(); } //create a fake (but valid) schmackoswap listing id @@ -148,7 +154,7 @@ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, Reentranc keccak256( abi.encode( SchmackoSwap.Listing( - IERC721(address(0)), //this should be the IPNFT address + IERC721(address(tokenizer.getIPNFTContract())), metadata.ipnftId, _msgSender(), paymentToken, @@ -164,15 +170,13 @@ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, Reentranc paymentToken.safeTransferFrom(_msgSender(), address(this), paidPrice); } - function _startClaimingPhase(IPToken tokenContract, uint256 fulfilledListingId, IERC20 _paymentToken, uint256 price, IPermissioner permissioner) - internal - { - //todo: this actually must be enforced before a sale starts + function _startClaimingPhase(IPToken ipt, uint256 fulfilledListingId, IERC20 _paymentToken, uint256 price, IPermissioner permissioner) internal { + //todo: this *should* 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); + sales[address(ipt)] = Sales(fulfilledListingId, _paymentToken, price, permissioner); + emit SalesActivated(address(ipt), address(_paymentToken), price); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } diff --git a/src/Tokenizer.sol b/src/Tokenizer.sol index 2c16c7e9..b8ab3934 100644 --- a/src/Tokenizer.sol +++ b/src/Tokenizer.sol @@ -9,7 +9,7 @@ import { IPToken, Metadata as TokenMetadata } from "./IPToken.sol"; import { IPermissioner } from "./Permissioner.sol"; import { IPNFT } from "./IPNFT.sol"; -error MustOwnIpnft(); +error MustControlIpnft(); error AlreadyTokenized(); error ZeroAddress(); error IPTNotControlledByTokenizer(); @@ -64,6 +64,10 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { _disableInitializers(); } + function getIPNFTContract() public view returns (IPNFT) { + return ipnft; + } + //todo: try breaking this with a faked IPToken modifier onlyControlledIPTs(IPToken ipToken) { TokenMetadata memory metadata = ipToken.metadata(); @@ -72,8 +76,8 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { revert IPTNotControlledByTokenizer(); } - if (_msgSender() != ipnft.ownerOf(metadata.ipnftId)) { - revert MustOwnIpnft(); + if (_msgSender() != controllerOf(metadata.ipnftId)) { + revert MustControlIpnft(); } _; } @@ -121,8 +125,8 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { string memory agreementCid, bytes calldata signedAgreement ) external returns (IPToken token) { - if (ipnft.ownerOf(ipnftId) != _msgSender()) { - revert MustOwnIpnft(); + if (_msgSender() != controllerOf(ipnftId)) { + revert MustControlIpnft(); } if (address(synthesized[ipnftId]) != address(0)) { revert AlreadyTokenized(); @@ -170,8 +174,8 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { ipToken.cap(); } - /// @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) { + /// @dev this will be called by IPTs. Right now the controller is the IPNFT's current owner, it can be a Governor in the future. + function controllerOf(uint256 ipnftId) public view returns (address) { return ipnft.ownerOf(ipnftId); } diff --git a/subgraph/abis/IPToken.json b/subgraph/abis/IPToken.json index 17730b81..3927254a 100644 --- a/subgraph/abis/IPToken.json +++ b/subgraph/abis/IPToken.json @@ -512,7 +512,7 @@ }, { "type": "error", - "name": "MustOwnIpnft", + "name": "MustControlIpnft", "inputs": [] }, { diff --git a/subgraph/abis/SharedSalesDistributor.json b/subgraph/abis/SharedSalesDistributor.json index 8291dfd0..91cbbdbf 100644 --- a/subgraph/abis/SharedSalesDistributor.json +++ b/subgraph/abis/SharedSalesDistributor.json @@ -1,7 +1,7 @@ [ { "type": "function", - "name": "afterSale", + "name": "UNSAFE_afterSale", "inputs": [ { "name": "tokenContract", @@ -32,7 +32,7 @@ "name": "afterSale", "inputs": [ { - "name": "tokenContract", + "name": "ipt", "type": "address", "internalType": "contract IPToken" }, @@ -363,6 +363,11 @@ "name": "ListingNotFulfilled", "inputs": [] }, + { + "type": "error", + "name": "MustControlIpnft", + "inputs": [] + }, { "type": "error", "name": "NotClaimingYet", @@ -370,7 +375,12 @@ }, { "type": "error", - "name": "OnlyIssuer", + "name": "NotSalesBeneficiary", + "inputs": [] + }, + { + "type": "error", + "name": "OnlySeller", "inputs": [] } ] diff --git a/subgraph/abis/Tokenizer.json b/subgraph/abis/Tokenizer.json index ac4e89c7..8f5fdb9b 100644 --- a/subgraph/abis/Tokenizer.json +++ b/subgraph/abis/Tokenizer.json @@ -17,6 +17,38 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "controllerOf", + "inputs": [ + { + "name": "ipnftId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getIPNFTContract", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IPNFT" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "initialize", @@ -84,25 +116,6 @@ ], "stateMutability": "view" }, - { - "type": "function", - "name": "ownerOf", - "inputs": [ - { - "name": "ipnftId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "permissioner", @@ -446,7 +459,7 @@ }, { "type": "error", - "name": "MustOwnIpnft", + "name": "MustControlIpnft", "inputs": [] }, { diff --git a/test/CrowdSalePermissioned.t.sol b/test/CrowdSalePermissioned.t.sol index 05c96484..86fb14b0 100644 --- a/test/CrowdSalePermissioned.t.sol +++ b/test/CrowdSalePermissioned.t.sol @@ -13,7 +13,7 @@ import { IPToken, Metadata } from "../src/IPToken.sol"; import { CrowdSale, Sale, SaleInfo, SaleState, BadDecimals } from "../src/crowdsale/CrowdSale.sol"; import { StakedLockingCrowdSale, BadPrice } from "../src/crowdsale/StakedLockingCrowdSale.sol"; import { IPermissioner, TermsAcceptedPermissioner, InvalidSignature, BlindPermissioner } from "../src/Permissioner.sol"; -import { MustOwnIpnft, AlreadyTokenized, Tokenizer, ZeroAddress } from "../src/Tokenizer.sol"; +import { MustControlIpnft, AlreadyTokenized, Tokenizer, ZeroAddress } from "../src/Tokenizer.sol"; import { IPNFT } from "../src/IPNFT.sol"; import { TokenVesting } from "@moleculeprotocol/token-vesting/TokenVesting.sol"; import { TimelockedToken } from "../src/TimelockedToken.sol"; diff --git a/test/Forking/Tokenizer13UpgradeForkTest.t.sol b/test/Forking/Tokenizer13UpgradeForkTest.t.sol index e257910a..15fccfa4 100644 --- a/test/Forking/Tokenizer13UpgradeForkTest.t.sol +++ b/test/Forking/Tokenizer13UpgradeForkTest.t.sol @@ -8,7 +8,7 @@ import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ import { IPNFT } from "../../src/IPNFT.sol"; -import { MustOwnIpnft, AlreadyTokenized, Tokenizer } from "../../src/Tokenizer.sol"; +import { MustControlIpnft, AlreadyTokenized, Tokenizer } from "../../src/Tokenizer.sol"; import { Tokenizer12 } from "../../src/helpers/test-upgrades/Tokenizer12.sol"; import { IPToken12, OnlyIssuerOrOwner } from "../../src/helpers/test-upgrades/IPToken12.sol"; import { IPToken, TokenCapped, Metadata } from "../../src/IPToken.sol"; @@ -96,7 +96,7 @@ contract Tokenizer13UpgradeForkTest is Test { tokenizer13.tokenizeIpnft(2, 1_000_000 ether, "VITA-FAST", "bafkreig274nfj7srmtnb5wd5wlwm3ig2s63wovlz7i3noodjlfz2tm3n5q", bytes("")); vm.startPrank(alice); - vm.expectRevert(MustOwnIpnft.selector); + vm.expectRevert(MustControlIpnft.selector); tokenizer13.tokenizeIpnft(2, 1_000_000 ether, "VITA-FAST", "bafkreig274nfj7srmtnb5wd5wlwm3ig2s63wovlz7i3noodjlfz2tm3n5q", bytes("")); } @@ -223,9 +223,9 @@ contract Tokenizer13UpgradeForkTest is Test { assertEq(vitaFAST13.balanceOf(bob), 1_000_000 ether); // but they cannot do that using the tokenizer: - vm.expectRevert(MustOwnIpnft.selector); + vm.expectRevert(MustControlIpnft.selector); tokenizer13.issue(vitaFAST13, 1_000_000 ether, alice); - vm.expectRevert(MustOwnIpnft.selector); + vm.expectRevert(MustControlIpnft.selector); tokenizer13.cap(vitaFAST13); //but they unfortunately also can cap the token: diff --git a/test/Permissioner.t.sol b/test/Permissioner.t.sol index ab26782b..b5400a0e 100644 --- a/test/Permissioner.t.sol +++ b/test/Permissioner.t.sol @@ -12,7 +12,7 @@ import { IPNFT } from "../src/IPNFT.sol"; import { Safe } from "safe-global/safe-contracts/Safe.sol"; import { SafeProxyFactory } from "safe-global/safe-contracts/proxies/SafeProxyFactory.sol"; import { Enum } from "safe-global/safe-contracts/common/Enum.sol"; -import { MustOwnIpnft, AlreadyTokenized, Tokenizer, ZeroAddress } from "../src/Tokenizer.sol"; +import { MustControlIpnft, AlreadyTokenized, Tokenizer, ZeroAddress } from "../src/Tokenizer.sol"; import "./helpers/MakeGnosisWallet.sol"; diff --git a/test/SalesShareDistributor.t.sol b/test/SalesShareDistributor.t.sol index c645b007..1085acbf 100644 --- a/test/SalesShareDistributor.t.sol +++ b/test/SalesShareDistributor.t.sol @@ -24,8 +24,10 @@ import { ListingMismatch, NotClaimingYet, UncappedToken, - OnlyIssuer, - InsufficientBalance + OnlySeller, + MustControlIpnft, + InsufficientBalance, + NotSalesBeneficiary } from "../src/SalesShareDistributor.sol"; import { IPToken } from "../src/IPToken.sol"; @@ -160,17 +162,17 @@ contract SalesShareDistributorTest is Test { vm.startPrank(ipnftBuyer); erc20.transfer(originalOwner, 1_000_000 ether); - // only the owner can manually start the claiming phase. + // only the origina owner can atm manually start the claiming phase. vm.startPrank(bob); - vm.expectRevert(OnlyIssuer.selector); - distributor.afterSale(tokenContract, erc20, 1_000_000 ether, blindPermissioner); + vm.expectRevert(MustControlIpnft.selector); + distributor.UNSAFE_afterSale(tokenContract, erc20, 1_000_000 ether, blindPermissioner); vm.startPrank(originalOwner); vm.expectRevert(); // not approved - distributor.afterSale(tokenContract, erc20, 1_000_000 ether, blindPermissioner); + distributor.UNSAFE_afterSale(tokenContract, erc20, 1_000_000 ether, blindPermissioner); erc20.approve(address(distributor), 1_000_000 ether); - distributor.afterSale(tokenContract, erc20, 1_000_000 ether, blindPermissioner); + distributor.UNSAFE_afterSale(tokenContract, erc20, 1_000_000 ether, blindPermissioner); assertEq(erc20.balanceOf(address(originalOwner)), 0); (uint256 fulfilledListingId,,,) = distributor.sales(address(tokenContract)); @@ -204,7 +206,7 @@ contract SalesShareDistributorTest is Test { vm.startPrank(originalOwner); erc20.approve(address(distributor), 1_000_000 ether); - distributor.afterSale(tokenContract, erc20, 1_000_000 ether, permissioner); + distributor.UNSAFE_afterSale(tokenContract, erc20, 1_000_000 ether, permissioner); vm.startPrank(alice); (, uint256 amount) = distributor.claimableTokens(tokenContract, alice); @@ -303,11 +305,14 @@ contract SalesShareDistributorTest is Test { vm.startPrank(originalOwner); erc20.approve(address(distributor), salesPrice); - distributor.afterSale(tokenContract, erc20, salesPrice, blindPermissioner); + distributor.UNSAFE_afterSale(tokenContract, erc20, salesPrice, blindPermissioner); vm.stopPrank(); } function testClaimingFraud() public { + address anotherOwner = makeAddr("anotherOwner"); + address unknown = makeAddr("unknown"); + vm.startPrank(originalOwner); IPToken tokenContract1 = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); tokenContract1.cap(); @@ -316,14 +321,14 @@ contract SalesShareDistributorTest is Test { uint256 listingId1 = schmackoSwap.list(IERC721(address(ipnft)), 1, erc20, 1000 ether, address(distributor)); schmackoSwap.changeBuyerAllowance(listingId1, ipnftBuyer, true); - vm.startPrank(bob); - vm.deal(bob, MINTING_FEE); + vm.startPrank(anotherOwner); + vm.deal(anotherOwner, MINTING_FEE); uint256 reservationId = ipnft.reserve(); - ipnft.mintReservation{ value: MINTING_FEE }(bob, reservationId, ipfsUri, DEFAULT_SYMBOL, ""); + ipnft.mintReservation{ value: MINTING_FEE }(anotherOwner, reservationId, ipfsUri, DEFAULT_SYMBOL, ""); ipnft.setApprovalForAll(address(schmackoSwap), true); IPToken tokenContract2 = tokenizer.tokenizeIpnft(2, 70_000, "MOLE", agreementCid, ""); tokenContract2.cap(); - uint256 listingId2 = schmackoSwap.list(IERC721(address(ipnft)), 2, erc20, 700 ether, address(originalOwner)); + uint256 listingId2 = schmackoSwap.list(IERC721(address(ipnft)), 2, erc20, 700 ether, unknown); schmackoSwap.changeBuyerAllowance(listingId2, ipnftBuyer, true); vm.stopPrank(); @@ -332,16 +337,16 @@ contract SalesShareDistributorTest is Test { schmackoSwap.fulfill(listingId1); schmackoSwap.fulfill(listingId2); - vm.startPrank(bob); - vm.expectRevert(InsufficientBalance.selector); + vm.startPrank(anotherOwner); + vm.expectRevert(NotSalesBeneficiary.selector); distributor.afterSale(tokenContract2, listingId2, blindPermissioner); - vm.startPrank(originalOwner); vm.expectRevert(ListingMismatch.selector); distributor.afterSale(tokenContract1, listingId2, blindPermissioner); - vm.expectRevert(OnlyIssuer.selector); - distributor.afterSale(tokenContract2, listingId2, blindPermissioner); + vm.startPrank(originalOwner); + vm.expectRevert(OnlySeller.selector); + distributor.afterSale(tokenContract1, listingId2, blindPermissioner); distributor.afterSale(tokenContract1, listingId1, blindPermissioner); diff --git a/test/Tokenizer.t.sol b/test/Tokenizer.t.sol index faec0a39..b33dc276 100644 --- a/test/Tokenizer.t.sol +++ b/test/Tokenizer.t.sol @@ -17,7 +17,7 @@ import { IPNFT } from "../src/IPNFT.sol"; import { AcceptAllAuthorizer } from "./helpers/AcceptAllAuthorizer.sol"; import { FakeERC20 } from "../src/helpers/FakeERC20.sol"; -import { MustOwnIpnft, AlreadyTokenized, Tokenizer, ZeroAddress } from "../src/Tokenizer.sol"; +import { MustControlIpnft, AlreadyTokenized, Tokenizer, ZeroAddress } from "../src/Tokenizer.sol"; import { IPToken, TokenCapped } from "../src/IPToken.sol"; import { Molecules } from "../src/helpers/test-upgrades/Molecules.sol"; @@ -132,14 +132,14 @@ contract TokenizerTest is Test { tokenizer.issue(tokenContract, 50_000, originalOwner); vm.startPrank(bob); - vm.expectRevert(MustOwnIpnft.selector); + vm.expectRevert(MustControlIpnft.selector); tokenContract.issue(bob, 12345); - vm.expectRevert(MustOwnIpnft.selector); + vm.expectRevert(MustControlIpnft.selector); tokenizer.issue(tokenContract, 12345, bob); - vm.expectRevert(MustOwnIpnft.selector); + vm.expectRevert(MustControlIpnft.selector); tokenContract.cap(); - vm.expectRevert(MustOwnIpnft.selector); + vm.expectRevert(MustControlIpnft.selector); tokenizer.cap(tokenContract); assertEq(tokenContract.balanceOf(alice), 25_000); @@ -170,10 +170,10 @@ contract TokenizerTest is Test { //the original owner *cannot* issue tokens anymore //this actually worked before 1.3 since IPTs were bound to their original owner vm.startPrank(originalOwner); - vm.expectRevert(MustOwnIpnft.selector); + vm.expectRevert(MustControlIpnft.selector); tokenContract.issue(alice, 50_000); - vm.expectRevert(MustOwnIpnft.selector); + vm.expectRevert(MustControlIpnft.selector); tokenizer.issue(tokenContract, 50_000, bob); } @@ -189,7 +189,7 @@ contract TokenizerTest is Test { function testCannotTokenizeIfNotOwner() public { vm.startPrank(alice); - vm.expectRevert(MustOwnIpnft.selector); + vm.expectRevert(MustControlIpnft.selector); tokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); vm.stopPrank(); } From d240fb90638274040c0e77c06d8b3fb48a4514aa Mon Sep 17 00:00:00 2001 From: stadolf Date: Tue, 9 Jul 2024 17:07:47 +0200 Subject: [PATCH 09/10] abstracts the controllable aspect of tokenizer demonstrates how decisions could be handed off to a governor later Signed-off-by: stadolf --- src/IControlIPTs.sol | 11 ++++++++++ src/IPToken.sol | 3 ++- src/Tokenizer.sol | 13 +++++------ test/Tokenizer.t.sol | 51 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 src/IControlIPTs.sol diff --git a/src/IControlIPTs.sol b/src/IControlIPTs.sol new file mode 100644 index 00000000..86c53443 --- /dev/null +++ b/src/IControlIPTs.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +/** + * @title IControlIPTs 1.3 + * @author molecule.xyz + * @notice must be implemented by contracts that should control IPTs + */ +interface IControlIPTs { + function controllerOf(uint256 ipnftId) external view returns (address); +} diff --git a/src/IPToken.sol b/src/IPToken.sol index c4c6aed4..84a0d19f 100644 --- a/src/IPToken.sol +++ b/src/IPToken.sol @@ -6,6 +6,7 @@ import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/O import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; import { Tokenizer, MustControlIpnft } from "./Tokenizer.sol"; +import { IControlIPTs } from "./IControlIPTs.sol"; struct Metadata { uint256 ipnftId; @@ -48,7 +49,7 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { } modifier onlyTokenizerOrIPNFTController() { - if (_msgSender() != owner() && _msgSender() != Tokenizer(owner()).controllerOf(_metadata.ipnftId)) { + if (_msgSender() != owner() && _msgSender() != IControlIPTs(owner()).controllerOf(_metadata.ipnftId)) { revert MustControlIpnft(); } _; diff --git a/src/Tokenizer.sol b/src/Tokenizer.sol index b8ab3934..d06586db 100644 --- a/src/Tokenizer.sol +++ b/src/Tokenizer.sol @@ -8,6 +8,7 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IPToken, Metadata as TokenMetadata } from "./IPToken.sol"; import { IPermissioner } from "./Permissioner.sol"; import { IPNFT } from "./IPNFT.sol"; +import { IControlIPTs } from "./IControlIPTs.sol"; error MustControlIpnft(); error AlreadyTokenized(); @@ -17,7 +18,7 @@ error IPTNotControlledByTokenizer(); /// @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 { +contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable, IControlIPTs { event TokensCreated( uint256 indexed moleculesId, uint256 indexed ipnftId, @@ -69,7 +70,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { } //todo: try breaking this with a faked IPToken - modifier onlyControlledIPTs(IPToken ipToken) { + modifier onlyController(IPToken ipToken) { TokenMetadata memory metadata = ipToken.metadata(); if (address(synthesized[metadata.ipnftId]) != address(ipToken)) { @@ -141,7 +142,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { //this has been called MoleculesCreated before emit TokensCreated( - //upwards compatibility: signaling an unique "Molecules ID" as first parameter ("sales cycle id"). This is unused and not interpreted. + //upwards compatibility: signaling a unique "Molecules ID" as first parameter ("sales cycle id"). This is unused and not interpreted. uint256(keccak256(abi.encodePacked(ipnftId))), ipnftId, address(token), @@ -161,7 +162,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { * @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) { + function issue(IPToken ipToken, uint256 amount, address receiver) external onlyController(ipToken) { ipToken.issue(receiver, amount); } @@ -170,12 +171,12 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { * @dev you must compute the ipt hash externally. * @param ipToken the IPToken to cap. */ - function cap(IPToken ipToken) external onlyControlledIPTs(ipToken) { + function cap(IPToken ipToken) external onlyController(ipToken) { ipToken.cap(); } /// @dev this will be called by IPTs. Right now the controller is the IPNFT's current owner, it can be a Governor in the future. - function controllerOf(uint256 ipnftId) public view returns (address) { + function controllerOf(uint256 ipnftId) public view override returns (address) { return ipnft.ownerOf(ipnftId); } diff --git a/test/Tokenizer.t.sol b/test/Tokenizer.t.sol index b33dc276..e23b593f 100644 --- a/test/Tokenizer.t.sol +++ b/test/Tokenizer.t.sol @@ -20,10 +20,28 @@ import { FakeERC20 } from "../src/helpers/FakeERC20.sol"; import { MustControlIpnft, AlreadyTokenized, Tokenizer, ZeroAddress } from "../src/Tokenizer.sol"; import { IPToken, TokenCapped } from "../src/IPToken.sol"; +import { IControlIPTs } from "../src/IControlIPTs.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"; +contract GovernorOfTheFuture is IControlIPTs { + function controllerOf(uint256 ipnftId) external view override returns (address) { + return address(0); //no one but me controls IPTs! + } + + function aMajorityWantsToIssueTokensTo(IPToken ipt, uint256 amount, address receiver) public { + ipt.issue(receiver, amount); + } +} + +contract TokenizerWithHandover is Tokenizer { + //this oc would be gated for the current IPNFT holder + function handoverControl(IPToken ipt, GovernorOfTheFuture governor) external onlyController(ipt) { + ipt.transferOwnership(address(governor)); + } +} + contract TokenizerTest is Test { using SafeERC20Upgradeable for IPToken; @@ -228,4 +246,37 @@ contract TokenizerTest is Test { assertEq(tokenContract.balanceOf(bob), 10_000); } + + function testTokenizerCanHandoverControl() public { + vm.startPrank(deployer); + TokenizerWithHandover htokenizer = TokenizerWithHandover(address(new ERC1967Proxy(address(new TokenizerWithHandover()), ""))); + htokenizer.initialize(ipnft, blindPermissioner); + htokenizer.setIPTokenImplementation(new IPToken()); + + vm.startPrank(originalOwner); + IPToken tokenContract = htokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); + tokenContract.issue(bob, 50_000); + + vm.startPrank(deployer); + GovernorOfTheFuture governor = new GovernorOfTheFuture(); + vm.stopPrank(); + + vm.startPrank(originalOwner); + htokenizer.handoverControl(tokenContract, governor); + + vm.startPrank(alice); // alice controls the governor, eg by proving that a vote has occured + governor.aMajorityWantsToIssueTokensTo(tokenContract, 50_000, alice); + assertEq(tokenContract.balanceOf(alice), 50_000); + + // -- from here on, *only* the new governor is in conrol + vm.expectRevert(MustControlIpnft.selector); + tokenContract.issue(alice, 50_000); + + vm.startPrank(originalOwner); + vm.expectRevert(MustControlIpnft.selector); + tokenContract.issue(bob, 50_000); + + vm.expectRevert(MustControlIpnft.selector); + htokenizer.issue(tokenContract, 50_000, bob); + } } From 3c848065747bdeb8c73133fa11cad9a8b4a79d7f Mon Sep 17 00:00:00 2001 From: stadolf Date: Thu, 11 Jul 2024 10:35:02 +0200 Subject: [PATCH 10/10] rollout sepolia subgraph version uses latest satsuma supported spec version 1.0.0 updates initial block heights Signed-off-by: stadolf --- subgraph/networks.json | 8 ++++---- subgraph/package.json | 2 +- subgraph/subgraph.yaml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/subgraph/networks.json b/subgraph/networks.json index f85ce339..e167cc28 100644 --- a/subgraph/networks.json +++ b/subgraph/networks.json @@ -10,19 +10,19 @@ }, "Tokenizer": { "address": "0xca63411FF5187431028d003eD74B57531408d2F9", - "startBlock": 5300057 + "startBlock": 5300776 }, "CrowdSale": { "address": "0x8cA737E2cdaE1Ceb332bEf7ba9eA711a3a2f8037", - "startBlock": 5300057 + "startBlock": 5300777 }, "StakedLockingCrowdSale": { "address": "0xd1cE2EA7d3b0C9cAB025A4aD762FC00315141ad7", - "startBlock": 5300057 + "startBlock": 5300777 }, "TermsAcceptedPermissioner": { "address": "0xC05D649368d8A5e2E98CAa205d47795de5fCB599", - "startBlock": 5300057 + "startBlock": 5300776 } }, "mainnet": { diff --git a/subgraph/package.json b/subgraph/package.json index 77af4e01..55a2d535 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -8,7 +8,7 @@ "build:sepolia": "graph codegen && graph build --network sepolia", "build:mainnet": "graph codegen && graph build --network mainnet", "deploy:local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 moleculeprotocol/ipnft-subgraph", - "deploy:sepolia": "env-cmd -x -f ../.env graph deploy ip-nft-sepolia --version-label 1.0.0 --node https://subgraphs.alchemy.com/api/subgraphs/deploy --ipfs https://ipfs.satsuma.xyz --deploy-key \\$SATSUMA_DEPLOY_KEY", + "deploy:sepolia": "env-cmd -x -f ../.env graph deploy ip-nft-sepolia --version-label 1.1.0 --node https://subgraphs.alchemy.com/api/subgraphs/deploy --ipfs https://ipfs.satsuma.xyz --deploy-key \\$SATSUMA_DEPLOY_KEY", "deploy:mainnet": "env-cmd -x -f ../.env graph deploy ip-nft-mainnet --version-label 1.0.0 --node https://subgraphs.alchemy.com/api/subgraphs/deploy --ipfs https://ipfs.satsuma.xyz --deploy-key \\$SATSUMA_DEPLOY_KEY", "create:local": "graph create --node http://localhost:8020/ moleculeprotocol/ipnft-subgraph", "remove:local": "graph remove --node http://localhost:8020/ moleculeprotocol/ipnft-subgraph", diff --git a/subgraph/subgraph.yaml b/subgraph/subgraph.yaml index 94d7b019..e0c804c0 100644 --- a/subgraph/subgraph.yaml +++ b/subgraph/subgraph.yaml @@ -1,4 +1,4 @@ -specVersion: 1.2.0 +specVersion: 1.0.0 schema: file: ./schema.graphql dataSources: