Project

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

Desynchronized minted count in Tier Upgrades

Vulnerability Details

The DAO tracks the number of tokens minted per tier using a counter in the tier configuration. When users join a DAO, this counter is incremented as shown in the joinDAO function:

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;
uint256 platformFees = (20 * tierPrice) / 100;
@> daos[daoMembershipAddress].tiers[tierIndex].minted += 1; // Increment minted count
IERC20(daos[daoMembershipAddress].currency).transferFrom(_msgSender(), owpWallet, platformFees);
IERC20(daos[daoMembershipAddress].currency).transferFrom(_msgSender(), daoMembershipAddress, tierPrice - platformFees);
IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), tierIndex, 1);
emit UserJoinedDAO(_msgSender(), daoMembershipAddress, tierIndex);
}

However, when users upgrade their tier by burning 2 tokens from a lower tier to mint 1 token in a higher tier, the minted count in the original tier is not decremented:

function upgradeTier(address daoMembershipAddress, uint256 fromTierIndex) external {
require(daos[daoMembershipAddress].daoType == DAOType.SPONSORED, "Upgrade not allowed.");
require(daos[daoMembershipAddress].noOfTiers >= fromTierIndex + 1, "No higher tier available.");
@> IMembershipERC1155(daoMembershipAddress).burn(_msgSender(), fromTierIndex, 2);
@> IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), fromTierIndex - 1, 1);
emit UserJoinedDAO(_msgSender(), daoMembershipAddress, fromTierIndex - 1);
}

This creates a discrepancy between the actual number of tokens and the tracked count. For example:

  1. A tier starts with 0 minted tokens

  2. 4 users join the tier (minted = 4)

  3. 2 users upgrade to a higher tier, burning 4 tokens

  4. The minted count stays at 4 even though there are only 2 tokens left

  5. The tier will appear full at 10 minted when there might be only 6 actual tokens

The tier's capacity check relies on this minted count:

require(daos[daoMembershipAddress].tiers[tierIndex].amount > daos[daoMembershipAddress].tiers[tierIndex].minted, "Tier full.");

So the inflated count causes tiers to reach their limit prematurely.

PoC

  1. Install foundry: foundryup then forge init --force

  2. In the test folder create the file MembershipFactory.t.sol and paste the code below:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
