Skip to content

IPN-21 remove concept of "sales cycle" IPTs #159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
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());
}
}
55 changes: 24 additions & 31 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 @@ -13,75 +14,67 @@ 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 onlyTokenizerOrIPNFTHolder() {
if (_msgSender() != owner() && _msgSender() != Tokenizer(owner()).ownerOf(_metadata.ipnftId)) {
revert MustOwnIpnft();
}
_;
}

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 onlyTokenizerOrIPNFTHolder {
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 {
function cap() external onlyTokenizerOrIPNFTHolder {
capped = true;
emit Capped(totalIssued);
}
Expand Down
28 changes: 18 additions & 10 deletions src/SalesShareDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ error OnlyIssuer();

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;

Expand All @@ -41,7 +47,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
Expand Down Expand Up @@ -85,19 +91,20 @@ 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
* rn we restrict it to the original owner since they must provide a permissioner that controls the claiming rules
* this is a deep dependency on our own sales contract
*
* @param tokenContract 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()) {
Metadata memory metadata = tokenContract.metadata();
//todo: this should be the *former* holder of the IPNFT, not the 1st owner :)
if (_msgSender() != metadata.originalOwner) {
revert OnlyIssuer();
}

Metadata memory metadata = tokenContract.metadata();
(, uint256 ipnftId,, IERC20 _paymentToken, uint256 askPrice, address beneficiary, ListingState listingState) =
schmackoSwap.listings(listingId);

Expand Down Expand Up @@ -130,12 +137,12 @@ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, Reentranc
* @param permissioner IPermissioner the permissioner that permits claims
*/
function afterSale(IPToken tokenContract, IERC20 paymentToken, uint256 paidPrice, IPermissioner permissioner) external nonReentrant {
if (_msgSender() != tokenContract.issuer()) {
Metadata memory metadata = tokenContract.metadata();
//todo: this should be the *former* holder of the IPNFT, not the 1st owner :)
if (_msgSender() != metadata.originalOwner) {
revert OnlyIssuer();
}

Metadata memory metadata = tokenContract.metadata();

//create a fake (but valid) schmackoswap listing id
uint256 fulfilledListingId = uint256(
keccak256(
Expand All @@ -160,9 +167,10 @@ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, Reentranc
function _startClaimingPhase(IPToken tokenContract, uint256 fulfilledListingId, IERC20 _paymentToken, uint256 price, IPermissioner permissioner)
internal
{
if (!tokenContract.capped()) {
revert UncappedToken();
}
//todo: this actually must be enforced before a sale starts
// if (!tokenContract.capped()) {
// revert UncappedToken();
// }
sales[address(tokenContract)] = Sales(fulfilledListingId, _paymentToken, price, permissioner);
emit SalesActivated(address(tokenContract), address(_paymentToken), price);
}
Expand Down
83 changes: 57 additions & 26 deletions src/Tokenizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,14 @@ import { IPNFT } from "./IPNFT.sol";
error MustOwnIpnft();
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 {
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 All @@ -43,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;

/**
Expand All @@ -63,11 +57,25 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
_disableInitializers();
}

//todo: try breaking this with a faked IPToken
modifier onlyControlledIPTs(IPToken ipToken) {
TokenMetadata memory metadata = ipToken.metadata();

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

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

/**
* @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 @@ -80,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 @@ -107,26 +117,47 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
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));

uint256 tokenHash = token.hash();
// ensure we can only call this once per sales cycle
if (address(synthesized[tokenHash]) != address(0)) {
revert AlreadyTokenized();
}
token.initialize(ipnftId, name, tokenSymbol, _msgSender(), agreementCid);

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 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 onlyControlledIPTs(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 onlyControlledIPTs(ipToken) {
ipToken.cap();
}

/// @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
function _authorizeUpgrade(address /*newImplementation*/ )
internal
Expand Down
Loading