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 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
34 changes: 4 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,38 +87,12 @@ VDAO_TOKEN_ADDRESS=0x19A3036b828bffB5E14da2659E950E76f8e6BAA2

---

### ~~Deprecated Goerli~~
### upgrading to Tokenizer 1.3

| Contract | Address | Actions |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| IP-NFT | [0xaf7358576C9F7cD84696D28702fC5ADe33cce0e9](https://goerli.etherscan.io/address/0xaf7358576C9F7cD84696D28702fC5ADe33cce0e9#code>) | <a href="https://thirdweb.com/goerli/0xaf7358576C9F7cD84696D28702fC5ADe33cce0e9?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0xaf7358576C9F7cD84696D28702fC5ADe33cce0e9&theme=dark&chainId=5" alt="View contract" /></a> |
| SchmackoSwap | [0x67D8ed102E2168A46FA342e39A5f7D16c103Bd0d](https://goerli.etherscan.io/address/0x67D8ed102E2168A46FA342e39A5f7D16c103Bd0d#code) | <a href="https://thirdweb.com/goerli/0x67D8ed102E2168A46FA342e39A5f7D16c103Bd0d?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0x67D8ed102E2168A46FA342e39A5f7D16c103Bd0d&theme=dark&chainId=5" alt="View contract" /></a> |
| Tokenizer | [0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c](https://goerli.etherscan.io/address/0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c#code) | <a href="https://thirdweb.com/goerli/0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c&theme=dark&chainId=5" alt="View contract" /></a> |
| Permissioner | [0xd735d9504cce32F2cd665b779D699B4157686fcd](https://goerli.etherscan.io/address/0xd735d9504cce32F2cd665b779D699B4157686fcd#code) | <a href="https://thirdweb.com/goerli/0xd735d9504cce32F2cd665b779D699B4157686fcd?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0xd735d9504cce32F2cd665b779D699B4157686fcd&theme=dark&chainId=5" alt="View contract" /></a> |
| Crowdsale | [0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373](https://goerli.etherscan.io/address/0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373#code>) | <a href="https://thirdweb.com/goerli/0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373&theme=dark&chainId=5" alt="View contract" /></a> |
| StakedLockingCrowdSale | [0x46c3369dece07176ad7164906d3593aa4c126d35](https://goerli.etherscan.io/address/0x46c3369dece07176ad7164906d3593aa4c126d35#code) | <a href="https://thirdweb.com/goerli/0x46c3369dece07176ad7164906d3593aa4c126d35?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0x46c3369dece07176ad7164906d3593aa4c126d35&theme=dark&chainId=5" alt="View contract" /></a> |
| SignedMintAuthorizer | [0x5e555eE24DB66825171Ac63EA614864987CEf1Af](https://goerli.etherscan.io/address/0x5e555eE24DB66825171Ac63EA614864987CEf1Af#code) | <a href="https://thirdweb.com/goerli/0x5e555eE24DB66825171Ac63EA614864987CEf1Af?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0x5e555eE24DB66825171Ac63EA614864987CEf1Af&theme=dark&chainId=5" alt="View contract" /></a> |
| IPToken Implementation | [0x38Ca0fEEc7cd48629f9388727bfA747859a6feE7](https://goerli.etherscan.io/address/0x38Ca0fEEc7cd48629f9388727bfA747859a6feE7#code) | <a href="https://thirdweb.com/goerli/0x38Ca0fEEc7cd48629f9388727bfA747859a6feE7?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0x38Ca0fEEc7cd48629f9388727bfA747859a6feE7&theme=dark&chainId=5" alt="View contract" /></a> |
forge script --private-key=$PRIVATE_KEY --rpc-url=$RPC_URL script/prod/RolloutTokenizerV13.s.sol --broadcast

- Subgraph: https://api.thegraph.com/subgraphs/name/moleculeprotocol/ip-nft-goerli

- Tokenizer Implementation 1.2: 0x18E5ae026CFC8020b2eDbA7050eA6144Fd313c02 (reinit 4) <https://goerli.etherscan.io/address/0x18E5ae026CFC8020b2eDbA7050eA6144Fd313c02#code>

- Bio pricefeed: 0x8647dEFdEAAdF5448d021B364B2F17815aba4360
<https://goerli.etherscan.io/address/0x8647defdeaadf5448d021b364b2f17815aba4360#code>

- old ("molecule") permissioner: 0x0045723801561079d94f0Bb1B65f322078E52635
<https://goerli.etherscan.io/address/0x0045723801561079d94f0Bb1B65f322078E52635#code>

- Blind Permissioner: 0xec68a1fc8d4c2834f8dfbdb56691f9f0a3d6be11
<https://goerli.etherscan.io/address/0xec68a1fc8d4c2834f8dfbdb56691f9f0a3d6be11#code>

#### ~~Tokens~~

| Token name | Symbol | address |
| ------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| BioDao Test token | BIODAO | [0x3110a768DC64f7aAB92F7Ae6E1371e5CE581F95F](https://goerli.etherscan.io/address/0x3110a768dc64f7aab92f7ae6e1371e5ce581f95f#code) |
| Vested BioDao Test token | vBIODAO | [0x6FFBd6325B2102F5f9AaB74d7418A27F9174c92f](https://goerli.etherscan.io/address/0x6FFBd6325B2102F5f9AaB74d7418A27F9174c92f) |
// 0xTokenizer 0xNewImpl 0xNewTokenImpl
cast send --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY 0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e "upgradeToAndCall(address,bytes)" 0x70e0bA845a1A0F2DA3359C97E0285013525FFC49 0x84646c1f000000000000000000000000998abeb3e57409262ae5b751f60747921b33613e

## Prerequisites

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"license": "MIT",
"scripts": {
"test": "hardhat test --network hardhat"
"test": "hardhat test --network hardhat",
"clean": "rm -rf out cache_forge"
},
"devDependencies": {
"@nomicfoundation/hardhat-foundry": "^1.0.0",
Expand Down
12 changes: 6 additions & 6 deletions src/IPToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +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";
import { Tokenizer, MustControlIpnft } from "./Tokenizer.sol";

struct Metadata {
uint256 ipnftId;
Expand Down Expand Up @@ -47,9 +47,9 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable {
_disableInitializers();
}

modifier onlyTokenizerOrIPNFTHolder() {
if (_msgSender() != owner() && _msgSender() != Tokenizer(owner()).ownerOf(_metadata.ipnftId)) {
revert MustOwnIpnft();
modifier onlyTokenizerOrIPNFTController() {
if (_msgSender() != owner() && _msgSender() != Tokenizer(owner()).controllerOf(_metadata.ipnftId)) {
revert MustControlIpnft();
}
_;
}
Expand All @@ -63,7 +63,7 @@ contract IPToken is ERC20BurnableUpgradeable, OwnableUpgradeable {
* @param receiver address
* @param amount uint256
*/
function issue(address receiver, uint256 amount) external onlyTokenizerOrIPNFTHolder {
function issue(address receiver, uint256 amount) external onlyTokenizerOrIPNFTController {
if (capped) {
revert TokenCapped();
}
Expand All @@ -74,7 +74,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 onlyTokenizerOrIPNFTHolder {
function cap() external onlyTokenizerOrIPNFTController {
capped = true;
emit Capped(totalIssued);
}
Expand Down
68 changes: 36 additions & 32 deletions src/SalesShareDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IPNFT } from "./IPNFT.sol";
import { IPToken, Metadata } from "./IPToken.sol";
import { Tokenizer, MustControlIpnft } from "./Tokenizer.sol";
import { SchmackoSwap, ListingState } from "./SchmackoSwap.sol";
import { IPermissioner, TermsAcceptedPermissioner } from "./Permissioner.sol";

Expand All @@ -21,9 +23,9 @@ struct Sales {
error ListingNotFulfilled();
error ListingMismatch();
error InsufficientBalance();
error NotSalesBeneficiary();
error UncappedToken();
error OnlyIssuer();

error OnlySeller();
error NotClaimingYet();

/**
Expand Down Expand Up @@ -90,65 +92,69 @@ 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 original owner since they must provide a permissioner that controls the claiming rules
* this is a deep dependency on our own sales contract
* @notice release sales shares for a Schmackoswap transaction
* @dev todo: *anyone* should be able to call this function after having observed the sale; right now we restrict it to the creator of the trade since they were in control of the IPNFT before
* @dev this has a deep dependency on our own swap contract
*
* @param tokenContract IPToken the tokenContract of the IPToken
* @param ipt 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 {
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();
}

(, uint256 ipnftId,, IERC20 _paymentToken, uint256 askPrice, address beneficiary, ListingState listingState) =
function afterSale(IPToken ipt, uint256 listingId, IPermissioner permissioner) external {
(, uint256 ipnftId, address seller, IERC20 _paymentToken, uint256 askPrice, address beneficiary, ListingState listingState) =
schmackoSwap.listings(listingId);

if (_msgSender() != seller) {
revert OnlySeller();
}

if (listingState != ListingState.FULFILLED) {
revert ListingNotFulfilled();
}
Metadata memory metadata = ipt.metadata();
if (ipnftId != metadata.ipnftId) {
revert ListingMismatch();
}
if (beneficiary != address(this)) {
revert InsufficientBalance();
revert NotSalesBeneficiary();
}

_startClaimingPhase(tokenContract, listingId, _paymentToken, askPrice, permissioner);
_startClaimingPhase(ipt, listingId, _paymentToken, askPrice, permissioner);
}

//audit: ensure that no one can withdraw arbitrary amounts here
//by simply creating a new IPToken instance and claim an arbitrary value

//todo: try breaking this by providing a fake IPT with a fake Tokenizer owner
//todo: this must be called by the beneficiary of a sale we don't control.
/**
* @notice When the sales beneficiary has not been set to the underlying erc20 token address but to the original owner's wallet instead,
* they can invoke this method to start the claiming phase manually. This e.g. allows sales off the record.
* they can invoke this method to start the claiming phase manually. This e.g. allows sales off the record ("OpenSea").
*
* Requires the originalOwner to behave honestly / in favor of the molecules holders
* Requires the caller to have approved `price` of `paymentToken` to this contract
* Requires the originalOwner to behave honestly / in favor of the IPT holders
* Requires the caller to have approved `paidPrice` of `paymentToken` to this contract
*
* @param tokenContract IPToken the IPToken token contract
* @param paymentToken IERC20 the payment token contract address
* @param paidPrice uint256 the price the NFT has been sold for
* @param permissioner IPermissioner the permissioner that permits claims
* @param paidPrice uint256 the price the NFT has been sold for
* @param permissioner IPermissioner the permissioner that permits claims
*/
function afterSale(IPToken tokenContract, IERC20 paymentToken, uint256 paidPrice, IPermissioner permissioner) external nonReentrant {
function UNSAFE_afterSale(IPToken tokenContract, IERC20 paymentToken, uint256 paidPrice, IPermissioner permissioner) external nonReentrant {
Metadata memory metadata = tokenContract.metadata();
//todo: this should be the *former* holder of the IPNFT, not the 1st owner :)

Tokenizer tokenizer = Tokenizer(tokenContract.owner());

//todo: this should be a selected beneficiary of the IPNFT's sales proceeds, and not the original owner :)
//idea is to allow *several* sales proceeds to be notified here, create unique sales ids for each and let users claim the all of them at once
if (_msgSender() != metadata.originalOwner) {
revert OnlyIssuer();
revert MustControlIpnft();
}

//create a fake (but valid) schmackoswap listing id
uint256 fulfilledListingId = uint256(
keccak256(
abi.encode(
SchmackoSwap.Listing(
IERC721(address(0)), //this should be the IPNFT address
IERC721(address(tokenizer.getIPNFTContract())),
metadata.ipnftId,
_msgSender(),
paymentToken,
Expand All @@ -164,15 +170,13 @@ contract SalesShareDistributor is UUPSUpgradeable, OwnableUpgradeable, Reentranc
paymentToken.safeTransferFrom(_msgSender(), address(this), paidPrice);
}

function _startClaimingPhase(IPToken tokenContract, uint256 fulfilledListingId, IERC20 _paymentToken, uint256 price, IPermissioner permissioner)
internal
{
//todo: this actually must be enforced before a sale starts
function _startClaimingPhase(IPToken ipt, uint256 fulfilledListingId, IERC20 _paymentToken, uint256 price, IPermissioner permissioner) internal {
//todo: this *should* 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);
sales[address(ipt)] = Sales(fulfilledListingId, _paymentToken, price, permissioner);
emit SalesActivated(address(ipt), address(_paymentToken), price);
}

function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }
Expand Down
18 changes: 11 additions & 7 deletions src/Tokenizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IPToken, Metadata as TokenMetadata } from "./IPToken.sol";
import { IPermissioner } from "./Permissioner.sol";
import { IPNFT } from "./IPNFT.sol";

error MustOwnIpnft();
error MustControlIpnft();
error AlreadyTokenized();
error ZeroAddress();
error IPTNotControlledByTokenizer();
Expand Down Expand Up @@ -64,6 +64,10 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
_disableInitializers();
}

function getIPNFTContract() public view returns (IPNFT) {
return ipnft;
}

//todo: try breaking this with a faked IPToken
modifier onlyControlledIPTs(IPToken ipToken) {
TokenMetadata memory metadata = ipToken.metadata();
Expand All @@ -72,8 +76,8 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
revert IPTNotControlledByTokenizer();
}

if (_msgSender() != ipnft.ownerOf(metadata.ipnftId)) {
revert MustOwnIpnft();
if (_msgSender() != controllerOf(metadata.ipnftId)) {
revert MustControlIpnft();
}
_;
}
Expand Down Expand Up @@ -121,8 +125,8 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
string memory agreementCid,
bytes calldata signedAgreement
) external returns (IPToken token) {
if (ipnft.ownerOf(ipnftId) != _msgSender()) {
revert MustOwnIpnft();
if (_msgSender() != controllerOf(ipnftId)) {
revert MustControlIpnft();
}
if (address(synthesized[ipnftId]) != address(0)) {
revert AlreadyTokenized();
Expand Down Expand Up @@ -170,8 +174,8 @@ contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable {
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) {
/// @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) {
return ipnft.ownerOf(ipnftId);
}

Expand Down
2 changes: 1 addition & 1 deletion subgraph/abis/IPToken.json
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@
},
{
"type": "error",
"name": "MustOwnIpnft",
"name": "MustControlIpnft",
"inputs": []
},
{
Expand Down
16 changes: 13 additions & 3 deletions subgraph/abis/SharedSalesDistributor.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"type": "function",
"name": "afterSale",
"name": "UNSAFE_afterSale",
"inputs": [
{
"name": "tokenContract",
Expand Down Expand Up @@ -32,7 +32,7 @@
"name": "afterSale",
"inputs": [
{
"name": "tokenContract",
"name": "ipt",
"type": "address",
"internalType": "contract IPToken"
},
Expand Down Expand Up @@ -363,14 +363,24 @@
"name": "ListingNotFulfilled",
"inputs": []
},
{
"type": "error",
"name": "MustControlIpnft",
"inputs": []
},
{
"type": "error",
"name": "NotClaimingYet",
"inputs": []
},
{
"type": "error",
"name": "OnlyIssuer",
"name": "NotSalesBeneficiary",
"inputs": []
},
{
"type": "error",
"name": "OnlySeller",
"inputs": []
}
]
Loading