No Access Control on Payable Fallback
Overview
- Severity: Low
- Confidence: Medium
- Affected Versions: All
What is the No Access Control on Payable Fallback vulnerability?
On the Ethereum platform, smart contracts may wish to handle ether as part of a decentralized finance application. The Solidity language provides special functions which can be explicitly used to receive ether (a fallback or receive()
function which is payable). If access to these functions is not controlled in some fashion, it is possible that users may be able to send ether to a smart contract with no direct means of recovering it.
Further reading: Solidity Documentation: Special Functions
Technical example of vulnerable code
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract RestrictedAccessEtherWallet {
address public owner;
mapping(address => bool) public isRegisteredUser;
mapping(address => uint) public userBalances;
event EtherReceived(address sender, uint256 amount);
event EtherWithdrawn(address recipient, uint256 amount);
constructor() {
owner = msg.sender;
}
// Function to register users who are allowed to deposit and withdraw Ether
function registerUser(address user) external {
require(msg.sender == owner, "Only the owner can register users.");
isRegisteredUser[user] = true;
}
// Receive function to accept Ether. No access control is applied here.
receive() external payable {
userBalances[msg.sender] += msg.value;
emit EtherReceived(msg.sender, msg.value);
}
// Function to withdraw Ether, restricted to registered users
function withdrawEther(uint256 amount) external {
require(isRegisteredUser[msg.sender], "Only registered users can withdraw Ether.");
require(address(this).balance >= amount, "Insufficient balance in wallet.");
payable(msg.sender).transfer(amount);
emit EtherWithdrawn(msg.sender, amount);
}
// Additional functionality and checks omitted for brevity
}
In the above example, contract RestrictedAccessEtherWallet
implements ether management for a limited set of registered users. However, the receive()
function used by the contract to accept ether does not control access in any way, meaning users who are not registered may be able to deposit but not withdraw ether, potentially locking it permanently unless the contract also implemented some emergency recovery mechanism.
Technical example of how to fix the vulnerability
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract RestrictedAccessEtherWalletUpdated {
address public owner;
mapping(address => bool) public isRegisteredUser;
mapping(address => uint) public userBalances;
event EtherReceived(address sender, uint256 amount);
event EtherWithdrawn(address recipient, uint256 amount);
constructor() {
owner = msg.sender;
}
// Function to register users who are allowed to deposit and withdraw Ether
function registerUser(address user) external {
require(msg.sender == owner, "Only the owner can register users.");
isRegisteredUser[user] = true;
}
// Receive function to accept Ether. No access control is applied here.
receive() external payable {
require(isRegisteredUser[msg.sender], "Only registered users may send ether");
userBalances[msg.sender] += msg.value;
emit EtherReceived(msg.sender, msg.value);
}
// Function to withdraw Ether, restricted to registered users
function withdrawEther(uint256 amount) external {
require(isRegisteredUser[msg.sender], "Only registered users can withdraw Ether.");
require(address(this).balance >= amount, "Insufficient balance in wallet.");
payable(msg.sender).transfer(amount);
emit EtherWithdrawn(msg.sender, amount);
}
// Additional functionality and checks omitted for brevity
}
In the revised example above, contract RestrictedAccessEtherWalletUpdated
now performs a require()
check inside of the receive()
function, to ensure that only registered users are able to deposit ether.