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/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/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/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/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/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 9dcc984d..84a0d19f 100644 --- a/src/IPToken.sol +++ b/src/IPToken.sol @@ -5,6 +5,8 @@ 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, MustControlIpnft } from "./Tokenizer.sol"; +import { IControlIPTs } from "./IControlIPTs.sol"; struct Metadata { uint256 ipnftId; @@ -13,67 +15,59 @@ struct Metadata { } error TokenCapped(); -error OnlyIssuerOrOwner(); /** - * @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. + * @title IPToken 1.3 + * @author molecule.xyz + * @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" */ 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 { + 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 }); } constructor() { _disableInitializers(); } - modifier onlyIssuerOrOwner() { - if (_msgSender() != _metadata.originalOwner && _msgSender() != owner()) { - revert OnlyIssuerOrOwner(); + modifier onlyTokenizerOrIPNFTController() { + if (_msgSender() != owner() && _msgSender() != IControlIPTs(owner()).controllerOf(_metadata.ipnftId)) { + revert MustControlIpnft(); } _; } - 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 + * @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 onlyTokenizerOrIPNFTController { + if (capped) { + revert TokenCapped(); + } totalIssued += amount; _mint(receiver, amount); } @@ -81,7 +75,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 onlyTokenizerOrIPNFTController { capped = true; emit Capped(totalIssued); } diff --git a/src/SalesShareDistributor.sol b/src/SalesShareDistributor.sol index 1cd3c1df..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,11 +23,17 @@ struct Sales { error ListingNotFulfilled(); error ListingMismatch(); error InsufficientBalance(); +error NotSalesBeneficiary(); error UncappedToken(); -error OnlyIssuer(); - +error OnlySeller(); 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 +49,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 @@ -84,64 +92,69 @@ 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 - * 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 { - if (_msgSender() != tokenContract.issuer()) { - revert OnlyIssuer(); - } - - Metadata memory metadata = tokenContract.metadata(); - (, 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 { - if (_msgSender() != tokenContract.issuer()) { - revert OnlyIssuer(); - } - + function UNSAFE_afterSale(IPToken tokenContract, IERC20 paymentToken, uint256 paidPrice, IPermissioner permissioner) external nonReentrant { Metadata memory metadata = tokenContract.metadata(); + 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 MustControlIpnft(); + } + //create a fake (but valid) schmackoswap listing id uint256 fulfilledListingId = uint256( keccak256( abi.encode( SchmackoSwap.Listing( - IERC721(address(0)), //this should be the IPNFT address + IERC721(address(tokenizer.getIPNFTContract())), metadata.ipnftId, _msgSender(), paymentToken, @@ -157,14 +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 - { - if (!tokenContract.capped()) { - revert UncappedToken(); - } - sales[address(tokenContract)] = Sales(fulfilledListingId, _paymentToken, price, permissioner); - emit SalesActivated(address(tokenContract), address(_paymentToken), price); + 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(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 56bb70b1..d06586db 100644 --- a/src/Tokenizer.sol +++ b/src/Tokenizer.sol @@ -8,15 +8,17 @@ 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 MustOwnIpnft(); +error MustControlIpnft(); 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 { +contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable, IControlIPTs { event TokensCreated( uint256 indexed moleculesId, uint256 indexed ipnftId, @@ -43,7 +45,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; /** @@ -63,11 +65,29 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { _disableInitializers(); } + function getIPNFTContract() public view returns (IPNFT) { + return ipnft; + } + + //todo: try breaking this with a faked IPToken + modifier onlyController(IPToken ipToken) { + TokenMetadata memory metadata = ipToken.metadata(); + + if (address(synthesized[metadata.ipnftId]) != address(ipToken)) { + revert IPTNotControlledByTokenizer(); + } + + if (_msgSender() != controllerOf(metadata.ipnftId)) { + revert MustControlIpnft(); + } + _; + } + /** * @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 */ @@ -80,16 +100,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 @@ -104,29 +126,60 @@ 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(); } // 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(); - // ensure we can only call this once per sales cycle - if (address(synthesized[tokenHash]) != 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( + //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), + _msgSender(), + tokenAmount, + agreementCid, + name, + tokenSymbol + ); permissioner.accept(token, _msgSender(), signedAgreement); token.issue(_msgSender(), tokenAmount); } + /** + * @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 + */ + function issue(IPToken ipToken, uint256 amount, address receiver) external onlyController(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 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 override returns (address) { + return ipnft.ownerOf(ipnftId); + } + /// @notice upgrade authorization logic function _authorizeUpgrade(address /*newImplementation*/ ) internal 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/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/abis/IPToken.json b/subgraph/abis/IPToken.json index a3300de1..3927254a 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": "MustControlIpnft", "inputs": [] }, { diff --git a/subgraph/abis/SharedSalesDistributor.json b/subgraph/abis/SharedSalesDistributor.json index 82976d74..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,12 +375,12 @@ }, { "type": "error", - "name": "OnlyIssuer", + "name": "NotSalesBeneficiary", "inputs": [] }, { "type": "error", - "name": "UncappedToken", + "name": "OnlySeller", "inputs": [] } ] diff --git a/subgraph/abis/Tokenizer.json b/subgraph/abis/Tokenizer.json index 47b09165..8f5fdb9b 100644 --- a/subgraph/abis/Tokenizer.json +++ b/subgraph/abis/Tokenizer.json @@ -4,6 +4,51 @@ "inputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "cap", + "inputs": [ + { + "name": "ipToken", + "type": "address", + "internalType": "contract IPToken" + } + ], + "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", @@ -35,6 +80,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", @@ -79,9 +147,9 @@ "name": "reinit", "inputs": [ { - "name": "_permissioner", + "name": "_ipTokenImplementation", "type": "address", - "internalType": "contract IPermissioner" + "internalType": "contract IPToken" } ], "outputs": [], @@ -386,7 +454,12 @@ }, { "type": "error", - "name": "MustOwnIpnft", + "name": "IPTNotControlledByTokenizer", + "inputs": [] + }, + { + "type": "error", + "name": "MustControlIpnft", "inputs": [] }, { diff --git a/subgraph/makeAbis.sh b/subgraph/makeAbis.sh index f701bb67..9ad940ae 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": [ diff --git a/subgraph/networks.json b/subgraph/networks.json new file mode 100644 index 00000000..e167cc28 --- /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": 5300776 + }, + "CrowdSale": { + "address": "0x8cA737E2cdaE1Ceb332bEf7ba9eA711a3a2f8037", + "startBlock": 5300777 + }, + "StakedLockingCrowdSale": { + "address": "0xd1cE2EA7d3b0C9cAB025A4aD762FC00315141ad7", + "startBlock": 5300777 + }, + "TermsAcceptedPermissioner": { + "address": "0xC05D649368d8A5e2E98CAa205d47795de5fCB599", + "startBlock": 5300776 + } + }, + "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..55a2d535 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", - "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", + "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.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", - "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/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.yaml similarity index 74% rename from subgraph/subgraph.template.yaml rename to subgraph/subgraph.yaml index 5636c0d8..e0c804c0 100644 --- a/subgraph/subgraph.template.yaml +++ b/subgraph/subgraph.yaml @@ -1,17 +1,17 @@ -specVersion: 0.0.4 +specVersion: 1.0.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,23 @@ dataSources: - name: Tokenizer file: ./abis/Tokenizer.json eventHandlers: - - 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 - # the legacy event - - 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: SalesActivated(uint256,address,uint256) - # handler: handleSalesActivated - # - 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 +101,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 +119,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 +138,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 +165,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 +183,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 +204,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 +221,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" diff --git a/test/CrowdSalePermissioned.t.sol b/test/CrowdSalePermissioned.t.sol index 4a6959cf..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"; @@ -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(); @@ -77,12 +80,14 @@ contract CrowdSalePermissionedTest is Test { auctionToken = tokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); 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 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/Tokenizer13UpgradeForkTest.t.sol b/test/Forking/Tokenizer13UpgradeForkTest.t.sol new file mode 100644 index 00000000..15fccfa4 --- /dev/null +++ b/test/Forking/Tokenizer13UpgradeForkTest.t.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; + +import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; + +import { IPNFT } from "../../src/IPNFT.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"; +import { IPermissioner, BlindPermissioner } from "../../src/Permissioner.sol"; + +contract Tokenizer13UpgradeForkTest is Test { + using SafeERC20Upgradeable for IPToken; + + uint256 mainnetFork; + + string ipfsUri = "ipfs://bafkreiankqd3jvpzso6khstnaoxovtyezyatxdy7t2qzjoolqhltmasqki"; + string agreementCid = "bafkreigk5dvqblnkdniges6ft5kmuly47ebw4vho6siikzmkaovq6sjstq"; + uint256 MINTING_FEE = 0.001 ether; + string DEFAULT_SYMBOL = "IPT-0001"; + + address mainnetDeployer = 0x34021576F01275A429163a56908Bd02b43e2B7e1; + address mainnetOwner = 0xCfA0F84660fB33bFd07C369E5491Ab02C449f71B; + address mainnetTokenizer = 0x58EB89C69CB389DBef0c130C6296ee271b82f436; + address mainnetIPNFT = 0xcaD88677CA87a7815728C72D74B4ff4982d54Fc1; + + address vitaDaoTreasury = 0xF5307a74d1550739ef81c6488DC5C7a6a53e5Ac2; + + // paulhaas.eth + address paulhaas = 0x45602BFBA960277bF917C1b2007D1f03d7bd29e4; + + IPNFT ipnft = IPNFT(mainnetIPNFT); + Tokenizer tokenizer13; + IPToken newIPTokenImplementation; + + address alice = makeAddr("alice"); + + function setUp() public { + mainnetFork = vm.createFork(vm.envString("MAINNET_RPC_URL"), 20240430); + vm.selectFork(mainnetFork); + } + + function upgradeToTokenizer13() public { + vm.startPrank(mainnetDeployer); + Tokenizer newTokenizerImplementation = new Tokenizer(); + newIPTokenImplementation = new IPToken(); + vm.stopPrank(); + + vm.startPrank(mainnetOwner); + 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); + tokenizer13 = Tokenizer(mainnetTokenizer); + } + + 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"); + tokenizer13.initialize(IPNFT(address(0)), BlindPermissioner(address(0))); + + vm.expectRevert("Initializable: contract is already initialized"); + newIPTokenImplementation.initialize(2, "Foo", "Bar", alice, "abcde"); + + vm.startPrank(mainnetOwner); + vm.expectRevert("Initializable: contract is already initialized"); + tokenizer13.initialize(IPNFT(address(0)), BlindPermissioner(address(0))); + + vm.expectRevert("Initializable: contract is already initialized"); + 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; + + vm.startPrank(vitaFASTMultisig); + vm.expectRevert(AlreadyTokenized.selector); + tokenizer13.tokenizeIpnft(2, 1_000_000 ether, "VITA-FAST", "bafkreig274nfj7srmtnb5wd5wlwm3ig2s63wovlz7i3noodjlfz2tm3n5q", bytes("")); + + vm.startPrank(alice); + vm.expectRevert(MustControlIpnft.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 = 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); + + ipt.cap(); + + vm.expectRevert(TokenCapped.selector); + ipt.issue(valleyDaoMultisig, 100); + + vm.stopPrank(); + } + + function testOldTokensAreStillControllable() public { + upgradeToTokenizer13(); + address vitaFASTAddress = 0x6034e0d6999741f07cb6Fb1162cBAA46a1D33d36; + address vitaFASTMultisig = 0xf7990CD398daFB4fe5Fd6B9228B8e6f72b296555; + + IPToken vitaFast = IPToken(vitaFASTAddress); + + assertEq(vitaFast.balanceOf(paulhaas), 16942857059768483219100); + assertEq(vitaFast.balanceOf(alice), 0); + + vm.startPrank(paulhaas); + vitaFast.transfer(alice, 100); + assertEq(vitaFast.balanceOf(paulhaas), 16942857059768483219000); + assertEq(vitaFast.balanceOf(alice), 100); + vm.stopPrank(); + + vm.startPrank(vitaFASTMultisig); + 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(), 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(MustControlIpnft.selector); + tokenizer13.issue(vitaFAST13, 1_000_000 ether, alice); + vm.expectRevert(MustControlIpnft.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); + } +} diff --git a/test/Forking/TokenizerFork.t.sol b/test/Forking/TokenizerFork.t.sol deleted file mode 100644 index b9e58d9a..00000000 --- a/test/Forking/TokenizerFork.t.sol +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: MIT -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 { Tokenizer11 } from "../../src/helpers/test-upgrades/Tokenizer11.sol"; -import { IPToken, OnlyIssuerOrOwner, TokenCapped, Metadata } from "../../src/IPToken.sol"; -import { IPermissioner, BlindPermissioner } from "../../src/Permissioner.sol"; - -//import { SchmackoSwap, ListingState } from "../../src/SchmackoSwap.sol"; - -contract TokenizerForkTest is Test { - using SafeERC20Upgradeable for IPToken; - - uint256 mainnetFork; - - string ipfsUri = "ipfs://bafkreiankqd3jvpzso6khstnaoxovtyezyatxdy7t2qzjoolqhltmasqki"; - string agreementCid = "bafkreigk5dvqblnkdniges6ft5kmuly47ebw4vho6siikzmkaovq6sjstq"; - uint256 MINTING_FEE = 0.001 ether; - string DEFAULT_SYMBOL = "IPT-0001"; - - address mainnetDeployer = 0x34021576F01275A429163a56908Bd02b43e2B7e1; - address mainnetOwner = 0xCfA0F84660fB33bFd07C369E5491Ab02C449f71B; - address mainnetTokenizer = 0x58EB89C69CB389DBef0c130C6296ee271b82f436; - address mainnetIPNFT = 0xcaD88677CA87a7815728C72D74B4ff4982d54Fc1; - - // old IP Token implementation - address vitaFASTAddress = 0x6034e0d6999741f07cb6Fb1162cBAA46a1D33d36; - - // https://app.safe.global/home?safe=eth:0xf7990CD398daFB4fe5Fd6B9228B8e6f72b296555 - address vitaFASTMultisig = 0xf7990CD398daFB4fe5Fd6B9228B8e6f72b296555; - - // paulhaas.eth - address paulhaas = 0x45602BFBA960277bF917C1b2007D1f03d7bd29e4; - - // ValleyDAO multisig - address valleyDaoMultisig = 0xD920E60b798A2F5a8332799d8a23075c9E77d5F8; - - // ValleyDAO IPNFT - uint256 valleyDaoIpnftId = 3; - - address alice = makeAddr("alice"); - - function setUp() public { - mainnetFork = vm.createFork(vm.envString("MAINNET_RPC_URL"), 18968463); - vm.selectFork(mainnetFork); - } - - function testCanUpgradeErc20TokenImplementation() public { - IPNFT ipnftMainnetInstance = IPNFT(mainnetIPNFT); - Tokenizer11 tokenizer11 = Tokenizer11(mainnetTokenizer); - - vm.startPrank(mainnetDeployer); - Tokenizer newTokenizerImplementation = new Tokenizer(); - IPToken newIPTokenImplementation = new IPToken(); - vm.stopPrank(); - - vm.startPrank(mainnetOwner); - bytes memory upgradeCallData = abi.encodeWithSelector(Tokenizer.setIPTokenImplementation.selector, address(newIPTokenImplementation)); - tokenizer11.upgradeToAndCall(address(newTokenizerImplementation), upgradeCallData); - Tokenizer upgradedTokenizer = Tokenizer(mainnetTokenizer); - - assertEq(address(upgradedTokenizer.ipTokenImplementation()), address(newIPTokenImplementation)); - - deployCodeTo("Permissioner.sol:BlindPermissioner", "", address(upgradedTokenizer.permissioner())); - vm.stopPrank(); - - vm.startPrank(alice); - 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")); - 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))); - vm.stopPrank(); - - assertEq(ipnftMainnetInstance.ownerOf(valleyDaoIpnftId), valleyDaoMultisig); - - vm.startPrank(valleyDaoMultisig); - IPToken ipt = upgradedTokenizer.tokenizeIpnft(valleyDaoIpnftId, 1_000_000 ether, "IPT", agreementCid, ""); - assertEq(ipt.balanceOf(valleyDaoMultisig), 1_000_000 ether); - ipt.transfer(alice, 100_000 ether); - - assertEq(ipt.balanceOf(valleyDaoMultisig), 900_000 ether); - - ipt.issue(valleyDaoMultisig, 1_000_000 ether); - assertEq(ipt.totalSupply(), 2_000_000 ether); - assertEq(ipt.balanceOf(valleyDaoMultisig), 1_900_000 ether); - - ipt.cap(); - - vm.expectRevert(TokenCapped.selector); - ipt.issue(valleyDaoMultisig, 100); - - vm.stopPrank(); - - IPToken vitaFast = IPToken(vitaFASTAddress); - - assertEq(vitaFast.balanceOf(paulhaas), 16942857059768483219100); - assertEq(vitaFast.balanceOf(alice), 0); - - vm.startPrank(paulhaas); - vitaFast.transfer(alice, 100); - assertEq(vitaFast.balanceOf(paulhaas), 16942857059768483219000); - assertEq(vitaFast.balanceOf(alice), 100); - vm.stopPrank(); - - vm.startPrank(vitaFASTMultisig); - assertEq(vitaFast.balanceOf(vitaFASTMultisig), 16940676213630533216614); - vitaFast.issue(vitaFASTMultisig, 100_000 ether); - assertEq(vitaFast.totalSupply(), 1129555 ether); - assertEq(vitaFast.balanceOf(vitaFASTMultisig), 116940676213630533216614); - vitaFast.cap(); - vm.expectRevert(TokenCapped.selector); - vitaFast.issue(vitaFASTMultisig, 100_000 ether); - } -} diff --git a/test/Permissioner.t.sol b/test/Permissioner.t.sol index 0b8be9d3..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"; @@ -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 ca277e44..1085acbf 100644 --- a/test/SalesShareDistributor.t.sol +++ b/test/SalesShareDistributor.t.sol @@ -24,11 +24,13 @@ import { ListingMismatch, NotClaimingYet, UncappedToken, - OnlyIssuer, - InsufficientBalance + OnlySeller, + MustControlIpnft, + InsufficientBalance, + NotSalesBeneficiary } 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 +79,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 +129,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 + tokenContract.cap(); - 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 +142,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,28 +156,23 @@ contract SalesShareDistributorTest is Test { function testManuallyStartClaimingPhase() public { vm.startPrank(originalOwner); IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "MOLE", agreementCid, ""); - ipnft.safeTransferFrom(originalOwner, ipnftBuyer, 1); - assertEq(tokenContract.issuer(), originalOwner); tokenContract.cap(); - vm.stopPrank(); + ipnft.safeTransferFrom(originalOwner, ipnftBuyer, 1); vm.startPrank(ipnftBuyer); erc20.transfer(originalOwner, 1_000_000 ether); - vm.stopPrank(); - // 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.stopPrank(); + 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); - vm.stopPrank(); + distributor.UNSAFE_afterSale(tokenContract, erc20, 1_000_000 ether, blindPermissioner); assertEq(erc20.balanceOf(address(originalOwner)), 0); (uint256 fulfilledListingId,,,) = distributor.sales(address(tokenContract)); @@ -220,11 +192,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 +203,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(); + distributor.UNSAFE_afterSale(tokenContract, erc20, 1_000_000 ether, permissioner); vm.startPrank(alice); (, uint256 amount) = distributor.claimableTokens(tokenContract, alice); @@ -337,31 +305,30 @@ 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(); + 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.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(); @@ -369,22 +336,19 @@ 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); + vm.startPrank(anotherOwner); + vm.expectRevert(NotSalesBeneficiary.selector); distributor.afterSale(tokenContract2, listingId2, blindPermissioner); - vm.stopPrank(); - 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); - 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..e23b593f 100644 --- a/test/Tokenizer.t.sol +++ b/test/Tokenizer.t.sol @@ -17,14 +17,30 @@ 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, OnlyIssuerOrOwner, TokenCapped } from "../src/IPToken.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"; -import { SchmackoSwap, ListingState } from "../src/SchmackoSwap.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; @@ -50,7 +66,7 @@ contract TokenizerTest is Test { IPNFT internal ipnft; Tokenizer internal tokenizer; - SchmackoSwap internal schmackoSwap; + IPermissioner internal blindPermissioner; FakeERC20 internal erc20; @@ -63,8 +79,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 +94,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 +109,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 +116,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 +133,32 @@ 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); - tokenContract.issue(originalOwner, 100_000); - vm.stopPrank(); + tokenContract.issue(originalOwner, 50_000); + tokenizer.issue(tokenContract, 50_000, originalOwner); vm.startPrank(bob); - vm.expectRevert(OnlyIssuerOrOwner.selector); + vm.expectRevert(MustControlIpnft.selector); tokenContract.issue(bob, 12345); - vm.stopPrank(); + vm.expectRevert(MustControlIpnft.selector); + tokenizer.issue(tokenContract, 12345, bob); + + vm.expectRevert(MustControlIpnft.selector); + tokenContract.cap(); + vm.expectRevert(MustControlIpnft.selector); + tokenizer.cap(tokenContract); assertEq(tokenContract.balanceOf(alice), 25_000); assertEq(tokenContract.balanceOf(bob), 25_000); @@ -149,16 +166,33 @@ contract TokenizerTest is Test { assertEq(tokenContract.totalSupply(), 200_000); assertEq(tokenContract.totalIssued(), 200_000); - vm.startPrank(bob); - vm.expectRevert(OnlyIssuerOrOwner.selector); - tokenContract.cap(); - vm.stopPrank(); - vm.startPrank(originalOwner); + // both work and cap can be called multiple times without reverting tokenContract.cap(); + tokenizer.cap(tokenContract); + vm.expectRevert(TokenCapped.selector); - tokenContract.issue(bob, 12345); - vm.stopPrank(); + 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(MustControlIpnft.selector); + tokenContract.issue(alice, 50_000); + + vm.expectRevert(MustControlIpnft.selector); + tokenizer.issue(tokenContract, 50_000, bob); } function testCanBeTokenizedOnlyOnce() public { @@ -173,7 +207,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(); } @@ -196,7 +230,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 @@ -209,8 +243,40 @@ 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); } + + 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); + } }