import "forge-std/Test.sol";
import "../contracts/dao/MembershipFactory.sol";
import "../contracts/dao/CurrencyManager.sol";
import "../contracts/dao/tokens/MembershipERC1155.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
contract MembershipFactoryDAOTest is Test {
MembershipFactory factory;
MembershipERC1155 implementation;
CurrencyManager currencyManager;
TestERC20 currency;
address alice = address(0x1);
address bob = address(0x2);
address owpWallet = address(0x3);
function setUp() public {
// Deploy test ERC20
currency = new TestERC20("Test", "TST");
// Deploy core contracts
currencyManager = new CurrencyManager();
implementation = new MembershipERC1155();
factory = new MembershipFactory(
address(currencyManager),
owpWallet,
"ipfs://",
address(implementation)
);
// Setup roles and whitelist currency
currencyManager.addCurrency(address(currency));
// Setup users
vm.deal(alice, 100 ether);
vm.deal(bob, 100 ether);
currency.mint(alice, 1000e18);
currency.mint(bob, 1000e18);
}
function testTierSupplyMismatch() public {
// Create DAO
TierConfig[] memory tiers = new TierConfig[]();
for(uint i = 0; i < 7; i++) {
tiers[i] = TierConfig({
amount: 1000,
price: 100e18,
power: 2**(6-i),
minted: 0
});
}
DAOInputConfig memory daoConfig = DAOInputConfig({
ensname: "test.eth",
daoType: DAOType.SPONSORED,
currency: address(currency),
maxMembers: 19825,
noOfTiers: 7
});
vm.prank(alice);
address daoAddress = factory.createNewDAOMembership(daoConfig, tiers);
MembershipERC1155 dao = MembershipERC1155(daoAddress);
// Track initial minted count for tier 6
TierConfig[] memory initialTiers = factory.tiers(daoAddress);
console.log("Initial tier 6 minted:", initialTiers[6].minted);
// Alice joins tier 6 multiple times
vm.startPrank(alice);
currency.approve(address(factory), 1000e18);
// Join tier 6 four times (getting 4 tokens)
for(uint i = 0; i < 4; i++) {
factory.joinDAO(daoAddress, 6);
}
// Track minted count after joins
TierConfig[] memory tiersAfterJoin = factory.tiers(daoAddress);
console.log("Tier 6 minted after joins:", tiersAfterJoin[6].minted);
console.log("Alice tier 6 balance:", dao.balanceOf(alice, 6));
// Track initial total supply
uint256 initialTotalSupply = dao.totalSupply();
console.log("Initial total supply:", initialTotalSupply);
// Upgrade using 2 tokens
factory.upgradeTier(daoAddress, 6);
// Check state after first upgrade
TierConfig[] memory tiersAfterUpgrade = factory.tiers(daoAddress);
console.log("Tier 6 minted after first upgrade:", tiersAfterUpgrade[6].minted);
console.log("Tier 5 minted after first upgrade:", tiersAfterUpgrade[5].minted);
console.log("Alice tier 6 balance:", dao.balanceOf(alice, 6));
console.log("Alice tier 5 balance:", dao.balanceOf(alice, 5));
// Upgrade again with remaining 2 tokens
factory.upgradeTier(daoAddress, 6);
// Check final state
TierConfig[] memory finalTiers = factory.tiers(daoAddress);
console.log("Final tier 6 minted:", finalTiers[6].minted);
console.log("Final tier 5 minted:", finalTiers[5].minted);
console.log("Final tier 6 balance:", dao.balanceOf(alice, 6));
console.log("Final tier 5 balance:", dao.balanceOf(alice, 5));
vm.stopPrank();
// Track total members from minted counts
uint256 totalMembers = 0;
for(uint i = 0; i < 7; i++) {
totalMembers += finalTiers[i].minted;
}
console.log("Total members according to minted:", totalMembers);
// Track actual total members from token balances
uint256 actualMembers = 0;
for(uint i = 0; i < 7; i++) {
actualMembers += dao.balanceOf(alice, i);
}
console.log("Actual members from balances:", actualMembers);
// The minted count in tiers should match the actual token distribution
assertEq(totalMembers, actualMembers, "Minted count mismatch with actual tokens");
}
}
// Simple ERC20 for testing
contract TestERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}

Run: forge test --match-test testTierSupplyMismatch -vv

Output:

@> [FAIL: Minted count mismatch with actual tokens: 4 != 2] testTierSupplyMismatch() (gas: 1932480)
Logs:
Initial tier 6 minted: 0
Tier 6 minted after joins: 4
Alice tier 6 balance: 4
Initial total supply: 4
Tier 6 minted after first upgrade: 4
Tier 5 minted after first upgrade: 0
Alice tier 6 balance: 2
Alice tier 5 balance: 1
Final tier 6 minted: 4
Final tier 5 minted: 0
Final tier 6 balance: 0
Final tier 5 balance: 2
@> Total members according to minted: 4
@> Actual members from balances: 2

Impact

  • Tiers appear full before actually reaching their member limit

  • Users get blocked from joining tiers that should still have space

  • DAO dashboard shows incorrect member counts per tier

Tools Used

Manual Review & Foundry

Recommendations

Update the upgradeTier function to properly maintain the minted count:

function upgradeTier(address daoMembershipAddress, uint256 fromTierIndex) external {
require(daos[daoMembershipAddress].daoType == DAOType.SPONSORED, "Upgrade not allowed.");
require(daos[daoMembershipAddress].noOfTiers >= fromTierIndex + 1, "No higher tier available.");
+ // Update minted counts for both tiers
+ daos[daoMembershipAddress].tiers[fromTierIndex].minted -= 2;
+ daos[daoMembershipAddress].tiers[fromTierIndex - 1].minted += 1;
IMembershipERC1155(daoMembershipAddress).burn(_msgSender(), fromTierIndex, 2);
IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), fromTierIndex - 1, 1);
emit UserJoinedDAO(_msgSender(), daoMembershipAddress, fromTierIndex - 1);
}
Updates

Lead Judging Commences

0xbrivan2 Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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