https://github.com/la-bomba-studio/hesty-contract/tree/29596447b9a06d4ad53360d4a15f349ebf9fa0d8
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.
The issue occurs due to how platform fees are handled across different scenarios:
// 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;
}
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 ...
}
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:
Large amounts can be locked:
No recovery mechanism:
completeRaise() requires threshold to be metImpact increases with:
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
);
// 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
// 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
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);
}
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);
}
completeRaise()