Medium severity
Locked Ether (payable, but no way out)
Updated 2026-10-12
A contract that can take in Ether needs a way to send it back out. When a contract has a payable function — an explicit payable method, or a payable receive / fallback — but nowhere in its code (or in any internal or library code it reaches) forwards Ether out with a transfer, a send, a call{value:…}, a new…{value:…}, or a selfdestruct, then every wei paid into it is stranded. Nobody can recover it: not the deployer, not the depositor, not an admin. This is not something an attacker runs against you; it is a shipped contract that has quietly become a one-way door for funds. The usual causes are mundane: a payable attribute left on out of habit, a refactor that deleted the withdraw function, or an escrow wired to receive deposits before its payout side was ever written.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Takes deposits from anyone, credits an internal balance -- but there is no
// withdraw, no transfer, no selfdestruct anywhere. The contract balance can
// only ever go up, and everything paid in is trapped for good.
contract Vault {
mapping(address => uint256) public deposits;
function deposit() external payable {
deposits[msg.sender] += msg.value;
}
receive() external payable {
deposits[msg.sender] += msg.value;
}
}The fix
Give the Ether a way out, or stop accepting it. If the contract is meant to hold funds, add an access-controlled withdrawal path and follow checks-effects-interactions: update the stored balance before the external call so the withdraw cannot be re-entered. If the contract was never supposed to hold Ether, remove the payable attribute (and the receive / fallback) so deposits revert at the door instead of piling up unrecoverably.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Vault {
mapping(address => uint256) public deposits;
function deposit() external payable {
deposits[msg.sender] += msg.value;
}
receive() external payable {
deposits[msg.sender] += msg.value;
}
// Each depositor can pull their own balance back out.
function withdraw(uint256 amount) external {
require(deposits[msg.sender] >= amount, "insufficient balance");
deposits[msg.sender] -= amount; // state update before the call
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}
}What clears the finding, and what it does not cover
The detector fires only when two things hold at once: the contract has at least one payable function, and no path it can reach moves Ether out. Add any genuine outbound value — a withdraw that calls, a transfer to a beneficiary, a reachable selfdestruct, or a forwarding delegatecall — and the finding clears, because the Ether is no longer trapped. The flip side is that the detector says nothing about a contract with no payable function at all: Ether can still be forced into any address as a selfdestruct beneficiary or a block-reward recipient, and that stray balance is just as unrecoverable without a withdraw, but the detector scopes itself to contracts that advertise that they take payments.
Scan your contract for this
pactlint flags locked-ether and other recurring DeFi bug classes in Solidity
source or a verified contract on Ethereum or Base. Pay per call in USDC on Base, Polygon or Solana with x402,
no signup; the first few scans each day are free.
Locked Ether is detected by Slither’s stock locked-ether detector,
rated medium impact at high confidence, which pactlint surfaces at medium severity when a contract has a payable
function but no reachable way to move its Ether out. pactlint runs worker-side triage on this class: it drops a finding to
info only for the proxy/extension pattern, where the contract’s fallback delegatecalls into implementation
code that can itself move Ether (a minimal proxy or a Safe-style extension looks locked in isolation but is not), so a genuine
payable contract with no withdrawal path stays Medium. Run it in CI with the
pactlint GitHub Action.
Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.