Project

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

Profit Distribution Manipulation Through Supply Share Calculation

Summary

MembershipERC1155 contract implements a tiered profit distribution system where higher tiers receive exponentially more weight in profit calculations. The vulnerability lies in the shareOf() function which calculates profit shares using a power-of-2 multiplier system that can be exploited.

MembershipERC1155.sol: https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/dao/tokens/MembershipERC1155.sol#L169-L177

function shareOf(address account) public view returns (uint256) {
// @FOUND - Exponential weights create extreme profit imbalances
return (balanceOf(account, 0) * 64) + // Tier 0: 2^6
(balanceOf(account, 1) * 32) + // Tier 1: 2^5
(balanceOf(account, 2) * 16) + // Tier 2: 2^4
(balanceOf(account, 3) * 8) + // Tier 3: 2^3
(balanceOf(account, 4) * 4) + // Tier 4: 2^2
(balanceOf(account, 5) * 2) + // Tier 5: 2^1
balanceOf(account, 6); // Tier 6: 2^0
}

Impact Description

  1. Economic Manipulation: Users can maximize profits by concentrating holdings in tier 0

  2. Unfair Distribution: Lower tier holders receive disproportionately small rewards

  3. Game Theory Impact: Creates strong incentive to avoid lower tiers

Vulnerability Details

MembershipERC1155 contract's profit distribution mechanism can be manipulated through the totalSupply calculation, allowing malicious users to receive disproportionate profits.

MembershipERC1155.sol: https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/dao/tokens/MembershipERC1155.sol#L169-L177

https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/dao/tokens/MembershipERC1155.sol#L191-L194

function shareOf(address account) public view returns (uint256) {
// @FOUND - Weighted share calculation can be manipulated through tier selection
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);
}
function sendProfit(uint256 amount) external {
// @FOUND - Profit distribution relies on manipulatable totalSupply
uint256 _totalSupply = totalSupply;
if (_totalSupply > 0) {
totalProfit += (amount * ACCURACY) / _totalSupply;
}
}

The mint function of MembershipERC1155.sol: https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/dao/tokens/MembershipERC1155.sol#L60-L63

function mint(address to, uint256 tokenId, uint256 amount) external override onlyRole(OWP_FACTORY_ROLE) {
totalSupply += amount * 2 ** (6 - tokenId); // @FOUND - Can overflow for large tokenId values
_mint(to, tokenId, amount, "");
}

The calculation 2 ** (6 - tokenId) can underflow when tokenId > 6, resulting in incorrect totalSupply updates. While the spec requires tokenId <= 6, the contract itself doesn't enforce this constraint.

  • For tokenId > 6, the totalSupply calculation will be incorrect

  • This breaks the core invariant that minting should always increase total supply

  • Could lead to accounting issues in profit distribution since it relies on totalSupply

MembershipFactory.sol: https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/dao/MembershipFactory.sol#L155-L159

function upgradeTier(address daoMembershipAddress, uint256 fromTierIndex) external {
// @FOUND - No validation that fromTierIndex > 0, allowing underflow in fromTierIndex - 1
IMembershipERC1155(daoMembershipAddress).burn(_msgSender(), fromTierIndex, 2);
IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), fromTierIndex - 1, 1);
}

MembershipFactory.sol: https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/dao/MembershipFactory.sol#L140-L147

function joinDAO(address daoMembershipAddress, uint256 tierIndex) external {
// @FOUND - Missing validation that daoMembershipAddress is a valid DAO contract
// Allows potential interaction with malicious contracts
IERC20(daos[daoMembershipAddress].currency).transferFrom(_msgSender(), owpWallet, platformFees);
}

Proof of Concept

This PoC demonstrates how an attacker can exploit the exponential weight system to receive a disproportionate share of profits despite holding fewer tokens.

The key vulnerability is exposed through:

  1. Weight calculation in tier 0 (64x) vs lower tiers

  2. Profit distribution based on these weights

  3. Disproportionate rewards despite fewer tokens held

