From 824538633091ebe97c0a0f38c9a28f09900fe173 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:45:35 -0500 Subject: [PATCH] feat: Dynamic Party Card Prices (#360) Co-authored-by: Brian Le --- .github/workflows/ci.yml | 7 +- .../authorities/BondingCurveAuthority.sol | 579 +++++++++ contracts/party/PartyGovernance.sol | 13 +- contracts/party/PartyGovernanceNFT.sol | 8 +- .../proposals/ProposalExecutionEngine.sol | 5 +- contracts/proposals/ProposalStorage.sol | 10 +- deploy/Deploy.s.sol | 20 + package.json | 26 +- test/authorities/BondingCurveAuthority.t.sol | 1156 +++++++++++++++++ test/crowdfund/CrowdfundFactory.t.sol | 19 +- test/crowdfund/InitialETHCrowdfund.t.sol | 2 +- test/party/PartyFactory.t.sol | 14 +- test/party/PartyGovernanceNFT.t.sol | 36 +- test/party/PartyGovernanceUnit.t.sol | 4 +- test/utils/SetupPartyHelper.sol | 10 +- utils/output-abis.ts | 1 + 16 files changed, 1844 insertions(+), 66 deletions(-) create mode 100644 contracts/authorities/BondingCurveAuthority.sol create mode 100644 test/authorities/BondingCurveAuthority.t.sol diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6ed1db01..8c2e28117 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,9 @@ jobs: run: yarn coverage - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true lint: runs-on: ubuntu-latest steps: @@ -112,6 +115,4 @@ jobs: - name: Install dependencies run: forge install - name: Check contract deployable mainnet - run: "node js/contracts-deployable.js --via-ir --optimize --optimizer-runs 0" - - name: Check contracts deployable base - run: "node js/contracts-deployable.js --via-ir --optimize --optimizer-runs 0 --evm-version paris" + run: "node js/contracts-deployable.js --via-ir --optimize --optimizer-runs 50" diff --git a/contracts/authorities/BondingCurveAuthority.sol b/contracts/authorities/BondingCurveAuthority.sol new file mode 100644 index 000000000..35bad9ac8 --- /dev/null +++ b/contracts/authorities/BondingCurveAuthority.sol @@ -0,0 +1,579 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.20; + +import { Party } from "../party/Party.sol"; +import { PartyFactory } from "../party/PartyFactory.sol"; +import { IERC721 } from "../tokens/IERC721.sol"; +import { MetadataProvider } from "../renderers/MetadataProvider.sol"; +import { LibSafeCast } from "contracts/utils/LibSafeCast.sol"; +import { ProposalStorage } from "contracts/proposals/ProposalStorage.sol"; + +contract BondingCurveAuthority { + using LibSafeCast for uint256; + + error InvalidMessageValue(); + error Unauthorized(); + error InvalidCreatorFee(); + error InvalidTreasuryFee(); + error InvalidPartyDaoFee(); + error PartyNotSupported(); + error ExistingParty(); + error InvalidTotalVotingPower(); + error ExecutionDelayTooShort(); + error EthTransferFailed(); + error ExcessSlippage(); + error AddAuthorityProposalNotSupported(); + error SellZeroPartyCards(); + error DistributionsNotSupported(); + error NeedAtLeastOneHost(); + + event BondingCurvePartyCreated( + Party indexed party, + address indexed creator, + BondingCurvePartyOptions partyOpts + ); + event TreasuryFeeUpdated(uint16 previousTreasuryFee, uint16 newTreasuryFee); + event PartyDaoFeeUpdated(uint16 previousPartyDaoFee, uint16 newPartyDaoFee); + event CreatorFeeUpdated(uint16 previousCreatorFee, uint16 newCreatorFee); + event PartyDaoFeesClaimed(uint96 amount); + event PartyCardsBought( + Party indexed party, + address indexed buyer, + uint256[] tokenIds, + uint256 totalPrice, + uint256 lastBondingCurvePrice, + uint256 partyDaoFee, + uint256 treasuryFee, + uint256 creatorFee + ); + event PartyCardsSold( + Party indexed party, + address indexed seller, + uint256[] tokenIds, + uint256 sellerProceeds, + uint256 lastBondingCurvePrice, + uint256 partyDaoFee, + uint256 treasuryFee, + uint256 creatorFee + ); + + /// @notice Info for each party controlled by this authority + mapping(Party => PartyInfo) public partyInfos; + /// @notice The global party dao fee basis points + uint16 public partyDaoFeeBps; + /// @notice The global treasury fee basis points + uint16 public treasuryFeeBps; + /// @notice The global creator fee basis points + uint16 public creatorFeeBps; + /// @notice The amount of party dao fees claimable + uint96 public partyDaoFeeClaimable; + + /// @notice The intrinsic voting power of party cards minted by this contract + uint96 private constant PARTY_CARD_VOTING_POWER = uint96(0.1 ether); + address payable private immutable PARTY_DAO; + uint16 private constant BPS = 10_000; + uint16 private constant MAX_CREATOR_FEE = 250; // 2.5% + uint16 private constant MAX_TREASURY_FEE = 1000; // 10% + uint16 private constant MAX_PARTY_DAO_FEE = 250; // 2.5% + /// @notice The minimum execution delay for party governance + uint40 private constant MIN_EXECUTION_DELAY = 1 hours; + + /// @notice Struct containing options for creating a party + struct BondingCurvePartyOptions { + // The party factory address to use + PartyFactory partyFactory; + // The party implementation address to use + Party partyImpl; + // Options for the party. See `Party.sol` for more info + Party.PartyOptions opts; + // boolean specifying if creator fees are collected + bool creatorFeeOn; + // The value of a in the bonding curve formula 1 ether * x ** 2 / a + b + // used by the Party to price cards + uint32 a; + // The value of b in the bonding curve formula 1 ether * x ** 2 / a + b + // used by the Party to price cards + uint80 b; + } + + /// @notice Struct containing info stored for a party + struct PartyInfo { + // The original creator of the party + address payable creator; + // The supply of party cards tracked by this contract + uint80 supply; + // boolean specifying if creator fees are collected + bool creatorFeeOn; + // The value of a in the bonding curve formula 1 ether * x ** 2 / a + b + // used by the Party to price cards + uint32 a; + // The value of b in the bonding curve formula 1 ether * x ** 2 / a + b + // used by the Party to price cards + uint80 b; + } + + modifier onlyPartyDao() { + if (msg.sender != PARTY_DAO) { + revert Unauthorized(); + } + _; + } + + constructor( + address payable partyDao, + uint16 initialPartyDaoFeeBps, + uint16 initialTreasuryFeeBps, + uint16 initialCreatorFeeBps + ) { + if (initialPartyDaoFeeBps > MAX_PARTY_DAO_FEE) { + revert InvalidPartyDaoFee(); + } + if (initialTreasuryFeeBps > MAX_TREASURY_FEE) { + revert InvalidTreasuryFee(); + } + if (initialCreatorFeeBps > MAX_CREATOR_FEE) { + revert InvalidCreatorFee(); + } + partyDaoFeeBps = initialPartyDaoFeeBps; + treasuryFeeBps = initialTreasuryFeeBps; + creatorFeeBps = initialCreatorFeeBps; + PARTY_DAO = partyDao; + } + + /** + * @notice Create a new party that will have a dynamic price + * @param partyOpts options specified for creating the party + * @param amountToBuy The amount of party cards the creator buys initially + * @return party The address of the newly created party + */ + function createParty( + BondingCurvePartyOptions memory partyOpts, + uint80 amountToBuy + ) external payable returns (Party party) { + address[] memory authorities = new address[](1); + authorities[0] = address(this); + + _validateGovernanceOpts(partyOpts.opts); + + party = partyOpts.partyFactory.createParty( + partyOpts.partyImpl, + authorities, + partyOpts.opts, + new IERC721[](0), + new uint256[](0), + 0 + ); + + if (partyInfos[party].creator != address(0)) { + revert ExistingParty(); + } + + partyInfos[party] = PartyInfo({ + creator: payable(msg.sender), + supply: 0, + creatorFeeOn: partyOpts.creatorFeeOn, + a: partyOpts.a, + b: partyOpts.b + }); + + emit BondingCurvePartyCreated(party, msg.sender, partyOpts); + + buyPartyCards(party, amountToBuy, address(0)); + } + + /** + * @notice Create a new party with metadata that will have a dynamic price + * @param partyOpts options specified for creating the party + * @param customMetadataProvider the metadata provider to use for the party + * @param customMetadata the metadata to use for the party + * @param amountToBuy The amount of party cards the creator buys initially + * @return party The address of the newly created party + */ + function createPartyWithMetadata( + BondingCurvePartyOptions memory partyOpts, + MetadataProvider customMetadataProvider, + bytes memory customMetadata, + uint80 amountToBuy + ) external payable returns (Party party) { + address[] memory authorities = new address[](1); + authorities[0] = address(this); + + _validateGovernanceOpts(partyOpts.opts); + + party = partyOpts.partyFactory.createPartyWithMetadata( + partyOpts.partyImpl, + authorities, + partyOpts.opts, + new IERC721[](0), + new uint256[](0), + 0, + customMetadataProvider, + customMetadata + ); + + if (partyInfos[party].creator != address(0)) { + revert ExistingParty(); + } + + partyInfos[party] = PartyInfo({ + creator: payable(msg.sender), + supply: 0, + creatorFeeOn: partyOpts.creatorFeeOn, + a: partyOpts.a, + b: partyOpts.b + }); + + emit BondingCurvePartyCreated(party, msg.sender, partyOpts); + + buyPartyCards(party, amountToBuy, address(0)); + } + + function _validateGovernanceOpts(Party.PartyOptions memory partyOpts) internal pure { + if (partyOpts.governance.totalVotingPower != 0) { + revert InvalidTotalVotingPower(); + } + // Note: while the `executionDelay` is not enforced to be over 1 hour, + // it is strongly recommended for it to be a long period + // (greater than 1 day). This prevents an attacker from buying cards, + // draining the party and then selling before a host can react. + if (partyOpts.governance.executionDelay < MIN_EXECUTION_DELAY) { + revert ExecutionDelayTooShort(); + } + + if (partyOpts.proposalEngine.enableAddAuthorityProposal) { + revert AddAuthorityProposalNotSupported(); + } + + if ( + partyOpts.proposalEngine.distributionsConfig != + ProposalStorage.DistributionsConfig.NotAllowed + ) { + revert DistributionsNotSupported(); + } + + if (partyOpts.governance.hosts.length == 0) { + revert NeedAtLeastOneHost(); + } + } + + /** + * @notice Buy party cards from the bonding curve + * @param party The party to buy cards for + * @param amount The amount of cards to buy + * @param initialDelegate The initial delegate for governance + * @return tokenIds The token ids of the party cards that were bought + */ + function buyPartyCards( + Party party, + uint80 amount, + address initialDelegate + ) public payable returns (uint256[] memory tokenIds) { + PartyInfo memory partyInfo = partyInfos[party]; + + if (partyInfo.creator == address(0)) { + revert PartyNotSupported(); + } + + uint256 bondingCurvePrice = _getBondingCurvePrice( + partyInfo.supply, + amount, + partyInfo.a, + partyInfo.b + ); + uint256 partyDaoFee = (bondingCurvePrice * partyDaoFeeBps) / BPS; + uint256 treasuryFee = (bondingCurvePrice * treasuryFeeBps) / BPS; + uint256 creatorFee = (bondingCurvePrice * (partyInfo.creatorFeeOn ? creatorFeeBps : 0)) / + BPS; + // Note: 1 is added for each NFT to account for rounding errors + uint256 totalCost = bondingCurvePrice + partyDaoFee + treasuryFee + creatorFee + amount; + + partyInfos[party].supply = partyInfo.supply + amount; + + (bool success, ) = address(party).call{ value: treasuryFee }(""); + if (!success) { + revert EthTransferFailed(); + } + + if (creatorFee != 0) { + // Creator fee payment can fail + // Gas limit is set to 100k to prevent consuming all gas + (bool creatorFeeSucceeded, ) = partyInfo.creator.call{ + value: creatorFee, + gas: 100_000 + }(""); + if (!creatorFeeSucceeded) { + totalCost -= creatorFee; + } + } + + if (amount == 0 || msg.value < totalCost) { + revert InvalidMessageValue(); + } + + partyDaoFeeClaimable += partyDaoFee.safeCastUint256ToUint96(); + party.increaseTotalVotingPower(PARTY_CARD_VOTING_POWER * amount); + tokenIds = new uint256[](amount); + for (uint256 i = 0; i < amount; i++) { + tokenIds[i] = party.mint(msg.sender, PARTY_CARD_VOTING_POWER, initialDelegate); + } + + uint256 lastBondingCurvePrice = _getBondingCurvePrice( + partyInfo.supply + amount - 1, + 1, + partyInfo.a, + partyInfo.b + ); + + emit PartyCardsBought( + party, + msg.sender, + tokenIds, + totalCost, + lastBondingCurvePrice, + partyDaoFee, + treasuryFee, + creatorFee + ); + + // Refund excess ETH + if (msg.value > totalCost) { + (success, ) = msg.sender.call{ value: msg.value - totalCost }(""); + if (!success) { + revert EthTransferFailed(); + } + } + } + + /** + * @notice Sell party cards to the bonding curve + * @param party The party to sell cards for + * @param tokenIds The token ids to sell + */ + function sellPartyCards(Party party, uint256[] memory tokenIds, uint256 minProceeds) external { + if (tokenIds.length == 0) { + revert SellZeroPartyCards(); + } + + PartyInfo memory partyInfo = partyInfos[party]; + + if (partyInfo.creator == address(0)) { + revert PartyNotSupported(); + } + + uint80 amount = uint80(tokenIds.length); + uint256 bondingCurvePrice = _getBondingCurvePrice( + partyInfo.supply - amount, + amount, + partyInfo.a, + partyInfo.b + ); + uint256 partyDaoFee = (bondingCurvePrice * partyDaoFeeBps) / BPS; + uint256 treasuryFee = (bondingCurvePrice * treasuryFeeBps) / BPS; + uint256 creatorFee = (bondingCurvePrice * (partyInfo.creatorFeeOn ? creatorFeeBps : 0)) / + BPS; + uint256 sellerProceeds = bondingCurvePrice - partyDaoFee - treasuryFee - creatorFee; + + partyInfos[party].supply = partyInfo.supply - amount; + + for (uint256 i = 0; i < amount; i++) { + address tokenOwner = party.ownerOf(tokenIds[i]); + if ( + tokenOwner != msg.sender && + party.isApprovedForAll(tokenOwner, msg.sender) != true && + party.getApproved(tokenIds[i]) != msg.sender + ) { + revert Unauthorized(); + } + party.burn(tokenIds[i]); + } + party.decreaseTotalVotingPower(PARTY_CARD_VOTING_POWER * amount); + + (bool success, ) = address(party).call{ value: treasuryFee }(""); + if (!success) { + revert EthTransferFailed(); + } + + if (creatorFee != 0) { + // Creator fee payment can fail + // Gas limit is set to 100k to prevent consuming all gas + (bool creatorFeeSucceeded, ) = partyInfo.creator.call{ + value: creatorFee, + gas: 100_000 + }(""); + if (!creatorFeeSucceeded) { + sellerProceeds += creatorFee; + } + } + + if (sellerProceeds < minProceeds) { + revert ExcessSlippage(); + } + + (success, ) = msg.sender.call{ value: sellerProceeds }(""); + if (!success) { + revert EthTransferFailed(); + } + + partyDaoFeeClaimable += partyDaoFee.safeCastUint256ToUint96(); + + uint256 lastBondingCurvePrice = _getBondingCurvePrice( + partyInfo.supply - amount, + 1, + partyInfo.a, + partyInfo.b + ); + + emit PartyCardsSold( + party, + msg.sender, + tokenIds, + sellerProceeds, + lastBondingCurvePrice, + partyDaoFee, + treasuryFee, + creatorFee + ); + } + + /** + * @notice Get the sale proceeds for a given amount of cards + * @param party The party to get the sale proceeds for + * @param amount The amount of cards that would be sold + * @return The sale proceeds for the given amount of cards that would be sent to the seller + */ + function getSaleProceeds(Party party, uint256 amount) external view returns (uint256) { + PartyInfo memory partyInfo = partyInfos[party]; + uint256 bondingCurvePrice = _getBondingCurvePrice( + partyInfo.supply - amount, + amount, + partyInfo.a, + partyInfo.b + ); + uint256 partyDaoFee = (bondingCurvePrice * partyDaoFeeBps) / BPS; + uint256 treasuryFee = (bondingCurvePrice * treasuryFeeBps) / BPS; + uint256 creatorFee = (bondingCurvePrice * (partyInfo.creatorFeeOn ? creatorFeeBps : 0)) / + BPS; + return bondingCurvePrice - partyDaoFee - treasuryFee - creatorFee; + } + + /** + * @notice Get the price to buy a given amount of cards + * @param party The party to get the price for + * @param amount The amount of cards that would be bought + * @return The price to buy the given amount of cards + */ + function getPriceToBuy(Party party, uint80 amount) external view returns (uint256) { + PartyInfo memory partyInfo = partyInfos[party]; + return + getPriceToBuy( + partyInfo.supply, + amount, + partyInfo.a, + partyInfo.b, + partyInfo.creatorFeeOn + ); + } + + /** + * @notice Get the price to buy a given amount of cards + * @param supply The current supply of the party + * @param amount The amount of cards that would be bought + * @param a The value of a in the bonding curve formula 1 ether * x ** 2 / a + b + * @param b The value of b in the bonding curve formula 1 ether * x ** 2 / a + b + * @param creatorFeeOn boolean specifying if creator fees are collected + * @return The price to buy the given amount of cards + */ + function getPriceToBuy( + uint80 supply, + uint80 amount, + uint32 a, + uint80 b, + bool creatorFeeOn + ) public view returns (uint256) { + uint256 bondingCurvePrice = _getBondingCurvePrice(supply, amount, a, b); + uint256 partyDaoFee = (bondingCurvePrice * partyDaoFeeBps) / BPS; + uint256 treasuryFee = (bondingCurvePrice * treasuryFeeBps) / BPS; + uint256 creatorFee = (bondingCurvePrice * (creatorFeeOn ? creatorFeeBps : 0)) / BPS; + // Note: 1 is added for each NFT to account for rounding errors + return bondingCurvePrice + partyDaoFee + treasuryFee + creatorFee + amount; + } + + /** + * @notice Returns the bonding curve price for a given amount of cards + * for a given lower supply. + * @param lowerSupply The lower supply of either the start supply or end supply + * For example: if burning, this would be the supply after burning. + * @param amount The number of cards to calculate the price for + * @return The bonding curve price for these cards + */ + function _getBondingCurvePrice( + uint256 lowerSupply, + uint256 amount, + uint32 a, + uint80 b + ) internal pure returns (uint256) { + // Using the function 1 ether * x ** 2 / a + b + uint256 amountSquared = amount * amount; + return + (1 ether * + (amount * + lowerSupply * + lowerSupply + + (amountSquared - amount) * + lowerSupply + + (2 * amountSquared * amount + amount - 3 * amountSquared) / + 6)) / + uint256(a) + + amount * + uint256(b); + } + + /** + * @notice Set the treasury fee. Only callable by party dao. + * @param newTreasuryFeeBps The new treasury fee + */ + function setTreasuryFee(uint16 newTreasuryFeeBps) external onlyPartyDao { + if (newTreasuryFeeBps > MAX_TREASURY_FEE) { + revert InvalidTreasuryFee(); + } + emit TreasuryFeeUpdated(treasuryFeeBps, newTreasuryFeeBps); + treasuryFeeBps = newTreasuryFeeBps; + } + + /** + * @notice Set the party dao fee. Only callable by party dao. + * @param newPartyDaoFeeBps The new party dao fee + */ + function setPartyDaoFee(uint16 newPartyDaoFeeBps) external onlyPartyDao { + if (newPartyDaoFeeBps > MAX_PARTY_DAO_FEE) { + revert InvalidPartyDaoFee(); + } + emit PartyDaoFeeUpdated(partyDaoFeeBps, newPartyDaoFeeBps); + partyDaoFeeBps = newPartyDaoFeeBps; + } + + /** + * @notice Set the creator fee for all parties. Can only be called by party dao. + * @param newCreatorFeeBps The new creator fee + */ + function setCreatorFee(uint16 newCreatorFeeBps) external onlyPartyDao { + if (newCreatorFeeBps > MAX_CREATOR_FEE) { + revert InvalidCreatorFee(); + } + emit CreatorFeeUpdated(creatorFeeBps, newCreatorFeeBps); + creatorFeeBps = newCreatorFeeBps; + } + + /** + * @notice Claim the party dao fees. Only callable by party dao. + */ + function claimPartyDaoFees() external onlyPartyDao { + uint96 _partyDaoFeeClaimable = partyDaoFeeClaimable; + partyDaoFeeClaimable = 0; + (bool success, ) = PARTY_DAO.call{ value: _partyDaoFeeClaimable }(""); + if (!success) { + revert EthTransferFailed(); + } + emit PartyDaoFeesClaimed(_partyDaoFeeClaimable); + } +} diff --git a/contracts/party/PartyGovernance.sol b/contracts/party/PartyGovernance.sol index b6a74c61d..79b842fbe 100644 --- a/contracts/party/PartyGovernance.sol +++ b/contracts/party/PartyGovernance.sol @@ -498,7 +498,10 @@ abstract contract PartyGovernance is // Must not require a vote to create a distribution, otherwise // distributions can only be created through a distribution // proposal. - if (_getSharedProposalStorage().opts.distributionsRequireVote) { + if ( + _getSharedProposalStorage().opts.distributionsConfig != + DistributionsConfig.AllowedWithoutVote + ) { revert DistributionsRequireVoteError(); } // Must be an active member. @@ -1117,14 +1120,6 @@ abstract contract PartyGovernance is return snapshotNumHosts > 0 && snapshotNumHosts == numHostsAccepted; } - function _areVotesPassing( - uint96 voteCount, - uint96 totalVotingPower, - uint16 passThresholdBps - ) private pure returns (bool) { - return (uint256(voteCount) * 1e4) / uint256(totalVotingPower) >= uint256(passThresholdBps); - } - function _setPreciousList( IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds diff --git a/contracts/party/PartyGovernanceNFT.sol b/contracts/party/PartyGovernanceNFT.sol index 4bbbbdfb8..fd6553181 100644 --- a/contracts/party/PartyGovernanceNFT.sol +++ b/contracts/party/PartyGovernanceNFT.sol @@ -99,7 +99,7 @@ abstract contract PartyGovernanceNFT is PartyGovernance, ERC721, IERC2981 { name = name_; symbol = symbol_; if (rageQuitTimestamp_ != 0) { - if (!proposalEngineOpts.distributionsRequireVote) { + if (proposalEngineOpts.distributionsConfig == DistributionsConfig.AllowedWithoutVote) { revert CannotEnableRageQuitIfNotDistributionsRequireVoteError(); } @@ -346,8 +346,10 @@ abstract contract PartyGovernanceNFT is PartyGovernance, ERC721, IERC2981 { } // Prevent enabling ragequit if distributions can be created without a vote. - if (!_getSharedProposalStorage().opts.distributionsRequireVote) - revert CannotEnableRageQuitIfNotDistributionsRequireVoteError(); + if ( + _getSharedProposalStorage().opts.distributionsConfig == + DistributionsConfig.AllowedWithoutVote + ) revert CannotEnableRageQuitIfNotDistributionsRequireVoteError(); uint40 oldRageQuitTimestamp = rageQuitTimestamp; diff --git a/contracts/proposals/ProposalExecutionEngine.sol b/contracts/proposals/ProposalExecutionEngine.sol index 596133947..c2a897357 100644 --- a/contracts/proposals/ProposalExecutionEngine.sol +++ b/contracts/proposals/ProposalExecutionEngine.sol @@ -263,7 +263,10 @@ contract ProposalExecutionEngine is _getSharedProposalStorage().opts.allowArbCallsToSpendPartyEth ); } else if (pt == ProposalType.Distribute) { - if (!_getSharedProposalStorage().opts.distributionsRequireVote) { + if ( + _getSharedProposalStorage().opts.distributionsConfig != + DistributionsConfig.AllowedWithVote + ) { revert ProposalDisabled(pt); } diff --git a/contracts/proposals/ProposalStorage.sol b/contracts/proposals/ProposalStorage.sol index 5a866523e..0b9a83395 100644 --- a/contracts/proposals/ProposalStorage.sol +++ b/contracts/proposals/ProposalStorage.sol @@ -24,6 +24,12 @@ abstract contract ProposalStorage { uint96 totalVotingPower; } + enum DistributionsConfig { + AllowedWithoutVote, + AllowedWithVote, + NotAllowed + } + struct ProposalEngineOpts { // Whether the party can add new authorities with the add authority proposal. bool enableAddAuthorityProposal; @@ -32,8 +38,8 @@ abstract contract ProposalStorage { bool allowArbCallsToSpendPartyEth; // Whether operators can be used. bool allowOperators; - // Whether distributions require a vote or can be executed by any active member. - bool distributionsRequireVote; + // Distributions config for the party. + DistributionsConfig distributionsConfig; } uint256 internal constant PROPOSAL_FLAG_UNANIMOUS = 0x1; diff --git a/deploy/Deploy.s.sol b/deploy/Deploy.s.sol index dc46bde29..68a559f9b 100644 --- a/deploy/Deploy.s.sol +++ b/deploy/Deploy.s.sol @@ -34,6 +34,7 @@ import { SellPartyCardsAuthority } from "../contracts/authorities/SellPartyCards import { SSTORE2MetadataProvider } from "../contracts/renderers/SSTORE2MetadataProvider.sol"; import { BasicMetadataProvider } from "../contracts/renderers/BasicMetadataProvider.sol"; import { OffChainSignatureValidator } from "../contracts/signature-validators/OffChainSignatureValidator.sol"; +import { BondingCurveAuthority } from "../contracts/authorities/BondingCurveAuthority.sol"; import "./LibDeployConstants.sol"; abstract contract Deploy { @@ -83,6 +84,7 @@ abstract contract Deploy { AddPartyCardsAuthority public addPartyCardsAuthority; SellPartyCardsAuthority public sellPartyCardsAuthority; OffChainSignatureValidator public offChainSignatureValidator; + BondingCurveAuthority public bondingCurveAuthority; function deploy(LibDeployConstants.DeployConstants memory deployConstants) public virtual { _switchDeployer(DeployerRole.Default); @@ -351,6 +353,20 @@ abstract contract Deploy { _trackDeployerGasAfter(); console.log(" Deployed - SellPartyCardsAuthority", address(sellPartyCardsAuthority)); + // Deploy_BONDING_CURVE_AUTHORITY + console.log(""); + console.log("### BondingCurveAuthority"); + console.log(" Deploying - BondingCurveAuthority"); + _trackDeployerGasBefore(); + bondingCurveAuthority = new BondingCurveAuthority( + payable(deployConstants.partyDaoMultisig), + 250, + 1000, + 250 + ); + _trackDeployerGasAfter(); + console.log(" Deployed - BondingCurveAuthority", address(bondingCurveAuthority)); + // DEPLOY_BATCH_BUY_OPERATOR console.log(""); console.log("### CollectionBatchBuyOperator"); @@ -754,6 +770,10 @@ contract DeployScript is Script, Deploy { "AddPartyCardsAuthority", address(addPartyCardsAuthority) ); + addressMapping[27] = AddressMapping( + "BondingCurveAuthority", + address(bondingCurveAuthority) + ); addressMapping[28] = AddressMapping( "SellPartyCardsAuthority", address(sellPartyCardsAuthority) diff --git a/package.json b/package.json index 618ff38ec..1629bae64 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,20 @@ "test:fork": "yarn test --fork-url $ETH_RPC_URL", "test:gas": "forge test --ffi --mc GasBenchmarks -vv", "deploy": "node js/deploy.js", - "deploy:goerli": "DRY_RUN=0 forge script ./deploy/Goerli.s.sol -vvv --rpc-url $GOERLI_RPC_URL --broadcast --etherscan-api-key $ETHERSCAN_API_KEY --via-ir --skip test --optimize --optimizer-runs 95 --ffi --slow", - "deploy:goerli:dry": "DRY_RUN=1 forge script ./deploy/Goerli.s.sol -vvv --rpc-url $GOERLI_RPC_URL --via-ir --skip test --optimize --optimizer-runs 95 --ffi", - "deploy:sepolia": "DRY_RUN=0 forge script ./deploy/Sepolia.s.sol -vvv --rpc-url $SEPOLIA_RPC_URL --broadcast --etherscan-api-key $ETHERSCAN_API_KEY --via-ir --skip test --optimize --optimizer-runs 95 --ffi --slow", - "deploy:sepolia:dry": "DRY_RUN=1 forge script ./deploy/Sepolia.s.sol -vvv --rpc-url $SEPOLIA_RPC_URL --via-ir --skip test --optimize --optimizer-runs 95 --ffi", - "deploy:mainnet": "DRY_RUN=0 forge script ./deploy/Mainnet.s.sol -vvv --rpc-url $ETH_RPC_URL --broadcast --etherscan-api-key $ETHERSCAN_API_KEY --via-ir --skip test --optimize --optimizer-runs 95 --ffi --slow", - "deploy:mainnet:dry": "DRY_RUN=1 forge script ./deploy/Mainnet.s.sol -vvv --rpc-url $ETH_RPC_URL --via-ir --skip test --optimize --optimizer-runs 95 --ffi", - "deploy:base": "DRY_RUN=0 forge script ./deploy/Base.s.sol -vvv --rpc-url $BASE_RPC_URL --via-ir --broadcast --etherscan-api-key $BASESCAN_API_KEY --evm-version paris --skip test --optimize --optimizer-runs 0 --ffi --slow", - "deploy:base:dry": "DRY_RUN=1 forge script ./deploy/Base.s.sol -vvv --rpc-url $BASE_RPC_URL --via-ir --evm-version paris --skip test --optimize --optimizer-runs 0 --ffi", - "deploy:base-goerli": "DRY_RUN=0 forge script ./deploy/BaseGoerli.s.sol -vvv --rpc-url $BASE_GOERLI_RPC_URL --via-ir --broadcast --etherscan-api-key $BASESCAN_API_KEY --evm-version paris --skip test --optimize --optimizer-runs 0 --ffi --slow", - "deploy:base-goerli:dry": "DRY_RUN=1 forge script ./deploy/BaseGoerli.s.sol -vvv --rpc-url $BASE_GOERLI_RPC_URL --via-ir --evm-version paris --skip test --optimize --optimizer-runs 0 --ffi", - "deploy:zora:dry": "DRY_RUN=1 forge script ./deploy/Zora.s.sol -vvv --rpc-url $ZORA_RPC_URL --via-ir --skip test --optimize --optimizer-runs 0 --evm-version paris --ffi --priority-gas-price 1", - "deploy:zora": "DRY_RUN=0 forge script ./deploy/Zora.s.sol -vvv --rpc-url $ZORA_RPC_URL --broadcast --via-ir --skip test --optimize --optimizer-runs 0 --evm-version paris --ffi --slow --priority-gas-price 1", + "deploy:goerli": "DRY_RUN=0 forge script ./deploy/Goerli.s.sol -vvv --rpc-url $GOERLI_RPC_URL --broadcast --etherscan-api-key $ETHERSCAN_API_KEY --via-ir --skip test --optimize --optimizer-runs 50 --ffi --slow", + "deploy:goerli:dry": "DRY_RUN=1 forge script ./deploy/Goerli.s.sol -vvv --rpc-url $GOERLI_RPC_URL --via-ir --skip test --optimize --optimizer-runs 50 --ffi", + "deploy:sepolia": "DRY_RUN=0 forge script ./deploy/Sepolia.s.sol -vvv --rpc-url $SEPOLIA_RPC_URL --broadcast --etherscan-api-key $ETHERSCAN_API_KEY --via-ir --skip test --optimize --optimizer-runs 50 --ffi --slow", + "deploy:sepolia:dry": "DRY_RUN=1 forge script ./deploy/Sepolia.s.sol -vvv --rpc-url $SEPOLIA_RPC_URL --via-ir --skip test --optimize --optimizer-runs 50 --ffi", + "deploy:mainnet": "DRY_RUN=0 forge script ./deploy/Mainnet.s.sol -vvv --rpc-url $ETH_RPC_URL --broadcast --etherscan-api-key $ETHERSCAN_API_KEY --via-ir --skip test --optimize --optimizer-runs 50 --ffi --slow", + "deploy:mainnet:dry": "DRY_RUN=1 forge script ./deploy/Mainnet.s.sol -vvv --rpc-url $ETH_RPC_URL --via-ir --skip test --optimize --optimizer-runs 50 --ffi", + "deploy:base": "DRY_RUN=0 forge script ./deploy/Base.s.sol -vvv --rpc-url $BASE_RPC_URL --via-ir --broadcast --etherscan-api-key $BASESCAN_API_KEY --skip test --optimize --optimizer-runs 50 --ffi --slow", + "deploy:base:dry": "DRY_RUN=1 forge script ./deploy/Base.s.sol -vvv --rpc-url $BASE_RPC_URL --via-ir --skip test --optimize --optimizer-runs 50 --ffi", + "deploy:base-sepolia": "DRY_RUN=0 forge script ./deploy/BaseSepolia.s.sol -vvv --rpc-url $BASE_SEPOLIA_RPC_URL --via-ir --broadcast --etherscan-api-key $BASESCAN_API_KEY --skip test --optimize --optimizer-runs 50 --ffi --slow", + "deploy:base-sepolia:dry": "DRY_RUN=1 forge script ./deploy/BaseSepolia.s.sol -vvv --rpc-url $BASE_SEPOLIA_RPC_URL --via-ir --skip test --optimize --optimizer-runs 50 --ffi", + "deploy:base-goerli": "DRY_RUN=0 forge script ./deploy/BaseGoerli.s.sol -vvv --rpc-url $BASE_GOERLI_RPC_URL --via-ir --broadcast --etherscan-api-key $BASESCAN_API_KEY --skip test --optimize --optimizer-runs 50 --ffi --slow", + "deploy:base-goerli:dry": "DRY_RUN=1 forge script ./deploy/BaseGoerli.s.sol -vvv --rpc-url $BASE_GOERLI_RPC_URL --via-ir --skip test --optimize --optimizer-runs 50 --ffi", + "deploy:zora:dry": "DRY_RUN=1 forge script ./deploy/Zora.s.sol -vvv --rpc-url $ZORA_RPC_URL --via-ir --skip test --optimize --optimizer-runs 50 --ffi --priority-gas-price 1", + "deploy:zora": "DRY_RUN=0 forge script ./deploy/Zora.s.sol -vvv --rpc-url $ZORA_RPC_URL --broadcast --via-ir --skip test --optimize --optimizer-runs 50 --ffi --slow --priority-gas-price 1", "decode-revert": "node js/decode-revert.js", "layout": "node js/gen-storage-layout.js", "coverage": "COVERAGE=true forge coverage --report lcov" diff --git a/test/authorities/BondingCurveAuthority.t.sol b/test/authorities/BondingCurveAuthority.t.sol new file mode 100644 index 000000000..f81ee3198 --- /dev/null +++ b/test/authorities/BondingCurveAuthority.t.sol @@ -0,0 +1,1156 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8; + +import { Party } from "../../contracts/party/Party.sol"; +import { PartyFactory } from "../../contracts/party/PartyFactory.sol"; +import { BondingCurveAuthority } from "../../contracts/authorities/BondingCurveAuthority.sol"; +import { SetupPartyHelper } from "../utils/SetupPartyHelper.sol"; +import { MetadataProvider } from "contracts/renderers/MetadataProvider.sol"; +import { ProposalStorage } from "../../contracts/proposals/ProposalStorage.sol"; +import { Test } from "forge-std/Test.sol"; +import { IERC721 } from "contracts/tokens/IERC721.sol"; + +contract BondingCurveAuthorityTest is SetupPartyHelper { + event BondingCurvePartyCreated( + Party indexed party, + address indexed creator, + BondingCurveAuthority.BondingCurvePartyOptions partyOpts + ); + event TreasuryFeeUpdated(uint16 previousTreasuryFee, uint16 newTreasuryFee); + event PartyDaoFeeUpdated(uint16 previousPartyDaoFee, uint16 newPartyDaoFee); + event CreatorFeeUpdated(uint16 previousCreatorFee, uint16 newCreatorFee); + event PartyDaoFeesClaimed(uint96 amount); + event PartyCardsBought( + Party indexed party, + address indexed buyer, + uint256[] tokenIds, + uint256 totalPrice, + uint256 lastBondingCurvePrice, + uint256 partyDaoFee, + uint256 treasuryFee, + uint256 creatorFee + ); + event PartyCardsSold( + Party indexed party, + address indexed seller, + uint256[] tokenIds, + uint256 sellerProceeds, + uint256 lastBondingCurvePrice, + uint256 partyDaoFee, + uint256 treasuryFee, + uint256 creatorFee + ); + + MockBondingCurveAuthority authority; + Party.PartyOptions opts; + + uint16 TREASURY_FEE_BPS = 0.1e4; // 10% + uint16 PARTY_DAO_FEE_BPS = 0.025e4; // 2.5% + uint16 CREATOR_FEE_BPS = 0.025e4; // 2.5% + + constructor() SetupPartyHelper(false) {} + + function setUp() public override { + super.setUp(); + + authority = new MockBondingCurveAuthority( + globalDaoWalletAddress, + PARTY_DAO_FEE_BPS, + TREASURY_FEE_BPS, + CREATOR_FEE_BPS + ); + + address[] memory hosts = new address[](1); + hosts[0] = _randomAddress(); + opts.name = "PARTY"; + opts.symbol = "PRT"; + opts.governance.hosts = hosts; + opts.governance.voteDuration = 1 hours; + opts.governance.executionDelay = 4 days; + opts.governance.passThresholdBps = 1000; + opts.governance.totalVotingPower = 0; + opts.proposalEngine.distributionsConfig = ProposalStorage.DistributionsConfig.NotAllowed; + + // Set a default treasury fee + vm.prank(globalDaoWalletAddress); + authority.setTreasuryFee(TREASURY_FEE_BPS); + + // Set a default Party DAO fee + vm.prank(globalDaoWalletAddress); + authority.setPartyDaoFee(PARTY_DAO_FEE_BPS); + } + + function _createParty( + uint80 initialBuyAmount, + bool creatorFeeOn + ) internal returns (Party party, address payable creator, uint256 initialPrice) { + creator = _randomAddress(); + + initialPrice = authority.getPriceToBuy( + 0, + initialBuyAmount, + 50_000, + uint80(0.001 ether), + creatorFeeOn + ); + + BondingCurveAuthority.BondingCurvePartyOptions + memory bondingCurveOpts = BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: partyFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: creatorFeeOn, + a: 50_000, + b: uint80(0.001 ether) + }); + + vm.deal(creator, initialPrice); + vm.expectEmit(false, true, true, true); // Don't check party address so we don't have to derive it + emit BondingCurvePartyCreated(Party(payable(address(0))), creator, bondingCurveOpts); + vm.prank(creator); + party = authority.createParty{ value: initialPrice }(bondingCurveOpts, initialBuyAmount); + } + + function test_initialize_revertIfGreaterThanMaxPartyDaoFee() public { + uint16 maxPartyDaoFeeBps = 250; + vm.expectRevert(BondingCurveAuthority.InvalidPartyDaoFee.selector); + new BondingCurveAuthority( + globalDaoWalletAddress, + maxPartyDaoFeeBps + 1, + TREASURY_FEE_BPS, + CREATOR_FEE_BPS + ); + } + + function test_initialize_revertIfGreaterThanMaxTreasuryFee() public { + uint16 maxTreasuryFeeBps = 1000; + vm.expectRevert(BondingCurveAuthority.InvalidTreasuryFee.selector); + new BondingCurveAuthority( + globalDaoWalletAddress, + PARTY_DAO_FEE_BPS, + maxTreasuryFeeBps + 1, + CREATOR_FEE_BPS + ); + } + + function test_initialize_revertIfGreaterThanMaxCreatorFee() public { + uint16 maxCreatorFeeBps = 250; + vm.expectRevert(BondingCurveAuthority.InvalidCreatorFee.selector); + new BondingCurveAuthority( + globalDaoWalletAddress, + PARTY_DAO_FEE_BPS, + TREASURY_FEE_BPS, + maxCreatorFeeBps + 1 + ); + } + + function test_createParty_works() public { + (Party party, address payable creator, ) = _createParty(1, true); + + uint256 expectedBondingCurvePrice = 0.001 ether; + uint256 expectedPartyDaoFee = (expectedBondingCurvePrice * PARTY_DAO_FEE_BPS) / 1e4; + uint256 expectedTreasuryFee = (expectedBondingCurvePrice * TREASURY_FEE_BPS) / 1e4; + uint256 expectedCreatorFee = (expectedBondingCurvePrice * CREATOR_FEE_BPS) / 1e4; + + (address payable partyCreator, uint80 supply, bool creatorFeeOn, , ) = authority.partyInfos( + party + ); + + assertEq(partyCreator, creator); + assertTrue(creatorFeeOn); + assertEq(supply, 1); + assertEq(party.balanceOf(creator), 1); + assertEq(party.getVotingPowerAt(creator, uint40(block.timestamp), 0), 0.1 ether); + assertEq(address(party).balance, expectedTreasuryFee); + assertEq(creator.balance, expectedCreatorFee); + assertEq( + address(authority).balance, + // Creator fee is held in BondingCurveAuthority until claimed. + expectedBondingCurvePrice + expectedPartyDaoFee + 1 + ); + } + + function test_createParty_revertAddAuthorityProposalNotSupported() external { + opts.proposalEngine.enableAddAuthorityProposal = true; + + vm.expectRevert(BondingCurveAuthority.AddAuthorityProposalNotSupported.selector); + authority.createParty( + BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: partyFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: true, + a: 50_000, + b: uint80(0.001 ether) + }), + 1 + ); + } + + function test_createParty_revertDistributionsNotSupported() external { + opts.proposalEngine.distributionsConfig = ProposalStorage + .DistributionsConfig + .AllowedWithoutVote; + + vm.expectRevert(BondingCurveAuthority.DistributionsNotSupported.selector); + authority.createParty( + BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: partyFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: true, + a: 50_000, + b: uint80(0.001 ether) + }), + 1 + ); + } + + function test_createParty_revertBelowMinExecutionDelay() external { + opts.governance.executionDelay = 0; + + vm.expectRevert(BondingCurveAuthority.ExecutionDelayTooShort.selector); + authority.createParty( + BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: partyFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: true, + a: 50_000, + b: uint80(0.001 ether) + }), + 1 + ); + } + + function test_createParty_revertNeedAtLeastOneHost() external { + opts.governance.hosts = new address[](0); + vm.expectRevert(BondingCurveAuthority.NeedAtLeastOneHost.selector); + authority.createParty( + BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: partyFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: true, + a: 50_000, + b: uint80(0.001 ether) + }), + 1 + ); + } + + function test_createParty_moreThanOnePartyCard() public { + (Party party, address payable creator, ) = _createParty(5, true); + + (address payable partyCreator, uint80 supply, bool creatorFeeOn, , ) = authority.partyInfos( + party + ); + + assertEq(partyCreator, creator); + assertTrue(creatorFeeOn); + assertEq(supply, 5); + assertEq(party.balanceOf(creator), 5); + assertEq(party.getVotingPowerAt(creator, uint40(block.timestamp), 0), 0.5 ether); + } + + function test_createParty_alreadyExists() external { + // Create party to attempt to take over + _createParty(1, true); + + PartyFactory trickFactory = PartyFactory(address(new TrickFactory(address(partyFactory)))); + + address creator = _randomAddress(); + uint256 initialPrice = authority.getPriceToBuy(0, 2, 50_000, uint80(0.001 ether), true); + + vm.deal(creator, initialPrice); + vm.prank(creator); + vm.expectRevert(BondingCurveAuthority.ExistingParty.selector); + Party party = authority.createParty{ value: initialPrice }( + BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: trickFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: true, + a: 50_000, + b: uint80(0.001 ether) + }), + 2 + ); + + vm.prank(creator); + vm.expectRevert(BondingCurveAuthority.ExistingParty.selector); + Party party2 = authority.createPartyWithMetadata{ value: initialPrice }( + BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: trickFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: true, + a: 50_000, + b: uint80(0.001 ether) + }), + MetadataProvider(address(0)), + "", + 2 + ); + } + + function test_createPartyWithMetadata_works() public { + MetadataProvider metadataProvider = new MetadataProvider(globals); + bytes memory metadata = abi.encodePacked("custom_metadata"); + + address payable creator = _randomAddress(); + + uint256 initialPrice = 0.001 ether; + initialPrice = + (initialPrice * (1e4 + authority.treasuryFeeBps() + authority.partyDaoFeeBps())) / + 1e4 + + 1; + + vm.deal(creator, initialPrice); + vm.prank(creator); + Party party = authority.createPartyWithMetadata{ value: initialPrice }( + BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: partyFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: false, + a: 50_000, + b: uint80(0.001 ether) + }), + metadataProvider, + metadata, + 1 + ); + + assertEq(address(metadataRegistry.getProvider(address(party))), address(metadataProvider)); + assertEq(metadataProvider.getMetadata(address(party), 1), metadata); + } + + function test_createParty_revertsIfTotalVotingPowerNonZero() public { + address creator = _randomAddress(); + + uint256 initialPrice = 0.001 ether; + initialPrice = + (initialPrice * + (1e4 + + authority.treasuryFeeBps() + + authority.partyDaoFeeBps() + + authority.creatorFeeBps())) / + 1e4; + + vm.deal(creator, initialPrice); + vm.prank(creator); + vm.expectRevert(BondingCurveAuthority.InvalidTotalVotingPower.selector); + opts.governance.totalVotingPower = 100; + party = authority.createParty{ value: initialPrice }( + BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: partyFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: true, + a: 50_000, + b: uint80(0.001 ether) + }), + 1 + ); + + vm.expectRevert(BondingCurveAuthority.InvalidTotalVotingPower.selector); + party = authority.createPartyWithMetadata{ value: initialPrice }( + BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: partyFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: true, + a: 50_000, + b: uint80(0.001 ether) + }), + MetadataProvider(address(0)), + "", + 1 + ); + } + + function test_buyPartyCards_works() + public + returns ( + Party party, + address payable creator, + uint256 initialBalanceExcludingPartyDaoFee, + address buyer, + uint256 expectedBondingCurvePrice + ) + { + (party, creator, ) = _createParty(1, true); + + initialBalanceExcludingPartyDaoFee = + address(authority).balance - + authority.partyDaoFeeClaimable(); + + uint256 expectedPriceToBuy = authority.getPriceToBuy(party, 10); + expectedBondingCurvePrice = + ((expectedPriceToBuy - 10) * 1e4) / + (1e4 + TREASURY_FEE_BPS + PARTY_DAO_FEE_BPS + CREATOR_FEE_BPS); + uint256 expectedPartyDaoFee = (expectedBondingCurvePrice * PARTY_DAO_FEE_BPS) / 1e4; + uint256 expectedTreasuryFee = (expectedBondingCurvePrice * TREASURY_FEE_BPS) / 1e4; + uint256 expectedCreatorFee = (expectedBondingCurvePrice * CREATOR_FEE_BPS) / 1e4; + + uint256 beforePartyTotalVotingPower = party.getGovernanceValues().totalVotingPower; + uint256 beforePartyBalance = address(party).balance; + uint256 beforeAuthorityBalance = address(authority).balance; + uint256 beforeCreatorBalance = creator.balance; + + buyer = _randomAddress(); + vm.deal(buyer, expectedPriceToBuy); + + { + uint256[] memory tokenIds = new uint256[](10); + for (uint256 i = 0; i < 10; i++) tokenIds[i] = i + 2; + + uint256 lastBondingCurvePrice = authority.getBondingCurvePrice( + 10, + 1, + 50_000, + uint80(0.001 ether) + ); + + vm.prank(buyer); + vm.expectEmit(true, true, true, true); + emit PartyCardsBought( + party, + buyer, + tokenIds, + expectedPriceToBuy, + lastBondingCurvePrice, + expectedPartyDaoFee, + expectedTreasuryFee, + expectedCreatorFee + ); + } + + authority.buyPartyCards{ value: expectedPriceToBuy }(party, 10, address(0)); + + (, uint80 supply, , , ) = authority.partyInfos(party); + + assertEq(party.balanceOf(buyer), 10); + assertEq( + party.getGovernanceValues().totalVotingPower, + beforePartyTotalVotingPower + 1 ether + ); + assertEq(party.getVotingPowerAt(buyer, uint40(block.timestamp), 0), 1 ether); + assertEq(supply, 11); + assertEq(buyer.balance, 0); + assertEq( + address(authority).balance - beforeAuthorityBalance, + // Creator fee is held in BondingCurveAuthority until claimed. + expectedBondingCurvePrice + expectedPartyDaoFee + 10 + ); + assertEq(address(party).balance - beforePartyBalance, expectedTreasuryFee); + assertEq(creator.balance - beforeCreatorBalance, expectedCreatorFee); + } + + function test_buyPartyCards_noCreatorFee() + public + returns (Party party, address payable creator, address buyer) + { + (party, creator, ) = _createParty(1, false); + uint256 expectedPriceToBuy = authority.getPriceToBuy(party, 3); + + buyer = _randomAddress(); + vm.deal(buyer, expectedPriceToBuy); + vm.prank(buyer); + uint256[] memory tokenIds = authority.buyPartyCards{ value: expectedPriceToBuy }( + party, + 3, + address(0) + ); + + uint256[] memory expectedTokenIds = new uint256[](3); + expectedTokenIds[0] = 2; + expectedTokenIds[1] = 3; + expectedTokenIds[2] = 4; + assertEq(tokenIds, expectedTokenIds); + + assertEq(creator.balance, 0); + } + + function test_buyPartyCards_creatorFeeFails() public { + (Party party, address creator, ) = _createParty(1, true); + uint256 creatorBalanceBefore = creator.balance; + + // Store this code to creator will cause eth transfer to revert + vm.etch(creator, address(authority).code); + + uint256 expectedPriceToBuy = authority.getPriceToBuy(party, 3); + uint256 expectedBondingCurvePrice = (expectedPriceToBuy * 1e4) / + (1e4 + TREASURY_FEE_BPS + PARTY_DAO_FEE_BPS + CREATOR_FEE_BPS); + uint256 expectedCreatorFee = (expectedBondingCurvePrice * CREATOR_FEE_BPS) / 1e4; + + address buyer = _randomAddress(); + vm.deal(buyer, expectedPriceToBuy); + vm.prank(buyer); + uint256[] memory tokenIds = authority.buyPartyCards{ value: expectedPriceToBuy }( + party, + 3, + address(0) + ); + assertEq(buyer.balance, expectedCreatorFee); // got back creator fee + assertEq(creator.balance, creatorBalanceBefore); + } + + function test_buyPartyCards_refundIfGreaterThanPriceToBuy() public { + (Party party, , ) = _createParty(1, true); + + uint256 priceToBuy = authority.getPriceToBuy(party, 10); + uint256 expectedBondingCurvePrice = (priceToBuy * 1e4) / + (1e4 + TREASURY_FEE_BPS + PARTY_DAO_FEE_BPS + CREATOR_FEE_BPS); + uint256 expectedPartyDaoFee = (expectedBondingCurvePrice * PARTY_DAO_FEE_BPS) / 1e4; + uint256 expectedTreasuryFee = (expectedBondingCurvePrice * TREASURY_FEE_BPS) / 1e4; + uint256 expectedCreatorFee = (expectedBondingCurvePrice * CREATOR_FEE_BPS) / 1e4; + + uint256[] memory tokenIds = new uint256[](10); + for (uint256 i = 0; i < 10; i++) tokenIds[i] = i + 2; + + uint256 lastBondingCurvePrice = authority.getBondingCurvePrice( + 10, + 1, + 50_000, + uint80(0.001 ether) + ); + + uint256 balanceBefore = address(this).balance; + vm.expectEmit(true, true, true, true); + emit PartyCardsBought( + party, + address(this), + tokenIds, + priceToBuy, + lastBondingCurvePrice, + expectedPartyDaoFee, + expectedTreasuryFee, + expectedCreatorFee + ); + + authority.buyPartyCards{ value: priceToBuy + 100 }(party, 10, address(0)); + + assertEq(address(this).balance, balanceBefore - priceToBuy); + } + + function test_buyPartyCards_revertIfLessThanPriceToBuy() public { + (Party party, , ) = _createParty(1, true); + + uint256 priceToBuy = authority.getPriceToBuy(party, 10); + + vm.expectRevert(BondingCurveAuthority.InvalidMessageValue.selector); + authority.buyPartyCards{ value: priceToBuy - 1 }(party, 10, address(0)); + } + + function test_buyPartyCards_revertIfZeroAmount() public { + (Party party, , ) = _createParty(1, true); + + vm.expectRevert(BondingCurveAuthority.InvalidMessageValue.selector); + authority.buyPartyCards(party, 0, address(0)); + } + + function test_buyPartyCards_revertIfPartyInfoNotFound() public { + Party party = Party(payable(_randomAddress())); + + vm.expectRevert(BondingCurveAuthority.PartyNotSupported.selector); + authority.buyPartyCards(party, 10, address(0)); + } + + function test_sellPartyCards_works() public { + ( + Party party, + address payable creator, + uint256 initialBalanceExcludingPartyDaoFee, + address buyer, + uint256 expectedBondingCurvePrice + ) = test_buyPartyCards_works(); + + uint256[] memory tokenIds = new uint256[](10); + for (uint256 i = 0; i < 10; i++) tokenIds[i] = i + 2; + + uint256 expectedSaleProceeds = authority.getSaleProceeds(party, 10); + expectedBondingCurvePrice = + (expectedSaleProceeds * 1e4) / + (1e4 - TREASURY_FEE_BPS - PARTY_DAO_FEE_BPS - CREATOR_FEE_BPS); + uint256 expectedPartyDaoFee = (expectedBondingCurvePrice * PARTY_DAO_FEE_BPS) / 1e4; + uint256 expectedTreasuryFee = (expectedBondingCurvePrice * TREASURY_FEE_BPS) / 1e4; + uint256 expectedCreatorFee = (expectedBondingCurvePrice * CREATOR_FEE_BPS) / 1e4; + + uint256 beforePartyTotalVotingPower = party.getGovernanceValues().totalVotingPower; + uint256 beforePartyBalance = address(party).balance; + uint256 beforeCreatorBalance = creator.balance; + + { + uint256 lastBondingCurvePrice = authority.getBondingCurvePrice( + 1, + 1, + 50_000, + uint80(0.001 ether) + ); + + vm.prank(buyer); + vm.expectEmit(true, true, true, true); + emit PartyCardsSold( + party, + buyer, + tokenIds, + expectedSaleProceeds, + lastBondingCurvePrice, + expectedPartyDaoFee, + expectedTreasuryFee, + expectedCreatorFee + ); + } + + authority.sellPartyCards(party, tokenIds, 0); + (, uint80 supply, , , ) = authority.partyInfos(party); + + assertEq(supply, 1); + assertEq(party.balanceOf(buyer), 0); + assertEq( + party.getGovernanceValues().totalVotingPower, + beforePartyTotalVotingPower - 1 ether + ); + assertEq(party.getVotingPowerAt(buyer, uint40(block.timestamp), 0), 0); + assertEq(buyer.balance, expectedSaleProceeds); + assertApproxEqAbs( + address(authority).balance, + // Should only be the initial balance and the unclaimed Party DAO + // fees leftover. + initialBalanceExcludingPartyDaoFee + authority.partyDaoFeeClaimable(), + 10 + ); + assertEq(address(party).balance - beforePartyBalance, expectedTreasuryFee); + assertEq(creator.balance - beforeCreatorBalance, expectedCreatorFee); + } + + function test_sellPartyCards_revertIfTooMuchSlippage() public { + ( + Party party, + address payable creator, + uint256 initialBalanceExcludingPartyDaoFee, + address buyer, + uint256 expectedBondingCurvePrice + ) = test_buyPartyCards_works(); + + uint256[] memory tokenIds = new uint256[](10); + for (uint256 i = 0; i < 10; i++) tokenIds[i] = i + 2; + + uint256 expectedSaleProceeds = authority.getSaleProceeds(party, 10); + + uint256[] memory creatorTokenIds = new uint256[](1); + creatorTokenIds[0] = 1; + vm.prank(creator); + authority.sellPartyCards(party, creatorTokenIds, 0); + + vm.prank(buyer); + vm.expectRevert(BondingCurveAuthority.ExcessSlippage.selector); + authority.sellPartyCards(party, tokenIds, expectedSaleProceeds); + } + + function test_sellPartyCards_partyNotRecognized() public { + vm.expectRevert(BondingCurveAuthority.PartyNotSupported.selector); + authority.sellPartyCards(Party(payable(_randomAddress())), new uint256[](1), 0); + } + + function test_sellPartyCards_cantSellZero() public { + vm.expectRevert(BondingCurveAuthority.SellZeroPartyCards.selector); + authority.sellPartyCards(Party(payable(_randomAddress())), new uint256[](0), 0); + } + + function test_sellPartyCards_isApprovedForAll() public { + (Party party, , , address buyer, ) = test_buyPartyCards_works(); + + address approved = _randomAddress(); + + vm.prank(buyer); + party.setApprovalForAll(approved, true); + + uint256[] memory tokenIds = new uint256[](10); + for (uint256 i = 0; i < 10; i++) tokenIds[i] = i + 2; + + vm.prank(approved); + authority.sellPartyCards(party, tokenIds, 0); + } + + function test_sellPartyCards_getApproved() public { + (Party party, , , address buyer, ) = test_buyPartyCards_works(); + + address approved = _randomAddress(); + uint256 tokenId = 2; + + vm.prank(buyer); + party.approve(approved, tokenId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + + vm.prank(approved); + authority.sellPartyCards(party, tokenIds, 0); + } + + function test_sellPartyCards_revertIfNotApproved() public { + (Party party, , , , ) = test_buyPartyCards_works(); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 2; + + vm.prank(_randomAddress()); + vm.expectRevert(BondingCurveAuthority.Unauthorized.selector); + authority.sellPartyCards(party, tokenIds, 0); + } + + function test_sellPartyCards_noCreatorFee() public { + (Party party, address payable creator, address buyer) = test_buyPartyCards_noCreatorFee(); + + uint256[] memory tokenIds = new uint256[](3); + for (uint256 i = 0; i < 3; i++) tokenIds[i] = i + 2; + + uint256 saleProceeds = authority.getSaleProceeds(party, 3); + uint256 expectedBondingCurvePrice = ((saleProceeds + tokenIds.length) * 1e4) / + (1e4 - TREASURY_FEE_BPS - PARTY_DAO_FEE_BPS); + uint256 expectedPartyDaoFee = (expectedBondingCurvePrice * PARTY_DAO_FEE_BPS) / 1e4; + uint256 expectedTreasuryFee = (expectedBondingCurvePrice * TREASURY_FEE_BPS) / 1e4; + + uint256 lastBondingCurvePrice = authority.getBondingCurvePrice( + 1, + 1, + 50_000, + uint80(0.001 ether) + ); + + vm.prank(buyer); + vm.expectEmit(true, true, true, true); + emit PartyCardsSold( + party, + buyer, + tokenIds, + saleProceeds, + lastBondingCurvePrice, + expectedPartyDaoFee, + expectedTreasuryFee, + 0 + ); + authority.sellPartyCards(party, tokenIds, 0); + + assertEq(address(creator).balance, 0); + } + + function test_sellPartyCards_creatorFeeFails() public { + (Party party, address payable creator, , address buyer, ) = test_buyPartyCards_works(); + + // Store this code to creator will cause eth transfer to revert + vm.etch(creator, address(authority).code); + + uint256[] memory tokenIds = new uint256[](3); + for (uint256 i = 0; i < 3; i++) tokenIds[i] = i + 2; + + uint256 saleProceeds = authority.getSaleProceeds(party, 3); + uint256 expectedBondingCurvePrice = ((saleProceeds + tokenIds.length) * 1e4) / + (1e4 - TREASURY_FEE_BPS - PARTY_DAO_FEE_BPS - CREATOR_FEE_BPS); + uint256 expectedCreatorFee = (expectedBondingCurvePrice * CREATOR_FEE_BPS) / 1e4; + + uint256 beforeBuyerBalance = buyer.balance; + vm.prank(buyer); + authority.sellPartyCards(party, tokenIds, 0); + + // Ensure buyer gets the creator fee as well + assertEq(buyer.balance, beforeBuyerBalance + expectedCreatorFee + saleProceeds); + } + + function test_setTreasuryFee_works(uint16 newTreasuryFee) public { + vm.assume(newTreasuryFee <= 1000); + + vm.expectEmit(true, true, true, true); + emit TreasuryFeeUpdated(authority.treasuryFeeBps(), newTreasuryFee); + + vm.prank(globalDaoWalletAddress); + authority.setTreasuryFee(newTreasuryFee); + + assertEq(authority.treasuryFeeBps(), newTreasuryFee); + } + + function test_setTreasuryFee_revertIfNotPartyDAO() public { + vm.prank(_randomAddress()); + vm.expectRevert(BondingCurveAuthority.Unauthorized.selector); + authority.setTreasuryFee(0); + } + + function test_setTreasuryFee_revertIfOutOfBounds() public { + vm.prank(globalDaoWalletAddress); + vm.expectRevert(BondingCurveAuthority.InvalidTreasuryFee.selector); + authority.setTreasuryFee(TREASURY_FEE_BPS + 1); + } + + function test_setCreatorFee_works(uint16 newCreatorFee) public { + vm.assume(newCreatorFee <= 250); + + vm.expectEmit(true, true, true, true); + emit CreatorFeeUpdated(authority.creatorFeeBps(), newCreatorFee); + + vm.prank(globalDaoWalletAddress); + authority.setCreatorFee(newCreatorFee); + + assertEq(authority.creatorFeeBps(), newCreatorFee); + } + + function test_setCreatorFee_revertIfNotPartyDAO() public { + vm.prank(_randomAddress()); + vm.expectRevert(BondingCurveAuthority.Unauthorized.selector); + authority.setCreatorFee(0); + } + + function test_setCreatorFee_revertIfOutOfBounds() public { + vm.prank(globalDaoWalletAddress); + vm.expectRevert(BondingCurveAuthority.InvalidCreatorFee.selector); + authority.setCreatorFee(CREATOR_FEE_BPS + 1); + } + + function test_setPartyDaoFee_works(uint16 newPartyDaoFee) public { + vm.assume(newPartyDaoFee <= 250); + + vm.expectEmit(true, true, true, true); + emit PartyDaoFeeUpdated(authority.partyDaoFeeBps(), newPartyDaoFee); + + vm.prank(globalDaoWalletAddress); + authority.setPartyDaoFee(newPartyDaoFee); + + assertEq(authority.partyDaoFeeBps(), newPartyDaoFee); + } + + function test_setPartyDaoFee_revertIfOutOfBounds() public { + vm.prank(globalDaoWalletAddress); + vm.expectRevert(BondingCurveAuthority.InvalidPartyDaoFee.selector); + authority.setPartyDaoFee(PARTY_DAO_FEE_BPS + 1); + } + + function test_setPartyDaoFee_revertIfNotPartyDAO() public { + vm.prank(_randomAddress()); + vm.expectRevert(BondingCurveAuthority.Unauthorized.selector); + authority.setPartyDaoFee(0); + } + + function test_claimPartyDaoFees_works() public { + _createParty(1, true); + + uint256 expectedBondingCurvePrice = 0.001 ether; + uint256 expectedPartyDaoFee = (expectedBondingCurvePrice * PARTY_DAO_FEE_BPS) / 1e4; + + vm.expectEmit(true, true, true, true); + emit PartyDaoFeesClaimed(uint96(expectedPartyDaoFee)); + + vm.prank(globalDaoWalletAddress); + authority.claimPartyDaoFees(); + + assertEq(address(authority).balance, expectedBondingCurvePrice + 1); + assertEq(globalDaoWalletAddress.balance, expectedPartyDaoFee); + } + + function test_claimPartyDaoFees_revertIfNotPartyDAO() public { + vm.prank(_randomAddress()); + vm.expectRevert(BondingCurveAuthority.Unauthorized.selector); + authority.claimPartyDaoFees(); + } + + function test_claimPartyDaoFees_revertIfTransferFails() public { + _createParty(1, true); + + uint256 expectedBondingCurvePrice = 0.001 ether; + uint256 expectedPartyDaoFee = (expectedBondingCurvePrice * PARTY_DAO_FEE_BPS) / 1e4; + + // Cause transfer to fail + vm.etch(address(globalDaoWalletAddress), address(authority).code); + vm.prank(globalDaoWalletAddress); + vm.expectRevert(BondingCurveAuthority.EthTransferFailed.selector); + authority.claimPartyDaoFees(); + + assertEq(authority.partyDaoFeeClaimable(), expectedPartyDaoFee); + } + + function test_buyThenSellPartyCards() public { + address buyer1 = _randomAddress(); + address buyer2 = _randomAddress(); + + (Party party, address payable creator, ) = _createParty(1, true); + + vm.prank(creator); + uint256[] memory creatorToken = new uint256[](1); + creatorToken[0] = 1; + authority.sellPartyCards(party, creatorToken, 0); + + uint256[] memory buyer1Tokens = new uint256[](10); + uint256[] memory buyer2Tokens = new uint256[](7); + + for (uint256 i = 0; i < 10; i++) { + if (i < 7) { + uint256 price = authority.getPriceToBuy(party, 1); + vm.deal(buyer2, price); + vm.prank(buyer2); + buyer2Tokens[i] = authority.buyPartyCards{ value: price }(party, 1, address(0))[0]; + } + uint256 price = authority.getPriceToBuy(party, 1); + vm.deal(buyer1, price); + vm.prank(buyer1); + buyer1Tokens[i] = authority.buyPartyCards{ value: price }(party, 1, address(0))[0]; + } + + vm.prank(buyer2); + authority.sellPartyCards(party, buyer2Tokens, 0); + + vm.prank(buyer1); + authority.sellPartyCards(party, buyer1Tokens, 0); + + vm.prank(globalDaoWalletAddress); + authority.claimPartyDaoFees(); + + assertApproxEqAbs(address(authority).balance, 0, 18); + } + + // Check bonding curve pricing calculations + function test_checkBondingCurvePrice_firstMints() public { + uint256 previousSupply = 0; + + for (uint i = 1; i < 10; i++) { + // Check if buying i works as expected + uint256 expectedBondingCurvePrice = 0; + + for (uint j = 1; j <= i; j++) { + expectedBondingCurvePrice += + (1 ether * (previousSupply + j - 1) * (previousSupply + j - 1)) / + 50_000 + + 0.001 ether; + } + + assertEq( + authority.getBondingCurvePrice(previousSupply, i, 50_000, uint80(0.001 ether)), + expectedBondingCurvePrice + ); + } + } + + function test_checkBondingCurvePrice_existingSupply() public { + for (uint i = 0; i < 10; i++) { + // Check if buying 3 works as expected with random existing supply 10 times + uint256 expectedBondingCurvePrice = 0; + uint256 previousSupply = _randomRange(1, 100); + + for (uint j = 1; j <= 3; j++) { + expectedBondingCurvePrice += + (1 ether * (previousSupply + j - 1) * (previousSupply + j - 1)) / + 50_000 + + 0.001 ether; + } + + assertEq( + authority.getBondingCurvePrice(previousSupply, 3, 50_000, uint80(0.001 ether)), + expectedBondingCurvePrice + ); + } + } + + function test_checkBondingCurvePrice_random_checkPrice( + uint16 amount, + uint16 previousSupply, + uint32 a, + uint80 b + ) public { + vm.assume(a > 0); + vm.assume(amount < 200); + + uint256 expectedBondingCurvePrice = 0; + + for (uint i = 1; i <= amount; i++) { + expectedBondingCurvePrice += + (1 ether * (previousSupply + i - 1) * (previousSupply + i - 1)) / + a + + b; + } + + assertApproxEqAbs( + authority.getBondingCurvePrice(previousSupply, amount, a, b), + expectedBondingCurvePrice, + amount + ); + } + + function test_checkBondingCurvePrice_randomAAndB_ensureSumMatches( + uint16 amount, + uint16 previousSupply, + uint32 a, + uint80 b + ) public { + vm.assume(a > 0); + vm.assume(amount < 200); + + uint256 expectedBondingCurvePrice = 0; + + uint256 aggregatePrice = 0; + for (uint i = 0; i < amount; i++) { + aggregatePrice += authority.getBondingCurvePrice(previousSupply + i, 1, a, b); + } + + assertApproxEqAbs( + aggregatePrice, + authority.getBondingCurvePrice(previousSupply, amount, a, b), + amount + ); + } + + receive() external payable {} +} + +contract MockBondingCurveAuthority is BondingCurveAuthority { + constructor( + address payable partyDao, + uint16 initialPartyDaoFeeBps, + uint16 initialTreasuryFeeBps, + uint16 initialCreatorFeeBps + ) + BondingCurveAuthority( + partyDao, + initialPartyDaoFeeBps, + initialTreasuryFeeBps, + initialCreatorFeeBps + ) + {} + + function getBondingCurvePrice( + uint256 lowerSupply, + uint256 amount, + uint32 a, + uint80 b + ) external pure returns (uint256) { + return super._getBondingCurvePrice(lowerSupply, amount, a, b); + } +} + +contract TrickFactory is Test { + address realFactory; + + constructor(address realFactory_) { + realFactory = realFactory_; + } + + function createParty( + Party, + address[] memory, + Party.PartyOptions memory, + IERC721[] memory, + uint256[] memory, + uint40 + ) external returns (Party party) { + return Party(payable(contractAddressFrom(realFactory, vm.getNonce(realFactory) - 1))); + } + + function createPartyWithMetadata( + Party, + address[] memory, + Party.PartyOptions memory, + IERC721[] memory, + uint256[] memory, + uint40, + MetadataProvider, + bytes memory + ) external returns (Party party) { + return Party(payable(contractAddressFrom(realFactory, vm.getNonce(realFactory) - 1))); + } + + function contractAddressFrom(address deployer, uint256 nonce) internal pure returns (address) { + if (nonce == 0x00) + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked(bytes1(0xd6), bytes1(0x94), deployer, bytes1(0x80)) + ) + ) + ) + ); + if (nonce <= 0x7f) + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xd6), + bytes1(0x94), + deployer, + bytes1(uint8(nonce)) + ) + ) + ) + ) + ); + if (nonce <= 0xff) + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xd7), + bytes1(0x94), + deployer, + bytes1(0x81), + uint8(nonce) + ) + ) + ) + ) + ); + if (nonce <= 0xffff) + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xd8), + bytes1(0x94), + deployer, + bytes1(0x82), + uint16(nonce) + ) + ) + ) + ) + ); + if (nonce <= 0xffffff) + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xd9), + bytes1(0x94), + deployer, + bytes1(0x83), + uint24(nonce) + ) + ) + ) + ) + ); + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xda), + bytes1(0x94), + deployer, + bytes1(0x84), + uint32(nonce) + ) + ) + ) + ) + ); + } +} diff --git a/test/crowdfund/CrowdfundFactory.t.sol b/test/crowdfund/CrowdfundFactory.t.sol index 38bcf6ea8..6e82bd61c 100644 --- a/test/crowdfund/CrowdfundFactory.t.sol +++ b/test/crowdfund/CrowdfundFactory.t.sol @@ -14,6 +14,7 @@ import "./MockMarketWrapper.sol"; import "contracts/globals/Globals.sol"; import "contracts/globals/LibGlobals.sol"; import "contracts/renderers/MetadataRegistry.sol"; +import { ProposalStorage } from "contracts/proposals/ProposalStorage.sol"; import "contracts/renderers/MetadataProvider.sol"; import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol"; import { LibSafeCast } from "contracts/utils/LibSafeCast.sol"; @@ -141,7 +142,7 @@ contract CrowdfundFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) }); @@ -219,7 +220,7 @@ contract CrowdfundFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) }); @@ -422,7 +423,7 @@ contract CrowdfundFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) }); @@ -495,7 +496,7 @@ contract CrowdfundFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) }); @@ -566,7 +567,7 @@ contract CrowdfundFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) }); @@ -638,7 +639,7 @@ contract CrowdfundFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }), preciousTokens: new IERC721[](0), preciousTokenIds: new uint256[](0), @@ -714,7 +715,7 @@ contract CrowdfundFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }), preciousTokens: new IERC721[](0), preciousTokenIds: new uint256[](0), @@ -804,7 +805,7 @@ contract CrowdfundFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }), preciousTokens: new IERC721[](0), preciousTokenIds: new uint256[](0), @@ -870,7 +871,7 @@ contract CrowdfundFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }), preciousTokens: new IERC721[](0), preciousTokenIds: new uint256[](0), diff --git a/test/crowdfund/InitialETHCrowdfund.t.sol b/test/crowdfund/InitialETHCrowdfund.t.sol index 1c0c6f402..5a9176492 100644 --- a/test/crowdfund/InitialETHCrowdfund.t.sol +++ b/test/crowdfund/InitialETHCrowdfund.t.sol @@ -1748,7 +1748,7 @@ contract InitialETHCrowdfundTest is InitialETHCrowdfundTestBase { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }), preciousTokens: new IERC721[](0), preciousTokenIds: new uint256[](0), diff --git a/test/party/PartyFactory.t.sol b/test/party/PartyFactory.t.sol index 623463d8e..10161b111 100644 --- a/test/party/PartyFactory.t.sol +++ b/test/party/PartyFactory.t.sol @@ -82,7 +82,7 @@ contract PartyFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) }); uint40 rageQuitTimestamp = uint40(block.timestamp + 30 days); @@ -111,7 +111,10 @@ contract PartyFactoryTest is Test, TestUtils { .getProposalEngineOpts(); assertEq(proposalEngineOpts.allowArbCallsToSpendPartyEth, true); assertEq(proposalEngineOpts.allowOperators, true); - assertEq(proposalEngineOpts.distributionsRequireVote, true); + assertEq( + uint8(proposalEngineOpts.distributionsConfig), + uint8(ProposalStorage.DistributionsConfig.AllowedWithVote) + ); assertEq(party.preciousListHash(), _hashPreciousList(preciousTokens, preciousTokenIds)); } @@ -135,7 +138,7 @@ contract PartyFactoryTest is Test, TestUtils { enableAddAuthorityProposal: true, allowArbCallsToSpendPartyEth: true, allowOperators: true, - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) }); bytes memory customMetadata = abi.encodePacked(_randomBytes32()); @@ -168,7 +171,10 @@ contract PartyFactoryTest is Test, TestUtils { .getProposalEngineOpts(); assertEq(proposalEngineOpts.allowArbCallsToSpendPartyEth, true); assertEq(proposalEngineOpts.allowOperators, true); - assertEq(proposalEngineOpts.distributionsRequireVote, true); + assertEq( + uint8(proposalEngineOpts.distributionsConfig), + uint8(ProposalStorage.DistributionsConfig.AllowedWithVote) + ); assertEq(party.preciousListHash(), _hashPreciousList(preciousTokens, preciousTokenIds)); assertEq(address(registry.getProvider(address(party))), address(provider)); assertEq(provider.getMetadata(address(party), 0), customMetadata); diff --git a/test/party/PartyGovernanceNFT.t.sol b/test/party/PartyGovernanceNFT.t.sol index d40e7bce6..d6429a02f 100644 --- a/test/party/PartyGovernanceNFT.t.sol +++ b/test/party/PartyGovernanceNFT.t.sol @@ -540,7 +540,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); uint40 newTimestamp = uint40(block.timestamp + 1); @@ -568,7 +568,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: true, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); address notHost = _randomAddress(); @@ -596,7 +596,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -632,7 +632,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -665,7 +665,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -693,7 +693,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -788,7 +788,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -923,7 +923,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -990,7 +990,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -1078,7 +1078,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -1148,7 +1148,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: false + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithoutVote }) ); @@ -1179,7 +1179,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -1248,7 +1248,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -1318,7 +1318,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -1409,7 +1409,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -1480,7 +1480,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -1549,7 +1549,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); @@ -1637,7 +1637,7 @@ contract PartyGovernanceNFTTest is PartyGovernanceNFTTestBase { allowArbCallsToSpendPartyEth: false, allowOperators: false, // Needs to be true to set non-zero rageQuitTimestamp - distributionsRequireVote: true + distributionsConfig: ProposalStorage.DistributionsConfig.AllowedWithVote }) ); diff --git a/test/party/PartyGovernanceUnit.t.sol b/test/party/PartyGovernanceUnit.t.sol index de2cbeda3..1eee2dec4 100644 --- a/test/party/PartyGovernanceUnit.t.sol +++ b/test/party/PartyGovernanceUnit.t.sol @@ -296,7 +296,9 @@ contract PartyGovernanceUnitTest is Test, TestUtils { uint256[] memory preciousTokenIds ) private returns (TestablePartyGovernance gov) { defaultGovernanceOpts.totalVotingPower = totalVotingPower; - defaultProposalEngineOpts.distributionsRequireVote = distributionsRequireVote; + defaultProposalEngineOpts.distributionsConfig = ProposalStorage.DistributionsConfig( + distributionsRequireVote ? 1 : 0 + ); return new TestablePartyGovernance( diff --git a/test/utils/SetupPartyHelper.sol b/test/utils/SetupPartyHelper.sol index d40e2f1db..e2ecf8534 100644 --- a/test/utils/SetupPartyHelper.sol +++ b/test/utils/SetupPartyHelper.sol @@ -17,6 +17,7 @@ import { ERC721Receiver } from "../../contracts/tokens/ERC721Receiver.sol"; import { MetadataRegistry } from "../../contracts/renderers/MetadataRegistry.sol"; import { TokenDistributor } from "../../contracts/distribution/TokenDistributor.sol"; import { OffChainSignatureValidator } from "../../contracts/signature-validators/OffChainSignatureValidator.sol"; +import { ProposalStorage } from "../../contracts/proposals/ProposalStorage.sol"; /// @notice This contract provides a fully functioning party instance for testing. /// Run setup from inheriting contract. @@ -34,6 +35,7 @@ abstract contract SetupPartyHelper is TestUtils, ERC721Receiver { Party internal partyImpl; Globals internal globals; PartyFactory internal partyFactory; + MetadataRegistry internal metadataRegistry; TokenDistributor internal tokenDistributor; uint256 internal johnPk = 0xa11ce; uint256 internal dannyPk = 0xb0b; @@ -48,6 +50,7 @@ abstract contract SetupPartyHelper is TestUtils, ERC721Receiver { IERC721[] internal preciousTokens = new IERC721[](0); uint256[] internal preciousTokenIds = new uint256[](0); uint40 internal constant _EXECUTION_DELAY = 99; + address payable globalDaoWalletAddress = payable(address(420)); constructor(bool isForked) { _isForked = isForked; @@ -65,7 +68,6 @@ abstract contract SetupPartyHelper is TestUtils, ERC721Receiver { globalsAdmin = new GlobalsAdmin(); globals = globalsAdmin.globals(); partyImpl = new Party(globals); - address globalDaoWalletAddress = address(420); globalsAdmin.setGlobalDaoWallet(globalDaoWalletAddress); ProposalExecutionEngine pe = new ProposalExecutionEngine( @@ -89,7 +91,7 @@ abstract contract SetupPartyHelper is TestUtils, ERC721Receiver { address[] memory registrars = new address[](2); registrars[0] = address(this); registrars[1] = address(partyFactory); - MetadataRegistry metadataRegistry = new MetadataRegistry(globals, registrars); + metadataRegistry = new MetadataRegistry(globals, registrars); globalsAdmin.setMetadataRegistry(address(metadataRegistry)); OffChainSignatureValidator offChainGlobalValidator = new OffChainSignatureValidator(); @@ -110,7 +112,9 @@ abstract contract SetupPartyHelper is TestUtils, ERC721Receiver { opts.governance.executionDelay = _EXECUTION_DELAY; opts.governance.passThresholdBps = 1000; opts.proposalEngine.allowArbCallsToSpendPartyEth = true; - opts.proposalEngine.distributionsRequireVote = true; + opts.proposalEngine.distributionsConfig = ProposalStorage + .DistributionsConfig + .AllowedWithVote; opts.governance.totalVotingPower = johnVotes + dannyVotes + steveVotes + thisVotes; address[] memory authorities = new address[](1); diff --git a/utils/output-abis.ts b/utils/output-abis.ts index 4029c4de7..4044093bd 100644 --- a/utils/output-abis.ts +++ b/utils/output-abis.ts @@ -31,6 +31,7 @@ const RELEVANT_ABIS = [ "AddPartyCardsAuthority", "SellPartyCardsAuthority", "OffChainSignatureValidator", + "BondingCurveAuthority", ]; // AFileName -> a_file_name