Project

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

Upgrade logic limitations for DAOs with intermediate fulfilled tiers

Summary

Note: This finding assumes the codebase is corrected and the DAOConfig struct is updated every time a tier upgrade is performed.

The current upgrade mechanism for memberships involves burning two tokens from a lower tier to mint one in the next higher tier. However, if the higher tier is already fully minted or has not been configured, it becomes impossible to upgrade memberships from the lower tier.

Vulnerability Details

A lower-tier membership may not be upgradeable if the target higher tier is already full or has no configuration. This limitation can create an unintended bottleneck where users are unable to upgrade, even if they meet the requirements, due to restrictions in the target tier.

Impact

In the following PoC created using Foundry, Tier 5 is intentionally left unset. When Alice attempts to upgrade her membership from Tier 6, the upgrade fails due to the maximum capacity limit for Tier 5.

To enable this PoC, add the following check in MembershipFactory::upgradeTier:

TierConfig[] memory tierConfig = daos[daoMembershipAddress].tiers;
if (
tierConfig[fromTierIndex - 1].minted >=
tierConfig[fromTierIndex - 1].amount
) revert("Tier full");
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 < 5; i++) {
tierConfig[i] = TierConfig({
amount: 10,
price: (7 - i) * 10 ** 18,
power: (7 - i) * 10 ** 18,
minted: 0
});
}
// Skip tier 5
tierConfig[6] = TierConfig({
amount: 10,
price: 1 * 10 ** 18,
power: 1 * 10 ** 18,
minted: 0
});
currencyManager.addCurrency(address(mockToken));
daoAddress = membershipFactory.createNewDAOMembership(
inputConfig,
tierConfig
);
vm.stopPrank();
}
function testUpgradeDiscontinuousTiers() public {
deal(address(mockToken), alice, 2 ether);
vm.startPrank(alice);
mockToken.approve(address(membershipFactory), type(uint256).max);
membershipFactory.joinDAO(daoAddress, 6);
vm.expectRevert("Tier full");
membershipFactory.upgradeTier(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

Allow users to upgrade to any higher tier, adjusting in the function the ratio burn/mint depending on the target tier.

Updates

Lead Judging Commences

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.