Monetization of publicly accessible written content has never had a strong basis on the internet, since it suffers from the public goods problem. As publishing has moved online, funding for high-quality, long-form writing has broken down.
NFTs give us new tools to solve this problem — by representing previously infinitely-reproducible creative works as scarce, tradable digital assets. This means that artists can now sell a scarce digital asset representing an essay as a unique collectible or artwork, while the content itself remains open and freely accessible (a public good). Still, the problem remains for funding the time and work necessary for a creator to produce impactful writing.
We imagine a world where writers on Mirror can publish an intention to research and produce high-quality writing, and receive crowdsourced funding. In this model, the contributors who fund the project also receive a stake in the future financial upside produced by the work, captured by subsequent sales of the NFT. This improves considerably on existing crowdfunding platforms, such as Kickstarter.
To achieve these outcomes, the content must be tradable as a single artifact (an NFT), and the ownership of that artifact must be fractional — allowing multiple people to own a small stake. By using Ethereum as the economic infrastructure, we can allow tradable, fractional ownership of the NFT using ERC20 tokens.
The funders of the project should be able to trade their own currency (e.g. ETH or DAI) for an ownership stake before the project is completed. The creator should be able to withdraw pledged funds and use them to fund the production of the public good. The backers should be allowed to redeem the underlying funds (including profits) once the NFT is traded, proportional to the percent that they contributed to the fund. They can do this by provably burning their equity tokens in a single transaction that also redeems their funds.
Since equity is represented as an ERC20, contributors might also trade their tokens on an exchange like Uniswap, instead of redeeming the underlying value — similar to trading options.
In the future, we imagine that the creator could be the operator of a DAO that produces many works, each of minted as an NFT, with ongoing revenue from trading accruing back to the DAO. Funders of the DAO can therefore expect profits beyond those coming from sales of the first NFT.
This could signal the beginning of a movement towards journalist and artist DAOs.
In this post, we include our technical considerations for this project and a sketch of the contract that might serve this purpose. We welcome any and all feedback on our ideas. We will target a demo of the working functionality on Friday during a live-stream.
mint()
tokens — we might call this an operator. The operator can accept bids on the NFT, and can simultaneously close funding and withdraw the funds that were raised.Basic sketch of the contract (please do not use in production, this is just a sketch and not intended to compile).
//SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.6.8;
pragma experimental ABIEncoderV2;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {
ReentrancyGuard
} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol";
import {Decimal} from "../Decimal.sol";
import {IMarket} from "../interfaces/IMarket.sol";
import {IMedia} from "../interfaces/IMedia.sol";
import {IWETH} from "./interfaces/IWETH.sol";
/**
* @title Crowdfund
* @author MirrorXYZ
*
* Crowdfund the creation of NFTs by issuing ERC20 tokens that
* can be redeemed for the underlying value of the NFT once sold.
*/
contract Crowdfund is ERC20, ReentrancyGuard {
using SafeMath for uint256;
// ============ Enums ============
enum Status {FUNDING, TRADING}
// ============ Constants ============
uint256 private constant funding_cap_eth = 10000000000000000000;
uint256 private constant SCALING_FACTOR = 1e27;
// ============ Immutable Storage ============
address public operator;
address public mediaAddress;
address public WETH;
uint256 public operatorEquityPercent;
// ============ Mutable Storage ============
// Represents the current state of the campaign.
Status public status;
// ============ Events ============
event FundingOpened(
address media,
address creator,
uint256 creatorEquityPercent
);
event Contribution(address contributor, uint256 amount);
event FundingClosed(uint256 amountRaised, uint256 creatorAllocation);
event BidAccepted(uint256 amount);
event Withdrawal(address contributor, uint256 amount);
// ============ Modifiers ============
/**
* @dev Modifier to check whether the `msg.sender` is the operator.
* If it is, it will run the function. Otherwise, it will revert.
*/
modifier onlyOperator() {
require(msg.sender == operator);
_;
}
// ============ Constructor ============
constructor(
address operator_,
address mediaAddress_,
address WETH_,
uint256 operatorEquityPercent_,
IMedia.MediaData memory data,
IMarket.BidShares memory bidShares
) public ERC20("Crowdfund", "CROWD") {
// Initialize immutable storage.
mediaAddress = mediaAddress_;
operator = operator_;
operatorEquityPercent = operatorEquityPercent_;
WETH = WETH_;
// Initialize mutable storage.
status = Status.FUNDING;
// Mint an NFT token.
IMedia(mediaAddress).mint(data, bidShares);
// Signal that funding has been opened.
emit FundingOpened(mediaAddress, operator, operatorEquityPercent);
}
// ============ Crowdfunding Methods ============
/**
* @notice Mints tokens for the sender propotional to the
* amount of ETH sent in the transaction.
* @dev Emits the Contribution event.
*/
function contribute() external payable nonReentrant {
require(status == Status.FUNDING, "Funding must be open");
require(
msg.value.add(address(this).balance) <= funding_cap_eth,
"Total contributions would exceed funding cap"
);
// Mint equity for the contributor.
_mint(msg.sender, msg.value);
emit Contribution(msg.sender, msg.value);
}
/**
* @notice Burns the sender's tokens and redeems underlying ETH.
* @dev Emits the Withdrawal event.
*/
function withdraw(uint256 tokenAmount) external nonReentrant {
require((balanceOf(msg.sender) >= tokenAmount), "Insufficient balance");
uint256 redeemable = redeemableFromTokens(tokenAmount);
_burn(msg.sender, tokenAmount);
msg.sender.transfer(redeemable);
emit Withdrawal(msg.sender, redeemable);
}
/**
* @notice Returns the amount of ETH that is redeemable for tokenAmount.
*/
function redeemableFromTokens(uint256 tokenAmount)
public
view
returns (uint256 redeemable)
{
uint256 stakeScaled =
tokenAmount.mul(SCALING_FACTOR).div(totalSupply());
// Round up after scaling.
redeemable = stakeScaled
.mul(address(this).balance)
.sub(1)
.div(SCALING_FACTOR)
.add(1);
}
// ============ Operator Methods ============
/**
* @notice Transfers all funds to operator, and mints tokens for the operator.
* Updates status to TRADING.
* @dev Emits the FundingClosed event.
*/
function closeFunding() external onlyOperator nonReentrant {
require(status == Status.FUNDING, "Funding must be open");
// Transfer all funds to the operator.
uint256 amountRaised = balanceOf(address(this));
transfer(operator, amountRaised);
// Mint the operator a percent of the total supply.
uint256 tokensForOperator =
totalSupply().div(100).mul(operatorEquityPercent);
_mint(operator, tokensForOperator);
// Close funding status, move to tradable.
status = Status.TRADING;
emit FundingClosed(amountRaised, tokensForOperator);
}
/**
* @notice Accepts the given bid on the associated market and unwraps WETH.
* @dev Emits the BidAccepted event.
*/
function acceptNFTBid(IMarket.Bid calldata bid)
external
onlyOperator
nonReentrant
{
require(status == Status.TRADING, "Trading must be open");
// This will work if the publication is the owner of the token
IMedia(mediaAddress).acceptBid(0, bid);
// Accepting the bid will transfer WETH into this contract.
IWETH(WETH).withdraw(bid.amount);
emit BidAccepted(bid.amount);
}
// Allows the operator to update metadata associated with the NFT.
function updateTokenURI(string calldata tokenURI)
external
onlyOperator
nonReentrant
{
IMedia(mediaAddress).updateTokenURI(0, tokenURI);
}
// Allows the operator to update metadata associated with the NFT.
function updateTokenMetadataURI(string calldata metadataURI)
external
onlyOperator
nonReentrant
{
IMedia(mediaAddress).updateTokenMetadataURI(0, metadataURI);
}
/**
* @notice Prevents ETH from being sent directly to the contract, except
* from the WETH contract, during acceptBid.
*/
receive() external payable {
assert(msg.sender == WETH);
}
}
Please comment with feedback on this thread: