A critical design flaw in the DAO membership system allows for voting power manipulation due to hardcoded tier weights in the shareOf() function that don't align with the flexible tier configuration system. When creating a DAO with inverted tier priorities (e.g., tier 6 as highest priority), the voting power calculation becomes severely incorrect, leading to significant governance implications.
The issue arises because the protocol assumes tier 0 will always be the highest priority tier (weight 64) and tier 6 the lowest (weight 1), but the createNewDAOMembership function doesn't enforce this ordering. A DAO can be created with reversed priority (tier 6 as highest), leading to incorrect voting power calculations.
As shown in the PoC, a tier 6 member (highest priority) receives a weight of 1 instead of 64, while a tier 0 member (should be lowest priority in this configuration) receives a weight of 64.
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";
import {MembershipERC1155} from "../dao/tokens/MembershipERC1155.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;
}
function shareOf(address account) external view returns (uint256) {
return (balances[account][0] * 64) +
(balances[account][1] * 32) +
(balances[account][2] * 16) +
(balances[account][3] * 8) +
(balances[account][4] * 4) +
(balances[account][5] * 2) +
balances[account][6];
}
function balanceOf(address account, uint256 id) external view returns (uint256) {
return balances[account][id];
}
}
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 testTierWeightBug() public {
TierConfig[] memory tiersHighPriorityFirst = new TierConfig[]();
TierConfig[] memory tiersHighPriorityLast = new TierConfig[]();
for (uint256 i = 0; i < TIER_MAX; i++) {
tiersHighPriorityFirst[i] = TierConfig({
amount: 100,
price: (TIER_MAX - i) * 1 ether,
power: TIER_MAX - i,
minted: 0
});
}
for (uint256 i = 0; i < TIER_MAX; i++) {
tiersHighPriorityLast[i] = TierConfig({
amount: 100,
price: (i + 1) * 1 ether,
power: i + 1,
minted: 0
});
}
DAOInputConfig memory daoConfig1 = DAOInputConfig({
ensname: "high-priority-first",
daoType: DAOType.SPONSORED,
currency: address(currency),
maxMembers: 1000,
noOfTiers: TIER_MAX
});
DAOInputConfig memory daoConfig2 = DAOInputConfig({
ensname: "high-priority-last",
daoType: DAOType.SPONSORED,
currency: address(currency),
maxMembers: 1000,
noOfTiers: TIER_MAX
});
address daoAddress1 = factory.createNewDAOMembership(daoConfig1, tiersHighPriorityFirst);
address daoAddress2 = factory.createNewDAOMembership(daoConfig2, tiersHighPriorityLast);
vm.startPrank(address(factory));
IMembershipERC1155(daoAddress1).mint(user1, 0, 1);
IMembershipERC1155(daoAddress2).mint(user1, 6, 1);
vm.stopPrank();
IMembershipERC1155 dao1 = IMembershipERC1155(daoAddress1);
IMembershipERC1155 dao2 = IMembershipERC1155(daoAddress2);
uint256 share1 = dao1.shareOf(user1);
uint256 share2 = dao2.shareOf(user1);
console.log("Share of highest priority tier in DAO1 (tier 0):", share1);
console.log("Share of highest priority tier in DAO2 (tier 6):", share2);
assertEq(share1, 64 * share2, "Highest priority tier in DAO2 has 64x less power than DAO1!");
}
}