52446 sc low withdrawing unsold tokens desynchronizes sale accounting
Submitted on Aug 10th 2025 at 18:26:11 UTC by @Afriauditor for Attackathon | Plume Network
Report ID: #52446
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcTokenPurchase.sol
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
In ArcTokenPurchase, the admin can withdraw unsold ArcTokens from the contract, but the sale ledger used by getMaxNumberOfTokens isn’t updated. After a withdrawal, the contract may report more tokens “available” than it actually holds, causing failed buys.
Vulnerability Details
The withdraw function only transfers tokens out; it does not update the sale counters:
function withdrawUnsoldArcTokens(address _tokenContract, address to, uint256 amount)
external onlyTokenAdmin(_tokenContract)
{
// ...checks...
ArcToken token = ArcToken(_tokenContract);
uint256 bal = token.balanceOf(address(this));
if (bal < amount) revert InsufficientUnsoldTokens();
bool ok = token.transfer(to, amount);
if (!ok) revert ArcTokenWithdrawalFailed();
}But the getter used by buyers ignores withdrawals and returns the original pool minus sold:
function getMaxNumberOfTokens(address _tokenContract) external view returns (uint256) {
TokenInfo storage info = _getPurchaseStorage().tokenInfo[_tokenContract];
return info.totalAmountForSale - info.amountSold;
}Impact Details
The contract can report more available tokens than it actually holds. Buyers relying on the getter may submit larger purchases and hit reverts when inventory isn’t actually there, wasting gas. The contract does not lose tokens or value, but it can fail to deliver promised amounts.
Proof of Concept
Step 6 — Buyer attempts purchase
Buyer attempts to buy 400 ARC based on that getter.
Inside
buy():remainingForSale = totalAmountForSale - amountSold = 400→ passes the “NotEnoughTokensForSale” check.Actual on-chain balance is 100 ARC → triggers
ContractBalanceInsufficient()and the tx reverts (buyer wastes gas).
References
(No additional references provided.)
Was this helpful?