https://github.com/la-bomba-studio/hesty-contract/tree/29596447b9a06d4ad53360d4a15f349ebf9fa0d8
The buyTokens function allows users to purchase property tokens and records the amount invested in the userInvested mapping. When a property is canceled using the cancelProperty function, users should be able to withdraw their entire investment. However, the recorded investment amount does not account for the investment fee, leading to incorrect refund calculations. Additionally, there is no function to allow system administrators to withdraw accumulated investment fees from canceled properties, resulting in those funds being permanently locked in the contract.
When a user buys tokens, the contract calculates and deducts an investment fee:
uint256 fee = boughtTokensPrice * platformFeeBasisPoints / BASIS_POINTS;
uint256 total = boughtTokensPrice + fee;
However, only boughtTokensPrice (excluding the fee) is recorded in the userInvested mapping:
userInvested[msg.sender][id] += boughtTokensPrice;
Upon property cancellation, users can only withdraw the recorded amount, meaning the investment fee is not refunded.
In the hesky project, run forge init --force. Copy the code below into the test file in the test folder and run it.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import { PropertyToken } from "../contracts/TokenFactory.sol";
import {ERC20PresetMinterPauser } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";
import {Token} from "./ERC20.sol";
import {HestyAccessControl} from "../contracts/HestyAccessControl.sol";
import {TokenFactory} from "../contracts/TokenFactory.sol";
import {ReferralSystem} from "../contracts/Referral/ReferralSystem.sol";
import {HestyAssetIssuance } from "../contracts/HestyAssetIssuance.sol";
contract PropertyTest is Test {
PropertyToken public propToken;
ERC20PresetMinterPauser public token;
HestyAccessControl public accessControl;
TokenFactory public tokenFact;
ReferralSystem public refSys;
HestyAssetIssuance public issuance;
Token public payment;
Token public revenue;
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
address user4 = makeAddr("user4");
address user5 = makeAddr("user5");
function setUp() public {
vm.startPrank(user1);
token = new ERC20PresetMinterPauser("name","symbol");
accessControl = new HestyAccessControl();
propToken = new PropertyToken(user1, 10, "Token", "TKN", address(token), address( accessControl), user1);
tokenFact = new TokenFactory(300, 100, user1, 1, address(accessControl));
refSys = new ReferralSystem (address(token),address( accessControl), address(tokenFact) );
issuance = new HestyAssetIssuance(address(tokenFact));
tokenFact.initialize(address(refSys),address(issuance));
accessControl.grantRole(keccak256("KYC_MANAGER"), user1);
deal(address( accessControl), 100 ether);
accessControl.approveUserKYC(user2);
accessControl.approveUserKYC(user3);
payment = new Token("payment","payment");
revenue = new Token("rev","rev");
payment.mint(user3, 100 ether);
tokenFact.addWhitelistedToken(address(payment));
tokenFact.addWhitelistedToken(address(revenue));
vm.stopPrank();
}
function test_InvestmentFeeStuck() public {
// User2 creates a property
vm.startPrank(user2);
tokenFact.createProperty(
100,
2000,
1e18,
100e18,
address(payment),
address(revenue),
"prop",
"prop",
user2
);
// User1 (Admin) approves the property
vm.startPrank(user1);
tokenFact.approveProperty(0, block.timestamp + 10 minutes);
// User3 (buyer) buys token
vm.startPrank(user3);
payment.approve(address(tokenFact), 100 ether);
tokenFact.buyTokens(
user3,
0,
10,
user4
);
// User1 (Admin) cancels property
vm.startPrank(user1);
tokenFact.cancelProperty(0);
// User3 tried to recover funds invested
vm.startPrank(user3);
tokenFact.recoverFundsInvested(user3, 0);
// User3 does not get full refund, the invested fee was not refunded
assert(payment.balanceOf(user3) != 100 ether);
// Investment Fee is stuck in the contract as admin cannot withdraw it as well
}
}