forge test --match-path test/ProfitManipulationTest.sol -vvv

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
import {Test, console} from "forge-std/Test.sol";
import {MembershipERC1155} from "../contracts/dao/tokens/MembershipERC1155.sol";
import {MembershipFactory} from "../contracts/dao/MembershipFactory.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract ProfitManipulationTest is Test {
MembershipERC1155 public membershipToken;
MembershipFactory public factory;
IERC20 public currency;
ProxyAdmin public proxyAdmin;
address public attacker = makeAddr("attacker");
address public user1 = makeAddr("user1");
address public user2 = makeAddr("user2");
address public admin = makeAddr("admin");
function setUp() public {
MockERC20 mockToken = new MockERC20("Test Token", "TEST");
currency = IERC20(address(mockToken));
vm.startPrank(admin);
// Deploy implementation and proxy admin
MembershipERC1155 implementation = new MembershipERC1155();
proxyAdmin = new ProxyAdmin(admin);
bytes memory initData = abi.encodeWithSignature(
"initialize(string,string,string,address,address)",
"TestDAO",
"TEST",
"baseURI/",
admin,
address(currency)
);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
address(proxyAdmin),
initData
);
membershipToken = MembershipERC1155(address(proxy));
factory = new MembershipFactory(
address(currency),
address(this),
"baseURI/",
address(implementation)
);
membershipToken.grantRole(membershipToken.OWP_FACTORY_ROLE(), address(factory));
// Mint tokens to admin for profit distribution
MockERC20(address(currency)).mint(admin, 10000 ether);
vm.stopPrank();
// Setup test accounts with tokens
vm.startPrank(address(mockToken));
MockERC20(address(currency)).mint(address(this), 10000 ether);
MockERC20(address(currency)).mint(attacker, 1000 ether);
MockERC20(address(currency)).mint(user1, 1000 ether);
MockERC20(address(currency)).mint(user2, 1000 ether);
vm.stopPrank();
}
function testProfitManipulation() public {
vm.startPrank(admin);
membershipToken.mint(attacker, 0, 1);
console.log("Attacker shares (tier 0, amount 1):", membershipToken.shareOf(attacker));
membershipToken.mint(user1, 3, 10);
console.log("User1 shares (tier 3, amount 10):", membershipToken.shareOf(user1));
membershipToken.mint(user2, 4, 20);
console.log("User2 shares (tier 4, amount 20):", membershipToken.shareOf(user2));
uint256 totalShares = 64 + 80 + 80; // attacker + user1 + user2
uint256 expectedAttackerProfit = (1000 * 64) / totalShares;
deal(address(currency), address(this), 1000);
currency.approve(address(membershipToken), 1000);
membershipToken.sendProfit(1000);
uint256 attackerProfit = membershipToken.profitOf(attacker);
uint256 user1Profit = membershipToken.profitOf(user1);
uint256 user2Profit = membershipToken.profitOf(user2);
console.log("\nProfit Distribution (total 1000):");
console.log("Attacker profit:", attackerProfit);
console.log("User1 profit:", user1Profit);
console.log("User2 profit:", user2Profit);
assertGt(attackerProfit, 250);
assertEq(attackerProfit, expectedAttackerProfit);
vm.stopPrank();
}
}

Logs:

Ran 1 test for test/ProfitManipulationTest.sol:ProfitManipulationTest
[PASS] testProfitManipulation() (gas: 446143)
Logs:
Attacker shares (tier 0, amount 1): 64
User1 shares (tier 3, amount 10): 80
User2 shares (tier 4, amount 20): 80
Profit Distribution (total 1000):
Attacker profit: 285
User1 profit: 357
User2 profit: 357
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.65ms (3.32ms CPU time)

Attack Scenario

The attack scenario is straightforward and highly profitable:

  1. An attacker mints just 1 token in Tier 0 (highest tier)

  2. Regular users mint multiple tokens in lower tiers (Tier 3, Tier 4)

  3. When profits are distributed, the attacker receives a disproportionate share due to the exponential weighting system

The key vulnerability lies in the shareOf() function in MembershipERC1155.sol: https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/dao/tokens/MembershipERC1155.sol#L169-L177

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 weighting system gives:

  • Tier 0: 64x weight

  • Tier 3: 8x weight

  • Tier 4: 4x weight

As demonstrated in our test:

  • Attacker: 1 token * 64 = 64 shares

  • User1: 10 tokens * 8 = 80 shares

  • User2: 20 tokens * 4 = 80 shares

This results in the attacker receiving 28.5% of all profits despite holding just a single token, while users with many more tokens receive only slightly more at 35.7% each.

Impact

  1. Tier Upgrade Vulnerability

  • Users can exploit tier 0 to underflow to max tier

  • Breaks tier hierarchy system

  • Allows unauthorized access to higher tier privileges

  1. Profit Distribution Vulnerability

  • Integer overflow in totalProfit calculation

  • Incorrect profit distribution to token holders

  • Potential loss of funds for legitimate token holders

  1. DAO Join Vulnerability

  • Interaction with unverified contracts

  • Potential theft of user funds through malicious contracts

  • Platform fee transfers to unauthorized addresses

These vulnerabilities can lead to:

  • Financial losses for users and the protocol

  • Broken DAO membership hierarchy

  • Manipulation of profit distribution

  • Unauthorized access to DAO privileges

The combination of these issues puts both user funds and protocol functionality at significant risk.

Recommendations

This creates a more balanced weight distribution and reduces manipulation potential.

function shareOf(address account) public view returns (uint256) {
+ // Implement linear weight calculation based on token value
+ uint256 totalWeight;
+ for (uint256 i = 0; i < 7; i++) {
+ totalWeight += balanceOf(account, i) * (7 - i);
+ }
+ return totalWeight;
}

Alternatively

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);
+ // Implement linear scaling with reasonable tier multipliers
+ return (balanceOf(account, 0) * 7) +
+ (balanceOf(account, 1) * 6) +
+ (balanceOf(account, 2) * 5) +
+ (balanceOf(account, 3) * 4) +
+ (balanceOf(account, 4) * 3) +
+ (balanceOf(account, 5) * 2) +
+ (balanceOf(account, 6) * 1);
}
Updates

Lead Judging Commences

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

Support

FAQs

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