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) {
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);
}
Impact Description
Economic Manipulation: Users can maximize profits by concentrating holdings in tier 0
Unfair Distribution: Lower tier holders receive disproportionately small rewards
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) {
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 {
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);
_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 {
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 {
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:
Weight calculation in tier 0 (64x) vs lower tiers
Profit distribution based on these weights
Disproportionate rewards despite fewer tokens held
forge test --match-path test/ProfitManipulationTest.sol -vvv
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);
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));
MockERC20(address(currency)).mint(admin, 10000 ether);
vm.stopPrank();
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;
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:
An attacker mints just 1 token in Tier 0 (highest tier)
Regular users mint multiple tokens in lower tiers (Tier 3, Tier 4)
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
Tier Upgrade Vulnerability
Users can exploit tier 0 to underflow to max tier
Breaks tier hierarchy system
Allows unauthorized access to higher tier privileges
Profit Distribution Vulnerability
Integer overflow in totalProfit calculation
Incorrect profit distribution to token holders
Potential loss of funds for legitimate token holders
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) {
+
+ 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);
}