Locked Ether
Overview
- Severity: High
- Confidence: High
- Affected Versions: All
What is the Locked Ether vulnerability?
In the context of the Ethereum network, Ether (ETH) is the native cryptocurrency that is used in every transaction, whether as an intrinsic unit of value transfer or as a fee for a more general transaction, e.g. using smart contract functionality. When ether is sent to a smart contract, it may inadvertently become "locked" within the contract due to errors in the smart contract's source code.
The Locked Ether vulnerability refers to smart contract code that may cause ether to become irretrievable or inaccessible from a smart contract's address. This ether can neither be accessed by the contract's creator nor any users who might have sent their funds to the contract. It essentially remains frozen within the blockchain.
A real-world example: the Parity multisignature wallet
On November 6th, 2017, a vulnerability in a smart contract system developed by Parity Technologies was exploited, leading to the locking of ether in vulnerable user's wallets. The wallets themselves were smart contract-based multisignature wallets, which relied on a shared library contract for core logical functionality if deployed by users after July 20th, 2017, including logic for the movement of funds. An anonymous user identified a flaw within this shared library code, allowing them to take control of the library contract and subsequently destroy it, impacting 587 wallets reliant on this functionality. This exploit resulted in 513,774.16 ether and additional token assets becoming inaccessible.
Further reading: A Postmortem on the Parity Multi-Sig Library Self-Destruct
Technical example of vulnerable code
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.0;
contract Locked {
mapping(address => uint) public balances;
function receiveEther() payable public {
balances[msg.sender] += msg.value;
}
function payToUser(address user, uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[user] += amount;
}
}
In the above example, contract Locked
implements simplified bank-style functionality for users' ether. It exposes a payable
function named receive()
that allows it to accept ether deposits from users, crediting them with a balance in the balances
mapping. The contract also exposes a function named payToUser()
that allows users to transfer their balance to another user. The function checks that the user has sufficient funds in their balance before transferring the funds to the recipient.
Notably, while the payToUser()
function allows for ether to "move" between users of the contract, that ether does not actually leave the ownership of the smart contract's address when the function is called; rather, balances on an internal ledger are updated to reflect ownership of the ether that the contract holds. As there is no mechanism to actually withdraw any credited ether balance to an external address, any ether sent to this contract using the receive()
function will be permanently locked within the contract.
Technical example of how to fix the vulnerability
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.0;
contract Unlocked {
mapping(address => uint) public balances;
function receiveEther() payable public{
balances[msg.sender] += msg.value;
}
function payToUser(address user, uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[user] += amount;
}
function withdraw(uint amount) public{
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
msg.sender.transfer(amount);
}
}
In the corrected code example, contract Unlocked
includes a function withdraw()
, which allows users of the contract who have credited balances to withdraw some or all of their balance in the form of ether. Because the contract now has both entry and exit points for ether, it is not subject to the Locked Ether vulnerability.