Skip to content

Commit ca27f47

Browse files
ronhuafengAmxxfrangio
committed
Add Ownable2Step extension with 2-step transfer (OpenZeppelin#3620)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: Francisco <frangio.1@gmail.com>
1 parent 0790afa commit ca27f47

File tree

4 files changed

+122
-0
lines changed

4 files changed

+122
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* `Checkpoints`: Add new lookup mechanisms. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
2828
* `Array`: Add `unsafeAccess` functions that allow reading and writing to an element in a storage array bypassing Solidity's "out-of-bounds" check. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
2929
* `Strings`: optimize `toString`. ([#3573](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3573))
30+
* `Ownable2Step`: extension of `Ownable` that makes the ownership transfers a two step process. ([#3620](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3620))
3031

3132
### Breaking changes
3233

contracts/access/Ownable2Step.sol

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)
3+
4+
pragma solidity ^0.8.0;
5+
6+
import "./Ownable.sol";
7+
8+
/**
9+
* @dev Contract module which provides access control mechanism, where
10+
* there is an account (an owner) that can be granted exclusive access to
11+
* specific functions.
12+
*
13+
* By default, the owner account will be the one that deploys the contract. This
14+
* can later be changed with {transferOwnership} and {acceptOwnership}.
15+
*
16+
* This module is used through inheritance. It will make available all functions
17+
* from parent (Ownable).
18+
*/
19+
abstract contract Ownable2Step is Ownable {
20+
address private _pendingOwner;
21+
22+
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
23+
24+
/**
25+
* @dev Returns the address of the pending owner.
26+
*/
27+
function pendingOwner() public view virtual returns (address) {
28+
return _pendingOwner;
29+
}
30+
31+
/**
32+
* @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one.
33+
* Can only be called by the current owner.
34+
*/
35+
function transferOwnership(address newOwner) public virtual override onlyOwner {
36+
_pendingOwner = newOwner;
37+
emit OwnershipTransferStarted(owner(), newOwner);
38+
}
39+
40+
/**
41+
* @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner.
42+
* Internal function without access restriction.
43+
*/
44+
function _transferOwnership(address newOwner) internal virtual override {
45+
delete _pendingOwner;
46+
super._transferOwnership(newOwner);
47+
}
48+
49+
/**
50+
* @dev The new owner accepts the ownership transfer.
51+
*/
52+
function acceptOwnership() external {
53+
address sender = _msgSender();
54+
require(pendingOwner() == sender, "Ownable2Step: caller is not the new owner");
55+
_transferOwnership(sender);
56+
}
57+
}

contracts/mocks/Ownable2StepMock.sol

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
4+
5+
import "../access/Ownable2Step.sol";
6+
7+
contract Ownable2StepMock is Ownable2Step {}

test/access/Ownable2Step.test.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
2+
const { ZERO_ADDRESS } = constants;
3+
const { expect } = require('chai');
4+
5+
const Ownable2Step = artifacts.require('Ownable2StepMock');
6+
7+
contract('Ownable2Step', function (accounts) {
8+
const [owner, accountA, accountB] = accounts;
9+
10+
beforeEach(async function () {
11+
this.ownable2Step = await Ownable2Step.new({ from: owner });
12+
});
13+
14+
describe('transfer ownership', function () {
15+
it('starting a transfer does not change owner', async function () {
16+
const receipt = await this.ownable2Step.transferOwnership(accountA, { from: owner });
17+
expectEvent(receipt, 'OwnershipTransferStarted', { previousOwner: owner, newOwner: accountA });
18+
expect(await this.ownable2Step.owner()).to.equal(owner);
19+
expect(await this.ownable2Step.pendingOwner()).to.equal(accountA);
20+
});
21+
22+
it('changes owner after transfer', async function () {
23+
await this.ownable2Step.transferOwnership(accountA, { from: owner });
24+
const receipt = await this.ownable2Step.acceptOwnership({ from: accountA });
25+
expectEvent(receipt, 'OwnershipTransferred', { previousOwner: owner, newOwner: accountA });
26+
expect(await this.ownable2Step.owner()).to.equal(accountA);
27+
expect(await this.ownable2Step.pendingOwner()).to.not.equal(accountA);
28+
});
29+
30+
it('changes owner after renouncing ownership', async function () {
31+
await this.ownable2Step.renounceOwnership({ from: owner });
32+
// If renounceOwnership is removed from parent an alternative is needed ...
33+
// without it is difficult to cleanly renounce with the two step process
34+
// see: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3620#discussion_r957930388
35+
expect(await this.ownable2Step.owner()).to.equal(ZERO_ADDRESS);
36+
});
37+
38+
it('pending owner resets after renouncing ownership', async function () {
39+
await this.ownable2Step.transferOwnership(accountA, { from: owner });
40+
expect(await this.ownable2Step.pendingOwner()).to.equal(accountA);
41+
await this.ownable2Step.renounceOwnership({ from: owner });
42+
expect(await this.ownable2Step.pendingOwner()).to.equal(ZERO_ADDRESS);
43+
await expectRevert(
44+
this.ownable2Step.acceptOwnership({ from: accountA }),
45+
'Ownable2Step: caller is not the new owner',
46+
);
47+
});
48+
49+
it('guards transfer against invalid user', async function () {
50+
await this.ownable2Step.transferOwnership(accountA, { from: owner });
51+
await expectRevert(
52+
this.ownable2Step.acceptOwnership({ from: accountB }),
53+
'Ownable2Step: caller is not the new owner',
54+
);
55+
});
56+
});
57+
});

0 commit comments

Comments
 (0)