Skip to content

Commit 2fec1e1

Browse files
committed
refactor: shared ipt functions into lib
1 parent af5d4c9 commit 2fec1e1

File tree

5 files changed

+150
-115
lines changed

5 files changed

+150
-115
lines changed

src/IPToken.sol

Lines changed: 36 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.18;
33

4-
import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
5-
import { ERC20BurnableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";
6-
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
7-
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
8-
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
9-
import { Tokenizer, MustControlIpnft } from "./Tokenizer.sol";
104
import { IControlIPTs } from "./IControlIPTs.sol";
115
import { IIPToken, Metadata } from "./IIPToken.sol";
6+
import { MustControlIpnft, Tokenizer } from "./Tokenizer.sol";
7+
import { IPTokenUtils } from "./libraries/IPTokenUtils.sol";
8+
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
9+
import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
10+
import { ERC20BurnableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";
1211

1312
error TokenCapped();
1413

@@ -51,32 +50,53 @@ contract IPToken is IIPToken, ERC20Upgradeable, ERC20BurnableUpgradeable, Ownabl
5150
_;
5251
}
5352

54-
function metadata() external view returns (Metadata memory) {
53+
function metadata() external view override returns (Metadata memory) {
5554
return _metadata;
5655
}
5756

58-
function balanceOf(address account) public view override(ERC20Upgradeable, IIPToken) returns (uint256) {
57+
// Override ERC20 functions to resolve diamond inheritance
58+
function totalSupply() public view override returns (uint256) {
59+
return ERC20Upgradeable.totalSupply();
60+
}
61+
62+
function balanceOf(address account) public view override returns (uint256) {
5963
return ERC20Upgradeable.balanceOf(account);
6064
}
6165

62-
function name() public view override(ERC20Upgradeable, IIPToken) returns (string memory) {
66+
function transfer(address to, uint256 amount) public override returns (bool) {
67+
return ERC20Upgradeable.transfer(to, amount);
68+
}
69+
70+
function allowance(address owner, address spender) public view override returns (uint256) {
71+
return ERC20Upgradeable.allowance(owner, spender);
72+
}
73+
74+
function approve(address spender, uint256 amount) public override returns (bool) {
75+
return ERC20Upgradeable.approve(spender, amount);
76+
}
77+
78+
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
79+
return ERC20Upgradeable.transferFrom(from, to, amount);
80+
}
81+
82+
function name() public view override returns (string memory) {
6383
return ERC20Upgradeable.name();
6484
}
6585

66-
function symbol() public view override(ERC20Upgradeable, IIPToken) returns (string memory) {
86+
function symbol() public view override returns (string memory) {
6787
return ERC20Upgradeable.symbol();
6888
}
6989

70-
function decimals() public view override(ERC20Upgradeable, IIPToken) returns (uint8) {
90+
function decimals() public view override returns (uint8) {
7191
return ERC20Upgradeable.decimals();
7292
}
93+
7394
/**
7495
* @notice the supply of IP Tokens is controlled by the tokenizer contract.
7596
* @param receiver address
7697
* @param amount uint256
7798
*/
78-
79-
function issue(address receiver, uint256 amount) external onlyTokenizerOrIPNFTController {
99+
function issue(address receiver, uint256 amount) external override onlyTokenizerOrIPNFTController {
80100
if (capped) {
81101
revert TokenCapped();
82102
}
@@ -87,7 +107,7 @@ contract IPToken is IIPToken, ERC20Upgradeable, ERC20BurnableUpgradeable, Ownabl
87107
/**
88108
* @notice mark this token as capped. After calling this, no new tokens can be `issue`d
89109
*/
90-
function cap() external onlyTokenizerOrIPNFTController {
110+
function cap() external override onlyTokenizerOrIPNFTController {
91111
capped = true;
92112
emit Capped(totalIssued);
93113
}
@@ -96,37 +116,7 @@ contract IPToken is IIPToken, ERC20Upgradeable, ERC20BurnableUpgradeable, Ownabl
96116
* @notice contract metadata, compatible to ERC1155
97117
* @return string base64 encoded data url
98118
*/
99-
function uri() external view returns (string memory) {
100-
string memory tokenId = Strings.toString(_metadata.ipnftId);
101-
102-
string memory props = string.concat(
103-
'"properties": {',
104-
'"ipnft_id": ',
105-
tokenId,
106-
',"agreement_content": "ipfs://',
107-
_metadata.agreementCid,
108-
'","original_owner": "',
109-
Strings.toHexString(_metadata.originalOwner),
110-
'","erc20_contract": "',
111-
Strings.toHexString(address(this)),
112-
'","supply": "',
113-
Strings.toString(totalIssued),
114-
'"}'
115-
);
116-
117-
return string.concat(
118-
"data:application/json;base64,",
119-
Base64.encode(
120-
bytes(
121-
string.concat(
122-
'{"name": "IP Tokens of IPNFT #',
123-
tokenId,
124-
'","description": "IP Tokens, derived from IP-NFTs, are ERC-20 tokens governing IP pools.","decimals": 18,"external_url": "https://molecule.xyz","image": "",',
125-
props,
126-
"}"
127-
)
128-
)
129-
)
130-
);
119+
function uri() external view override returns (string memory) {
120+
return IPTokenUtils.generateURI(_metadata, address(this), totalIssued);
131121
}
132122
}

src/WrappedIPToken.sol

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
pragma solidity 0.8.18;
33

44
import { IIPToken, Metadata } from "./IIPToken.sol";
5-
5+
import { IPTokenUtils } from "./libraries/IPTokenUtils.sol";
66
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
77
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
8-
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
9-
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
108

119
/**
1210
* @title WrappedIPToken
@@ -38,32 +36,52 @@ contract WrappedIPToken is IIPToken, Initializable {
3836
/**
3937
* @dev Returns the name of the token.
4038
*/
41-
function name() public view override returns (string memory) {
39+
function name() public view returns (string memory) {
4240
return wrappedToken.name();
4341
}
4442

4543
/**
4644
* @dev Returns the symbol of the token.
4745
*/
48-
function symbol() public view override returns (string memory) {
46+
function symbol() public view returns (string memory) {
4947
return wrappedToken.symbol();
5048
}
5149

5250
/**
5351
* @dev Returns the decimals places of the token.
5452
*/
55-
function decimals() public view override returns (uint8) {
53+
function decimals() public view returns (uint8) {
5654
return wrappedToken.decimals();
5755
}
5856

59-
function totalIssued() public view override returns (uint256) {
57+
function totalSupply() public view returns (uint256) {
6058
return wrappedToken.totalSupply();
6159
}
6260

63-
function balanceOf(address account) public view override returns (uint256) {
61+
function balanceOf(address account) public view returns (uint256) {
6462
return wrappedToken.balanceOf(account);
6563
}
6664

65+
function transfer(address to, uint256 amount) public returns (bool) {
66+
return wrappedToken.transfer(to, amount);
67+
}
68+
69+
function allowance(address owner, address spender) public view returns (uint256) {
70+
return wrappedToken.allowance(owner, spender);
71+
}
72+
73+
function approve(address spender, uint256 amount) public returns (bool) {
74+
return wrappedToken.approve(spender, amount);
75+
}
76+
77+
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
78+
return wrappedToken.transferFrom(from, to, amount);
79+
}
80+
81+
function totalIssued() public view override returns (uint256) {
82+
return wrappedToken.totalSupply();
83+
}
84+
6785
function issue(address, uint256) public virtual override {
6886
revert("WrappedIPToken: cannot issue");
6987
}
@@ -73,38 +91,6 @@ contract WrappedIPToken is IIPToken, Initializable {
7391
}
7492

7593
function uri() external view override returns (string memory) {
76-
string memory tokenId = Strings.toString(_metadata.ipnftId);
77-
78-
string memory props = string.concat(
79-
'"properties": {',
80-
'"ipnft_id": ',
81-
tokenId,
82-
',"agreement_content": "ipfs://',
83-
_metadata.agreementCid,
84-
'","original_owner": "',
85-
Strings.toHexString(_metadata.originalOwner),
86-
'","erc20_contract": "',
87-
Strings.toHexString(address(wrappedToken)),
88-
'","supply": "',
89-
Strings.toString(wrappedToken.totalSupply()),
90-
'"}'
91-
);
92-
93-
return string.concat(
94-
"data:application/json;base64,",
95-
Base64.encode(
96-
bytes(
97-
string.concat(
98-
'{"name": "IP Tokens of IPNFT #',
99-
tokenId,
100-
'","description": "IP Tokens, derived from IP-NFTs, are ERC-20 tokens governing IP pools.","decimals": ',
101-
Strings.toString(wrappedToken.decimals()),
102-
',"external_url": "https://molecule.xyz","image": "",',
103-
props,
104-
"}"
105-
)
106-
)
107-
)
108-
);
94+
return IPTokenUtils.generateURI(_metadata, address(wrappedToken), wrappedToken.totalSupply());
10995
}
11096
}

src/libraries/IPTokenUtils.sol

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8;
3+
4+
import { Metadata } from "../IIPToken.sol";
5+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
7+
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
8+
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
9+
10+
/// @title IP Token Utilities Library
11+
/// @notice Shared utilities for IP Token contracts
12+
/// @dev Contains common functions used by both IPToken and WrappedIPToken
13+
library IPTokenUtils {
14+
/// @notice Generates a base64-encoded data URL containing token metadata
15+
/// @param metadata_ The metadata struct containing IPNFT information
16+
/// @param tokenContract The ERC20 token contract address
17+
/// @param supply The token supply to include in metadata
18+
/// @return The complete data URL string
19+
function generateURI(Metadata memory metadata_, address tokenContract, uint256 supply) internal view returns (string memory) {
20+
string memory tokenId = Strings.toString(metadata_.ipnftId);
21+
22+
string memory props = string.concat(
23+
'"properties": {',
24+
'"ipnft_id": ',
25+
tokenId,
26+
',"agreement_content": "ipfs://',
27+
metadata_.agreementCid,
28+
'","original_owner": "',
29+
Strings.toHexString(metadata_.originalOwner),
30+
'","erc20_contract": "',
31+
Strings.toHexString(tokenContract),
32+
'","supply": "',
33+
Strings.toString(supply),
34+
'"}'
35+
);
36+
37+
return string.concat(
38+
"data:application/json;base64,",
39+
Base64.encode(
40+
bytes(
41+
string.concat(
42+
'{"name": "IP Tokens of IPNFT #',
43+
tokenId,
44+
'","description": "IP Tokens, derived from IP-NFTs, are ERC-20 tokens governing IP pools.","decimals": ',
45+
Strings.toString(IERC20Metadata(tokenContract).decimals()),
46+
',"external_url": "https://molecule.xyz","image": "",',
47+
props,
48+
"}"
49+
)
50+
)
51+
)
52+
);
53+
}
54+
}

test/Forking/Tokenizer14UpgradeForkTest.t.sol

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@ import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/
88

99
import { IPNFT } from "../../src/IPNFT.sol";
1010

11-
import { MustControlIpnft, AlreadyTokenized, Tokenizer } from "../../src/Tokenizer.sol";
12-
import { Tokenizer13 } from "../../src/helpers/test-upgrades/Tokenizer13.sol";
13-
import { IPToken13 } from "../../src/helpers/test-upgrades/IPToken13.sol";
14-
import { IPToken, TokenCapped } from "../../src/IPToken.sol";
1511
import { Metadata } from "../../src/IIPToken.sol";
16-
import { OnlyIssuerOrOwner } from "../../src/helpers/test-upgrades/IPToken12.sol";
12+
1713
import { IIPToken } from "../../src/IIPToken.sol";
14+
import { IPToken, TokenCapped } from "../../src/IPToken.sol";
15+
16+
import { BlindPermissioner, IPermissioner } from "../../src/Permissioner.sol";
17+
import { AlreadyTokenized, MustControlIpnft, Tokenizer } from "../../src/Tokenizer.sol";
1818
import { WrappedIPToken } from "../../src/WrappedIPToken.sol";
19-
import { IPermissioner, BlindPermissioner } from "../../src/Permissioner.sol";
20-
import { AcceptAllAuthorizer } from "../helpers/AcceptAllAuthorizer.sol";
19+
2120
import { FakeERC20 } from "../../src/helpers/FakeERC20.sol";
21+
import { OnlyIssuerOrOwner } from "../../src/helpers/test-upgrades/IPToken12.sol";
22+
import { IPToken13 } from "../../src/helpers/test-upgrades/IPToken13.sol";
23+
import { Tokenizer13 } from "../../src/helpers/test-upgrades/Tokenizer13.sol";
24+
25+
import { AcceptAllAuthorizer } from "../helpers/AcceptAllAuthorizer.sol";
2226

2327
contract Tokenizer14UpgradeForkTest is Test {
2428
using SafeERC20Upgradeable for IPToken;
@@ -270,10 +274,10 @@ contract Tokenizer14UpgradeForkTest is Test {
270274
IIPToken iiptToken = IIPToken(address(newToken));
271275

272276
// Test interface functions work
273-
assertEq(iiptToken.name(), string.concat("IP Tokens of IPNFT #", vm.toString(newIpnftId)));
274-
assertEq(iiptToken.symbol(), "NEW-IPT");
275-
assertEq(iiptToken.decimals(), 18);
276-
assertEq(iiptToken.balanceOf(alice), 1_000_000 ether);
277+
assertEq(newToken.name(), string.concat("IP Tokens of IPNFT #", vm.toString(newIpnftId)));
278+
assertEq(newToken.symbol(), "NEW-IPT");
279+
assertEq(newToken.decimals(), 18);
280+
assertEq(newToken.balanceOf(alice), 1_000_000 ether);
277281
assertEq(iiptToken.totalIssued(), 1_000_000 ether);
278282

279283
// Test that metadata is accessible through interface
@@ -337,11 +341,8 @@ contract Tokenizer14UpgradeForkTest is Test {
337341
// Test that both IPToken and IIPToken interfaces work identically
338342
IIPToken interfaceToken = IIPToken(address(newToken));
339343

340-
// Compare results from both interfaces
341-
assertEq(newToken.name(), interfaceToken.name());
342-
assertEq(newToken.symbol(), interfaceToken.symbol());
343-
assertEq(newToken.decimals(), interfaceToken.decimals());
344-
assertEq(newToken.balanceOf(alice), interfaceToken.balanceOf(alice));
344+
// Compare results from both interfaces (only IP-specific functions available on interface)
345+
assertEq(newToken.totalIssued(), interfaceToken.totalIssued());
345346
assertEq(newToken.totalIssued(), interfaceToken.totalIssued());
346347

347348
// Test metadata access
@@ -386,10 +387,11 @@ contract Tokenizer14UpgradeForkTest is Test {
386387
assertEq(address(tokenizer14.synthesized(newIpnftId)), address(wrappedToken));
387388

388389
// Test the wrapped token implements IIPToken interface
389-
assertEq(wrappedToken.name(), "Test Wrapped Token");
390-
assertEq(wrappedToken.symbol(), "TWT");
391-
assertEq(wrappedToken.decimals(), 18);
392-
assertEq(wrappedToken.balanceOf(alice), 5_000_000 ether);
390+
WrappedIPToken wrappedImpl = WrappedIPToken(address(wrappedToken));
391+
assertEq(wrappedImpl.name(), "Test Wrapped Token");
392+
assertEq(wrappedImpl.symbol(), "TWT");
393+
assertEq(wrappedImpl.decimals(), 18);
394+
assertEq(wrappedImpl.balanceOf(alice), 5_000_000 ether);
393395
assertEq(wrappedToken.totalIssued(), 5_000_000 ether);
394396

395397
// Test metadata is accessible through interface

0 commit comments

Comments
 (0)