Project

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

Calling `MembershipFactory::updateDAOMembership` with a tier length less than the current tier length will lead to removed members and an incorrect minted count.

Summary

MembershipFactory::updateDAOMembership allows a user with the EXTERNAL_CALLER role to update an already created DAO. They can update the TierConfig of the DAO. There is nothing stopping the new TierConfig length (tier count) from being less than the current `TierConfig length (tier count).

Vulnerability Details

Consider the situation where a created DAO has 2 tiers, tier 1 and tier 2. User Bob has paid and is a member of tier 2 in the DAO. Another user with the EXTERNAL_CALLER role calls MembershipFactory::updateDAOMembership and passes a new TierConfig that only includes 1 tier, tier 1. This DAO now has a minted count of 0 users, even though Bob had minted a tier 2 membership from the last implementation.

See the below Foundry test which outlines this:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
import {Test, console} from "forge-std/Test.sol";
import "../contracts/OWPIdentity.sol";
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "../lib/forge-std/src/mocks/MockERC20.sol";
import {MembershipFactory} from "../contracts/dao/MembershipFactory.sol";
import "../contracts/dao/CurrencyManager.sol";
import {MembershipERC1155} from "../contracts/dao/tokens/MembershipERC1155.sol";
import "../contracts/dao/libraries/MembershipDAOStructs.sol";
contract IdTest is Test {
OWPIdentity public identity;
MembershipFactory public factory;
CurrencyManager public currencyManager;
MockERC20 public weth;
MembershipERC1155 public membership;
address public admin;
address public minter;
address public alice;
address public bob;
address public owpWallet;
function setUp() public {
admin = makeAddr("admin");
minter = makeAddr("minter");
alice = makeAddr("alice");
bob = makeAddr("bob");
owpWallet = makeAddr("owpWallet");
identity = new OWPIdentity(admin, minter, "https://example.com/");
vm.startPrank(admin);
weth = new MockERC20();
weth.initialize("WrappedEth", "weth", 18);
vm.stopPrank();
vm.startPrank(admin);
currencyManager = new CurrencyManager();
currencyManager.addCurrency(address(weth));
vm.stopPrank();
vm.startPrank(admin);
membership = new MembershipERC1155();
vm.stopPrank();
vm.startPrank(admin);
factory =
new MembershipFactory(address(currencyManager), owpWallet, "https://example.com/", address(membership));
vm.stopPrank();
}
function test_badMintCount() public {
TierConfig[] memory tiersInput = new TierConfig[]();
tiersInput[0] = TierConfig({amount: 10, price: 10, power: 2, minted: 0});
tiersInput[1] = TierConfig({amount: 20, price: 5, power: 1, minted: 0});
DAOInputConfig memory daoInputConfig = DAOInputConfig("daoPublic1", DAOType.PUBLIC, address(weth), 30, 2);
vm.startPrank(alice);
address aliceDao = factory.createNewDAOMembership(daoInputConfig, tiersInput);
vm.stopPrank();
vm.startPrank(bob);
address daoCheck = factory.userCreatedDAOs(alice, "daoPublic1");
assertEq(daoCheck, aliceDao);
deal(address(weth), bob, 100);
IERC20(weth).approve(address(factory), 100);
factory.joinDAO(daoCheck, 1); //joining the second tier
uint256 nftBalance = MembershipERC1155(daoCheck).balanceOf(bob, 1);
assertEq(nftBalance, 1);
vm.stopPrank();
//bob is now a member of alices DAO, and alices DAO has a total minted count of 1
TierConfig[] memory tiersReturned = new TierConfig[]();
tiersReturned = factory.tiers(address(daoCheck));
uint256 totalMinted;
console.log("tiersReturned length BEFORE: ", tiersReturned.length);
for (uint256 i = 0; i < tiersReturned.length; i++) {
totalMinted += tiersReturned[i].minted;
}
assertEq(tiersReturned.length, 2); //2 tiers
assertEq(totalMinted, 1);//1 minted NFT (bob)
//now we will update the DAO to have 1 tier instead of 2 tiers
TierConfig[] memory tiersInputNew = new TierConfig[]();
tiersInputNew[0] = TierConfig({amount: 5, price: 25, power: 5, minted: 0});
vm.startPrank(admin);
factory.updateDAOMembership("daoPublic1", tiersInputNew);
vm.stopPrank();
//now lets check the total minted count of the DAO again
TierConfig[] memory tiersReturnedSecond = new TierConfig[]();
tiersReturnedSecond = factory.tiers(address(daoCheck));
uint256 totalMintedSecond;
for (uint256 i = 0; i < tiersReturnedSecond.length; i++) {
totalMintedSecond += tiersReturnedSecond[i].minted;
}
assertEq(tiersReturnedSecond.length, 1); //reduced to 1 tier
assertEq(totalMintedSecond, 0); //no minted NFTs found, although bob had minted one
}
}

Impact

If being a member of a removed tier, should still keep you as a member of the DAO, then the minted count that is calculated is incorrect.
If being a member of a removed tier, should remove your membership from the DAO, then that is not fair, as that user paid a price for that membership slot that they are not getting back.

Tools Used

Manual Review

Recommendations

The fix is non-trivial as business choices must be made as to whether tiers of DAOs should be able to be 'removed'. An option could be to only allow additional tiers to be added. An option could be to not allow the updating of DAO TierConfigs once created.

Updates

Lead Judging Commences

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

Support

FAQs

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