Summary
The MembershipERC1155 contract experiences precision loss during profit calculations due to the interaction between weighted token shares and profit distribution scaling. This affects profit distribution accuracy, particularly for holders of higher-tier tokens (those with lower weights) where the multiplication factors in shareOf() can lead to larger accumulated rounding errors when combined with the ACCURACY scaling factor in profit calculations.
The precision loss originates from two interacting mechanisms:
The weighted share calculation in shareOf() which uses different powers of 2 (64, 32, 16, etc.)
The subsequent use of these weighted shares in profit calculations with ACCURACY (1e30) scaling
https://github.com/Cyfrin/2024-11-one-world/blob/main/contracts/dao/tokens/MembershipERC1155.sol#L169
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 getUnsaved(address account) internal view returns (uint256 profit) {
return ((totalProfit - lastProfit[account]) * shareOf(account)) / ACCURACY;
}
When this weighted share is used in getUnsaved(), the multiplication and subsequent division by ACCURACY can cause rounding errors to accumulate, especially for higher token tiers where the weights are smaller.
Recommended Fix
contract MembershipERC1155 {
uint256 private constant WEIGHT_SCALE = 1e6;
function shareOf(address account) public view returns (uint256) {
return (balanceOf(account, 0) * 64 * WEIGHT_SCALE) +
(balanceOf(account, 1) * 32 * WEIGHT_SCALE) +
(balanceOf(account, 2) * 16 * WEIGHT_SCALE) +
(balanceOf(account, 3) * 8 * WEIGHT_SCALE) +
(balanceOf(account, 4) * 4 * WEIGHT_SCALE) +
(balanceOf(account, 5) * 2 * WEIGHT_SCALE) +
(balanceOf(account, 6) * WEIGHT_SCALE);
}
function getUnsaved(address account) internal view returns (uint256 profit) {
uint256 share = shareOf(account);
uint256 profitDifference = totalProfit - lastProfit[account];
uint256 scaledShare = share / WEIGHT_SCALE;
return (profitDifference * scaledShare) / ACCURACY;
}
}
This solution better preserves precision across different token tiers while maintaining the intended weight ratios in profit distribution.
Scenario
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 getUnsaved(address account) internal view returns (uint256 profit) {
return ((totalProfit - lastProfit[account]) * shareOf(account)) / ACCURACY;
}
Let's consider these holders:
Alice holds 1 token of tier 6 (weight: 1)
Bob holds 1 token of tier 0 (weight: 64)
Charlie holds 2 tokens of tier 3 (weight: 8 * 2 = 16)
Total weighted supply = 81 (64 + 1 + 16)
When 100 tokens are distributed:
totalProfit += (100 * 1e30) / 81
Alice (tier 6):
(100 * 1e30 * 1) / (81 * 1e30) = 1.234567... tokens
Actual result: 1 tokens (lost 0.234567... due to division)
Bob (tier 0):
(100 * 1e30 * 64) / (81 * 1e30) = 79.012345... tokens
Actual result: 79 tokens (lost 0.012345...)
Charlie (tier 3):
(100 * 1e30 * 16) / (81 * 1e30) = 19.753086... tokens
Actual result: 19 tokens (lost 0.753086...)
Total distributed: 99 tokens (1 token lost to rounding)
Fixed Version Scenario
uint256 private constant WEIGHT_SCALE = 1e6;
function shareOf(address account) public view returns (uint256) {
return (balanceOf(account, 0) * 64 * WEIGHT_SCALE) +
(balanceOf(account, 1) * 32 * WEIGHT_SCALE) +
(balanceOf(account, 2) * 16 * WEIGHT_SCALE) +
(balanceOf(account, 3) * 8 * WEIGHT_SCALE) +
(balanceOf(account, 4) * 4 * WEIGHT_SCALE) +
(balanceOf(account, 5) * 2 * WEIGHT_SCALE) +
(balanceOf(account, 6) * WEIGHT_SCALE);
}
function getUnsaved(address account) internal view returns (uint256 profit) {
uint256 share = shareOf(account);
uint256 profitDifference = totalProfit - lastProfit[account];
uint256 scaledShare = share / WEIGHT_SCALE;
return (profitDifference * scaledShare) / ACCURACY;
}
Same holders:
Alice: 1 * WEIGHT_SCALE (tier 6)
Bob: 64 * WEIGHT_SCALE (tier 0)
Charlie: 16 * WEIGHT_SCALE (tier 3)
When 100 tokens are distributed:
Alice (tier 6):
share = 1 * 1e6 = 1,000,000
scaledShare = 1
Result: 1.234567 tokens (preserved more decimals)
Bob (tier 0):
share = 64 * 1e6 = 64,000,000
scaledShare = 64
Result: 79.012345 tokens (preserved more decimals)
Charlie (tier 3):
share = 16 * 1e6 = 16,000,000
scaledShare = 16
Result: 19.753086 tokens (preserved more decimals)
Total distributed: 100 tokens (minimized rounding loss)
The fixed version:
Preserves more precision during intermediate calculations by scaling the weights up first
Reduces cumulative rounding errors in the final distribution
Ensures more accurate proportional distribution especially for smaller weight tokens
Maintains better precision when multiple distributions occur over time
The key difference is that the fixed version handles the scaling of weights separately from the profit calculations, allowing for better precision preservation throughout the calculation steps. This becomes especially important when dealing with multiple token tiers and frequent profit distributions.