Project

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

Updating Dao via `MembershipFactory::updateDAOMembership` could lead to more minted tokens than the allowed max members for the DAO

Summary

The MembershipFactory::updateDAOMembership contract lacks proper validation when updating tier configurations through the given function. Administrators could mistakenly modify tiers in a way that enables users to mint more membership tokens than intended, exceeding the DAO's maxMembers limit. This vulnerability can lead to an over-allocation of membership tokens and undermine the DAO's governance structure.

Vulnerability Details

The problem is presented in MembershipFactory::updateDAOMembership function of the contract. When an administrator updates the tier configurations, there are insufficient checks to ensure that the new amount of a tier configuration is not less than the currently minted tokens and at the same time we don't allow more people to join another tier.

function updateDAOMembership(string calldata ensName, TierConfig[] memory tierConfigs)
external onlyRole(EXTERNAL_CALLER) returns (address) {
address daoAddress = getENSAddress[ensName];
require(tierConfigs.length <= TIER_MAX, "Invalid tier count.");
require(tierConfigs.length > 0, "Invalid tier count.");
require(daoAddress != address(0), "DAO does not exist.");
DAOConfig storage dao = daos[daoAddress];
if(dao.daoType == DAOType.SPONSORED){
require(tierConfigs.length == TIER_MAX, "Invalid tier count.");
}
uint256 maxMembers = 0;
// Preserve minted values and adjust the length of dao.tiers
for (uint256 i = 0; i < tierConfigs.length; i++) {
if (i < dao.tiers.length) {
tierConfigs[i].minted = dao.tiers[i].minted;
}
}
// Reset and update the tiers array
delete dao.tiers;
for (uint256 i = 0; i < tierConfigs.length; i++) {
dao.tiers.push(tierConfigs[i]);
maxMembers += tierConfigs[i].amount;
}
// updating the ceiling limit acc to new data
if(maxMembers > dao.maxMembers){
dao.maxMembers = maxMembers;
}
dao.noOfTiers = tierConfigs.length;
return daoAddress;
}

Impact

This vulnerability allows:

Exceeding Membership Limits: Users can mint more tokens than intended, surpassing the DAO's maxMembers limit.
Unfair Advantage: Users may gain excessive influence or benefits within the DAO due to holding more membership tokens.
Undermined Governance: The DAO's governance structure and integrity can be compromised, leading to potential disputes among members.

Proof Of Concept

In the provided proof of concept (PoC), the following steps highlight the vulnerability:

1 - DAO Creation: A DAO is created with a maxMembers limit of 5 and two tiers:

  • Tier 0: Amount of 4 tokens.

  • Tier 1: Amount of 1 token.

3 - Minting Tokens: A user mints tokens up to the limit in both tiers:

  • Mints 4 tokens in Tier 0.

  • Mints 1 token in Tier 1.

4 - Updating Tiers: The administrator updates the tier configurations:

  • Tier 0: Amount reduced to 1 token.

  • Tier 1: Amount increased to 4 tokens.

3 - Additional Minting: The user is now able to mint additional tokens in Tier 1:

  • Mints 3 more tokens in Tier 1.

  • Total tokens held: 4 in Tier 0 and 4 in Tier 1.

5 - Exceeding maxMembers: The user now holds 8 tokens, exceeding the maxMembers limit of 5.
The vulnerability exists because the updateDAOMembership function does not validate whether the new tier amounts, combined with already minted tokens, respect the DAO's maxMembers limit.

Add the following tests to MembershipFactory.test.ts

