Skip to content

Instantly share code, notes, and snippets.

@graemecode
Last active October 3, 2021 19:08
Show Gist options
  • Save graemecode/0811326589450829ccd0641e8a5408d7 to your computer and use it in GitHub Desktop.
Save graemecode/0811326589450829ccd0641e8a5408d7 to your computer and use it in GitHub Desktop.
Crowdfunding NFTs with Zora
//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 public constant fundingCap = 10000000000000000000;
uint256 private constant SCALING_FACTOR = 1e27;
// ============ Immutable Storage ============
address payable 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 payable 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) <= fundingCap,
"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");
// Close funding status, move to tradable.
status = Status.TRADING;
// Mint the operator a percent of the total supply.
uint256 tokensForOperator =
totalSupply().mul(operatorEquityPercent).div(100);
_mint(operator, tokensForOperator);
emit FundingClosed(address(this).balance, tokensForOperator);
// Transfer all funds to the operator.
operator.transfer(address(this).balance);
}
/**
* @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);
}
}
Crowdfund
when Zora Media has been deployed
when deployed with appropriate arguments
✓ deploys a crowdfund contract with the correct configuration (65ms)
✓ gives no balance to the creator at deployment
✓ emits a FundingOpened event
when a contributor attempts to contribute 2 ETH
✓ uses 69992 gas
✓ increases the contract's balance by 2 ETH
✓ decrease the contributor's ETH balance by 2 ETH plus gas for the tx
✓ mints tokens for the contributor equal to the amount of ETH given
✓ grants them 2 ETH redeemable
✓ emits a Transfer and Contribution event
when the contributor attempts to withdraw 1.2 ETH from their contributions
✓ burns their tokens, so that their token balance is 800000000000000000
✓ totalSupply() is now 800000000000000000
✓ decreases the contract's balance by 1.2 ETH
✓ increases the sender's balance by 1.2 ETH, minus gas
✓ uses 49726 gas
✓ emits a Transfer and Withdrawal event
when another contributor adds 3.3 ETH
✓ uses 54992 gas
✓ increases the contract's balance by 3.3 ETH
✓ decrease the contributor's ETH balance by 3.3 ETH plus gas for the tx
✓ mints tokens for the contributor equal to the amount of ETH given
✓ grants them 3.3 ETH redeemable
✓ emits a Transfer and Contribution event
✓ totalSupply() is now 4100000000000000000
when the contributor attempts to withdraw 4 ETH from the contract
✓ reverts the transaction
when the contributor attempts to withdraw .2 ETH from their contributions
✓ burns their tokens, so that their token balance is 3100000000000000000
✓ decreases the contract's balance by 0.2 ETH
✓ increases the sender's balance by 0.2 ETH, minus gas
✓ uses 49726 gas
✓ emits a Transfer and Withdrawal event
✓ totalSupply() is now 3900000000000000000
and the operator attempts to accept a bid before closing funding
✓ reverts the transaction
when the operator closes funding
✓ mints 5 percent of tokens to the operator
✓ sets the status to TRADING
when a contribution is attempted after funding is closed
✓ reverts the transaction
and a bid exists on the market
and the operator attempts accepts the bid
✓ increases the contract's balance by the bid amount
when a contributor attempts to contribute more than the funding cap
✓ reverts the transaction
if a contributor sends ETH directly to the contract
✓ reverts the transaction
36 passing (29s)
@j-s
Copy link

j-s commented Jan 25, 2021

Beautiful ☼☽

@chejazi
Copy link

chejazi commented Jan 25, 2021

Nice! Couple things – 

  1. Is the crowdfunding contract the initial market, or the permanent market? Once the NFT has been sold, does the creator (the operator) choose how it gets resold? Or am I misunderstanding.

  2. A code nitpick:

operatorEquityPercent looks like it's an integer between 0 and 100 based on the way it's used Line 168:
totalSupply().div(100).mul(operatorEquityPercent);

I would change this to:
totalSupply().mul(operatorEquityPercent).div(100);

to avoid potential errors if you decide to allow greater decimal precision.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment