Project

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

DAO membership update might exceed maximum allowed members per tier

Summary

The MembershipFactory::updateDAOMembership function allows external callers to update the membership parameters of a DAO, including limits on the maximum number of members. Although this function correctly checks that the total member count does not exceed the maximum allowed, it fails to enforce this restriction at the tier level. As a result, it is possible to lower the maximum member count for a specific tier to a value below the current number of members in that tier, which can lead to an invalid state.

Vulnerability Details

This vulnerability allows DAOs to retain more members in a specific tier than the newly configured limit permits. Specifically, if a tier has already reached or exceeded the new maximum member limit after the update, the function does not prevent the configuration change, allowing an inconsistency between the allowed and actual member count.

Impact

The PoC below demonstrates an exploit scenario in which 10 users mint memberships in tier 6. Subsequently, the maximum member count for this tier is reduced to half the original amount. Under the updated configuration, the number of current members would exceed the allowed limit, creating an inconsistency that could disrupt DAO governance or violate expected membership constraints.

See PoC
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Vm, Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {DAOConfig, DAOInputConfig, TierConfig, DAOType, TIER_MAX} from "../src/dao/libraries/MembershipDAOStructs.sol";
import {MembershipFactory} from "../src/dao/MembershipFactory.sol";
import {CurrencyManager} from "../src/dao/CurrencyManager.sol";
import {MembershipERC1155} from "../src/dao/tokens/MembershipERC1155.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MembershipFactoryTest is Test {
MembershipFactory membershipFactory;
CurrencyManager currencyManager;
MembershipERC1155 membershipImplementation;
MockToken mockToken;
address alice = makeAddr("alice");
address daoAddress;
function setUp() public {
vm.startPrank(alice);
mockToken = new MockToken("TOKEN", "TOKEN");
membershipImplementation = new MembershipERC1155();
currencyManager = new CurrencyManager();
membershipFactory = new MembershipFactory(
address(currencyManager),
address(this),
"baseURI",
address(membershipImplementation)
);
DAOInputConfig memory inputConfig = DAOInputConfig({
ensname: "TEST",
daoType: DAOType.SPONSORED,
currency: address(mockToken),
maxMembers: 700,
noOfTiers: 7
});
TierConfig[] memory tierConfig = new TierConfig[](); // This should be new TierConfig[](7)
for (uint256 i = 0; i < 7; i++) {
tierConfig[i] = TierConfig({
amount: 10,
price: (7 - i) * 10 ** 18,
power: (7 - i) * 10 ** 18,
minted: 0
});
}
currencyManager.addCurrency(address(mockToken));
daoAddress = membershipFactory.createNewDAOMembership(
inputConfig,
tierConfig
);
vm.stopPrank();
}
/**
* This function reduces the tiers amount to 5 when already ten members have joined
* tier 6
*/
function testReduceMaxMembers() public {
populateTier6();
TierConfig[] memory tierConfig = new TierConfig[](); // This should be new TierConfig[](7)
for (uint256 i = 0; i < 7; i++) {
tierConfig[i] = TierConfig({
amount: 5,
price: (7 - i) * 10 ** 18,
power: (7 - i) * 10 ** 18,
minted: 0
});
}
vm.startPrank(alice);
membershipFactory.updateDAOMembership("TEST", tierConfig);
tierConfig = membershipFactory.tiers(daoAddress);
assertEq(tierConfig[6].minted, 10);
assertEq(tierConfig[6].amount, 5);
}
///////////////////////////////////////////////////////////////////////////////////
//////////////////////////////// HELPER FUNCTIONS ////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
function populateTier6() public {
for (uint256 i = 0; i < 10; i++) {
deal(address(mockToken), address(uint160(i + 1)), 1 ether);
vm.startPrank(address(uint160(i + 1)));
mockToken.approve(address(membershipFactory), type(uint256).max);
membershipFactory.joinDAO(daoAddress, 6);
vm.stopPrank();
}
}
}
contract MockToken is ERC20 {
constructor(
string memory name_,
string memory symbol_
) ERC20(name_, symbol_) {
_mint(msg.sender, 100 ether);
}
}

Tools Used

Manual review

Recommendations

Check in each tier whether the new set amount is exceeded by the minted memberships and revert if that's the case.

Updates

Lead Judging Commences

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

Appeal created

robertodf99 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.