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); // Inaccurate
Impact 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 shares
- Proxy 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.
Missing Rescue Functions — Steps to Trigger
- User accidentally transfers 1000 USDC to proxy:
USDC.transfer(proxyAddress, 1000e6);
- Proxy has no
rescueTokensfunction. - Owner cannot recover funds → 1000 USDC permanently locked.
Result:
- Any non-standard asset transfer causes permanent loss.
Summary of Exploit Paths
| Vulnerability | Trigger Action | Consequence |
|---|---|---|
| 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 |