From b58a912fee162ce6bb58afba79f3c2656b7b6bff Mon Sep 17 00:00:00 2001 From: stadolf Date: Fri, 5 Jul 2024 12:20:20 +0200 Subject: [PATCH] IPTs: save a storage slot for not storing the tokenizer explicitly tests IPTs control after IPNFT transfers Signed-off-by: stadolf --- src/IPToken.sol | 7 ++--- src/Tokenizer.sol | 2 +- ...t.sol => Tokenizer13UpgradeForkTest.t.sol} | 2 +- test/Tokenizer.t.sol | 27 +++++++++++++++++-- 4 files changed, 29 insertions(+), 9 deletions(-) rename test/Forking/{TokenizerFork.t.sol => Tokenizer13UpgradeForkTest.t.sol} (99%) diff --git a/src/IPToken.sol b/src/IPToken.sol index 5a7cccea..ecab5dd9 100644 --- a/src/IPToken.sol +++ b/src/IPToken.sol @@ -18,7 +18,7 @@ error TokenCapped(); /** * @title IPToken 1.3 * @author molecule.xyz - * @notice this is a template contract that's spawned by the Tokenizer + * @notice this is a template contract that's cloned by the Tokenizer * @notice the owner of this contract is always the Tokenizer contract which enforces IPNFT holdership rules. * The owner can increase the token supply as long as it's not explicitly capped. * @dev formerly known as "molecules" @@ -34,8 +34,6 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { Metadata internal _metadata; - Tokenizer internal tokenizer; - function initialize(uint256 ipnftId, string calldata name, string calldata symbol, address originalOwner, string memory agreementCid) external initializer @@ -43,7 +41,6 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { __Ownable_init(); __ERC20_init(name, symbol); _metadata = Metadata({ ipnftId: ipnftId, originalOwner: originalOwner, agreementCid: agreementCid }); - tokenizer = Tokenizer(owner()); } constructor() { @@ -51,7 +48,7 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable { } modifier onlyTokenizerOrIPNFTHolder() { - if (_msgSender() != owner() && _msgSender() != tokenizer.ownerOf(_metadata.ipnftId)) { + if (_msgSender() != owner() && _msgSender() != Tokenizer(owner()).ownerOf(_metadata.ipnftId)) { revert MustOwnIpnft(); } _; diff --git a/src/Tokenizer.sol b/src/Tokenizer.sol index 36c01116..3f33690d 100644 --- a/src/Tokenizer.sol +++ b/src/Tokenizer.sol @@ -37,7 +37,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable { /// @dev the permissioner checks if senders have agreed to legal requirements IPermissioner public permissioner; - /// @notice the IPToken implementation this Tokenizer spawns + /// @notice the IPToken implementation this Tokenizer clones from IPToken public ipTokenImplementation; /** diff --git a/test/Forking/TokenizerFork.t.sol b/test/Forking/Tokenizer13UpgradeForkTest.t.sol similarity index 99% rename from test/Forking/TokenizerFork.t.sol rename to test/Forking/Tokenizer13UpgradeForkTest.t.sol index 1a5c22de..279afd3b 100644 --- a/test/Forking/TokenizerFork.t.sol +++ b/test/Forking/Tokenizer13UpgradeForkTest.t.sol @@ -17,7 +17,7 @@ import { IPermissioner, BlindPermissioner } from "../../src/Permissioner.sol"; // an error thrown by IPToken before 1.3 //error OnlyIssuerOrOwner(); -contract TokenizerForkTest is Test { +contract Tokenizer13UpgradeForkTest is Test { using SafeERC20Upgradeable for IPToken; uint256 mainnetFork; diff --git a/test/Tokenizer.t.sol b/test/Tokenizer.t.sol index 1ab69824..faec0a39 100644 --- a/test/Tokenizer.t.sol +++ b/test/Tokenizer.t.sol @@ -129,15 +129,18 @@ contract TokenizerTest is Test { tokenContract.transfer(bob, 25_000); tokenContract.issue(originalOwner, 50_000); - // this allows new owners of legacy IPTs to increase their supply: tokenizer.issue(tokenContract, 50_000, originalOwner); vm.startPrank(bob); vm.expectRevert(MustOwnIpnft.selector); tokenContract.issue(bob, 12345); + vm.expectRevert(MustOwnIpnft.selector); + tokenizer.issue(tokenContract, 12345, bob); vm.expectRevert(MustOwnIpnft.selector); tokenContract.cap(); + vm.expectRevert(MustOwnIpnft.selector); + tokenizer.cap(tokenContract); assertEq(tokenContract.balanceOf(alice), 25_000); assertEq(tokenContract.balanceOf(bob), 25_000); @@ -154,6 +157,26 @@ contract TokenizerTest is Test { tokenizer.issue(tokenContract, 12345, bob); } + function testIPNFTHolderControlsIPT() public { + vm.startPrank(originalOwner); + IPToken tokenContract = tokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); + tokenContract.issue(bob, 50_000); + ipnft.transferFrom(originalOwner, alice, 1); + + vm.startPrank(alice); + tokenContract.issue(alice, 50_000); + assertEq(tokenContract.balanceOf(alice), 50_000); + + //the original owner *cannot* issue tokens anymore + //this actually worked before 1.3 since IPTs were bound to their original owner + vm.startPrank(originalOwner); + vm.expectRevert(MustOwnIpnft.selector); + tokenContract.issue(alice, 50_000); + + vm.expectRevert(MustOwnIpnft.selector); + tokenizer.issue(tokenContract, 50_000, bob); + } + function testCanBeTokenizedOnlyOnce() public { vm.startPrank(originalOwner); tokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, ""); @@ -189,7 +212,7 @@ contract TokenizerTest is Test { assertEq(tokenContract.balanceOf(address(wallet)), 100_000); - //test the SAFE can send molecules to another account + //test the SAFE can send IPTs to another account bytes memory transferCall = abi.encodeCall(tokenContract.transfer, (bob, 10_000)); bytes32 encodedTxDataHash = wallet.getTransactionHash( address(tokenContract), 0, transferCall, Enum.Operation.Call, 80_000, 1 gwei, 20 gwei, address(0x0), payable(0x0), 0