52732 sc medium permanent dos of purchase token change
Submitted on Aug 12th 2025 at 18:18:10 UTC by @Afriauditor for Attackathon | Plume Network
Report ID: #52732
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcTokenPurchase.sol
Impacts: Smart contract unable to operate due to lack of token funds
Description
Brief/Intro
ArcTokenPurchase.setPurchaseToken() cannot be called while any token is in the enabledTokens set. Because anyone can create an ArcToken through the factory and becomes that token’s admin, an attacker can enable a “dust” sale (e.g., 1 wei) that only they can later disable. This permanently blocks the Purchase admin from rotating the purchase currency (e.g., during a stablecoin depeg or migration), creating an indefinite denial-of-service.
Vulnerability Details
The setter for purchase token includes this guard:
function setPurchaseToken(address purchaseTokenAddress) external onlyRole(DEFAULT_ADMIN_ROLE) {
PurchaseStorage storage ps = _getPurchaseStorage();
if (ps.enabledTokens.length() > 0) {
revert CannotChangePurchaseTokenWithActiveSales();
}
...
}setPurchaseToken() hard-reverts while any token remains in enabledTokens. Only token admins can remove themselves from that set:
function disableToken(address _tokenContract) external onlyTokenAdmin(_tokenContract) {
PurchaseStorage storage ps = _getPurchaseStorage();
TokenInfo storage info = ps.tokenInfo[_tokenContract];
if (!info.isEnabled) revert TokenNotEnabled();
info.isEnabled = false;
ps.enabledTokens.remove(_tokenContract);
emit TokenSaleDisabled(_tokenContract);
}disableToken() requires onlyTokenAdmin(_tokenContract), i.e., the ArcToken’s own ADMIN_ROLE holder (not the ArcTokenPurchase admin).
Because ArcTokenFactory.createToken(...) is external and permissionless, an attacker can create an ArcToken where they hold DEFAULT_ADMIN_ROLE/ADMIN_ROLE/MINTER_ROLE, transfer 1 wei to ArcTokenPurchase, and call enableToken() which only checks that the token came from the configured factory and that the contract holds _numberOfTokens—to add it to enabledTokens. This works even if no purchase token is set yet. From there, the DoS is effectively permanent: there’s no admin override to clear enabledTokens; buying out the dust doesn’t help because isEnabled stays true and the token remains in the set; only the malicious token admin can call disableToken(). Changing the factory via setTokenFactory() doesn’t remove the already-enabled entry.
Impact Details
Permanent inability to rotate the purchase currency regardless of whether any legitimate sale exists.
If a purchase currency needs to be rotated (e.g., stablecoin depeg or migration), the admin cannot do so while any token remains enabled. A malicious actor can create and enable a dust sale that only they can disable, resulting in indefinite DoS of the rotation operation.
Proof of Concept
Admin is blocked from rotating the purchase token
ArcTokenPurchase.setPurchaseToken()now reverts becauseenabledTokens.length() > 0.disableToken(tokenAddr)is callable only by the token’s admin (the attacker).Buying the 1 wei does not clear the entry:
isEnabledremains true and the token stays inenabledTokens.
Result: Indefinite DoS on purchase-currency rotation, regardless of whether any legitimate sale exists.
References
(Original code referenced at the target link above.)
Was this helpful?