52620 sc medium permanently dos to arctokenpurchase contract
Submitted on Aug 12th 2025 at 03:48:59 UTC by @IronsideSec for Attackathon | Plume Network
Report ID: #52620
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcTokenPurchase.sol
Impacts:
Protocol insolvency
Description
Any factory user can permanently DoS the contract’s purchase-token configuration. An attacker can enable a sale for a token they control (minted via the public factory) before the admin sets purchaseToken. After any token is enabled, setPurchaseToken always reverts, and only the token’s admin can disable the sale. This permanently blocks the purchase-token configuration and operational use of the storefront.
Short summary:
setPurchaseTokenneeds to be set in order for users to buy the enabled arc tokens. If an attacker enables a token sale before the admin callssetPurchaseToken, the admin can never set the purchase token becausesetPurchaseTokenreverts whenever there are enabled sales.Calling
setPurchaseTokenin the contract's initialization transaction would avoid this race; relying on a subsequent transaction risks permanent DoS.
Vulnerability Details
Root causes
setPurchaseTokenreverts if any arc token is enabled.enableTokenlacks a guard ensuring a non-zeropurchaseToken.Only token admins can call
disableToken. The factory grantsADMIN_ROLEto the token creator; that creator can revoke roles granted to the factory during token creation, preventing admins from disabling the sale.
Exploit flow (PoC steps)
Notes
This issue does not occur if
enableTokenvalidates thatpurchaseTokenis set (non-zero) before enabling a sale.
Relevant code snippets
setPurchaseToken (reverts if any enabled tokens exist):
function setPurchaseToken(address purchaseTokenAddress) external onlyRole(DEFAULT_ADMIN_ROLE) {
PurchaseStorage storage ps = _getPurchaseStorage();
if (ps.enabledTokens.length() > 0) {
revert CannotChangePurchaseTokenWithActiveSales();
}
if (purchaseTokenAddress == address(0)) {
revert InvalidPurchaseTokenAddress();
}
ps.purchaseToken = IERC20(purchaseTokenAddress);
emit PurchaseTokenUpdated(purchaseTokenAddress);
}Source: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcTokenPurchase.sol#L293-L295
enableToken (no check that purchase token is set):
function enableToken(
address _tokenContract,
uint256 _numberOfTokens,
uint256 _tokenPrice
) external
onlyTokenAdmin(_tokenContract) {
// ...
ps.tokenInfo[_tokenContract] =
TokenInfo({ isEnabled: true, tokenPrice: _tokenPrice, totalAmountForSale: _numberOfTokens, amountSold: 0 });
ps.enabledTokens.add(_tokenContract);
// ...
}Source: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcTokenPurchase.sol#L142-L175
disableToken (only token admin can call):
function disableToken(address _tokenContract)
external
onlyTokenAdmin(_tokenContract)
{
PurchaseStorage storage ps = _getPurchaseStorage();
TokenInfo storage info = ps.tokenInfo[_tokenContract];
// ...
info.isEnabled = false;
ps.enabledTokens.remove(_tokenContract);
emit TokenSaleDisabled(_tokenContract);
}Source: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcTokenPurchase.sol#L184
Impact Details
Permanent denial-of-service on purchase-token configuration:
Admin cannot set or change
purchaseTokenonce any sale is enabled by any token admin.Only token admins can disable their sale; the purchase admin is locked out.
Operational halt of storefront lifecycle:
Prevents initial configuration and future rotation of the purchase currency.
Requires contract upgrade or external intervention to recover.
Severity: High. Any factory user can permanently brick a fresh
ArcTokenPurchaseinstance before configuration.
Remediation
Proof of Concept
Exploit gist: https://gist.github.com/IronsideSec/ea8dc6359e40bae2e323bc0e89d02bc1
Follow the steps in the gist for a working PoC.
References
See code snippets with GitHub links above.
Was this helpful?