In the MembershipFactory contract, when users upgrade their tier membership, the tiers[tierIndex].minted counter is not decremented when tokens are burned. This leads to a permanent desynchronization between the actual number of tokens in circulation and the tracked minted amount, eventually causing tiers to appear full when they still have available capacity.
This check becomes inaccurate as the minted counter is never decremented during burns.
Paste the following code in a new File.
pragma solidity 0.8.22;
import {Test, console} from "forge-std/Test.sol";
import {MembershipFactory} from "../dao/MembershipFactory.sol";
import {IMembershipERC1155} from "../dao/interfaces/IERC1155Mintable.sol";
import {DAOConfig, DAOInputConfig, TierConfig, DAOType, TIER_MAX} from "../dao/libraries/MembershipDAOStructs.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockERC20 is IERC20 {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
function mint(address account, uint256 amount) public {
_totalSupply += amount;
_balances[account] += amount;
}
function totalSupply() external view returns (uint256) { return _totalSupply; }
function balanceOf(address account) external view returns (uint256) { return _balances[account]; }
function transfer(address to, uint256 amount) external returns (bool) {
_balances[msg.sender] -= amount;
_balances[to] += amount;
return true;
}
function allowance(address owner, address spender) external view returns (uint256) { return _allowances[owner][spender]; }
function approve(address spender, uint256 amount) external returns (bool) {
_allowances[msg.sender][spender] = amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
require(_allowances[from][msg.sender] >= amount, "ERC20: insufficient allowance");
_allowances[from][msg.sender] -= amount;
_balances[from] -= amount;
_balances[to] += amount;
return true;
}
}
contract MockMembershipERC1155 is IMembershipERC1155 {
mapping(address => mapping(uint256 => uint256)) public balances;
function initialize(
string memory _name,
string memory _symbol,
string memory _baseURI,
address _owner,
address _currency
) external {}
function mint(address to, uint256 id, uint256 amount) external {
balances[to][id] += amount;
}
function burn(address from, uint256 id, uint256 amount) external {
require(balances[from][id] >= amount, "Insufficient balance");
balances[from][id] -= amount;
}
}
contract MockCurrencyManager {
mapping(address => bool) public whitelistedCurrencies;
function whitelistCurrency(address currency) external {
whitelistedCurrencies[currency] = true;
}
function isCurrencyWhitelisted(address currency) external view returns (bool) {
return whitelistedCurrencies[currency];
}
}
contract MembershipFactoryTest is Test {
MembershipFactory factory;
MockERC20 currency;
MockCurrencyManager currencyManager;
MockMembershipERC1155 membershipNFT;
address owpWallet;
address user1;
address daoMembershipAddr;
function setUp() public {
owpWallet = makeAddr("owpWallet");
user1 = makeAddr("user1");
currency = new MockERC20();
currencyManager = new MockCurrencyManager();
membershipNFT = new MockMembershipERC1155();
currencyManager.whitelistCurrency(address(currency));
factory = new MembershipFactory(
address(currencyManager),
owpWallet,
"baseURI/",
address(membershipNFT)
);
daoMembershipAddr = testCreateDAO();
currency.mint(user1, 1000 ether);
vm.startPrank(user1);
currency.approve(address(factory), type(uint256).max);
vm.stopPrank();
}
function testCreateDAO() internal returns (address) {
TierConfig[] memory tierConfigs = new TierConfig[]();
for (uint256 i = 0; i < TIER_MAX; i++) {
uint256 amount = (i == 4) ? 2 : 100;
tierConfigs[i] = TierConfig({
amount: 100,
price: 1 ether,
power: i + 1,
minted: 0
});
}
DAOInputConfig memory daoConfig = DAOInputConfig({
ensname: "test-dao",
daoType: DAOType.SPONSORED,
currency: address(currency),
maxMembers: 1000,
noOfTiers: TIER_MAX
});
return factory.createNewDAOMembership(daoConfig, tierConfigs);
}
function testBurnDoesNotDecrementMinted() public {
uint256 fromTierIndex = 5;
vm.startPrank(user1);
console.log("\n1. Initial state for tier 5:");
console.log("Initial tier 5 minted:", factory.getTierMinted(daoMembershipAddr, fromTierIndex));
factory.joinDAO(daoMembershipAddr, fromTierIndex);
factory.joinDAO(daoMembershipAddr, fromTierIndex);
uint256 mintedBeforeBurn = factory.getTierMinted(daoMembershipAddr, fromTierIndex);
console.log("\n2. After minting 2 tokens:");
console.log("Tier 5 minted count:", mintedBeforeBurn);
console.log("\n3. Performing upgrade (burns 2 tokens)");
factory.upgradeTier(daoMembershipAddr, fromTierIndex);
uint256 mintedAfterBurn = factory.getTierMinted(daoMembershipAddr, fromTierIndex);
console.log("\n4. After burning 2 tokens:");
console.log("Tier 5 minted count:", mintedAfterBurn);
assertEq(mintedAfterBurn, mintedBeforeBurn,
"Bug not demonstrated: minted count actually changed!");
uint256 tierAmount = factory.getTierAmount(daoMembershipAddr, fromTierIndex);
console.log("\n5. Tier state:");
console.log("Tier 5 'minted' count:", mintedAfterBurn);
console.log("Tokens were burned but minted count wasn't decremented!");
vm.stopPrank();
}
Test output demonstrates the counter remaining at 2 even after tokens are burned: