Skip to content

Commit

Permalink
fix: apply contribution fee per mint (#297)
Browse files Browse the repository at this point in the history
* update contribution fee for multiple mints

* support NFT crowdfunds

* fix test

* remove batch contribute nft crowdfund

---------

Co-authored-by: Arr00 <[email protected]>
  • Loading branch information
0xble and arr00 committed Sep 11, 2023
1 parent af1cdab commit ea68ed4
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 36 deletions.
34 changes: 22 additions & 12 deletions contracts/crowdfund/ContributionRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ pragma solidity 0.8.20;

import { LibAddress } from "../utils/LibAddress.sol";
import { LibRawResult } from "../utils/LibRawResult.sol";
import { InitialETHCrowdfund } from "../crowdfund/InitialETHCrowdfund.sol";

contract ContributionRouter {
using LibRawResult for bytes;
using LibAddress for address payable;

event FeePerContributionUpdated(uint96 oldFeePerContribution, uint96 newFeePerContribution);
event FeePerMintUpdated(uint96 oldFeePerMint, uint96 newFeePerMint);
event ReceivedFees(address indexed sender, uint256 amount);
event ClaimedFees(address indexed partyDao, address indexed recipient, uint256 amount);

Expand All @@ -17,25 +18,25 @@ contract ContributionRouter {
/// @notice The address allowed to claim fees from the contract.
address public immutable OWNER;

/// @notice The amount of fees to pay to the DAO per contribution.
uint96 public feePerContribution;
/// @notice The amount of fees to pay to the DAO per mint.
uint96 public feePerMint;

constructor(address owner, uint96 initialFeePerContribution) {
constructor(address owner, uint96 initialFeePerMint) {
OWNER = owner;
feePerContribution = initialFeePerContribution;
feePerMint = initialFeePerMint;
}

modifier onlyOwner() {
if (msg.sender != OWNER) revert OnlyOwner();
_;
}

/// @notice Set the fee per contribution. Only the owner can call.
/// @param newFeePerContribution The new amount to set fee per contribution to.
function setFeePerContribution(uint96 newFeePerContribution) external onlyOwner {
emit FeePerContributionUpdated(feePerContribution, newFeePerContribution);
/// @notice Set the fee per mint. Only the owner can call.
/// @param newFeePerMint The new amount to set fee per mint to.
function setFeePerMint(uint96 newFeePerMint) external onlyOwner {
emit FeePerMintUpdated(feePerMint, newFeePerMint);

feePerContribution = newFeePerContribution;
feePerMint = newFeePerMint;
}

/// @notice Claim fees from the contract. Only the owner can call.
Expand All @@ -52,10 +53,19 @@ contract ContributionRouter {
/// and keeps the fee amount. The target contract is expected to
/// be appended to the calldata.
fallback() external payable {
uint256 feeAmount = feePerContribution;
uint256 feeAmount = feePerMint;
address target;
assembly {
target := shr(96, calldataload(sub(calldatasize(), 0x14)))
target := shr(96, calldataload(sub(calldatasize(), 20)))
}
if (msg.sig == InitialETHCrowdfund.batchContributeFor.selector) {
uint256 numOfMints;
assembly {
// 228 is the offset of the length of `tokenIds` in the
// calldata.
numOfMints := calldataload(228)
}
feeAmount *= numOfMints;
}
(bool success, bytes memory res) = target.call{ value: msg.value - feeAmount }(msg.data);
if (!success) res.rawRevert();
Expand Down
42 changes: 18 additions & 24 deletions test/crowdfund/ContributionRouter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,34 @@ import "../../contracts/crowdfund/ContributionRouter.sol";
import "../TestUtils.sol";

contract ContributionRouterTest is TestUtils {
event FeePerContributionUpdated(uint96 oldFeePerContribution, uint96 newFeePerContribution);
event FeePerMintUpdated(uint96 oldFeePerMint, uint96 newFeePerMint);
event ReceivedFees(address indexed sender, uint256 amount);
event ClaimedFees(address indexed partyDao, address indexed recipient, uint256 amount);

address owner;
uint96 feePerContribution;
uint96 feePerMint;
ContributionRouter router;

constructor() {
function setUp() public {
owner = _randomAddress();
feePerContribution = 0.01 ether;
router = new ContributionRouter(owner, feePerContribution);
feePerMint = 0.01 ether;
router = new ContributionRouter(owner, feePerMint);
}

function test_initialization() public {
assertEq(router.OWNER(), owner);
assertEq(router.feePerContribution(), feePerContribution);
assertEq(router.feePerMint(), feePerMint);
}

function test_fallback_works() external {
MockPayableContract target = new MockPayableContract();
uint256 amount = 1 ether;
vm.deal(address(this), amount);
uint256 feeAmount = feePerContribution;
uint256 feeAmount = feePerMint;
vm.expectEmit(true, true, true, true);
emit ReceivedFees(address(this), feeAmount);
(bool success, bytes memory res) = address(router).call{ value: amount }(
abi.encodePacked(
abi.encodeWithSelector(MockPayableContract.pay.selector),
address(target)
)
abi.encodePacked(abi.encodeWithSelector(MockPayableContract.pay.selector), target)
);
assertEq(success, true);
assertEq(res.length, 0);
Expand All @@ -45,31 +42,28 @@ contract ContributionRouterTest is TestUtils {

function test_fallback_insufficientFee() public {
MockPayableContract target = new MockPayableContract();
uint256 amount = feePerContribution - 1;
uint256 amount = feePerMint - 1;
vm.deal(address(this), amount);
(bool success, bytes memory res) = address(router).call{ value: amount }(
abi.encodePacked(
abi.encodeWithSelector(MockPayableContract.pay.selector),
address(target)
)
abi.encodePacked(abi.encodeWithSelector(MockPayableContract.pay.selector), target)
);
assertEq(success, false);
assertEq(res, stdError.arithmeticError);
}

function test_setFeePerContribution_works() external {
uint96 newFeePerContribution = 0.02 ether;
function test_setFeePerMint_works() external {
uint96 newFeePerMint = 0.02 ether;
vm.prank(owner);
vm.expectEmit(true, true, true, true);
emit FeePerContributionUpdated(feePerContribution, newFeePerContribution);
router.setFeePerContribution(newFeePerContribution);
assertEq(router.feePerContribution(), newFeePerContribution);
emit FeePerMintUpdated(feePerMint, newFeePerMint);
router.setFeePerMint(newFeePerMint);
assertEq(router.feePerMint(), newFeePerMint);
}

function test_setFeePerContribution_onlyOwner() external {
uint96 newFeePerContribution = 0.02 ether;
function test_setFeePerMint_onlyOwner() external {
uint96 newFeePerMint = 0.02 ether;
vm.expectRevert(ContributionRouter.OnlyOwner.selector);
router.setFeePerContribution(newFeePerContribution);
router.setFeePerMint(newFeePerMint);
}

function test_claimFees_works() external {
Expand Down
163 changes: 163 additions & 0 deletions test/crowdfund/ContributionRouterIntegration.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8;

import "../../contracts/globals/Globals.sol";
import "../../contracts/party/PartyFactory.sol";
import "../../contracts/crowdfund/InitialETHCrowdfund.sol";
import "../../contracts/crowdfund/ContributionRouter.sol";
import "./TestableCrowdfund.sol";

import "../TestUtils.sol";

contract ContributionRouterIntegrationTest is TestUtils {
InitialETHCrowdfund ethCrowdfund;
TestableCrowdfund nftCrowdfund;
Globals globals;
Party partyImpl;
PartyFactory partyFactory;

uint96 feePerMint;
ContributionRouter router;

function setUp() public {
feePerMint = 0.01 ether;
router = new ContributionRouter(address(this), feePerMint);

globals = new Globals(address(this));
partyImpl = new Party(globals);
partyFactory = new PartyFactory(globals);

InitialETHCrowdfund initialETHCrowdfundImpl = new InitialETHCrowdfund(globals);

InitialETHCrowdfund.InitialETHCrowdfundOptions memory ethCrowdfundOpts;
ethCrowdfundOpts.maxContribution = type(uint96).max;
ethCrowdfundOpts.maxTotalContributions = type(uint96).max;
ethCrowdfundOpts.duration = 7 days;
ethCrowdfundOpts.exchangeRateBps = 1e4;

InitialETHCrowdfund.ETHPartyOptions memory partyOpts;
partyOpts.name = "Test Party";
partyOpts.symbol = "TEST";
partyOpts.governanceOpts.partyImpl = partyImpl;
partyOpts.governanceOpts.partyFactory = partyFactory;
partyOpts.governanceOpts.voteDuration = 7 days;
partyOpts.governanceOpts.executionDelay = 1 days;
partyOpts.governanceOpts.passThresholdBps = 0.5e4;
partyOpts.governanceOpts.hosts = new address[](1);
partyOpts.governanceOpts.hosts[0] = address(this);

ethCrowdfund = InitialETHCrowdfund(
payable(
new Proxy(
initialETHCrowdfundImpl,
abi.encodeCall(
InitialETHCrowdfund.initialize,
(ethCrowdfundOpts, partyOpts, MetadataProvider(address(0)), "")
)
)
)
);

Crowdfund.CrowdfundOptions memory nftCrowdfundOpts;
nftCrowdfundOpts.name = "Test Party";
nftCrowdfundOpts.symbol = "TEST";
nftCrowdfundOpts.maxContribution = type(uint96).max;

nftCrowdfund = TestableCrowdfund(
payable(
new Proxy(
Implementation(new TestableCrowdfund(globals)),
abi.encodeCall(TestableCrowdfund.initialize, (nftCrowdfundOpts))
)
)
);
}

function test_contributionFee_ethCrowdfund_withSingleMint() public {
// Setup for contribution.
address payable member = _randomAddress();
uint256 amount = 1 ether;
vm.deal(member, amount);
bytes memory data = abi.encodeCall(
InitialETHCrowdfund.contributeFor,
(0, member, member, "")
);

// Make contribution.
vm.prank(member);
(bool success, bytes memory res) = address(router).call{ value: amount }(
abi.encodePacked(data, ethCrowdfund)
);

// Check results.
assertEq(success, true);
assertEq(res.length, 0);
assertEq(address(ethCrowdfund).balance, amount - feePerMint);
assertEq(address(router).balance, feePerMint);
assertEq(member.balance, 0);
}

function test_contributionFee_ethCrowdfund_withBatchMint() public {
// Setup for contribution.
address payable member = _randomAddress();
uint96 amount = 1 ether;
uint256 numOfMints = 4;
uint256[] memory tokenIds = new uint256[](numOfMints);
address payable[] memory recipients = new address payable[](numOfMints);
address[] memory delegates = new address[](numOfMints);
uint96[] memory values = new uint96[](numOfMints);
bytes[] memory gateDatas = new bytes[](numOfMints);
for (uint256 i; i < numOfMints; ++i) {
delegates[i] = recipients[i] = _randomAddress();
values[i] = amount - feePerMint;
}
vm.deal(member, amount * numOfMints);
bytes memory data = abi.encodeCall(
InitialETHCrowdfund.batchContributeFor,
(
InitialETHCrowdfund.BatchContributeForArgs({
tokenIds: tokenIds,
recipients: recipients,
initialDelegates: delegates,
values: values,
gateDatas: gateDatas,
revertOnFailure: true
})
)
);

// Make contribution.
vm.prank(member);
(bool success, bytes memory res) = address(router).call{ value: amount * numOfMints }(
abi.encodePacked(data, ethCrowdfund)
);

// Check results.
assertEq(success, true);
assertEq(res.length, 0);
assertEq(address(ethCrowdfund).balance, (amount - feePerMint) * numOfMints);
assertEq(address(router).balance, feePerMint * numOfMints);
assertEq(member.balance, 0);
}

function test_contributionFee_nftCrowdfund_withSingleMint() public {
// Setup for contribution.
address payable member = _randomAddress();
uint256 amount = 1 ether;
vm.deal(member, amount);
bytes memory data = abi.encodeCall(Crowdfund.contributeFor, (member, member, ""));

// Make contribution.
vm.prank(member);
(bool success, bytes memory res) = address(router).call{ value: amount }(
abi.encodePacked(data, nftCrowdfund)
);

// Check results.
assertEq(success, true);
assertEq(res.length, 0);
assertEq(address(nftCrowdfund).balance, amount - feePerMint);
assertEq(address(router).balance, feePerMint);
assertEq(member.balance, 0);
}
}

0 comments on commit ea68ed4

Please sign in to comment.