diff --git a/contracts/authorities/BondingCurveAuthority.sol b/contracts/authorities/BondingCurveAuthority.sol index 35bad9ac..cc50cb40 100644 --- a/contracts/authorities/BondingCurveAuthority.sol +++ b/contracts/authorities/BondingCurveAuthority.sol @@ -26,6 +26,7 @@ contract BondingCurveAuthority { error SellZeroPartyCards(); error DistributionsNotSupported(); error NeedAtLeastOneHost(); + error InvalidDiscount(); event BondingCurvePartyCreated( Party indexed party, @@ -94,6 +95,8 @@ contract BondingCurveAuthority { // The value of b in the bonding curve formula 1 ether * x ** 2 / a + b // used by the Party to price cards uint80 b; + // Discount when buying/selling the first card in the party + uint80 firstCardDiscount; } /// @notice Struct containing info stored for a party @@ -110,6 +113,8 @@ contract BondingCurveAuthority { // The value of b in the bonding curve formula 1 ether * x ** 2 / a + b // used by the Party to price cards uint80 b; + // Discount when buying/selling the first card in the party + uint80 firstCardDiscount; } modifier onlyPartyDao() { @@ -153,6 +158,10 @@ contract BondingCurveAuthority { address[] memory authorities = new address[](1); authorities[0] = address(this); + if (partyOpts.firstCardDiscount > partyOpts.b) { + revert InvalidDiscount(); + } + _validateGovernanceOpts(partyOpts.opts); party = partyOpts.partyFactory.createParty( @@ -173,7 +182,8 @@ contract BondingCurveAuthority { supply: 0, creatorFeeOn: partyOpts.creatorFeeOn, a: partyOpts.a, - b: partyOpts.b + b: partyOpts.b, + firstCardDiscount: partyOpts.firstCardDiscount }); emit BondingCurvePartyCreated(party, msg.sender, partyOpts); @@ -198,6 +208,10 @@ contract BondingCurveAuthority { address[] memory authorities = new address[](1); authorities[0] = address(this); + if (partyOpts.firstCardDiscount > partyOpts.b) { + revert InvalidDiscount(); + } + _validateGovernanceOpts(partyOpts.opts); party = partyOpts.partyFactory.createPartyWithMetadata( @@ -220,7 +234,8 @@ contract BondingCurveAuthority { supply: 0, creatorFeeOn: partyOpts.creatorFeeOn, a: partyOpts.a, - b: partyOpts.b + b: partyOpts.b, + firstCardDiscount: partyOpts.firstCardDiscount }); emit BondingCurvePartyCreated(party, msg.sender, partyOpts); @@ -278,7 +293,8 @@ contract BondingCurveAuthority { partyInfo.supply, amount, partyInfo.a, - partyInfo.b + partyInfo.b, + partyInfo.firstCardDiscount ); uint256 partyDaoFee = (bondingCurvePrice * partyDaoFeeBps) / BPS; uint256 treasuryFee = (bondingCurvePrice * treasuryFeeBps) / BPS; @@ -321,7 +337,8 @@ contract BondingCurveAuthority { partyInfo.supply + amount - 1, 1, partyInfo.a, - partyInfo.b + partyInfo.b, + partyInfo.firstCardDiscount ); emit PartyCardsBought( @@ -365,7 +382,8 @@ contract BondingCurveAuthority { partyInfo.supply - amount, amount, partyInfo.a, - partyInfo.b + partyInfo.b, + partyInfo.firstCardDiscount ); uint256 partyDaoFee = (bondingCurvePrice * partyDaoFeeBps) / BPS; uint256 treasuryFee = (bondingCurvePrice * treasuryFeeBps) / BPS; @@ -420,7 +438,8 @@ contract BondingCurveAuthority { partyInfo.supply - amount, 1, partyInfo.a, - partyInfo.b + partyInfo.b, + partyInfo.firstCardDiscount ); emit PartyCardsSold( @@ -447,7 +466,8 @@ contract BondingCurveAuthority { partyInfo.supply - amount, amount, partyInfo.a, - partyInfo.b + partyInfo.b, + partyInfo.firstCardDiscount ); uint256 partyDaoFee = (bondingCurvePrice * partyDaoFeeBps) / BPS; uint256 treasuryFee = (bondingCurvePrice * treasuryFeeBps) / BPS; @@ -470,7 +490,8 @@ contract BondingCurveAuthority { amount, partyInfo.a, partyInfo.b, - partyInfo.creatorFeeOn + partyInfo.creatorFeeOn, + partyInfo.firstCardDiscount ); } @@ -488,9 +509,10 @@ contract BondingCurveAuthority { uint80 amount, uint32 a, uint80 b, - bool creatorFeeOn + bool creatorFeeOn, + uint80 firstCardDiscount ) public view returns (uint256) { - uint256 bondingCurvePrice = _getBondingCurvePrice(supply, amount, a, b); + uint256 bondingCurvePrice = _getBondingCurvePrice(supply, amount, a, b, firstCardDiscount); uint256 partyDaoFee = (bondingCurvePrice * partyDaoFeeBps) / BPS; uint256 treasuryFee = (bondingCurvePrice * treasuryFeeBps) / BPS; uint256 creatorFee = (bondingCurvePrice * (creatorFeeOn ? creatorFeeBps : 0)) / BPS; @@ -510,8 +532,10 @@ contract BondingCurveAuthority { uint256 lowerSupply, uint256 amount, uint32 a, - uint80 b + uint80 b, + uint80 firstCardDiscount ) internal pure returns (uint256) { + uint256 discount = lowerSupply == 0 ? firstCardDiscount : 0; // Using the function 1 ether * x ** 2 / a + b uint256 amountSquared = amount * amount; return @@ -525,7 +549,8 @@ contract BondingCurveAuthority { 6)) / uint256(a) + amount * - uint256(b); + uint256(b) - + discount; } /** diff --git a/test/authorities/BondingCurveAuthority.t.sol b/test/authorities/BondingCurveAuthority.t.sol index f81ee319..5dac726a 100644 --- a/test/authorities/BondingCurveAuthority.t.sol +++ b/test/authorities/BondingCurveAuthority.t.sol @@ -91,7 +91,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { initialBuyAmount, 50_000, uint80(0.001 ether), - creatorFeeOn + creatorFeeOn, + 0 ); BondingCurveAuthority.BondingCurvePartyOptions @@ -101,7 +102,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { opts: opts, creatorFeeOn: creatorFeeOn, a: 50_000, - b: uint80(0.001 ether) + b: uint80(0.001 ether), + firstCardDiscount: 0 }); vm.deal(creator, initialPrice); @@ -152,9 +154,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { uint256 expectedTreasuryFee = (expectedBondingCurvePrice * TREASURY_FEE_BPS) / 1e4; uint256 expectedCreatorFee = (expectedBondingCurvePrice * CREATOR_FEE_BPS) / 1e4; - (address payable partyCreator, uint80 supply, bool creatorFeeOn, , ) = authority.partyInfos( - party - ); + (address payable partyCreator, uint80 supply, bool creatorFeeOn, , , ) = authority + .partyInfos(party); assertEq(partyCreator, creator); assertTrue(creatorFeeOn); @@ -181,7 +182,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { opts: opts, creatorFeeOn: true, a: 50_000, - b: uint80(0.001 ether) + b: uint80(0.001 ether), + firstCardDiscount: 0 }), 1 ); @@ -200,7 +202,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { opts: opts, creatorFeeOn: true, a: 50_000, - b: uint80(0.001 ether) + b: uint80(0.001 ether), + firstCardDiscount: 0 }), 1 ); @@ -217,7 +220,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { opts: opts, creatorFeeOn: true, a: 50_000, - b: uint80(0.001 ether) + b: uint80(0.001 ether), + firstCardDiscount: 0 }), 1 ); @@ -233,7 +237,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { opts: opts, creatorFeeOn: true, a: 50_000, - b: uint80(0.001 ether) + b: uint80(0.001 ether), + firstCardDiscount: 0 }), 1 ); @@ -242,9 +247,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { function test_createParty_moreThanOnePartyCard() public { (Party party, address payable creator, ) = _createParty(5, true); - (address payable partyCreator, uint80 supply, bool creatorFeeOn, , ) = authority.partyInfos( - party - ); + (address payable partyCreator, uint80 supply, bool creatorFeeOn, , , ) = authority + .partyInfos(party); assertEq(partyCreator, creator); assertTrue(creatorFeeOn); @@ -260,33 +264,35 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { PartyFactory trickFactory = PartyFactory(address(new TrickFactory(address(partyFactory)))); address creator = _randomAddress(); - uint256 initialPrice = authority.getPriceToBuy(0, 2, 50_000, uint80(0.001 ether), true); + uint256 initialPrice = authority.getPriceToBuy(0, 2, 50_000, uint80(0.001 ether), true, 0); vm.deal(creator, initialPrice); vm.prank(creator); vm.expectRevert(BondingCurveAuthority.ExistingParty.selector); - Party party = authority.createParty{ value: initialPrice }( + authority.createParty{ value: initialPrice }( BondingCurveAuthority.BondingCurvePartyOptions({ partyFactory: trickFactory, partyImpl: partyImpl, opts: opts, creatorFeeOn: true, a: 50_000, - b: uint80(0.001 ether) + b: uint80(0.001 ether), + firstCardDiscount: 0 }), 2 ); vm.prank(creator); vm.expectRevert(BondingCurveAuthority.ExistingParty.selector); - Party party2 = authority.createPartyWithMetadata{ value: initialPrice }( + authority.createPartyWithMetadata{ value: initialPrice }( BondingCurveAuthority.BondingCurvePartyOptions({ partyFactory: trickFactory, partyImpl: partyImpl, opts: opts, creatorFeeOn: true, a: 50_000, - b: uint80(0.001 ether) + b: uint80(0.001 ether), + firstCardDiscount: 0 }), MetadataProvider(address(0)), "", @@ -315,7 +321,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { opts: opts, creatorFeeOn: false, a: 50_000, - b: uint80(0.001 ether) + b: uint80(0.001 ether), + firstCardDiscount: 0 }), metadataProvider, metadata, @@ -349,7 +356,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { opts: opts, creatorFeeOn: true, a: 50_000, - b: uint80(0.001 ether) + b: uint80(0.001 ether), + firstCardDiscount: 0 }), 1 ); @@ -362,7 +370,8 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { opts: opts, creatorFeeOn: true, a: 50_000, - b: uint80(0.001 ether) + b: uint80(0.001 ether), + firstCardDiscount: 0 }), MetadataProvider(address(0)), "", @@ -429,7 +438,7 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { authority.buyPartyCards{ value: expectedPriceToBuy }(party, 10, address(0)); - (, uint80 supply, , , ) = authority.partyInfos(party); + (, uint80 supply, , , , ) = authority.partyInfos(party); assertEq(party.balanceOf(buyer), 10); assertEq( @@ -488,11 +497,7 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { address buyer = _randomAddress(); vm.deal(buyer, expectedPriceToBuy); vm.prank(buyer); - uint256[] memory tokenIds = authority.buyPartyCards{ value: expectedPriceToBuy }( - party, - 3, - address(0) - ); + authority.buyPartyCards{ value: expectedPriceToBuy }(party, 3, address(0)); assertEq(buyer.balance, expectedCreatorFee); // got back creator fee assertEq(creator.balance, creatorBalanceBefore); } @@ -605,7 +610,7 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { } authority.sellPartyCards(party, tokenIds, 0); - (, uint80 supply, , , ) = authority.partyInfos(party); + (, uint80 supply, , , , ) = authority.partyInfos(party); assertEq(supply, 1); assertEq(party.balanceOf(buyer), 0); @@ -627,13 +632,7 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { } function test_sellPartyCards_revertIfTooMuchSlippage() public { - ( - Party party, - address payable creator, - uint256 initialBalanceExcludingPartyDaoFee, - address buyer, - uint256 expectedBondingCurvePrice - ) = test_buyPartyCards_works(); + (Party party, address payable creator, , address buyer, ) = test_buyPartyCards_works(); uint256[] memory tokenIds = new uint256[](10); for (uint256 i = 0; i < 10; i++) tokenIds[i] = i + 2; @@ -908,6 +907,31 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { assertApproxEqAbs(address(authority).balance, 0, 18); } + function test_firstPartyCardFree() public { + Party party = authority.createParty{ value: 1 }( + BondingCurveAuthority.BondingCurvePartyOptions({ + partyFactory: partyFactory, + partyImpl: partyImpl, + opts: opts, + creatorFeeOn: true, + a: 50_000, + b: uint80(0.001 ether), + firstCardDiscount: uint80(0.001 ether) + }), + 1 + ); + + assertEq(authority.getSaleProceeds(party, 1), 0); + + uint256[] memory partyCardsToSell = new uint256[](1); + partyCardsToSell[0] = 1; + + uint256 balanceBefore = address(this).balance; + authority.sellPartyCards(party, partyCardsToSell, 0); + // Ensure no proceeds + assertEq(address(this).balance, balanceBefore); + } + // Check bonding curve pricing calculations function test_checkBondingCurvePrice_firstMints() public { uint256 previousSupply = 0; @@ -984,8 +1008,6 @@ contract BondingCurveAuthorityTest is SetupPartyHelper { 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); @@ -1022,7 +1044,7 @@ contract MockBondingCurveAuthority is BondingCurveAuthority { uint32 a, uint80 b ) external pure returns (uint256) { - return super._getBondingCurvePrice(lowerSupply, amount, a, b); + return super._getBondingCurvePrice(lowerSupply, amount, a, b, 0); } } @@ -1040,7 +1062,7 @@ contract TrickFactory is Test { IERC721[] memory, uint256[] memory, uint40 - ) external returns (Party party) { + ) external view returns (Party party) { return Party(payable(contractAddressFrom(realFactory, vm.getNonce(realFactory) - 1))); } @@ -1053,7 +1075,7 @@ contract TrickFactory is Test { uint40, MetadataProvider, bytes memory - ) external returns (Party party) { + ) external view returns (Party party) { return Party(payable(contractAddressFrom(realFactory, vm.getNonce(realFactory) - 1))); }