Summary
The MembershipERC1155 contract inherits from AccessControlUpgradeable but fails to implement access control on the critical sendProfit function. This oversight allows any external actor to manipulate the profit distribution system.
https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/dao/tokens/MembershipERC1155.sol#L191-L200
Vulnerability Details
The contract implements AccessControlUpgradeable and defines roles:
contract MembershipERC1155 is ERC1155Upgradeable, AccessControlUpgradeable {
bytes32 public constant OWP_FACTORY_ROLE = keccak256("OWP_FACTORY_ROLE");
bytes32 public constant DAO_CREATOR = keccak256("DAO_CREATOR");
function sendProfit(uint256 amount) external {
uint256 _totalSupply = totalSupply;
if (_totalSupply > 0) {
totalProfit += (amount * ACCURACY) / _totalSupply;
IERC20(currency).safeTransferFrom(msg.sender, address(this), amount);
emit Profit(amount);
}
}
}
Attack Vectors
Profit Manipulation Attack
contract ProfitManipulator {
MembershipERC1155 target;
function attack(uint256 amount) external {
target.sendProfit(amount);
}
}
Supply-Based Attack
Monitor for low totalSupply periods
Exploit the division in (amount * ACCURACY) / _totalSupply
Maximize profit manipulation impact
Impact
Direct manipulation of profit calculations
Unauthorized redistribution of profits
Compromises the entire profit-sharing mechanism
Proof of Impact
Initial state:
totalSupply = 1000 tokens
totalProfit = 100 ether
Attack:
amount = 10 ether
New totalProfit = 100 ether + (10 ether * 1e30) / 1000
Tools Used
-
SolidityScan
-
Manual Code Review
Pattern matching with known vulnerabilities (e.g., Perennial's Issue H-5)
Control flow analysis
Access control verification
Recommendations
Immediate Fix
contract MembershipERC1155 is ERC1155Upgradeable, AccessControlUpgradeable {
bytes32 public constant PROFIT_DISTRIBUTOR_ROLE = keccak256("PROFIT_DISTRIBUTOR_ROLE");
function sendProfit(uint256 amount) external onlyRole(PROFIT_DISTRIBUTOR_ROLE) {
require(amount > 0 && amount <= maxProfitAmount, "Invalid profit amount");
require(block.timestamp >= lastProfitTime + minProfitInterval, "Too frequent");
uint256 _totalSupply = totalSupply;
require(_totalSupply >= minTotalSupply, "Supply too low");
if (_totalSupply > 0) {
totalProfit += (amount * ACCURACY) / _totalSupply;
IERC20(currency).safeTransferFrom(msg.sender, address(this), amount);
emit Profit(amount);
lastProfitTime = block.timestamp;
}
}
}