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

feat: ERC20 crowdfunds #381

Merged
merged 15 commits into from
Mar 26, 2024
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@
[submodule "lib/party-addresses"]
path = lib/party-addresses
url = https://github.com/PartyDAO/party-addresses
[submodule "lib/v2-periphery"]
path = lib/v2-periphery
url = https://github.com/Uniswap/v2-periphery
[submodule "lib/v2-core"]
path = lib/v2-core
url = https://github.com/Uniswap/v2-core
[submodule "lib/erc20-creator"]
path = lib/erc20-creator
url = https://github.com/PartyDAO/erc20-creator
60 changes: 60 additions & 0 deletions contracts/crowdfund/CrowdfundFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CollectionBuyCrowdfund } from "./CollectionBuyCrowdfund.sol";
import { RollingAuctionCrowdfund } from "./RollingAuctionCrowdfund.sol";
import { CollectionBatchBuyCrowdfund } from "./CollectionBatchBuyCrowdfund.sol";
import { InitialETHCrowdfund, ETHCrowdfundBase } from "./InitialETHCrowdfund.sol";
import { ERC20LaunchCrowdfund } from "./ERC20LaunchCrowdfund.sol";
import { MetadataProvider } from "../renderers/MetadataProvider.sol";
import { Party } from "../party/Party.sol";

Expand Down Expand Up @@ -53,6 +54,14 @@ contract CrowdfundFactory {
InitialETHCrowdfund.InitialETHCrowdfundOptions crowdfundOpts,
InitialETHCrowdfund.ETHPartyOptions partyOpts
);
event ERC20LaunchCrowdfundCreated(
address indexed creator,
ERC20LaunchCrowdfund indexed crowdfund,
Party indexed party,
ERC20LaunchCrowdfund.InitialETHCrowdfundOptions crowdfundOpts,
ERC20LaunchCrowdfund.ETHPartyOptions partyOpts,
ERC20LaunchCrowdfund.ERC20LaunchOptions tokenOpts
);

/// @notice Create a new crowdfund to purchase a specific NFT (i.e., with a
/// known token ID) listing for a known price.
Expand Down Expand Up @@ -199,6 +208,57 @@ contract CrowdfundFactory {
emit InitialETHCrowdfundCreated(msg.sender, inst, inst.party(), crowdfundOpts, partyOpts);
}

function createERC20LaunchCrowdfund(
ERC20LaunchCrowdfund crowdfundImpl,
ERC20LaunchCrowdfund.InitialETHCrowdfundOptions memory crowdfundOpts,
ERC20LaunchCrowdfund.ETHPartyOptions memory partyOpts,
ERC20LaunchCrowdfund.ERC20LaunchOptions memory tokenOpts,
bytes memory createGateCallData
) external payable returns (ERC20LaunchCrowdfund inst) {
return
createERC20LaunchCrowdfundWithMetadata(
crowdfundImpl,
crowdfundOpts,
partyOpts,
tokenOpts,
MetadataProvider(address(0)),
"",
createGateCallData
);
}

function createERC20LaunchCrowdfundWithMetadata(
ERC20LaunchCrowdfund crowdfundImpl,
ERC20LaunchCrowdfund.InitialETHCrowdfundOptions memory crowdfundOpts,
ERC20LaunchCrowdfund.ETHPartyOptions memory partyOpts,
ERC20LaunchCrowdfund.ERC20LaunchOptions memory tokenOpts,
MetadataProvider customMetadataProvider,
bytes memory customMetadata,
bytes memory createGateCallData
) public payable returns (ERC20LaunchCrowdfund inst) {
crowdfundOpts.gateKeeperId = _prepareGate(
crowdfundOpts.gateKeeper,
crowdfundOpts.gateKeeperId,
createGateCallData
);
inst = ERC20LaunchCrowdfund(address(crowdfundImpl).clone());
inst.initialize{ value: msg.value }(
crowdfundOpts,
partyOpts,
tokenOpts,
customMetadataProvider,
customMetadata
);
emit ERC20LaunchCrowdfundCreated(
msg.sender,
inst,
inst.party(),
crowdfundOpts,
partyOpts,
tokenOpts
);
}

