52499 sc high arctoken factory s admin cannot upgrade an arctoken
- Submitted on Aug 11th 2025 at 09:21:24 UTC by @IronsideSec for Attackathon | Plume Network
- Report ID: #52499
- Report Type: Smart Contract
- Report severity: High
- Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcTokenFactory.sol
- Impacts:
- Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
ArcToken governance is inconsistent with the intended “factory-controlled upgrades.” The token deployer (token DEFAULT_ADMIN_ROLE) can revoke the factory’s UPGRADER_ROLE on a token, causing the factory’s upgradeToken to revert. Additionally, the token deployer can grant themselves UPGRADER_ROLE and upgrade the token to any implementation, bypassing the factory’s whitelist. This can lead to governance DoS on upgrades or arbitrary/malicious upgrades.
Note: see Recommendations section, if you intend this is an intended design.
Vulnerability Details
- Because the token deployer has
DEFAULT_ADMIN_ROLEon the token, they can:- Revoke the factory’s
UPGRADER_ROLEon that token, making factory upgrades revert. - Grant themselves
UPGRADER_ROLEand then callupgradeTo/upgradeToAndCallto a non-whitelisted implementation, bypassing factory controls.
- Revoke the factory’s
High-level steps (replicated as a POC test) are shown below.
Step: Create a token via factory
User creates a token via factory; user is token DEFAULT_ADMIN_ROLE.
Step: Revoke factory upgrader role
User revokes factory’s UPGRADER_ROLE on that token:
ArcToken(token).revokeRole(ArcToken(token).UPGRADER_ROLE(), address(factory))
Step: Factory admin whitelists implementation and attempts upgrade
Factory admin whitelists a new implementation in the factory and calls:
factory.upgradeToken(token, newImpl)
Step: Call reverts
Call reverts with AccessControlUnauthorizedAccount(factory, UPGRADER_ROLE) from the token’s _authorizeUpgrade.
Additional technical observations:
- Factory grants itself
UPGRADER_ROLEat token creation:
Source: arc/src/ArcTokenFactory.sol
// ... in createToken()
token.grantRole(token.DEFAULT_ADMIN_ROLE(), msg.sender);
token.grantRole(token.ADMIN_ROLE(), msg.sender);
token.grantRole(token.MANAGER_ROLE(), msg.sender);
token.grantRole(token.YIELD_MANAGER_ROLE(), msg.sender);
token.grantRole(token.YIELD_DISTRIBUTOR_ROLE(), msg.sender);
token.grantRole(token.MINTER_ROLE(), msg.sender);
token.grantRole(token.BURNER_ROLE(), msg.sender);
token.grantRole(token.UPGRADER_ROLE(), address(this));
- Token upgrade authorization checks only for
UPGRADER_ROLE; it does not verify the factory whitelist:
Source: arc/src/ArcToken.sol
function initialize(
// ...
) public initializer {
require(routerAddress_ != address(0), "Router address cannot be zero");
// ...
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(ADMIN_ROLE, msg.sender);
_grantRole(MANAGER_ROLE, msg.sender);
_grantRole(YIELD_MANAGER_ROLE, msg.sender);
_grantRole(YIELD_DISTRIBUTOR_ROLE, msg.sender);
// ...
}
function _authorizeUpgrade(
address newImplementation
) internal override onlyRole(UPGRADER_ROLE) { }
- Factory’s upgrade entrypoint enforces whitelist but relies on being able to call the token’s UUPS upgrade, which requires the token-level
UPGRADER_ROLE:
Source: arc/src/ArcTokenFactory.sol
function upgradeToken(address token, address newImplementation) external onlyRole(DEFAULT_ADMIN_ROLE) {
// ...
bytes32 codeHash = _getCodeHash(newImplementation);
if (!fs.allowedImplementations[codeHash]) {
revert ImplementationNotWhitelisted();
}
UUPSUpgradeable(token).upgradeToAndCall(newImplementation, "");
fs.tokenToImplementation[token] = newImplementation;
}
Impact Details
- Factory cannot upgrade tokens it created (and is expected to govern) if the token deployer revokes
UPGRADER_ROLE→ governance DoS on applying critical fixes or parameter changes. - Token deployer can self-assign
UPGRADER_ROLEand upgrade to arbitrary, non-whitelisted implementations → potential malicious logic deployment and asset loss. - Severity: High for governance integrity; potentially Critical if the token holds or manages funds post-upgrade.
Recommendations
- If the factory is intended to be the sole upgrade authority:
- Restrict token upgrades to the factory: in
ArcToken._authorizeUpgrade, requiremsg.sender == factoryOR a factory-owned role that is not administrable by the token deployer (e.g., set the role admin to a factory-controlled admin role, not the tokenDEFAULT_ADMIN_ROLE). - Alternatively, make the token’s
UPGRADER_ROLEadmin a role held by the factory, preventing the token deployer from revoking or reassigning it.
- Restrict token upgrades to the factory: in
- If the design intentionally allows token-level sovereignty:
- Enforce factory whitelist at the token level: in
ArcToken._authorizeUpgrade, also validate that thenewImplementationis whitelisted by the factory (e.g., call a factory getter likeisImplementationWhitelisted(newImplementation)), so even sovereign upgrades cannot bypass the whitelist. - Consider requiring that any upgrade be initiated via the factory (e.g., token delegates upgrade authority to factory), even if deployer retains other management roles.
- Enforce factory whitelist at the token level: in
References
See code snippets with GitHub links in the Vulnerability Details section above:
- Factory createToken: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcTokenFactory.sol#L200
- ArcToken initialize & _authorizeUpgrade: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L113-L117 and https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L770-L772
- Factory upgradeToken: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcTokenFactory.sol#L261-L276
Proof of Concept
Link to PoC: https://gist.github.com/IronsideSec/3181bbdc8a917b6207580c69a96610ef
Follow the steps in the gist above to reproduce.