Skip to content

Commit

Permalink
follows the original concept of self sufficient IPToken contracts
Browse files Browse the repository at this point in the history
Tokenizer indexes IPTs by IPNFT ids
drops "hashes" completely
reinit adds existing IPTs to the known ipt mapping
IPToken asks its owner (Tokenizer) if senders hold the underlying IPNFT
even works on top of the previous 1.1 -> 1.2 chain state

Signed-off-by: stadolf <[email protected]>
  • Loading branch information
elmariachi111 committed Jul 4, 2024
1 parent 4173cb4 commit cdda55f
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 111 deletions.
10 changes: 2 additions & 8 deletions script/dev/Synthesizer.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion script/dev/Tokenizer.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
35 changes: 20 additions & 15 deletions src/IPToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ERC20BurnableUpgradeable } from "@openzeppelin/contracts-upgradeable/to
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
import { Tokenizer, MustOwnIpnft } from "./Tokenizer.sol";

struct Metadata {
uint256 ipnftId;
Expand All @@ -14,15 +15,15 @@ struct Metadata {

error TokenCapped();
error OnlyIssuer();

/**
* @title IPToken
* @author molecule.to
* @author molecule.xyz
* @notice this is a template contract that's spawned by the Tokenizer
* @notice the owner of this contract is always the Tokenizer contract which enforces IPNFT holdership rules.
* The owner can increase the token supply as long as it's not explicitly capped.
* @dev formerly known as "molecules"
*/

contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable {
event Capped(uint256 atSupply);

Expand All @@ -34,35 +35,39 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable {

Metadata internal _metadata;

function initialize(string calldata name, string calldata symbol, Metadata calldata metadata_) external initializer {
Tokenizer internal tokenizer;

function initialize(uint256 ipnftId, string calldata name, string calldata symbol, address originalOwner, string memory agreementCid)
external
initializer
{
__Ownable_init();
__ERC20_init(name, symbol);
_metadata = metadata_;
_metadata = Metadata({ ipnftId: ipnftId, originalOwner: originalOwner, agreementCid: agreementCid });
tokenizer = Tokenizer(owner());
}

constructor() {
_disableInitializers();
}

function metadata() external view returns (Metadata memory) {
return _metadata;
modifier onlyTokenizerOrIPNFTHolder() {
if (_msgSender() != owner() && _msgSender() != tokenizer.ownerOf(_metadata.ipnftId)) {
revert MustOwnIpnft();
}
_;
}
/**
* @notice ip tokens are identified by underlying ipnft token id
* @dev this once also included the current IPNFT owner address. We're leaving it a hash to stay downward compatible.
* @return uint256 a token hash that's unique for this token instance's `ipnftId`
*/

function hash() external view returns (uint256) {
return uint256(keccak256(abi.encodePacked(_metadata.ipnftId)));
function metadata() external view returns (Metadata memory) {
return _metadata;
}

/**
* @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 onlyOwner {
function issue(address receiver, uint256 amount) external onlyTokenizerOrIPNFTHolder {
if (capped) {
revert TokenCapped();
}
Expand All @@ -73,7 +78,7 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable {
/**
* @notice mark this token as capped. After calling this, no new tokens can be `issue`d
*/
function cap() external onlyOwner {
function cap() external onlyTokenizerOrIPNFTHolder {
capped = true;
emit Capped(totalIssued);
}
Expand Down
74 changes: 29 additions & 45 deletions src/Tokenizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,7 @@ error IPTNotControlledByTokenizer();
/// @notice tokenizes an IPNFT to an ERC20 token (called IPToken or IPT) and controls its supply.
contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
event TokensCreated(
uint256 indexed moleculesId,
uint256 indexed ipnftId,
address indexed tokenContract,
address emitter,
uint256 amount,
string agreementCid,
string name,
string symbol
uint256 indexed ipnftId, address indexed tokenContract, address emitter, uint256 amount, string agreementCid, string name, string symbol
);

event IPTokenImplementationUpdated(IPToken indexed old, IPToken indexed _new);
Expand Down Expand Up @@ -64,21 +57,14 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
_disableInitializers();
}

modifier onlyIPNFTHolder(uint256 ipnftId) {
if (ipnft.ownerOf(ipnftId) != _msgSender()) {
revert MustOwnIpnft();
}
_;
}

//todo: try breaking this with a faked IPToken
modifier onlyControlledIPTs(IPToken ipToken) {
IPToken token = synthesized[ipToken.hash()];
if (address(token) != address(ipToken)) {
TokenMetadata memory metadata = ipToken.metadata();

if (address(synthesized[metadata.ipnftId]) != address(ipToken)) {
revert IPTNotControlledByTokenizer();
}

TokenMetadata memory metadata = token.metadata();
if (_msgSender() != ipnft.ownerOf(metadata.ipnftId)) {
revert MustOwnIpnft();
}
Expand All @@ -89,7 +75,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
* @notice sets the new implementation address of the IPToken
* @param _ipTokenImplementation address pointing to the new implementation
*/
function setIPTokenImplementation(IPToken _ipTokenImplementation) external onlyOwner {
function setIPTokenImplementation(IPToken _ipTokenImplementation) public onlyOwner {
/*
could call some functions on old contract to make sure its tokenizer not another contract behind a proxy for safety
*/
Expand All @@ -102,16 +88,18 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
}

/**
* @dev called after an upgrade to reinitialize a new permissioner impl.
* @param _permissioner the new TermsPermissioner
* @dev sets legacy IPTs on the tokenized mapping
*/
function reinit(IPermissioner _permissioner) public onlyOwner reinitializer(4) {
permissioner = _permissioner;
function reinit(IPToken _ipTokenImplementation) public onlyOwner reinitializer(5) {
synthesized[2] = IPToken(0x6034e0d6999741f07cb6Fb1162cBAA46a1D33d36);
synthesized[28] = IPToken(0x7b66E84Be78772a3afAF5ba8c1993a1B5D05F9C2);
synthesized[37] = IPToken(0xBcE56276591128047313e64744b3EBE03998783f);

setIPTokenImplementation(_ipTokenImplementation);
}

/**
* @notice initializes synthesis on ipnft#id for the current asset holder.
* IPTokens are identified by the original token holder and the token id
* @notice tokenizes ipnft#id for the current asset holder.
* @param ipnftId the token id on the underlying nft collection
* @param tokenAmount the initially issued supply of IP tokens
* @param tokenSymbol the ip token's ticker symbol
Expand All @@ -125,30 +113,30 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
string memory tokenSymbol,
string memory agreementCid,
bytes calldata signedAgreement
) external onlyIPNFTHolder(ipnftId) returns (IPToken token) {
) external returns (IPToken token) {
if (ipnft.ownerOf(ipnftId) != _msgSender()) {
revert MustOwnIpnft();
}
if (address(synthesized[ipnftId]) != address(0)) {
revert AlreadyTokenized();
}

// https://github.com/OpenZeppelin/workshops/tree/master/02-contracts-clone
token = IPToken(Clones.clone(address(ipTokenImplementation)));
string memory name = string.concat("IP Tokens of IPNFT #", Strings.toString(ipnftId));
token.initialize(name, tokenSymbol, TokenMetadata(ipnftId, _msgSender(), agreementCid));
token.initialize(ipnftId, name, tokenSymbol, _msgSender(), agreementCid);

uint256 tokenHash = token.hash();

if (address(synthesized[tokenHash]) != address(0) || address(synthesized[legacyHash(ipnftId, _msgSender())]) != address(0)) {
revert AlreadyTokenized();
}

synthesized[tokenHash] = token;
synthesized[ipnftId] = token;

//this has been called MoleculesCreated before
emit TokensCreated(tokenHash, ipnftId, address(token), _msgSender(), tokenAmount, agreementCid, name, tokenSymbol);
emit TokensCreated(ipnftId, address(token), _msgSender(), tokenAmount, agreementCid, name, tokenSymbol);
permissioner.accept(token, _msgSender(), signedAgreement);
token.issue(_msgSender(), tokenAmount);
}

/**
* @notice issues more IPTs for a given IPNFT
* @dev you must compute the ipt hash externally.
* @param ipToken the hash of the IPToken. See `IPToken.hash()` and `legacyHash()`. Some older IPT implementations required to compute the has as `uint256(keccak256(abi.encodePacked(owner,ipnftId)))`
* @notice issues more IPTs when not capped. This can be used for new owners of legacy IPTs that otherwise wouldn't be able to pass their `onlyIssuerOrOwner` gate
* @param ipToken The ip token to control
* @param amount the amount of tokens to issue
* @param receiver the address that receives the tokens
*/
Expand All @@ -165,13 +153,9 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
ipToken.cap();
}

/**
* @dev computes the legacy hash for an IPToken. It depended on the IPNFT owner before. Helps ensuring that the current holder cannot refractionalize an IPT
* @param ipnftId IPNFT token id
* @param owner the owner for the current IPT sales cycle (old concept)
*/
function legacyHash(uint256 ipnftId, address owner) public pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(owner, ipnftId)));
/// @dev this will be called by IPTs to avoid handing over yet another IPNFT address (they already know this Tokenizer contract as their owner)
function ownerOf(uint256 ipnftId) external view returns (address) {
return ipnft.ownerOf(ipnftId);
}

/// @notice upgrade authorization logic
Expand Down
126 changes: 126 additions & 0 deletions src/helpers/test-upgrades/IPToken12.sol
Original file line number Diff line number Diff line change
@@ -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,
"}"
)
)
)
);
}
}
Loading

0 comments on commit cdda55f

Please sign in to comment.