Project

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

Incorrect Voting Power Distribution Due to Hardcoded Tier Weights

Summary

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.

Vulnerability Details

The vulnerability exists in two interconnected components:

  1. The createNewDAOMembership() function allows flexible tier configurations without enforcing tier order:

function createNewDAOMembership(DAOInputConfig calldata daoConfig, TierConfig[] calldata tierConfigs)
external returns (address) {
// ... validation checks ...
// No validation of tier order/priority
for (uint256 i = 0; i < tierConfigs.length; i++) {
dao.tiers.push(tierConfigs[i]);
}
}
  1. The shareOf() function has hardcoded weights that assume tier 0 is always the highest priority:

function shareOf(address account) public view returns (uint256) {
return (balanceOf(account, 0) * 64) +
(balanceOf(account, 1) * 32) +
(balanceOf(account, 2) * 16) +
(balanceOf(account, 3) * 8) +
(balanceOf(account, 4) * 4) +
(balanceOf(account, 5) * 2) +
balanceOf(account, 6);
}

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.

Impact

The vulnerability can lead to:

  1. Severe misrepresentation of voting power in DAOs with non-standard tier configurations

  2. Highest priority members receiving up to 64x less voting power than intended(reverse order)

  3. Potential manipulation of DAO governance decisions due to incorrect voting weight calculations

  4. Breaking of the intended power structure in sponsored DAOs

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.

[PASS] testTierWeightBug() (gas: 266291)
Share of highest priority tier in DAO1 (tier 0): 64
Share of highest priority tier in DAO2 (tier 6): 1

PoC:

Add this line of code in interface IMembershipERC1155

function shareOf(address account) external view returns (uint256);

Paste the following code in a new file:

// SPDX-License-Identifier: MIT
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];
}
// Add this to match ERC1155 interface
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 {
// Setup addresses
owpWallet = makeAddr("owpWallet");
user1 = makeAddr("user1");
// Deploy mock contracts
currency = new MockERC20();
currencyManager = new MockCurrencyManager();
membershipNFT = new MockMembershipERC1155();
// Setup currency
currencyManager.whitelistCurrency(address(currency));
// Deploy factory
factory = new MembershipFactory(
address(currencyManager),
owpWallet,
"baseURI/",
address(membershipNFT)
);
// Setup test DAO
daoMembershipAddr = testCreateDAO();
// Setup user balances
currency.mint(user1, 1000 ether);
vm.startPrank(user1);
currency.approve(address(factory), type(uint256).max);
vm.stopPrank();
}
function testCreateDAO() internal returns (address) {
// Create tier configs
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
});
}
// Create DAO config
DAOInputConfig memory daoConfig = DAOInputConfig({
ensname: "test-dao",
daoType: DAOType.SPONSORED,
currency: address(currency),
maxMembers: 1000,
noOfTiers: TIER_MAX
});
// Create DAO
return factory.createNewDAOMembership(daoConfig, tierConfigs);
}
function testTierWeightBug() public {
TierConfig[] memory tiersHighPriorityFirst = new TierConfig[]();
TierConfig[] memory tiersHighPriorityLast = new TierConfig[]();
// First configuration: Highest priority tier (most expensive/exclusive) is tier 0
// This is what the shareOf() function expects (tier 0 = weight 64)
for (uint256 i = 0; i < TIER_MAX; i++) {
tiersHighPriorityFirst[i] = TierConfig({
amount: 100,
price: (TIER_MAX - i) * 1 ether, // Tier 0 most expensive
power: TIER_MAX - i, // Tier 0 highest power
minted: 0
});
}
// Second configuration: Highest priority tier is tier 6
// This breaks shareOf() assumptions as tier 6 should have highest weight but gets weight 1
for (uint256 i = 0; i < TIER_MAX; i++) {
tiersHighPriorityLast[i] = TierConfig({
amount: 100,
price: (i + 1) * 1 ether, // Tier 6 most expensive
power: i + 1, // Tier 6 highest power
minted: 0
});
}
// Create two DAOs
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);
// EXPLOIT: Mint highest priority tier in both DAOs
vm.startPrank(address(factory));
// In DAO1: Mint tier 0 (highest priority, gets weight 64)
IMembershipERC1155(daoAddress1).mint(user1, 0, 1);
// In DAO2: Mint tier 6 (should be highest priority but gets weight 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); // Will be 64
console.log("Share of highest priority tier in DAO2 (tier 6):", share2); // Will be 1
// The highest priority tier in DAO2 gets 64x less voting power than it should!
assertEq(share1, 64 * share2, "Highest priority tier in DAO2 has 64x less power than DAO1!");
}
}
[PASS] testTierWeightBug() (gas: 266291)
Share of highest priority tier in DAO1 (tier 0): 64
Share of highest priority tier in DAO2 (tier 6): 1

Tools Used

  • Manual code review

Recommended Mitigation

Enforce strict tier ordering in createNewDAOMembership:

function createNewDAOMembership(DAOInputConfig calldata daoConfig, TierConfig[] calldata tierConfigs)
external returns (address) {
// ... existing checks ...
// Add tier order validation
for (uint256 i = 1; i < tierConfigs.length; i++) {
require(
tierConfigs[i-1].price >= tierConfigs[i].price,
"Tiers must be ordered by descending price"
);
}
// ... rest of the function
}

Output running the same test:

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 8.20ms (337.60μs CPU time)
Ran 1 test suite in 951.37ms (8.20ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in contracts/dao/Test2.t.sol:MembershipFactoryTest
[FAIL: revert: Tiers must be ordered by descending price] testTierWeightBug() (gas: 1313902)
Updates

Lead Judging Commences

0xbrivan2 Lead Judge
about 1 year ago
0xbrivan2 Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

echokly Submitter
about 1 year ago
0xbrivan2 Lead Judge
about 1 year ago
0xbrivan2 Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!