Hesty Disclosed Report

Permanent Lock of Platform Fees in Failed or Cancelled Property Raises

Company
Created date
Mar 12 2025

Target

https://github.com/la-bomba-studio/hesty-contract/tree/29596447b9a06d4ad53360d4a15f349ebf9fa0d8

Vulnerability Details

Permanent Lock of Platform Fees in Failed or Cancelled Property Raises

Short Description

The TokenFactory contract collects platform fees (up to ~ 30%) during token purchases but these fees become permanently locked if the property raise * price fails to meet its threshold or is cancelled. While users can recover their investment through recoverFundsInvested(), the platform fees remain stuck in the contract as there is no mechanism to recover them.

Vulnerability Details

The issue occurs due to how platform fees are handled across different scenarios:

  1. Fee collection during token purchase:
// TokenFactory.sol
function buyTokens(address onBehalfOf, uint256 id, uint256 amount, address ref) external {
    // ... other checks ...
    
    // Calculate and collect platform fee
    uint256 fee = (boughtTokensPrice * platformFeeBasisPoints) / BASIS_POINTS;
    uint256 total = boughtTokensPrice + fee;
    
    // Transfer total amount including fee
    SafeERC20.safeTransferFrom(p.paymentToken, msg.sender, address(this), total);
    
    // Track fee separately
    platformFee[id] += fee;
    userInvested[msg.sender][id] += boughtTokensPrice;
}
  1. Fee distribution only happens in successful raises:
function completeRaise(uint256 id) external onlyAdmin {
    PropertyInfo storage p = property[id];
    require(p.approved && !p.isCompleted, "Canceled or Already Completed");
    require(p.raised * p.price >= property[id].threshold, "Threshold not met");

    property[id].isCompleted = true;

    // Transfer platform fees to treasury
    SafeERC20.safeTransfer(p.paymentToken, treasury, platformFee[id] - refFee[id]);
    platformFee[id] = 0;
    // ... other code ...
}
  1. User fund recovery ignores platform fees:
function recoverFundsInvested(address user, uint256 id) external {
    PropertyInfo storage p = property[id];
    require(p.raiseDeadline < block.timestamp && !p.isCompleted, "Time not valid");
    require(p.raised * p.price < p.threshold, "Threshold reached, cannot recover funds");

    uint256 amount = userInvested[user][id];
    userInvested[user][id] = 0;
    rightForTokens[user][id] = 0;

    // Only returns user investment, not platform fee
    SafeERC20.safeTransfer(p.paymentToken, user, amount);
}

This is critical because:

  1. Large amounts can be locked:

    • Platform fees can be up to ~30% (MAX_FEE_POINTS)
    • For a failed $1M raise, up to $300K could be locked
    • Fees from multiple failed properties accumulate
  2. No recovery mechanism:

    • Users can only recover their base investment
    • Protocol can't recover fees as completeRaise() requires threshold to be met
    • No admin function exists to recover stuck fees
  3. Impact increases with:

    • Higher property values
    • Higher platform fee percentages
    • More failed or cancelled properties

Validation steps

Validation Steps

  1. Deploy contracts and create a property:
const platformFee = 3000; // 30%
const tokenFactory = await TokenFactory.deploy(platformFee, ...);

await tokenFactory.createProperty(
    1000, // amount
    100, // listingTokenFee
    ethers.utils.parseEther("1"), // price
    ethers.utils.parseEther("500"), // threshold
    USDC.address,
    revenueToken,
    "Property",
    "PROP",
    admin
);
  1. Have users invest and accumulate platform fees:
// User invests 100 USDC
const investment = ethers.utils.parseUnits("100", 6); // USDC has 6 decimals
await tokenFactory.buyTokens(user.address, 0, investment, referrer);

// Check platform fee accumulation
const platformFeeAccrued = await tokenFactory.platformFee(0);
console.log("Platform fee locked:", platformFeeAccrued.toString()); // ~30 USDC
  1. Simulate failed raise and attempt recovery:
// Wait for deadline to pass
await network.provider.send("evm_increaseTime", [86400 * 7]);
await network.provider.send("evm_mine");

// User can recover investment
await tokenFactory.recoverFundsInvested(user.address, 0);

// Check platform fee is still locked
const lockedFee = await tokenFactory.platformFee(0);
console.log("Platform fee still locked:", lockedFee.toString()); // ~30 USDC

// No mechanism to recover these fees

Recommended Mitigation

  1. Add platform fee refund in recovery:
function recoverFundsInvested(address user, uint256 id) external {
    // ... existing checks ...
    
    uint256 userFee = (amount * platformFeeBasisPoints) / BASIS_POINTS;
    platformFee[id] -= userFee;
    
    // Return both investment and fee
    SafeERC20.safeTransfer(p.paymentToken, user, amount + userFee);
}
  1. Add admin recovery function for cancelled properties:
function recoverPlatformFees(uint256 id) external onlyAdmin {
    require(property[id].raiseDeadline < block.timestamp || deadProperty[id], "Still active");
    require(platformFee[id] > 0, "No fees to recover");
    
    uint256 amount = platformFee[id];
    platformFee[id] = 0;
    SafeERC20.safeTransfer(property[id].paymentToken, treasury, amount);
}
  1. Consider not charging platform fees until raise is successful:
    • Collect fees during completeRaise()
    • Simplifies recovery process
    • Improves user experience

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
Critical
Bounty$400
Visibilitypartially
VulnerabilityBlockchain
Participants (3)
company admin
author
company admin