describe("update_tiers_missing_checks", function () {
it("it fails to create dao if the maximum amount of member for more than 1 tier is set to uint256 max", async function () {
[owner, addr1, addr2, ...addrs] = await ethers.getSigners();
CurrencyManager = await ethers.getContractFactory("CurrencyManager");
currencyManager = await CurrencyManager.deploy();
await currencyManager.deployed();
MembershipERC1155 = await ethers.getContractFactory('MembershipERC1155');
const membershipImplementation = await MembershipERC1155.deploy();
await membershipImplementation.deployed();
MembershipFactory = await ethers.getContractFactory("MembershipFactory");
membershipFactory = await MembershipFactory.deploy(currencyManager.address, owner.address, "https://baseuri.com/", membershipImplementation.address);
await membershipFactory.deployed();
await currencyManager.addCurrency(testERC20.address);
const newDAOConfig = { ensname: "testdao.eth", daoType: DAOType.GENERAL, currency: testERC20.address, maxMembers: 5, noOfTiers: 2 };
const newTierConfig = [{ price: 50, amount: 4, minted: 0, power: 12 }, { price: 25, amount: 1, minted: 0, power: 6 }];
const tx = await membershipFactory.connect(addr1).createNewDAOMembership(newDAOConfig, newTierConfig);
const receipt = await tx.wait();
const event = receipt.events.find((event: any) => event.event === "MembershipDAONFTCreated");
const ensName = event.args[2][0];
const nftAddress = event.args[1];
const ensToAddress = await membershipFactory.getENSAddress("testdao.eth")
expect(ensName).to.equal("testdao.eth");
expect(ensToAddress).to.equal(nftAddress);
// Mint tokens to the owner
await testERC20.mint(owner.address, ethers.utils.parseEther('10000'));
// user 1 joins DAO at tier 0 4 times
for (let i = 0; i < 4; i++) {
// Transfer tokens to addr1
await testERC20.transfer(addr1.address, ethers.utils.parseEther('100'));
// addr1 approves MembershipFactory to spend
await testERC20.connect(addr1).approve(membershipFactory.address, ethers.utils.parseEther('100'));
// addr1 joins DAO at tier 0
await membershipFactory.connect(addr1).joinDAO(nftAddress, 0);
}
// user 1 joins DAO at tier 1
await testERC20.transfer(addr1.address, ethers.utils.parseEther('100'));
// addr1 approves MembershipFactory to spend
await testERC20.connect(addr1).approve(membershipFactory.address, ethers.utils.parseEther('100'));
// addr1 joins DAO at tier 0
await membershipFactory.connect(addr1).joinDAO(nftAddress, 1);
// update tier config
// we set the first tier to be now with max amount of 1 and amount of the second tier to be 4
const updateTierConfig = [{ price: 20, amount: 1, minted: 0, power: 4 }, { price: 10, amount: 4, minted: 0, power: 4 }];
await membershipFactory.updateDAOMembership("testdao.eth", updateTierConfig);
// user 1 joins DAO at tier 1 3 more times thus the equal amount of tokens he has is 4 of tier 1 and 4 of tier 0
// which is bigger than the max amount of members for the dao
for (let i = 0; i < 3; i++) {
// Transfer tokens to addr1
await testERC20.transfer(addr1.address, ethers.utils.parseEther('100'));
// addr1 approves MembershipFactory to spend
await testERC20.connect(addr1).approve(membershipFactory.address, ethers.utils.parseEther('100'));
// addr1 joins DAO at tier 0
await membershipFactory.connect(addr1).joinDAO(nftAddress, 1);
}
const membershipContract = await ethers.getContractAt("MembershipERC1155", nftAddress);
// retrieve the current balance of user with addr1 for tier 0 and tier 1
const balanceOfFirstTier = await membershipContract.balanceOf(addr1.address, 0);
const balanceOfSecondTier = await membershipContract.balanceOf(addr1.address, 1);
// expected that he has more than the max amount of members for the dao
expect(balanceOfFirstTier).to.equal(4);
expect(balanceOfSecondTier).to.equal(4);
// get dao configuration
const daoConfig = await membershipFactory.daos(nftAddress);
// expect that the sum of the two tiers is bigger than the max amount of members for the dao
expect(balanceOfFirstTier.add(balanceOfSecondTier)).to.be.gt(daoConfig.maxMembers);
});
});

Tools Used

  • Manual Code Review: Analyzing the smart contract code to identify the lack of validation checks.

  • Testing Frameworks: Using Hardhat and Chai to replicate the issue and confirm the vulnerability through the provided PoC.

Recommendations

To mitigate this vulnerability:

  • Add the following check in the MembershipFactory::updateDAOMembership to ensure that we don't reduce the amount of a given tier to less than the already minted amount of that tier

function updateDAOMembership(string calldata ensName, TierConfig[] memory tierConfigs)
external onlyRole(EXTERNAL_CALLER) returns (address) {
address daoAddress = getENSAddress[ensName];
require(tierConfigs.length <= TIER_MAX, "Invalid tier count.");
require(tierConfigs.length > 0, "Invalid tier count.");
require(daoAddress != address(0), "DAO does not exist.");
DAOConfig storage dao = daos[daoAddress];
if(dao.daoType == DAOType.SPONSORED){
require(tierConfigs.length == TIER_MAX, "Invalid tier count.");
}
uint256 maxMembers = 0;
// Preserve minted values and adjust the length of dao.tiers
for (uint256 i = 0; i < tierConfigs.length; i++) {
if (i < dao.tiers.length) {
+ if (dao.tiers[i].minted > tierConfigs[i].amount) {
+ revert("Cannot reduce tier amount.");
+ }
tierConfigs[i].minted = dao.tiers[i].minted;
}
}
// Reset and update the tiers array
delete dao.tiers;
for (uint256 i = 0; i < tierConfigs.length; i++) {
dao.tiers.push(tierConfigs[i]);
maxMembers += tierConfigs[i].amount;
}
// updating the ceiling limit acc to new data
if(maxMembers > dao.maxMembers){
dao.maxMembers = maxMembers;
}
dao.noOfTiers = tierConfigs.length;
return daoAddress;
}

Potentially, it is a good idea to consider burning minted tokens for users in a tier that has been removed on update.

Updates

Lead Judging Commences

0xbrivan2 Lead Judge
10 months ago
0xbrivan2 Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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