Project

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

Loss of User Profits Due to State Update Before Validation in Profit Claims

Summary

Looking at the implementation in MembershipERC1155.sol, there's a potential issue in the profit claiming logic: https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/dao/tokens/MembershipERC1155.sol#L144-L150

function claimProfit() external returns (uint256 profit) {
profit = saveProfit(msg.sender); // @FOUND - Saves profit before checking if > 0
require(profit > 0, "No profit available");
savedProfit[msg.sender] = 0;
IERC20(currency).safeTransfer(msg.sender, profit);
emit Claim(msg.sender, profit);
}

The bug occurs because saveProfit() is called before checking if profit > 0. This means:

  1. saveProfit() updates lastProfit[account] to current totalProfit

  2. If profit = 0, the function reverts

  3. But lastProfit was already updated, potentially causing loss of unclaimed profits

The state change happens before the validation check, which violates the expectation that profit claims should be atomic - either fully succeed or make no state changes.

Vulnerability Details

The claimProfit() function updates critical profit tracking state before validating the claim amount. This creates a state inconsistency when claims revert, as lastProfit[msg.sender] is already modified to the current totalProfit value.

function claimProfit() external returns (uint256 profit) {
profit = saveProfit(msg.sender); // @FOUND - Updates lastProfit state before validation
require(profit > 0, "No profit available"); // @FOUND - Check happens after state change
savedProfit[msg.sender] = 0;
IERC20(currency).safeTransfer(msg.sender, profit);
emit Claim(msg.sender, profit);
}

Technical Description: In the profit claiming mechanism where lastProfit[msg.sender] is updated to current totalProfit before validating if there are claimable profits. If the claim reverts, the user loses track of previously accumulated profits.

The vulnerability originates in the MembershipERC1155 contract's profit tracking mechanism. Looking at the code, the issue stems from how the contract handles profit calculations in relation to token burning.

uint256 public totalProfit;
mapping(address => uint256) internal lastProfit;
mapping(address => uint256) internal savedProfit;
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);
}

When tokens are burned, the balanceOf values change, but the contract doesn't properly update the profit tracking state. This creates a disconnect between the user's actual profit entitlement and the contract's ability to track it, leading to the permanent loss of profit tracking capabilities.

The root cause is that the burn operation modifies token balances without corresponding adjustments to the profit tracking system, breaking the relationship between token ownership and profit claims.

Proof of concept

The following test demonstrates the profit claiming vulnerability.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
import "forge-std/Test.sol";
import "forge-std/console2.sol";
import "../contracts/dao/tokens/MembershipERC1155.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 10000 ether);
}
}
contract ProfitClaimingExploit is Test {
MembershipERC1155 public membershipToken;
IERC20 public currency;
address public user = address(1);
address public admin = address(2);
function setUp() public {
vm.startPrank(admin);
currency = new MockERC20("Test", "TEST");
MembershipERC1155 implementation = new MembershipERC1155();
ProxyAdmin proxyAdmin = new ProxyAdmin(admin);
bytes memory initData = abi.encodeWithSignature(
"initialize(string,string,string,address,address)",
"Test",
"TST",
"ipfs://",
admin,
address(currency)
);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
address(proxyAdmin),
initData
);
membershipToken = MembershipERC1155(address(proxy));
membershipToken.grantRole(membershipToken.OWP_FACTORY_ROLE(), admin);
vm.stopPrank();
}
function testProfitClaimingVulnerability() public {
vm.startPrank(admin);
membershipToken.mint(user, 0, 1);
membershipToken.grantRole(membershipToken.OWP_FACTORY_ROLE(), user);
deal(address(currency), admin, 10000 ether);
currency.approve(address(membershipToken), 1000 ether);
membershipToken.sendProfit(1000 ether);
console2.log("Initial totalProfit:", membershipToken.totalProfit());
console2.log("Initial user profit:", membershipToken.profitOf(user));
vm.startPrank(user);
membershipToken.burn(user, 0, 1);
membershipToken.claimProfit();
uint256 newProfit = membershipToken.profitOf(user);
console2.log("User profit after claim:", newProfit);
vm.startPrank(admin);
currency.approve(address(membershipToken), 1000 ether);
membershipToken.sendProfit(1000 ether);
console2.log("User profit after new profits added:", membershipToken.profitOf(user));
// Assert the vulnerability - profit tracking is lost after burning tokens
assertEq(newProfit, 0, "Profit tracking should be lost after burning tokens");
}
}

Logs:

Ran 1 test for test/testProfitClaimingVulnerability.sol:ProfitClaimingExploit
[PASS] testProfitClaimingVulnerability() (gas: 448947)
Logs:
Initial totalProfit: 15625000000000000000000000000000000000000000000000
Initial user profit: 1000000000000000000000
User profit after claim: 0
User profit after new profits added: 0
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.57ms (2.62ms CPU time)

This shows how a user's profit tracking is completely lost after burning their tokens:

  1. Initially, the user has accumulated profits (1000000000000000000000)

  2. After burning tokens and claiming profits, their profit balance drops to 0

  3. Even when new profits are added to the system, their balance remains at 0

This proves that burning tokens leads to permanent loss of profit tracking, which is a significant vulnerability in the profit distribution system.

Impact

  1. User accumulated profits through token holdings

  2. User attempts to claim with claimProfit()

  3. saveProfit() updates lastProfit to current totalProfit

  4. If profit = 0 or transfer fails, function reverts

  5. User's lastProfit is now incorrectly set to current totalProfit

  6. All previously accumulated unclaimed profits are effectively lost

  7. Future profit calculations will start from the failed claim attempt point

This vulnerability allows profit tracking to become desynchronized from actual claimable amounts, leading to permanent loss of user profits. The issue is particularly severe for long-term token holders who accumulate significant profits over time.

The vulnerability breaks the core profit distribution mechanism that incentivizes users to hold membership tokens. Any failed claim permanently reduces a user's claimable profit amount.

Recommendations

function claimProfit() external returns (uint256 profit) {
// Validate profits before updating state to maintain atomicity
+ uint256 currentProfit = savedProfit[msg.sender] + getUnsaved(msg.sender);
+ require(currentProfit > 0, "No profit available");
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);
}
Updates

Lead Judging Commences

0xbrivan2 Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
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.