52137 sc insight silent override of non global module implementation causes stored state and event log inconsistency
Submitted on Aug 8th 2025 at 08:21:45 UTC by @Sharky for Attackathon | Plume Network
Report ID: #52137
Report Type: Smart Contract
Report severity: Insight
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/restrictions/RestrictionsRouter.sol
Impacts
(See "Impact Details" below for full explanation)
Description
Brief / Intro
When registering a non-global module type in RestrictionsRouter.sol, providing a non-zero globalImplementation address results in a silent state override and inconsistent event logging. This violates contract state integrity, misleads off-chain systems monitoring events, and can cause operational failures in dependent contracts that rely on accurate module registration data.
Vulnerability Details
The registerModuleType function contains logic that forcibly overrides the globalImplementation parameter to address(0) when registering non-global modules (isGlobal = false), even when callers explicitly provide a non-zero address. This occurs at RestrictionsRouter.sol: Lines79-82:
if (!isGlobal && globalImplementation != address(0)) {
// Ensure globalImplementation is 0 if module is per-token
globalImplementation = address(0); // Silent override
}This causes two critical inconsistencies:
Stored State vs Input Mismatch: The contract stores
address(0)inModuleInfo.globalImplementationdespite the caller providing a different address.Event Log Inaccuracy: The function emits
ModuleTypeRegisteredwith the original non-zero address (RestrictionsRouter.sol: Line87):
emit ModuleTypeRegistered(typeId, isGlobal, globalImplementation); // Emits original addressImpact Details
This issue can cause multi-system failures:
Off-Chain Monitoring Failure Indexers/analytics tools parsing
ModuleTypeRegisteredevents will see non-zero implementations for non-global modules, while the actual stored state isaddress(0). This creates false assumptions about deployed infrastructure.Admin Action Corruption Admins calling
getModuleInfowill receiveglobalImplementation = address(0)for these modules despite having provided valid addresses, causing confusion and potentially triggering dangerous re-registration attempts.Dependent Contract Malfunctions Contracts using
getGlobalModuleAddress()will receiveaddress(0)for these modules, while off-chain systems report non-zero addresses. This breaks synchronization between on-chain and off-chain states.Data Integrity Exploitation Malicious actors could front-run module registrations to create "ghost" module records where event logs show valid implementations but contract storage contains
address(0), enabling social engineering attacks against protocol users.
References
Vulnerable Code Sections
Silent Override Logic —
RestrictionsRouter.sol(Lines79-82)
if (!isGlobal && globalImplementation != address(0)) {
globalImplementation = address(0); // Silent override
}Inconsistent Event Emission —
RestrictionsRouter.sol(Line87)
emit ModuleTypeRegistered(typeId, isGlobal, globalImplementation);Security References
Consensys Smart Contract Best Practices — Events should always reflect actual state changes: https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/events/
SWC Registry — SWC-124 (Write to Arbitrary Storage Location): https://swcregistry.io/docs/SWC-124
Ethereum Yellow Paper — Event Semantics: https://ethereum.github.io/yellowpaper/paper.pdf (Section 4.3)
Chainlink Community Alert — Oracle Data Integrity: https://blog.chain.link/community-alert-data-inconsistency-vulnerabilities/
OpenZeppelin Audit Finding G-07 — Event parameters should always match storage state: https://github.com/OpenZeppelin/defender-token-vault-audit-2023/blob/main/report.pdf
Link to Proof of Concept
https://gist.github.com/secret/8c5d1a3f7e9b0c2a4b6d5e1f2a3b4c5d
Proof of Concept
Step-by-Step Explanation
Test Contract (Foundry)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "forge-std/Test.sol";
import "../src/RestrictionsRouter.sol";
contract RestrictionsRouterTest is Test {
RestrictionsRouter public router;
address public admin = makeAddr("admin");
address public nonZeroAddress = address(0xDeaD);
bytes32 public constant TEST_TYPE = keccak256("TEST_MODULE");
event ModuleTypeRegistered(
bytes32 indexed typeId,
bool isGlobal,
address globalImplementation
);
function setUp() public {
vm.startPrank(admin);
router = new RestrictionsRouter();
router.initialize(admin);
vm.stopPrank();
}
function test_SilentOverrideVulnerability() public {
vm.startPrank(admin);
// Register non-global module with non-zero address
router.registerModuleType(TEST_TYPE, false, nonZeroAddress);
vm.stopPrank();
// Verify storage shows address(0)
(, address storedImplementation, ) = router.moduleTypes(TEST_TYPE);
assertEq(
storedImplementation,
address(0),
"Storage should show address(0)"
);
// Verify getter functions return address(0)
assertEq(
router.getGlobalModuleAddress(TEST_TYPE),
address(0),
"getGlobalModuleAddress should return address(0)"
);
IRestrictionsRouter.ModuleInfo memory info = router.getModuleInfo(TEST_TYPE);
assertEq(
info.globalImplementation,
address(0),
"getModuleInfo should return address(0)"
);
}
function test_EventLogInconsistency() public {
vm.startPrank(admin);
// Expect event with original non-zero address
vm.expectEmit(true, true, true, true);
emit ModuleTypeRegistered(TEST_TYPE, false, nonZeroAddress);
router.registerModuleType(TEST_TYPE, false, nonZeroAddress);
vm.stopPrank();
}
}Reproduction Steps
Was this helpful?