50624 sc low there is a missing emergency pause in predicate proxy
Submitted on Jul 26th 2025 at 19:55:31 UTC by @XDZIBECX for Attackathon | Plume Network
- Report ID: #50624
- Report Type: Smart Contract
- Report severity: Low
- Target: https://github.com/immunefi-team/attackathon-plume-network-nucleus-boring-vault/blob/main/src/base/Roles/TellerWithMultiAssetSupportPredicateProxy.sol
- Impacts:
- Temporary freezing of funds for at least 24 hours
Description
Brief/Intro
The TellerWithMultiAssetSupportPredicateProxy contract inherits from OpenZeppelin's Pausable contract but is missing public pause() and unpause() functions. Because of this, administrators cannot pause the contract during emergency situations (e.g., security incidents or critical bugs). This can result in temporary freezing of user funds for at least 24 hours, as users could continue depositing through the predicate proxy even when the underlying system should be paused, leading to stuck transactions and inability to halt operations during emergencies.
Vulnerability Details
The TellerWithMultiAssetSupportPredicateProxy contract inherits from Pausable but does not implement the necessary public functions to control the pause state:
contract TellerWithMultiAssetSupportPredicateProxy is Ownable, ReentrancyGuard, PredicateClient, Pausable {
// there is No public pause() function
// there is No public unpause() function
OpenZeppelin's Pausable provides internal _pause() and _unpause() functions, but these are only accessible if the inheriting contract implements public wrapper functions. Without these functions, the contract is effectively unpausable because:
- The internal
_pause()and_unpause()functions cannot be called externally - The
paused()state can never be changed from its initialfalsevalue - Manual pause checks in functions will always pass since
paused()returnsfalse
The contract does include manual pause checks in its functions:
if (paused()) {
revert TellerWithMultiAssetSupportPredicateProxy__Paused();
}
These checks are ineffective because the pause state can never be set to true without the missing public functions. Suggested additions:
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
Also consider using the whenNotPaused / whenPaused modifiers on user-entry functions instead of manual if (paused()) checks.
Impact Details
The bug can cause a temporary freezing of funds for at least 24 hours. Example scenario:
During an emergency: administrators attempt to pause the system
The predicate proxy cannot be paused due to missing pause functions.
Users continue depositing
The predicate proxy remains operational even when the underlying system should be paused.
Funds become temporarily frozen
Deposits may fail or get stuck in the predicate proxy, but users can still initiate transactions that do not complete as expected.
Recovery time exceeds 24 hours
The team may need to:
- Deploy a new predicate proxy with proper pause functionality
- Migrate users to the new contract
- Handle stuck transactions and refunds
- Update all dependent contracts (like
DexAggregatorWrapperWithPredicateProxy)
Financial impact:
- User deposits could be stuck in the predicate proxy during emergencies
- Cross-chain bridging operations may fail but still consume user funds
- Emergency response time is significantly delayed due to inability to pause operations
- Potential loss of user confidence and reputation damage
Duration: The funds would be frozen for at least 24 hours while the team attempts to resolve the emergency and deploy fixes.
References
- TellerWithMultiAssetSupportPredicateProxy.sol
- OpenZeppelin Pausable Contract
- DexAggregatorWrapperWithPredicateProxy.sol - Shows dependency on the predicate proxy
Proof of Concept
function testCannotPausePredicateProxy() public {
// Deploy the contract
TellerWithMultiAssetSupportPredicateProxy proxy = new TellerWithMultiAssetSupportPredicateProxy(
address(this), address(0x1234), "policy"
);
// The contract is not paused by default
assertEq(proxy.paused(), false);
// Try to call pause() - this should fail to compile or revert at runtime
(bool success, ) = address(proxy).call(abi.encodeWithSignature("pause()"));
assertTrue(!success, "pause() should not exist");
// Try to call _pause() - this should also fail
(success, ) = address(proxy).call(abi.encodeWithSignature("_pause()"));
assertTrue(!success, "_pause() should not exist");
// The contract is still not paused
assertEq(proxy.paused(), false);
}