We define a multi-stepped contract ownership interface for more secure contract ownership management. This makes the ownership transfer into 3 distinct steps. With the first 2 steps, performed by the original owner (initiate → confirm) and the remaining 1 step performed by the new owner (accept).
We enforce an optional time window between the initiate and confirm stages to give additional room for review, and make ownership key compromise scenarios less fatal.
Motivation
Ownership management is crucial in on-chain security and a significant portion of the security assumptions of defi protocols, smart contract wallets and on-chain utilities rely on the contract ownership.
The single-step transferOwnership() style ownership mechanism has been in the industry for a long time, (e.g.,ERC-173), and has been used ubiquitously as an industry standard. As the industry evolves and attacks get more sophisticated there is a strong need for a multi-step, time gated ownership management process to enhance the ecosystem’s contract ownership to be more reviewable, stoppable, thorough and secure.
The main objective of this standard is to make ownership more secure and handled in a multi-stepped approach that enables the operation to be conducted with more caution and lesser operational mistakes, while being immune to potential scam attacks.
Key factors taken into consideration for the standard:
Reduce probability of operational mistakes.
Foster on-chain reviewal practice for ownership transfer.
Ability to rollback ownership transfer, during the transfer process.
Simplicity.
1. Reduce probability of operational mistakes: The standard makes the ownership transfer stage into 3 different clear steps. Initiation → Confirmation → Acceptance. The owner will have the enforced ability to review the newOwner address secured by the pre-set buffer time. Also to reduce any operational mistakes or possible scams (e.g., address poisoning) the address is required as the parameter in each Initiation & Confirmation stage.
2. Foster on-chain reviewal practice for ownership transfer: We not only targets this as a contract interface and implementation methodology, but also hopes to foster an ecosystem-level awareness and security practice to thoroughly review, confirm the ownership transfer. The standard helps operators of Smart Contract to review newOwner address on-chain, and further confirm again if the address is indeed correct.
3. Ability to rollback ownership transfer, during the transfer process: Whether through an operational mistake or private key leak of owner account, or other reasons, the ability to rollback ownership transfer within the time buffer highly increases the security and operational burden.
Even in the extreme case of owner private key leak, if the buffer time is set enough, the original owner can earn time to evacuate the funds from the protocol, and possibly prohibit ownership transfer through DoS of ownership (attack → initiate , original owner(defender) → re initiate. which will reset the time back to 0).
4. Simplicity:MultistepOwnable is targeted to be a simple contract given the diverse use cases and scenarios it could be applied. The process for ownership transfer is concise but thorough enough to allow owners review each step. This is the rationale behind making the ownership transfer time buffer and buffer time update capability optional.
Specification
The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
A multi-step ownable contract MUST implement the following interface:
/// @title Multistep Ownership Standard
interfaceMultiStepOwnable{eventOwnershipTransferInitiated(addressindexedprevOwner,addressindexednewOwner);eventOwnershipTransferConfirmed(addressindexedprevOwner,addressindexednewOwner);eventOwnwershipTransferred(addressindexedprevOwner,addressindexednewOwner);/// @dev initiate the ownership transfer. First step of ownership transfer.
/// moves the newOwner to the preConfirmed stage.
///
/// @param newOwner the address of the new owner of the contract.
/// stored as preConfirmedOwner.
functioninitiateOwnershipTransfer(addressnewOwner)external;/// @dev confirm the ownership transfer. Second step of ownership transfer.
/// confirmation can only be done after the transfer-buffer period from initiation.
/// newOwner should match with the initiation step's newOwner.
/// To initiate ownership transfer to a different newOwner, initiation step should be re-conducted.
///
/// @param newOwner the address of the new owner of the contract.
/// stored as pendingOwner.
functionconfirmOwnershipTransfer(addressnewOwner)external;/// @dev cancels the pending ownership transfer. Before the final step of ownership transfer (acceptOwnershipTransfer()).
/// This function should wipe out the pendingOwner.
/// By calling this function, ownership transfer process is canceled and should be reinitiated from initiateOwnershipTransfer().
functioncancelPendingOwnershipTransfer()external;/// @dev accepts the ownership transfer. Final step of ownership transfer.
/// This function can only be called by the newOwner that was confirmed in step 2.
/// The contract should perform access control e.g.,
/// msg.sender == pendingOwner()
functionacceptOwnershipTransfer()external;/// @notice only the address returned by owner() has authority as the owner.
/// pendingOwner() and preConfirmedOwner() should not possess any
/// authority/access/right.
/// @dev returns the owner of the contract
functionowner()externalviewreturns(address);/// @dev returns the pending owner of the account.
/// pending owner should not have any authority/access/right.
functionpendingOwner()externalviewreturns(address);/// @dev returns the pre-confirmed owner of the account.
/// pre-confirmed owner should not have any authority/access/right.
functionpreConfirmedOwner()externalviewreturns(address);/// @dev returns the ownership transfer buffer time (in seconds).
/// the buffer is enforced between initiation <> confirmation of ownership transfer.
/// the standard does not enforce the value range. it is highly recommended to be between 2 <> 14 days.
functiongetOwnershipTransferBuffer()externalviewreturns(uint256);}
The MultiStepOwnable contract MAY implement the UpdateableOwnershipTransferBuffer interface to enable buffer period modification.
The contract MUST update the buffer period with a 2 step approach of initiation (initiateOwnershipBufferUpdate()) and then confirmation (confirmOwnershipBufferUpdate()) after the existing buffer period. The buffer period should be enforced between these 2 function calls.
If this behavior is not enforced, the security of ownershipTransferBuffer could break during owner key compromise scenario.
/// @title UpdateableOwnershipTransferBuffer. Extension of MultiStepOwnable.
interfaceUpdateableOwnershipTransferBuffer{/// @dev initiates the update of ownership transfer time buffer.
functioninitiateOwnershipBufferUpdate(uint256newBuffer)external;/// @dev confirms the update of ownership transfer time buffer.
/// confirmation SHOULD revert if existing time buffer did not pass since
/// initiation of ownership transfer time buffer.
functionconfirmOwnershipBufferUpdate(uint256newBuffer)external;}
Rationale
A time buffer for ownership transfer is introduced to foster a process of reviewing the new owner address on-chain. However, this remains an optional behavior to allow flexibility in ownership management. Removing the optional time buffer would be similar to the implementation of Ownable2Step with an additional step for confirmation.
Enforcing a time buffer to update the ownership transfer time buffer is crucial for maintaining the security of the MultiStepOwnable contract. When the owner key is compromised, this allows the original owner of the account to be able to DoS and prohibit the ownership transfer to the malicious entity when the ownership transfer buffer is sufficiently long enough.
For compatibility with existing ownership mechanisms, the standard is designed to be compatible with the existing ownership mechanism for fetching the owner through owner().
Backwards Compatibility
TBD
Security Considerations
Only owner should be available to call initiateOwnershipTransfer() & confirmOwnershipTransfer() & cancelPendingOwnershipTransfer().
OwnershipTransferBuffer should be set together when owner is set. e.g., constructor(), initialize().
If OwnershipTransferBuffer is set, it should be strictly enforced between initiation and confirmation.
Before the new owner performs acceptOwnership(), the original, existing owner should still hold all rights as the owner. Because the owner is still unchanged.