50040 sc low missing pause controls eth refund flaws and miscalculated shares enable fund loss and protocol inconsistency in depositandbridge
Submitted on: Jul 21st 2025 at 09:11:24 UTC by @Sharky for Attackathon | Plume Network
Report ID: #50040
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
Permanent freezing of funds
Description
Brief/Intro
The TellerWithMultiAssetSupportPredicateProxy contract contains vulnerabilities that compromise fund safety, emergency response capabilities, and protocol integrity. If exploited, these issues can lead to permanent loss of user/contract funds, broken emergency shutdowns, and inconsistent protocol accounting.
Vulnerability Details
Broken Pause Mechanism (Critical)
Issue: The contract inherits
Pausablebut lackspause()/unpause()functions.Technical Insight:
The
paused()checks indeposit/depositAndBridgeare functional, but without externalpause/unpausecalls (enforceable only byowner), the contract can never be paused.Inheriting
Pausablealone is insufficient; the OpenZeppelin design requires explicit pause control functions.
Code Proof:
contract TellerWithMultiAssetSupportPredicateProxy is ... Pausable {
// Missing: pause()/unpause() functions
function deposit(...) {
if (paused()) revert ...; // Always false
}
}ETH Trapping Vulnerability (Critical)
Issue: ETH refunds fail after
depositAndBridge, causing permanent fund loss.Technical Insight:
lastSenderis reset toaddress(0)afterteller.depositAndBridgecompletes.If the teller (or bridge) refunds excess ETH after this reset (e.g., for overpaid fees), the
receive()function attempts to forward ETH toaddress(0), which reverts and traps funds.
Code Proof:
function depositAndBridge(...) payable {
lastSender = msg.sender; // Set temporarily
teller.depositAndBridge{value: msg.value}(...); // Refunds might occur AFTER this
lastSender = address(0); // Reset too early
}
receive() external payable {
(bool success,) = lastSender.call{value: msg.value}(""); // Fails if lastSender=0
}Share Calculation Mismatch (Major)
Issue:
depositAndBridgemiscalculates shares inDepositevents, breaking fee-aware accounting.Technical Insight:
deposituses actual shares minted (shares = teller.deposit(...)), which deducts fees.depositAndBridgeuses a raw quote (shares = depositAmount.mulDivDown(...)) that ignores fees.This creates inconsistent event data, misleading off-chain systems about user balances.
Code Proof:
// deposit (correct):
shares = teller.deposit(...); // Fee-aware
emit Deposit(..., shares); // Accurate
// depositAndBridge (incorrect):
teller.depositAndBridge(...); // Shares minted internally (with fees)
uint256 shares = depositAmount.mulDivDown(...); // Fee-ignorant calculation
emit Deposit(..., shares); // InaccurateImpact Details
Fund Loss Scenarios:
Stuck ETH: Users overpaying bridge fees lose refunds permanently (trapped in contract).
Token Lockup: No rescue mechanism for accidentally sent ERC20 tokens.
Operational Risks:
No Emergency Pause: Attackers exploit vulnerabilities unhindered during crises.
Accounting Corruption: Fee discrepancies in events break integrations (e.g., tax/reporting systems).
Quantifiable Losses:
Direct: 100% of overpaid ETH fees + any non-standard tokens sent to contract.
Indirect: Protocol insolvency risk due to inconsistent state and loss of user trust.
References
Contract Code: Provided in report
OpenZeppelin Pausable: https://docs.openzeppelin.com/contracts/5.x/api/utils#Pausable
Solmate SafeTransferLib: https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol
Proof of Concept
Broken Pause Mechanism — Steps to Trigger
An attacker discovers a vulnerability in the deposit logic that allows fund theft.
Users continue depositing funds through
deposit/depositAndBridgesince:paused()always returnsfalse(unpaused state)Owner cannot activate pause as
pause()function doesn't exist
Attacker exploits vulnerability and drains user funds.
Result:
All deposits are vulnerable during the attack window with no emergency mitigation.
ETH Trapping Vulnerability — Steps to Trigger
User calls
depositAndBridgewith excess ETH (example: 1 ETH for 0.9 ETH bridge fee):
depositAndBridge{value: 1 ether}(...)During execution:
lastSender = msg.sender(set to user address)teller.depositAndBridgeconsumes 0.9 ETHlastSender = address(0)(reset after call)
Teller contract refunds 0.1 ETH to proxy:
payable(proxy).transfer(0.1 ether);Proxy's
receive()triggers and attempts:
(bool success,) = address(0).call{value: 0.1 ether}("");Call to
address(0)reverts → 0.1 ETH permanently locked in proxy.
Result:
100% of overpaid ETH is irrecoverable.
Share Calculation Mismatch — Steps to Trigger
Teller applies a 1% deposit fee in
depositAndBridge.User deposits 100 USDC (price: 1 USDC = 1 share):
Actual shares minted: 99 (after 1% fee)
Proxy calculates shares incorrectly:
shares = 100 USDC * (10**18 / 1e6) / 1e18 = 100 sharesProxy emits event with 100 shares (vs actual 99 shares).
Result:
Off-chain monitors see inconsistent share balances (100 vs 99).
Protocol appears to steal 1 share from user in analytics.
Unsafe Allowance Handling — Steps to Trigger
User deposits 100 USDC via
deposit:
depositAsset.safeApprove(vault, 100 USDC);Later, same user deposits 50 USDC:
Proxy tries:
safeApprove(vault, 50 USDC)Reverts because existing allowance (100 USDC) ≠ 0.
Result:
Subsequent deposits fail for any token with a prior non-zero approval.
Summary of Exploit Paths
Broken Pause
Exploit during attack
No emergency shutdown
ETH Trapping
Overpay bridge fee
ETH permanently stuck
Share Mismatch
Use depositAndBridge
Incorrect event emissions
Allowance Bug
Second deposit with same asset
Deposit reverts
No Rescue
Accidental token transfer
Funds permanently locked
Was this helpful?