Project

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

Post-Distribution Minting Allows Users to Capture More Profit Than Intended, potentially diluting other users' profit

Summary

Users who increase their holdings after profit distributions are made will receive a larger share of future profits, despite not contributing proportionally to past distributions.

Vulnerability Details

Let's assume userA and userB gets 100 tokens with tokenId=0 through the mint() when joining a DAO:

function mint(address to, uint256 tokenId, uint256 amount) external override onlyRole(OWP_FACTORY_ROLE) {
totalSupply += amount * 2 ** (6 - tokenId); // Update total supply with weight
_mint(to, tokenId, amount, "");
}

Assuming totalsupply is 0, Let's now go step by step:

Step 1:

  • userA mints 100 tokens with tokenId = 0 (weight = 64).

  • Increase in totalSupply: 100 × 64 = 6400`

  • userB also does the same thing, which will make totalSupply equal to 12800

Step 2:

  • sendProfit(50) is called, distributing 50 tokens as profit.

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);
} else {
IERC20(currency).safeTransferFrom(msg.sender, creator, amount); // Redirect profit to creator if no supply
}
}
  • totalProfit = 50 × 10^30 / 12800 = 0.00390625 × 10^30. - sototalProfitis now0.00390625 × 10^30`.

Step 3:

  • userA claims their profit by calling claimProfit() method, which makes a chain of calls. Let's focus on the last function getUnsaved():

function claimProfit() external returns (uint256 profit) {
profit = saveProfit(msg.sender);
require(profit > 0, "No profit available");
savedProfit[msg.sender] = 0;
IERC20(currency).safeTransfer(msg.sender, profit);
emit Claim(msg.sender, profit);
}
function saveProfit(address account) internal returns (uint256 profit) {
uint256 unsaved = getUnsaved(account);
lastProfit[account] = totalProfit;
profit = savedProfit[account] + unsaved;
savedProfit[account] = profit;
}
function getUnsaved(address account) internal view returns (uint256 profit) {
return ((totalProfit - lastProfit[account]) * shareOf(account)) / ACCURACY;
}
  • lastProfit[userA] is initially 0 (first claim).

  • The unsaved profit for userA is:unsaved = (0.00390625 × 10^30 - 0) × (100 × 64) / 10^30 = 25 tokens

  • userA receives 25 tokens.

  • lastProfit[userA] is updated to totalProfit(in saveProfit() method), which is 0.00390625 × 10^30.

Step 4:

  • userA mints another 100 tokens with tokenId = 0 (weight = 64).

  • Increase in totalSupply:12800 + 100 × 64 = 19200

  • userA now holds a total of 200 tokens, while userB still holds 100 tokens. totalSupply is now 19,200.

Step 5:

  • The platform calls sendProfit(50) again to distribute another 50 tokens.Here we are assuming no one joined the DAO for simplicity

  • The new profit per token is:
    profitPerToken = 50 × 10^30 / 19200 = 0.002604167 × 10^30

  • totalProfit is updated:
    new totalProfit = 0.00390625 × 10^30 + 0.002604167 × 10^30 = 0.006510417 × 10^30

Step 6:

  • userA claims profit based on the difference between totalProfit and lastProfit[userA].

  • The unsaved profit for userA is:
    unsaved = (0.006510417 × 10^30 - 0.00390625 × 10^30) × (200 × 64) / 10^30 = 33.33 tokens

  • userA receives 33.33 tokens in this second claim.

As it can be seen from the steps above, each user’s profit entitlement is based on the difference between totalProfit and lastProfit[user] .When a user claims their profit, lastProfit[user] is set to the current totalProfit value, recording a "snapshot" of the profit they’ve received up to that point. However this is not sound mechanism, because the system doesn’t take into account the user's token balance changes between claims . If a user mints additional tokens after some profit has been distributed, the lastProfit mechanism doesn’t adjust to reflect the increased token balance.
This means that when the user next claims profit, they will calculate their share based on their new, larger balance—even though they did not hold this larger balance at the time of the previous distributions.

Impact

Users who increase their holdings after distributions end up receiving a larger share of future profits without contributing proportionally to past distributions.
Users who are aware of this can game the system.
The share of profits for Long-term holders can be diluted over time by users who “game” the system through minting.

Tools Used

Manual inspection, VsCode

Recommendations

I do not have an easy solution.However tracking each user's balance changes separately and calculating profit shares based on the exact balance at each distribution point should fix the issue.

Updates

Lead Judging Commences

0xbrivan2 Lead Judge
10 months ago
0xbrivan2 Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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