High severity
Multiple constructors in one contract
Updated 2026-10-13
Before Solidity 0.4.22 a contract’s constructor was an ordinary function that shared the contract’s name. Version 0.4.22 introduced the dedicated constructor keyword and deprecated the name-matching form. In the narrow window of 0.4.22 and 0.4.23 a contract could still declare both — a constructor() and a function <ContractName>() — and the compiler accepted it, keeping the first as the real constructor and silently discarding the second. The setup you believed ran at deployment might never run: an owner stays address(0), a guard flag stays false, a fee or cap stays at its zero default. The deployment transaction succeeds, nothing reverts, and no event hints that half of your initialization was dropped on the floor.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity 0.4.23;
contract Vault {
address public owner;
// Added during a refactor to the new keyword, but left as a stub.
constructor() public {}
// The original setup, still here. On 0.4.22/0.4.23 this compiles,
// but the compiler treats the constructor above as THE constructor
// and never runs this one at deployment.
function Vault() public {
owner = msg.sender;
}
function sweep() public {
require(msg.sender == owner);
msg.sender.transfer(address(this).balance);
}
}The deployer expects owner to be set to their address. It is not: the empty constructor() is the one the compiler runs, and the function Vault() that assigned owner is ignored as a constructor. The contract ships with owner left at address(0). Now sweep requires msg.sender == address(0), which no transaction can ever satisfy, so every wei paid into the vault is locked forever. Flip the two bodies and the failure mirrors: if the dropped constructor was the one that tightened a permission or set a cap, the contract ships wide open instead of bricked. Either way the damage is invisible at deploy time and only surfaces when someone tries to use the state that was never initialized.
The fix
Declare exactly one constructor, and prefer the modern constructor(...) form. Delete the name-matching function entirely. A contract has one place where deployment-time setup lives, and there is no second definition for the compiler to pick between.
// SPDX-License-Identifier: MIT
pragma solidity 0.4.23;
contract Vault {
address public owner;
constructor() public {
owner = msg.sender;
}
function sweep() public {
require(msg.sender == owner);
msg.sender.transfer(address(this).balance);
}
}A note on scope
This is a legacy-compiler shape, and the compiler closed it in stages. The name-matching constructor was deprecated the moment 0.4.22 added the constructor keyword, so declaring one already draws a warning. From 0.4.24 the compiler rejects a contract that declares two constructors outright — Error: More than one constructor defined — which we confirmed by compiling the pattern above with solc 0.4.24 and 0.4.26. Solidity 0.5.0 removed the name-matching form completely. So the only versions where this compiles and ships are 0.4.22 and 0.4.23, and a live finding points at a contract built with one of them. That still matters: plenty of value sits in contracts deployed in that era, and a scanner that reads verified source or old bytecode will meet them. The detector simply counts the constructors a single contract declares and flags any contract with more than one, so it never fires on code that would not compile in the first place.
Scan your contract for this
pactlint flags multiple-constructors 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.
Two constructor definitions in one contract are detected by Slither’s stock
multiple-constructors detector, rated high impact at high confidence. Because high impact only
ever drops a notch when confidence is low, pactlint surfaces this one at high severity, and it runs no
worker-side triage on it — it is neither suppressed as noise nor downgraded, so a reported finding stays
High as the analyser rates it. Run it in CI with the
pactlint GitHub Action.
Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.