function _prepareGate(
IGateKeeper gateKeeper,
bytes12 gateKeeperId,
Expand Down
138 changes: 138 additions & 0 deletions contracts/crowdfund/ERC20LaunchCrowdfund.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;

import { InitialETHCrowdfund } from "./InitialETHCrowdfund.sol";
import { Party } from "../party/Party.sol";
import { MetadataProvider } from "../renderers/MetadataProvider.sol";
import { IGlobals } from "../globals/IGlobals.sol";
import { IERC20Creator, TokenConfiguration, ERC20 } from "../utils/IERC20Creator.sol";

/// @notice A crowdfund for launching ERC20 tokens.
/// Unlike other crowdfunds that are started for the purpose of
/// acquiring NFT(s), this crowdfund bootstraps an ERC20 token
/// and sends a share of the total supply to the new party.
contract ERC20LaunchCrowdfund is InitialETHCrowdfund {
struct ERC20LaunchOptions {
// The name of the ERC20 token launched.
string name;
// The symbol of the ERC20 token launched.
string symbol;
// An arbitrary address to receive ERC20 tokens.
address recipient;
// The total supply to mint for the ERC20 token.
uint256 totalSupply;
// The number of tokens to distribute to the party.
uint256 numTokensForDistribution;
// The number of tokens to send to an arbitrary recipient.
uint256 numTokensForRecipient;
// The number of tokens to use for the Uniswap LP pair.
uint256 numTokensForLP;
}

error InvalidTokenDistribution();
error TokenAlreadyLaunched();

IERC20Creator public immutable ERC20_CREATOR;

ERC20LaunchOptions public tokenOpts;

bool public isTokenLaunched;

constructor(IGlobals globals, IERC20Creator erc20Creator) InitialETHCrowdfund(globals) {
ERC20_CREATOR = erc20Creator;
}

/// @notice Initializer to be called prior to using the contract.
/// @param crowdfundOpts Options to initialize the crowdfund with.
/// @param partyOpts Options to initialize the party with.
/// @param customMetadataProvider Optional provider to use for the party for
/// rendering custom metadata.
/// @param customMetadata Optional custom metadata to use for the party.
function initialize(
InitialETHCrowdfundOptions memory crowdfundOpts,
ETHPartyOptions memory partyOpts,
ERC20LaunchOptions memory _tokenOpts,
MetadataProvider customMetadataProvider,
bytes memory customMetadata
) external payable {
if (
_tokenOpts.numTokensForDistribution +
_tokenOpts.numTokensForRecipient +
_tokenOpts.numTokensForLP !=
_tokenOpts.totalSupply ||
_tokenOpts.totalSupply > type(uint112).max ||
_tokenOpts.numTokensForLP < 1e4 ||
crowdfundOpts.fundingSplitBps > 5e3 ||
crowdfundOpts.minTotalContributions < 1e4
) {
revert InvalidTokenDistribution();
}

tokenOpts = _tokenOpts;

InitialETHCrowdfund.initialize(
crowdfundOpts,
partyOpts,
customMetadataProvider,
customMetadata
);
}

/// @notice Launch the ERC20 token for the Party.
function launchToken() public returns (ERC20 token) {
if (isTokenLaunched) revert TokenAlreadyLaunched();

CrowdfundLifecycle lc = getCrowdfundLifecycle();
if (lc != CrowdfundLifecycle.Finalized) revert WrongLifecycleError(lc);

isTokenLaunched = true;

// Update the party's total voting power
uint96 totalContributions_ = totalContributions;

uint16 fundingSplitBps_ = fundingSplitBps;
if (fundingSplitBps_ > 0) {
// Assuming fundingSplitBps_ <= 1e4, this cannot overflow uint96
totalContributions_ -= uint96((uint256(totalContributions_) * fundingSplitBps_) / 1e4);
}

// Create the ERC20 token.
ERC20LaunchOptions memory _tokenOpts = tokenOpts;
token = ERC20_CREATOR.createToken{ value: totalContributions_ }(
address(party),
_tokenOpts.name,
_tokenOpts.symbol,
TokenConfiguration({
totalSupply: _tokenOpts.totalSupply,
numTokensForDistribution: _tokenOpts.numTokensForDistribution,
numTokensForRecipient: _tokenOpts.numTokensForRecipient,
numTokensForLP: _tokenOpts.numTokensForLP
}),
_tokenOpts.recipient
);
}

/// @notice Finalize the crowdfund and launch the ERC20 token.
function finalize() public override {
super.finalize();
launchToken();
}

function _finalize(uint96 totalContributions_) internal override {
// Finalize the crowdfund.
delete expiry;

// Transfer funding split to recipient if applicable.
uint16 fundingSplitBps_ = fundingSplitBps;
if (fundingSplitBps_ > 0) {
// Assuming fundingSplitBps_ <= 1e4, this cannot overflow uint96
totalContributions_ -= uint96((uint256(totalContributions_) * fundingSplitBps_) / 1e4);
}

// Update the party's total voting power.
uint96 newVotingPower = _calculateContributionToVotingPower(totalContributions_);
party.increaseTotalVotingPower(newVotingPower);

emit Finalized();
}
}
7 changes: 4 additions & 3 deletions contracts/crowdfund/ETHCrowdfundBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ abstract contract ETHCrowdfundBase is Implementation {

function _calculateContributionToVotingPower(
uint96 contribution
) private view returns (uint96) {
) internal view returns (uint96) {
return contribution.mulDivDown(exchangeRate, 1e18).safeCastUint256ToUint96();
}

Expand Down Expand Up @@ -325,7 +325,8 @@ abstract contract ETHCrowdfundBase is Implementation {
return contribution;
}

function finalize() external {
/// @notice Finalize the crowdfund and transfer the funds to the Party.
function finalize() public virtual {
uint96 totalContributions_ = totalContributions;

// Check that the crowdfund is not already finalized.
Expand All @@ -351,7 +352,7 @@ abstract contract ETHCrowdfundBase is Implementation {
_finalize(totalContributions_);
}

function _finalize(uint96 totalContributions_) internal {
function _finalize(uint96 totalContributions_) internal virtual {
// Finalize the crowdfund.
delete expiry;

Expand Down
2 changes: 1 addition & 1 deletion contracts/crowdfund/InitialETHCrowdfund.sol
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ contract InitialETHCrowdfund is ETHCrowdfundBase {
ETHPartyOptions memory partyOpts,
MetadataProvider customMetadataProvider,
bytes memory customMetadata
) external payable onlyInitialize {
) public payable onlyInitialize {
// Create party the initial crowdfund will be for.
Party party_ = _createParty(partyOpts, customMetadataProvider, customMetadata);

Expand Down
21 changes: 21 additions & 0 deletions contracts/utils/IERC20Creator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;

import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

struct TokenConfiguration {
uint256 totalSupply;
uint256 numTokensForDistribution;
uint256 numTokensForRecipient;
uint256 numTokensForLP;
}

interface IERC20Creator {
function createToken(
address partyAddress,
string calldata name,
string calldata symbol,
TokenConfiguration calldata config,
address recipientAddress
) external payable returns (ERC20 token);
}
1 change: 1 addition & 0 deletions lib/erc20-creator
Submodule erc20-creator added at 9370b7
2 changes: 1 addition & 1 deletion lib/party-addresses
Submodule party-addresses updated 310 files
1 change: 1 addition & 0 deletions lib/v2-core
Submodule v2-core added at 4dd590
1 change: 1 addition & 0 deletions lib/v2-periphery
Submodule v2-periphery added at 0335e8
7 changes: 6 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
forge-std/=lib/forge-std/src/
openzeppelin/=lib/openzeppelin-contracts/
solmate/=lib/solmate/src/
solmate/=lib/solmate/src/
ds-test/=lib/forge-std/lib/ds-test/src/
party-addresses/=lib/party-addresses/
uniswap-v2-core/=lib/v2-core/contracts/
uniswap-v2-periphery/=lib/v2-periphery/contracts/
erc20-creator/=lib/erc20-creator/src
Loading
Loading