**For the `executeMetaTransaction` issue, please focus only at the DOS scenario in this report.
The rest of this report raises a lack of validation to comply with the under `joinDAO()` check/invariant, if there was validation implemented (see: Recommendation 1) within `updateDAOMembership`, this edge case wouldn't even be possible for the `executeMetaTransaction` issue.**
More context:
1. Sig to update a DAO config that sets `amount` value to 100 for each TierConfig.
**call fails - nonce didn't increment**
2. After a while, minted has significantly increased due to the popularity of the DAO value of minted tokens has doubled, and is based on the current used configuration.
3. An attacker replay the sig that will modify the current DAO config to the outdated config of a 100 `amount` value. Which permanently DOS the `joinDAO` function.
## Incorrect amount validation in `updateDAOMembership` enables permanent DOS
### Summary
When updating DAO tier configurations, the `updateDAOMembership` function fails to validate that new tier amounts are greater than or equal to the number of already minted tokens. This allows tier amounts to be set below currently minted values, permanently blocking new mints due to the minted counter never decreasing.
### Vulnerability Details
Let's take a look at how `updateDAOMembership()` handles tier updates:
```solidity
function updateDAOMembership(string calldata ensName, TierConfig[] memory tierConfigs)
external onlyRole(EXTERNAL_CALLER) returns (address) {
// ...
// Preserve minted values but without validation
for (uint256 i = 0; i < tierConfigs.length; i++) {
if (i < dao.tiers.length) {
tierConfigs[i].minted = dao.tiers[i].minted; // @audit: Preserves minted without checking new amount
}
}
delete dao.tiers;
for (uint256 i = 0; i < tierConfigs.length; i++) {
dao.tiers.push(tierConfigs[i]); // @audit: No validation amount >= minted
maxMembers += tierConfigs[i].amount;
}
// ...
}
```
***NOTE: any value set for `.amount` that is under `minted` amount is enough to trigger the DOS***
\
**DOS Scenario:**
1. Initial state:
\=> Tier 1 has amount=50, minted=40 
2. Admin updates configuration:
\=> Sets new amount=35 for Tier 1
\=> minted stays at 40 (previous value)
3. Random user call `joinDAO` function
```solidity
function joinDAO(address daoMembershipAddress, uint256 tierIndex) external {
require(daos[daoMembershipAddress].tiers[tierIndex].amount >
daos[daoMembershipAddress].tiers[tierIndex].minted, "Tier full.");
// ...
}
```
1. joinDAO() check fails:
\=> require(35 > 40, "Tier full") // Always reverts
**Moreover `minted` never decreases**, meaning that the DOS would remain permanently even if Tokens are burned, this is actually a second critical issue that makes the DOS permanent without the intervention of another update. So the main issue is that the function would never be available.
### Impact
Permanent DOS on one of the protocol's main functionality (`joinDAO`) due to lack of validation coupled of another issue which is `minted` variable never decrement, **even when tokens are being burned**.
### Tools Used
Manual review
### Recommendations
1. The main recommendation would be to validate the `amount` field to ensure it doesn't break the invariant within the `joinDAO` function.
2. Making sure to apply state changes for `mint` field to be decreased accordingly when tokens are being burned, ideally track also those who are directly burned on the `MembershipERC1155` contract.
```diff
function updateDAOMembership(string calldata ensName, TierConfig[] memory tierConfigs) {
for (uint256 i = 0; i < tierConfigs.length; i++) {
if (i < dao.tiers.length) {
tierConfigs[i].minted = dao.tiers[i].minted;
+ require(tierConfigs[i].amount >= tierConfigs[i].minted,
+ "New amount must be >= minted");
}
dao.tiers.push(tierConfigs[i]);
}
}
/// @notice Allows users to upgrade their tier within a sponsored DAO
/// @param daoMembershipAddress The address of the DAO membership NFT
/// @param fromTierIndex The current tier index of the user
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);
+ daos[daoMembershipAddress].tiers[fromTierIndex].minted -= 2;
IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), fromTierIndex - 1, 1);
+ daos[daoMembershipAddress].tiers[fromTierIndex - 1].minted += 1;
emit UserJoinedDAO(_msgSender(), daoMembershipAddress, fromTierIndex - 1);
}
```