Crowdfunding Writing with NFTs

Mirror Team
Mirror Team
0x9651
January 20th, 2021

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.

Problems We're Solving

  • Writers want funding for a long-form piece of content before writing it
  • Contributors want to fund their favorite writers, and receive some return on their investment
  • Contributors want to be recognized as patrons of public goods
  • Speculators want to invest in NFTs, including those that represent written works
  • Nobody wants to pay excessive gas costs that make it unprofitable to solve these problems

What Success Looks Like

  • Contributors can exchange currency (e.g. ETH) for some ownership stake in a future work
  • The Creator can withdraw the funds that are raised, and thereby close the funding session
  • The work is represented by a tradable NFT
  • The Creator can update the NFT's metadata once the work is finished
  • The Creator can trade that NFT for a profit
  • Contributors can trade their ownership stake on Uniswap
  • Contributors can divest their stake for accumulated revenue
  • The Creator can receive an ownership stake once the funding is closed, entitling them to a share of the future profits as well

Out of Scope

  • Bringing this into the scope of the Publication Contract, and thereby blocking experimentation on that work-stream.

Implementation Summary

  • There is an ERC20-compatible contract that mints and owns an NFT upon deployment
  • The contract can have an owner, but the owner cannot 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.
  • Contributors are able to send funds (e.g. ETH) to the contract in exchange for a proportional ownership stake in the future work.
  • Contributors are able to exit by using a method that burns their equity tokens and sends them their funds (initial contribution + profits)
  • The contract has a "hard cap", beyond which nobody else can join the crowdfund.
    • This limits the original valuation for the NFT to something less than the expected profitability, limits exploitation of vulnerabilities, and also creates some scarcity around being part of the funding opportunity.
    • The downside is that one person might just buy all of it, which is less fun.

Potential Downsides

  • It could have a vulnerability, and lose funds.

Implementation Details

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:

Arweave TX
RkQwGZ…otFH6E
Ethereum Address
0x9651…d5e902
Content Digest
llJ_Ab…LafewQ