Skip to content
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

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

Merged
11 changes: 11 additions & 0 deletions src/IControlIPTs.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

/**
* @title IControlIPTs 1.3
* @author molecule.xyz
* @notice must be implemented by contracts that should control IPTs
*/
interface IControlIPTs {
function controllerOf(uint256 ipnftId) external view returns (address);
}
3 changes: 2 additions & 1 deletion src/IPToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/O
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
import { Tokenizer, MustControlIpnft } from "./Tokenizer.sol";
import { IControlIPTs } from "./IControlIPTs.sol";

struct Metadata {
uint256 ipnftId;
Expand Down Expand Up @@ -48,7 +49,7 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable {
}

modifier onlyTokenizerOrIPNFTController() {
if (_msgSender() != owner() && _msgSender() != Tokenizer(owner()).controllerOf(_metadata.ipnftId)) {
if (_msgSender() != owner() && _msgSender() != IControlIPTs(owner()).controllerOf(_metadata.ipnftId)) {
revert MustControlIpnft();
}
_;
Expand Down
13 changes: 7 additions & 6 deletions src/Tokenizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { IPToken, Metadata as TokenMetadata } from "./IPToken.sol";
import { IPermissioner } from "./Permissioner.sol";
import { IPNFT } from "./IPNFT.sol";
import { IControlIPTs } from "./IControlIPTs.sol";

error MustControlIpnft();
error AlreadyTokenized();
Expand All @@ -17,7 +18,7 @@ error IPTNotControlledByTokenizer();
/// @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 {
contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable, IControlIPTs {
event TokensCreated(
uint256 indexed moleculesId,
uint256 indexed ipnftId,
Expand Down Expand Up @@ -69,7 +70,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
}

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

if (address(synthesized[metadata.ipnftId]) != address(ipToken)) {
elmariachi111 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -141,7 +142,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {

//this has been called MoleculesCreated before
emit TokensCreated(
//upwards compatibility: signaling an unique "Molecules ID" as first parameter ("sales cycle id"). This is unused and not interpreted.
//upwards compatibility: signaling a unique "Molecules ID" as first parameter ("sales cycle id"). This is unused and not interpreted.
uint256(keccak256(abi.encodePacked(ipnftId))),
ipnftId,
address(token),
Expand All @@ -161,7 +162,7 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
* @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) {
function issue(IPToken ipToken, uint256 amount, address receiver) external onlyController(ipToken) {
ipToken.issue(receiver, amount);
}

Expand All @@ -170,12 +171,12 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
* @dev you must compute the ipt hash externally.
* @param ipToken the IPToken to cap.
*/
function cap(IPToken ipToken) external onlyControlledIPTs(ipToken) {
function cap(IPToken ipToken) external onlyController(ipToken) {
ipToken.cap();
}

/// @dev this will be called by IPTs. Right now the controller is the IPNFT's current owner, it can be a Governor in the future.
function controllerOf(uint256 ipnftId) public view returns (address) {
function controllerOf(uint256 ipnftId) public view override returns (address) {
return ipnft.ownerOf(ipnftId);
}

Expand Down
51 changes: 51 additions & 0 deletions test/Tokenizer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,28 @@ import { FakeERC20 } from "../src/helpers/FakeERC20.sol";
import { MustControlIpnft, AlreadyTokenized, Tokenizer, ZeroAddress } from "../src/Tokenizer.sol";

import { IPToken, TokenCapped } from "../src/IPToken.sol";
import { IControlIPTs } from "../src/IControlIPTs.sol";
import { Molecules } from "../src/helpers/test-upgrades/Molecules.sol";
import { Synthesizer } from "../src/helpers/test-upgrades/Synthesizer.sol";
import { IPermissioner, BlindPermissioner } from "../src/Permissioner.sol";

contract GovernorOfTheFuture is IControlIPTs {
function controllerOf(uint256 ipnftId) external view override returns (address) {
return address(0); //no one but me controls IPTs!
}

function aMajorityWantsToIssueTokensTo(IPToken ipt, uint256 amount, address receiver) public {
ipt.issue(receiver, amount);
}
}

contract TokenizerWithHandover is Tokenizer {
//this oc would be gated for the current IPNFT holder
function handoverControl(IPToken ipt, GovernorOfTheFuture governor) external onlyController(ipt) {
ipt.transferOwnership(address(governor));
}
}

contract TokenizerTest is Test {
using SafeERC20Upgradeable for IPToken;

Expand Down Expand Up @@ -228,4 +246,37 @@ contract TokenizerTest is Test {

assertEq(tokenContract.balanceOf(bob), 10_000);
}

function testTokenizerCanHandoverControl() public {
vm.startPrank(deployer);
TokenizerWithHandover htokenizer = TokenizerWithHandover(address(new ERC1967Proxy(address(new TokenizerWithHandover()), "")));
htokenizer.initialize(ipnft, blindPermissioner);
htokenizer.setIPTokenImplementation(new IPToken());

vm.startPrank(originalOwner);
IPToken tokenContract = htokenizer.tokenizeIpnft(1, 100_000, "IPT", agreementCid, "");
tokenContract.issue(bob, 50_000);

vm.startPrank(deployer);
GovernorOfTheFuture governor = new GovernorOfTheFuture();
vm.stopPrank();

vm.startPrank(originalOwner);
htokenizer.handoverControl(tokenContract, governor);

vm.startPrank(alice); // alice controls the governor, eg by proving that a vote has occured
governor.aMajorityWantsToIssueTokensTo(tokenContract, 50_000, alice);
assertEq(tokenContract.balanceOf(alice), 50_000);

// -- from here on, *only* the new governor is in conrol
vm.expectRevert(MustControlIpnft.selector);
tokenContract.issue(alice, 50_000);

vm.startPrank(originalOwner);
vm.expectRevert(MustControlIpnft.selector);
tokenContract.issue(bob, 50_000);

vm.expectRevert(MustControlIpnft.selector);
htokenizer.issue(tokenContract, 50_000, bob);
}
}