Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Tokenizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable, IControlIPTs {
string symbol
);

// @dev @TODO: index these topics
event TokenWrapped(IERC20Metadata tokenContract, IIPToken wrappedIpt);
event IPTokenImplementationUpdated(IIPToken indexed old, IIPToken indexed _new);
event WrappedIPTokenImplementationUpdated(WrappedIPToken indexed old, WrappedIPToken indexed _new);
Expand Down
27 changes: 7 additions & 20 deletions src/WrappedIPToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/I
/**
* @title WrappedIPToken
* @author molecule.xyz
* @notice this is a template contract that's cloned by the Tokenizer
* @notice this contract is used to wrap an ERC20 token and extend its metadata
* @notice A read-only wrapper that extends existing ERC20 tokens with IP metadata
* @dev This contract provides IP metadata for an existing ERC20 token without proxying
* state-changing operations. It only implements IIPToken interface functions and
* read-only ERC20 view functions. Users must interact with the underlying token
* directly for transfers, approvals, and other state changes.
*/
contract WrappedIPToken is IIPToken, Initializable {
IERC20Metadata public wrappedToken;
Expand Down Expand Up @@ -62,32 +65,16 @@ contract WrappedIPToken is IIPToken, Initializable {
return wrappedToken.balanceOf(account);
}

function transfer(address to, uint256 amount) public returns (bool) {
return wrappedToken.transfer(to, amount);
}

function allowance(address owner, address spender) public view returns (uint256) {
return wrappedToken.allowance(owner, spender);
}

function approve(address spender, uint256 amount) public returns (bool) {
return wrappedToken.approve(spender, amount);
}

function transferFrom(address from, address to, uint256 amount) public returns (bool) {
return wrappedToken.transferFrom(from, to, amount);
}

function totalIssued() public view override returns (uint256) {
return wrappedToken.totalSupply();
}

function issue(address, uint256) public virtual override {
revert("WrappedIPToken: cannot issue");
revert("WrappedIPToken: read-only wrapper - use underlying token for minting");
}

function cap() public virtual override {
revert("WrappedIPToken: cannot cap");
revert("WrappedIPToken: read-only wrapper - use underlying token for cappping");
}

function uri() external view override returns (string memory) {
Expand Down
3 changes: 3 additions & 0 deletions test/Forking/Tokenizer14UpgradeForkTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,9 @@ contract Tokenizer14UpgradeForkTest is Test {
vm.expectRevert(); // WrappedIPToken should not allow cap operations
WrappedIPToken(address(wrappedToken)).cap();

// Test that WrappedIPToken only implements IIPToken state-changing operations (which revert)
// ERC20 state-changing functions are no longer implemented

// Test that we cannot attach another token to the same IPNFT
FakeERC20 anotherToken = new FakeERC20("Another Token", "ANT");
vm.expectRevert(AlreadyTokenized.selector);
Expand Down
68 changes: 66 additions & 2 deletions test/TokenizerWrapped.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,77 @@ contract TokenizerWrappedTest is Test {
IIPToken tokenContract = tokenizer.attachIpt(1, agreementCid, "", erc20);

// Wrapped tokens should not be able to issue or cap
vm.expectRevert("WrappedIPToken: cannot issue");
vm.expectRevert("WrappedIPToken: read-only wrapper - use underlying token for minting");
tokenContract.issue(alice, 1000);

vm.expectRevert("WrappedIPToken: cannot cap");
vm.expectRevert("WrappedIPToken: read-only wrapper - use underlying token for cappping");
tokenContract.cap();
}

function testWrappedTokenStateChangingOperationsRevert() public {
vm.startPrank(originalOwner);
erc20 = new FakeERC20("TestToken", "TEST");
erc20.mint(originalOwner, 1_000_000 ether);

IIPToken tokenContract = tokenizer.attachIpt(1, agreementCid, "", erc20);

// Test that IIPToken state-changing operations revert with proper messages
vm.expectRevert("WrappedIPToken: read-only wrapper - use underlying token for minting");
tokenContract.issue(alice, 1000);

vm.expectRevert("WrappedIPToken: read-only wrapper - use underlying token for cappping");
tokenContract.cap();
}

function testWrappedTokenReadOnlyOperationsWork() public {
vm.startPrank(originalOwner);
erc20 = new FakeERC20("TestToken", "TEST");
erc20.mint(originalOwner, 1_000_000 ether);

IIPToken tokenContract = tokenizer.attachIpt(1, agreementCid, "", erc20);
WrappedIPToken wrappedToken = WrappedIPToken(address(tokenContract));

// Test that read-only operations still work
assertEq(wrappedToken.balanceOf(originalOwner), 1_000_000 ether);
assertEq(wrappedToken.totalSupply(), 1_000_000 ether);
assertEq(wrappedToken.name(), "TestToken");
assertEq(wrappedToken.symbol(), "TEST");
assertEq(wrappedToken.decimals(), 18);
assertEq(wrappedToken.totalIssued(), 1_000_000 ether);

// Test IP metadata functions work
assertEq(wrappedToken.metadata().ipnftId, 1);
assertEq(wrappedToken.metadata().originalOwner, originalOwner);
assertEq(wrappedToken.metadata().agreementCid, agreementCid);

// Test URI function works
string memory uri = wrappedToken.uri();
assertTrue(bytes(uri).length > 0);
}

function testUnderlyingTokenOperationsStillWork() public {
vm.startPrank(originalOwner);
erc20 = new FakeERC20("TestToken", "TEST");
erc20.mint(originalOwner, 1_000_000 ether);

IIPToken tokenContract = tokenizer.attachIpt(1, agreementCid, "", erc20);

// Users can still use the underlying token directly for transfers
assertTrue(erc20.transfer(alice, 100_000 ether));
assertEq(erc20.balanceOf(alice), 100_000 ether);
assertEq(erc20.balanceOf(originalOwner), 900_000 ether);

// Approvals work on the underlying token
assertTrue(erc20.approve(alice, 50_000 ether));
assertEq(erc20.allowance(originalOwner, alice), 50_000 ether);

// Transfer from works on the underlying token
vm.startPrank(alice);
assertTrue(erc20.transferFrom(originalOwner, alice, 25_000 ether));
assertEq(erc20.balanceOf(alice), 125_000 ether);
assertEq(erc20.balanceOf(originalOwner), 875_000 ether);
}

// Helper function to check if a string contains a substring
function contains(string memory source, string memory search) internal pure returns (bool) {
bytes memory sourceBytes = bytes(source);
Expand Down