SCWE-139: Single-Step Ownership Transfer Without Confirmation
Stable Version v1.0
This content is in the version-(v1.0) and still under active development, so it is subject to change any time (e.g. structure, IDs, content, URLs, etc.).
Relationships¶
- CWE-670: Always-Incorrect Control Flow Implementation
https://cwe.mitre.org/data/definitions/670.html
Description¶
A single-step transferOwnership(newOwner) immediately assigns ownership to the new address. If the wrong address is used (typo, burn address address(0), or a contract that cannot receive ownership), the contract can be permanently locked—no one can perform owner-only actions or correct the mistake. A two-step process (propose → accept) allows the intended recipient to confirm and prevents accidental loss of control.
Remediation¶
- Implement a two-step ownership transfer:
transferOwnershipPending(newOwner)sets a pending owner, andacceptOwnership()(callable only by the pending owner) completes the transfer. - Validate that
newOwner != address(0)and consider rejecting contract addresses if the owner must be an EOA for operational reasons.
Examples¶
Vulnerable¶
pragma solidity ^0.8.0;
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
function transferOwnership(address newOwner) external {
require(msg.sender == owner, "Not owner");
owner = newOwner; // Single step: typo or address(0) permanently locks contract
}
}
Fixed¶
pragma solidity ^0.8.0;
contract Ownable {
address public owner;
address public pendingOwner;
constructor() {
owner = msg.sender;
}
function transferOwnership(address newOwner) external {
require(msg.sender == owner, "Not owner");
require(newOwner != address(0), "Zero address");
pendingOwner = newOwner;
}
function acceptOwnership() external {
require(msg.sender == pendingOwner, "Not pending owner");
owner = pendingOwner;
delete pendingOwner;
}
}