https://github.com/la-bomba-studio/hesty-contract/tree/29596447b9a06d4ad53360d4a15f349ebf9fa0d8
Description:
The cancelProperty function in TokenFactory contract can prevent users from claiming refunds via recoverFundsInvested if the investment threshold is reached before cancellation.
When cancelProperty is called, it modifies the contract state by setting raiseDeadline to 0, approved to false, and deadProperty to true:
function cancelProperty(uint256 id) external onlyAdmin{
@> property[id].raiseDeadline = 0; // Important to allow investors to recover funds
@> property[id].approved = false; // Prevent more investments
@> deadProperty[id] = true;
emit CancelProperty(id);
}
These state changes ensure that:
approveProperty function checks for deadProperty.extendRaiseForProperty function cannot be executed since raiseDeadline is set to 0.This results in a situation where, once a property is canceled, it cannot be reactivated.
Now, as the property is canceled, users/investors should be able to retrieve their investments via recoverFundsInvested function. However, if the investment threshold is reached before cancellation, the following statement prevents users from reclaiming their funds:
@> require(p.raised * p.price < p.threshold, "Threshold reached, cannot recover funds");
This means users are permanently unable to withdraw their investments, leading to funds being indefinitely locked in the contract with no mechanism for retrieval.
This issue can occur in the following scenarios:
cancelProperty function.cancelProperty is executed, the contract state changes, preventing users from claiming refunds via recoverFundsInvested.buyToken function, ensuring that the threshold is reached before the admin cancels the property.cancelProperty executes after the threshold is met, making it impossible for investors to claim refunds.Impact:
Mitigation:
Modify the recoverFundsInvested function to include a deadProperty check, ensuring that if a property is canceled, investors can claim refunds regardless of whether the threshold has been reached.
Attack Scenario:
cancelProperty.cancelProperty is executed, the contract state changes.recoverFundsInvested, but the function no longer allows withdrawals due to the threshold being met.Add this test code in TokenFactory.test.js file and run the test:
describe("Users cannot claim refunds", function () {
beforeEach(async function () {
//Not yet initialized so therefore address(0)
expect(await tokenFactory.referralSystemCtr()).to.equal("0x0000000000000000000000000000000000000000");
await tokenFactory.initialize(referral.address, issuance.address)
await hestyAccessControlCtr.connect(addr2).approveUserKYC(propertyManager.address);
await tokenFactory.addWhitelistedToken(token.address);
await tokenFactory.connect(propertyManager).createProperty(1000000, 1000, 1, 10, token.address, token.address, "token", "TKN", hestyAccessControlCtr.address)
expect(await tokenFactory.propertyCounter()).to.equal(1);
await tokenFactory.approveProperty(0, 2937487238472834);
// Approve owner kyc to allow him to buy property token
await hestyAccessControlCtr.connect(addr2).approveUserKYC(owner.address);
await token.approve(tokenFactory.address, 20);
await token.mint(owner.address, 15);
await tokenFactory.buyTokens(owner.address, 0, 2, addr3.address);
})
it("FundsNotClaimable", async function () {
// Property reaches threshold with this
await tokenFactory.buyTokens(owner.address, 0, 10, addr3.address);
// Property is canceled
await tokenFactory.cancelProperty(0);
await ethers.provider.send("evm_mine", [2937487238472844]);
// User cannot get refund
await expect(token.recoverFundsInvested(owner.address, 0)).to.be.revertedWith("Threshold reached, cannot recover funds");
});
})