From bdd51d788916339a6b3a2fffb6b945eb187a42f5 Mon Sep 17 00:00:00 2001 From: Brian Le Date: Wed, 20 Mar 2024 14:32:30 -0400 Subject: [PATCH] WIP --- .gitmodules | 6 + contracts/crowdfund/ERC20LaunchCrowdfund.sol | 126 ++++++++++++++++++- contracts/crowdfund/ETHCrowdfundBase.sol | 2 +- contracts/tokens/GovernableERC20.sol | 35 ++++++ lib/forge-std | 2 +- lib/party-addresses | 2 +- lib/v2-core | 1 + lib/v2-periphery | 1 + remappings.txt | 6 +- 9 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 contracts/tokens/GovernableERC20.sol create mode 160000 lib/v2-core create mode 160000 lib/v2-periphery diff --git a/.gitmodules b/.gitmodules index dc951e5b4..1b2a2aa2b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,9 @@ [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 diff --git a/contracts/crowdfund/ERC20LaunchCrowdfund.sol b/contracts/crowdfund/ERC20LaunchCrowdfund.sol index e5276ebe3..9227ff244 100644 --- a/contracts/crowdfund/ERC20LaunchCrowdfund.sol +++ b/contracts/crowdfund/ERC20LaunchCrowdfund.sol @@ -9,9 +9,13 @@ import { LibSafeCast } from "../utils/LibSafeCast.sol"; import { Party, PartyGovernance } from "../party/Party.sol"; import { Crowdfund } from "../crowdfund/Crowdfund.sol"; import { MetadataProvider } from "../renderers/MetadataProvider.sol"; +import { GovernableERC20, ERC20 } from "../tokens/GovernableERC20.sol"; import { IGateKeeper } from "../gatekeepers/IGateKeeper.sol"; import { IGlobals } from "../globals/IGlobals.sol"; import { IERC721 } from "../tokens/IERC721.sol"; +import { ITokenDistributor, IERC20 } from "../distribution/ITokenDistributor.sol"; +import { IUniswapV2Router02 } from "uniswap-v2-periphery/interfaces/IUniswapV2Router02.sol"; +import { IUniswapV2Factory } from "uniswap-v2-core/interfaces/IUniswapV2Factory.sol"; /// @notice A crowdfund for raising the initial funds for new parties. /// Unlike other crowdfunds that are started for the purpose of @@ -62,6 +66,22 @@ contract ERC20LaunchCrowdfund is ETHCrowdfundBase { address[] authorities; } + // TODO: Add comments + // TODO: Pack storage? + struct ERC20LaunchOptions { + // The name of the ERC20 token launched. + string name; + // The symbol of the ERC20 token launched. + string symbol; + // The total supply to mint for the ERC20 token. + 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; + address recipient; + } + struct BatchContributeArgs { // IDs of cards to credit the contributions to. When set to 0, it means // a new one should be minted. @@ -97,9 +117,36 @@ contract ERC20LaunchCrowdfund is ETHCrowdfundBase { } event Refunded(address indexed contributor, uint256 indexed tokenId, uint256 amount); - - // Set the `Globals` contract. - constructor(IGlobals globals) ETHCrowdfundBase(globals) {} + event ERC20Created(ERC20 indexed token, Party indexed party, ERC20LaunchOptions opts); + + error InvalidTokenDistribution(); + + // TODO: Pack storage? + uint16 public immutable FEE_BPS; + address payable public immutable FEE_RECIPIENT; + ITokenDistributor public immutable TOKEN_DISTRIBUTOR; + IUniswapV2Router02 public immutable UNISWAP_V2_ROUTER; + IUniswapV2Factory public immutable UNISWAP_V2_FACTORY; + address public immutable WETH; + + ERC20LaunchOptions public tokenOpts; + + constructor( + IGlobals globals, + uint16 feeBps, + address payable feeRecipient, + ITokenDistributor tokenDistributor, + IUniswapV2Router02 uniswapV2Router, + IUniswapV2Factory uniswapV2Factory, + address weth + ) ETHCrowdfundBase(globals) { + FEE_BPS = feeBps; + FEE_RECIPIENT = feeRecipient; + TOKEN_DISTRIBUTOR = tokenDistributor; + UNISWAP_V2_ROUTER = uniswapV2Router; + UNISWAP_V2_FACTORY = uniswapV2Factory; + WETH = weth; + } /// @notice Initializer to be called prior to using the contract. /// @param crowdfundOpts Options to initialize the crowdfund with. @@ -110,9 +157,21 @@ contract ERC20LaunchCrowdfund is ETHCrowdfundBase { function initialize( InitialETHCrowdfundOptions memory crowdfundOpts, ETHPartyOptions memory partyOpts, + ERC20LaunchOptions memory _tokenOpts, MetadataProvider customMetadataProvider, bytes memory customMetadata ) external payable onlyInitialize { + if ( + _tokenOpts.numTokensForDistribution + + _tokenOpts.numTokensForRecipient + + _tokenOpts.numTokensForLP != + _tokenOpts.totalSupply + ) { + revert InvalidTokenDistribution(); + } + + tokenOpts = _tokenOpts; + // Create party the initial crowdfund will be for. Party party_ = _createParty(partyOpts, customMetadataProvider, customMetadata); @@ -456,4 +515,65 @@ contract ERC20LaunchCrowdfund is ETHCrowdfundBase { ); } } + + function _finalize(uint96 totalContributions_) internal override { + Party _party = party; + + // 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(); + + ERC20LaunchOptions memory _tokenOpts = tokenOpts; + + // Create token + ERC20 token = new GovernableERC20( + _tokenOpts.name, + _tokenOpts.symbol, + _tokenOpts.totalSupply, + address(this) + ); + + // Create distribution + token.transfer(address(TOKEN_DISTRIBUTOR), _tokenOpts.numTokensForDistribution); + TOKEN_DISTRIBUTOR.createErc20Distribution( + IERC20(address(token)), + _party, + payable(address(0)), + 0 + ); + + // Take fee + uint256 ethValue = msg.value; + uint256 feeAmount = (ethValue * FEE_BPS) / 1e4; + payable(FEE_RECIPIENT).transfer(feeAmount); + + // Create locked LP pair + uint256 numETHForLP = ethValue - feeAmount; + token.approve(address(UNISWAP_V2_ROUTER), _tokenOpts.numTokensForLP); + UNISWAP_V2_ROUTER.addLiquidityETH{ value: numETHForLP }( + address(token), + _tokenOpts.numTokensForLP, + _tokenOpts.numTokensForLP, + numETHForLP, + address(0), // Burn LP position + block.timestamp + 10 minutes + ); + + // Transfer tokens to recipient + token.transfer(_tokenOpts.recipient, _tokenOpts.numTokensForRecipient); + + emit ERC20Created(token, _party, _tokenOpts); + } } diff --git a/contracts/crowdfund/ETHCrowdfundBase.sol b/contracts/crowdfund/ETHCrowdfundBase.sol index 78a1bf71c..8e4dc84c2 100644 --- a/contracts/crowdfund/ETHCrowdfundBase.sol +++ b/contracts/crowdfund/ETHCrowdfundBase.sol @@ -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(); } diff --git a/contracts/tokens/GovernableERC20.sol b/contracts/tokens/GovernableERC20.sol new file mode 100644 index 000000000..7f65fad9e --- /dev/null +++ b/contracts/tokens/GovernableERC20.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Votes.sol"; + +contract GovernableERC20 is ERC20, ERC20Permit, ERC20Votes { + constructor( + string memory _name, + string memory _symbol, + uint256 _totalSupply, + address _receiver + ) ERC20(_name, _symbol) ERC20Permit(_name) { + _mint(_receiver, _totalSupply); + } + + // Default to self-delegation if no delegate is set. This enables snapshots + // to work as expected, otherwise when transferring votes to undelegated addresses + // the votes would not be moved (see `Votes._moveDelegateVotes`). + function delegates(address account) public view override returns (address) { + address delegate = super.delegates(account); + return delegate == address(0) ? account : delegate; + } + + // The following functions are overrides required by Solidity. + + function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) { + super._update(from, to, value); + } + + function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/lib/forge-std b/lib/forge-std index 2f43c7e69..2f6762e4f 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 2f43c7e69b820910e9d4f3b8cc8d3b4e6382786e +Subproject commit 2f6762e4f73f3d835457c220b5f62dfeeb6f6341 diff --git a/lib/party-addresses b/lib/party-addresses index 9e3ee3c4f..eeab8a7c9 160000 --- a/lib/party-addresses +++ b/lib/party-addresses @@ -1 +1 @@ -Subproject commit 9e3ee3c4fef00420388e37bbb98167cc6d4fd497 +Subproject commit eeab8a7c92e24e9d985a835d2b24f599d0465968 diff --git a/lib/v2-core b/lib/v2-core new file mode 160000 index 000000000..4dd59067c --- /dev/null +++ b/lib/v2-core @@ -0,0 +1 @@ +Subproject commit 4dd59067c76dea4a0e8e4bfdda41877a6b16dedc diff --git a/lib/v2-periphery b/lib/v2-periphery new file mode 160000 index 000000000..0335e8f7e --- /dev/null +++ b/lib/v2-periphery @@ -0,0 +1 @@ +Subproject commit 0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f diff --git a/remappings.txt b/remappings.txt index 8df44c43e..60096651c 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,7 @@ forge-std/=lib/forge-std/src/ openzeppelin/=lib/openzeppelin-contracts/ -solmate/=lib/solmate/src/ \ No newline at end of file +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/ \ No newline at end of file