Default Visibility Remediation
How to eliminate default visibility vulnerabilities by always specifying explicit visibility modifiers on all functions and state variables, and upgrading to Solidity 0.5.0+ which enforces this at compile time.
Default Visibility Remediation
Overview
Default visibility vulnerabilities arise in contracts compiled with Solidity versions before 0.5.0, where omitting a visibility modifier caused functions to default to public. An attacker can call any publicly visible function from any address, including initialization functions that set the owner, destructor functions that call selfdestruct, and admin helper functions that modify critical state variables.
Two canonical historical exploits demonstrate the severity. The Rubixi contract (2016) was renamed from DynamicPyramid to Rubixi without updating the constructor function name. In Solidity 0.4.x, constructors were identified by matching the contract name, so the old DynamicPyramid() function became an ordinary public function. Attackers called it to claim ownership and drain the contract. The Parity multi-sig wallet hack (2017) involved an initWallet function that was publicly callable after the contract was deployed, allowing any user to call it and become the owner.
The primary fix is to upgrade to Solidity 0.5.0 or later, which requires an explicit visibility modifier on all functions and state variables and rejects missing ones at compile time.
Related Detector: Access Control Detector
Recommended Fix
Before (Vulnerable)
// VULNERABLE — Solidity 0.4.x: missing visibility defaults to public
pragma solidity ^0.4.24;
contract VulnerableWallet {
address owner; // Missing visibility — defaults to internal (state vars)
uint256 public balance;
// CRITICAL: No visibility — defaults to PUBLIC in Solidity 0.4.x
// Any address can call this after deployment and become the owner
function initOwner(address _owner) {
owner = _owner;
}
// CRITICAL: No visibility — anyone can destroy the contract
function kill() {
selfdestruct(msg.sender);
}
// HIGH: No visibility — admin function exposed publicly
function resetBalance() {
balance = 0;
}
function withdraw(uint256 amount) public {
require(msg.sender == owner);
require(balance >= amount);
balance -= amount;
msg.sender.transfer(amount);
}
}
After (Fixed)
// SAFE — Solidity 0.5.0+: compiler requires explicit visibility
pragma solidity ^0.8.0;
contract SafeWallet {
address private owner; // Explicit: private
uint256 public balance; // Explicit: public
// Constructor replaces the function-name constructor pattern
// Cannot be called after deployment — not a regular function
constructor(address _owner) {
require(_owner != address(0), "Invalid owner");
owner = _owner;
}
// All functions have explicit visibility
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
// external: callable from outside only (cheaper than public for calldata args)
function withdraw(uint256 amount) external onlyOwner {
require(amount > 0 && amount <= balance, "Invalid amount");
balance -= amount;
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// internal: only callable from this contract or derived contracts
function _resetBalance() internal onlyOwner {
balance = 0;
}
// private: only callable from this contract
function _validateOwner(address candidate) private view returns (bool) {
return candidate == owner;
}
receive() external payable {
balance += msg.value;
}
}
In Solidity 0.5.0+, the following fails to compile, eliminating the vulnerability class entirely:
// Solidity 0.5.0+: compile error — visibility required
function initOwner(address _owner) { // Error: Visibility must be specified
owner = _owner;
}
Alternative Mitigations
constructor keyword instead of function-name constructors — Solidity 0.4.22 introduced the constructor keyword as a safer alternative to function-name constructors. The constructor function is only callable during deployment and cannot be accidentally exposed as a public function through a rename:
// Solidity 0.4.22+: Use constructor keyword
pragma solidity ^0.4.22;
contract ModernConstructor {
address public owner;
// Safe: constructor keyword — not callable post-deployment
constructor() public {
owner = msg.sender;
}
// All other functions must have explicit visibility
function adminReset() internal {
// internal — not publicly callable
}
}
Explicit visibility on all state variables — while state variable visibility defaults to internal (which is safe from external callers), always specify it explicitly for documentation and auditability:
address private owner; // Private: not accessible in derived contracts
uint256 internal feeRate; // Internal: accessible in derived contracts
mapping(address => uint256) public balances; // Public: auto-generates getter
Access control library — use OpenZeppelin’s Ownable or AccessControl for ownership and role management. These libraries have been extensively audited and handle initialization, transfer, and renouncement correctly:
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract RoleBasedContract is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(ADMIN_ROLE, msg.sender);
}
function sensitiveOperation() external onlyRole(ADMIN_ROLE) {
// Protected by role check, not implicit visibility
}
}
Static analysis in CI — configure Slither with the suicidal, uninitialized-state, and missing-zero-check detectors, and Solhint with the func-visibility rule to catch missing visibility modifiers before deployment:
# Catch missing visibility modifiers
slither . --detect functions-that-send-ether-to-arbitrary-destinations
solhint --rules func-visibility contracts/**/*.sol
Naming conventions — adopt naming conventions that make visibility explicit by visual inspection. Prefix internal and private functions with a leading underscore (_internalHelper). While this is not enforced by the compiler in older Solidity versions, it provides an audit signal:
function _computeFee(uint256 amount) internal returns (uint256) { ... } // internal
function __sensitiveReset() private { ... } // private helper
function publicEntry() external { ... } // external API
Common Mistakes
Relying on naming conventions alone in Solidity 0.4.x — naming a function _internal does not make it internal in Solidity 0.4.x. Only the explicit internal keyword restricts access. Always verify the compiler version and the actual visibility modifier.
Upgrading to 0.5.0+ without auditing existing function names — when migrating a 0.4.x contract, the compiler will flag all functions lacking explicit visibility as errors. This is helpful, but developers may add public to all flagged functions without considering whether internal, private, or external would be more appropriate.
Forgetting that state variable getters are public — declaring uint256 public secretKey auto-generates a public getter. Sensitive values should use private or internal, and should be retrieved only through controlled functions if access is needed at all.
Constructor function name mismatch after rename — the canonical Rubixi mistake: renaming a contract without updating the constructor function name in Solidity 0.4.x. Always use the constructor keyword (0.4.22+) or confirm the contract name matches the constructor function name when working with older compilers.