Constructor Allows Zero Owner - Permanent Loss of Admin Control (Contract Bricking)
Description
-
Normal behavior
The contract assigns an owner during deployment, which is responsible for executing privileged administrative functions such as revealing the collection, managing the whitelist, and withdrawing collected fees.
-
Issue
The constructor does not validate the _owner parameter. If address(0) is provided, all onlyOwner functions become permanently inaccessible, since no entity can ever satisfy msg.sender == address(0). This results in irreversible loss of administrative control and renders critical protocol functionality unusable.
constructor(
address _owner,
address _usdc,
string memory _collectionName,
string memory _symbol,
string memory _collectionImage,
uint256 _lockAmount
) ERC721(_collectionName, _symbol) {
owner = _owner;
usdc = IERC20(_usdc);
collectionName = _collectionName;
tokenSymbol = _symbol;
collectionImage = _collectionImage;
lockAmount = _lockAmount;
}
Risk
Likelihood: Low
-
Misconfiguration during deployment (manual deploy scripts, factory contracts, or parameter injection)
-
No validation prevents assigning an invalid owner
Impact: High
-
Permanent loss of all administrative capabilities
-
Critical protocol features become unusable (reveal, whitelist management, fee withdrawal)
Proof of Concept
This PoC demonstrates that the contract can be deployed with owner = address(0), resulting in an invalid administrative state.
As a result, all administrative functions using owner = _owner become permanently inaccessible, effectively bricking the contract.
pragma solidity ^0.8.18;
import "forge-std/Test.sol";
import "../src/NFTDealers.sol";
import "../src/MockUSDC.sol";
contract ZeroOwner_PoC is Test {
NFTDealers nft;
MockUSDC usdc;
address wrong_owner = address(0);
function setUp() public {
usdc = new MockUSDC();
nft = new NFTDealers(
wrong_owner,
address(usdc),
"NFT Dealers",
"NFTD",
"ipfs://image",
20e6
);
}
function test_owner_is_zero_address() public {
assertEq(nft.owner(), address(0));
}
}
Results
forge test --match-contract ZeroOwner_PoC -vvvv
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/NFTDealers_ZeroOwner_PoC.t.sol:ZeroOwner_PoC
[PASS] test_owner_is_zero_address() (gas: 11232)
Traces:
[11232] ZeroOwner_PoC::test_owner_is_zero_address()
├─ [2598] NFTDealers::owner() [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000000000
├─ [0] VM::assertEq(0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000) [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 824.95µs (60.40µs CPU time)
Recommended Mitigation
Adding a validation check in the constructor prevents deployment with an invalid owner.
This ensures that at least one valid address can execute privileged functions, preserving administrative control and preventing irreversible contract bricking.
constructor(
address _owner,
address _usdc,
string memory _collectionName,
string memory _symbol,
string memory _collectionImage,
uint256 _lockAmount
) ERC721(_collectionName, _symbol) {
+ if (_owner == address(0)) revert InvalidAddress();
owner = _owner;
usdc = IERC20(_usdc);
collectionName = _collectionName;
tokenSymbol = _symbol;
collectionImage = _collectionImage;
lockAmount = _lockAmount;
}