Project

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

Minted amounts for tiers not preserved during DAOUpdate allowing more users to join than intended

Summary

Protocol claims to preserve tiers.minted amount during MembershipFactory::updateDAOMembership, but does follow
through with it, if the tierConfig ordering is different.

Vulnerability Details

Protocol claims to preserve tiers.minted amount during MembershipFactory::updateDAOMembership, but does follow
through with it, if the tierConfig ordering is different.
The updateDAOMembership allows EXTERNAL_CALLER to update DAOMembership by influencing the following,

  1. dao.maxMembers

  2. dao.noOfTiers

  3. dao.tiers.amount

  4. dao.tiers.price

  5. dao.tiers.power

Entities the EXTERNAL_CALLER cannot influence

  1. dao.ensname

  2. dao.daoType

  3. dao.currency

  4. dao.tiers.minted => this is preserved unless EXTERNAL_CALLER downgrades noOfTiers.

The minted amount isn't actually preserved if tierConfig ordering is different. For example,

Let's say alice creates DAO by providing following tierConfigs,
[tier4, tier5, tier6]

Then, userA mints tier6.

tier6.minted becomes 1.

Now, protocol decides to update pricings of the tier for the DAO created by alice, they provide the following configs,
[tier6, tier5, tier4] => with updated prices.

Since the order is reserved (The code logic permits it), minted information is lost.

Instead of getting preserved, tier6.minted becomes 0. This means one additional person can now join the DAO.

Impact

Protocol loses track of actual minted amounts of tokens, resulting in inaccurate records, allowing more mints thanintended, affecting overall governance and management.

Proof-Of-Code

it("tierConfig.minted not preserved during DAO update", async () => {
[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);
// 1. tiers 6, 5, 4 added in ascending order.
DAOConfig1 = { ensname: "testdao.eth", daoType: DAOType.GENERAL, currency: testERC20.address, maxMembers: 70, noOfTiers: 3 };
TierConfig1 = [
{ price: 10, amount: 10, minted: 0, power:1 },
{ price: 20, amount: 20, minted: 0, power:2 },
{ price: 40, amount: 40, minted: 0, power:4 },
];
const tx = await membershipFactory.connect(addr2).createNewDAOMembership(DAOConfig1, TierConfig1);
await tx.wait();
const ensAddress = await membershipFactory.getENSAddress("testdao.eth");
membershipERC1155 = await MembershipERC1155.attach(ensAddress);
await testERC20.mint(addr1.address, ethers.utils.parseEther("300"));
await testERC20.connect(addr1).approve(membershipFactory.address, ethers.utils.parseEther("200"));
// addr1 joins tier6
await membershipFactory.connect(addr1).joinDAO(membershipERC1155.address, 0);
assert.equal(await membershipFactory.mintedSoFar(membershipERC1155.address, 0), BigInt(1));
// newTierConfig adds updated pricings for tiers 4, 5, 6 (wrong ordering)
const newTierConfig = [
{ price: 400, amount: 40, minted: 0, power:4 },
{ price: 200, amount: 20, minted: 0, power:2 },
{ price: 100, amount: 10, minted: 0, power:1 },
];
await membershipFactory.updateDAOMembership("testdao.eth", newTierConfig);
// tier6 is at index2 now
// Minted amount of tier6 not preserved, but butchered because of wrong ordering!
assert.equal(await membershipFactory.mintedSoFar(membershipERC1155.address, 2), BigInt(0));
});

Tools Used

Manual Review & Hardhat Testing

Recommendations

Consider adding tierLevel in tierConfig that aligns with tierConfigs being provided.

struct TierConfig {
+ uint256 tierLevel; // The tier level (0~6)
uint256 amount; // The total number of tokens available for that tier
uint256 price; // The price of a token in the given currency.
uint256 power; // The voting power of the token.
uint256 minted; // The total number of tokens already minted for that tier
}
function updateDAOMembership(string calldata ensName, TierConfig[] memory tierConfigs)
external onlyRole(EXTERNAL_CALLER) returns (address) {
...
...
for (uint256 i = 0; i < tierConfigs.length; i++) {
if (i < dao.tiers.length) {
+ uint256 currentTierLevel = dao.tiers[i].tierLevel;
+ uint256 providedTierLevel = tierConfigs[i].tierLevel;
+ require(currentTierLevel == providedTierLevel, "Tier indexing discrepancies detected");
tierConfigs[i].minted = dao.tiers[i].minted;
}
}
}
Updates

Lead Judging Commences

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

Appeal created

falsegenius Submitter
10 months ago
0xbrivan2 Lead Judge
9 months ago
0xbrivan2 Lead Judge 9 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.