-
Notifications
You must be signed in to change notification settings - Fork 11.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate Ownable tests #4657
Migrate Ownable tests #4657
Changes from 8 commits
cf3c377
673ff79
c9b4e8d
c46a45e
328a344
7c12232
517ad87
9002e5f
74558f8
9ea2db1
fd7100a
1a9a046
b569ca8
b24fec4
5e96021
f382329
95be46d
55b0406
e45e482
b391c2f
74bab81
98e5ecd
4db1800
0a8a69d
ec3bfc5
5d5a150
300fa1e
ae4381e
b02770e
4e6a1ca
69b91a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,72 +1,79 @@ | ||
const { constants, expectEvent } = require('@openzeppelin/test-helpers'); | ||
const { expectRevertCustomError } = require('../helpers/customError'); | ||
|
||
const { ZERO_ADDRESS } = constants; | ||
|
||
const { ethers } = require('hardhat'); | ||
const { expect } = require('chai'); | ||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); | ||
|
||
const Ownable = artifacts.require('$Ownable'); | ||
|
||
contract('Ownable', function (accounts) { | ||
const [owner, other] = accounts; | ||
async function fixture() { | ||
const accounts = await ethers.getSigners(); // this is slow :/ | ||
const owner = accounts.shift(); | ||
const other = accounts.shift(); | ||
const ownable = await ethers.deployContract('$Ownable', [owner.address]); | ||
return { accounts, owner, other, ownable }; | ||
} | ||
|
||
describe('Ownable', function () { | ||
beforeEach(async function () { | ||
this.ownable = await Ownable.new(owner); | ||
await loadFixture(fixture).then(results => Object.assign(this, results)); | ||
}); | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
it('rejects zero address for initialOwner', async function () { | ||
await expectRevertCustomError(Ownable.new(constants.ZERO_ADDRESS), 'OwnableInvalidOwner', [constants.ZERO_ADDRESS]); | ||
// checking a custom error requires a contract, or at least the interface | ||
// we can get it from the contract factory | ||
const { interface } = await ethers.getContractFactory('$Ownable'); | ||
|
||
await expect(ethers.deployContract('$Ownable', [ethers.ZeroAddress])) | ||
.to.be.revertedWithCustomError({ interface }, 'OwnableInvalidOwner') | ||
.withArgs(ethers.ZeroAddress); | ||
}); | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
it('has an owner', async function () { | ||
expect(await this.ownable.owner()).to.equal(owner); | ||
expect(await this.ownable.owner()).to.equal(this.owner.address); | ||
}); | ||
|
||
describe('transfer ownership', function () { | ||
it('changes owner after transfer', async function () { | ||
const receipt = await this.ownable.transferOwnership(other, { from: owner }); | ||
expectEvent(receipt, 'OwnershipTransferred'); | ||
await expect(this.ownable.connect(this.owner).transferOwnership(this.other.address)) | ||
.to.emit(this.ownable, 'OwnershipTransferred') | ||
.withArgs(this.owner.address, this.other.address); | ||
|
||
expect(await this.ownable.owner()).to.equal(other); | ||
expect(await this.ownable.owner()).to.equal(this.other.address); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using Are there any workarounds? Is it worth it to contribute to Hardhat to improve this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I once wrote an helper that is const getAddress = (account : string | { address: string }) : string => account.address ?? account; but calling it everywhere is even worst. Maybe it could be integrated by default ... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: I'd love to simplify the code by removing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree with migrating using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For anyone curious, this PR NomicFoundation/hardhat#4449 resolves the issue in hardhat-chai-matchers |
||
|
||
it('prevents non-owners from transferring', async function () { | ||
await expectRevertCustomError( | ||
this.ownable.transferOwnership(other, { from: other }), | ||
'OwnableUnauthorizedAccount', | ||
[other], | ||
); | ||
await expect(this.ownable.connect(this.other).transferOwnership(this.other.address)) | ||
.to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') | ||
.withArgs(this.other.address); | ||
}); | ||
|
||
it('guards ownership against stuck state', async function () { | ||
await expectRevertCustomError( | ||
this.ownable.transferOwnership(ZERO_ADDRESS, { from: owner }), | ||
'OwnableInvalidOwner', | ||
[ZERO_ADDRESS], | ||
); | ||
await expect(this.ownable.connect(this.owner).transferOwnership(ethers.ZeroAddress)) | ||
.to.be.revertedWithCustomError(this.ownable, 'OwnableInvalidOwner') | ||
.withArgs(ethers.ZeroAddress); | ||
}); | ||
}); | ||
|
||
describe('renounce ownership', function () { | ||
it('loses ownership after renouncement', async function () { | ||
const receipt = await this.ownable.renounceOwnership({ from: owner }); | ||
expectEvent(receipt, 'OwnershipTransferred'); | ||
await expect(this.ownable.connect(this.owner).renounceOwnership()) | ||
.to.emit(this.ownable, 'OwnershipTransferred') | ||
.withArgs(this.owner.address, ethers.ZeroAddress); | ||
|
||
expect(await this.ownable.owner()).to.equal(ZERO_ADDRESS); | ||
expect(await this.ownable.owner()).to.equal(ethers.ZeroAddress); | ||
}); | ||
|
||
it('prevents non-owners from renouncement', async function () { | ||
await expectRevertCustomError(this.ownable.renounceOwnership({ from: other }), 'OwnableUnauthorizedAccount', [ | ||
other, | ||
]); | ||
await expect(this.ownable.connect(this.other).renounceOwnership()) | ||
.to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') | ||
.withArgs(this.other.address); | ||
}); | ||
|
||
it('allows to recover access using the internal _transferOwnership', async function () { | ||
await this.ownable.renounceOwnership({ from: owner }); | ||
const receipt = await this.ownable.$_transferOwnership(other); | ||
expectEvent(receipt, 'OwnershipTransferred'); | ||
await this.ownable.connect(this.owner).renounceOwnership(); | ||
|
||
await expect(this.ownable.$_transferOwnership(this.other.address)) | ||
.to.emit(this.ownable, 'OwnershipTransferred') | ||
.withArgs(ethers.ZeroAddress, this.other.address); | ||
|
||
expect(await this.ownable.owner()).to.equal(other); | ||
expect(await this.ownable.owner()).to.equal(this.other.address); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,70 +1,83 @@ | ||
const { constants, expectEvent } = require('@openzeppelin/test-helpers'); | ||
const { ZERO_ADDRESS } = constants; | ||
const { ethers } = require('hardhat'); | ||
const { expect } = require('chai'); | ||
const { expectRevertCustomError } = require('../helpers/customError'); | ||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); | ||
|
||
const Ownable2Step = artifacts.require('$Ownable2Step'); | ||
|
||
contract('Ownable2Step', function (accounts) { | ||
const [owner, accountA, accountB] = accounts; | ||
async function fixture() { | ||
const accounts = await ethers.getSigners(); | ||
const owner = accounts.shift(); | ||
const accountA = accounts.shift(); | ||
const accountB = accounts.shift(); | ||
const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner.address]); | ||
return { accounts, owner, accountA, accountB, ownable2Step }; | ||
} | ||
|
||
describe('Ownable2Step', function () { | ||
beforeEach(async function () { | ||
this.ownable2Step = await Ownable2Step.new(owner); | ||
await loadFixture(fixture).then(results => Object.assign(this, results)); | ||
}); | ||
|
||
describe('transfer ownership', function () { | ||
it('starting a transfer does not change owner', async function () { | ||
const receipt = await this.ownable2Step.transferOwnership(accountA, { from: owner }); | ||
expectEvent(receipt, 'OwnershipTransferStarted', { previousOwner: owner, newOwner: accountA }); | ||
expect(await this.ownable2Step.owner()).to.equal(owner); | ||
expect(await this.ownable2Step.pendingOwner()).to.equal(accountA); | ||
await expect(this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address)) | ||
.to.emit(this.ownable2Step, 'OwnershipTransferStarted') | ||
.withArgs(this.owner.address, this.accountA.address); | ||
|
||
expect(await this.ownable2Step.owner()).to.equal(this.owner.address); | ||
expect(await this.ownable2Step.pendingOwner()).to.equal(this.accountA.address); | ||
}); | ||
|
||
it('changes owner after transfer', async function () { | ||
await this.ownable2Step.transferOwnership(accountA, { from: owner }); | ||
const receipt = await this.ownable2Step.acceptOwnership({ from: accountA }); | ||
expectEvent(receipt, 'OwnershipTransferred', { previousOwner: owner, newOwner: accountA }); | ||
expect(await this.ownable2Step.owner()).to.equal(accountA); | ||
expect(await this.ownable2Step.pendingOwner()).to.not.equal(accountA); | ||
await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address); | ||
|
||
await expect(this.ownable2Step.connect(this.accountA).acceptOwnership()) | ||
.to.emit(this.ownable2Step, 'OwnershipTransferred') | ||
.withArgs(this.owner.address, this.accountA.address); | ||
|
||
expect(await this.ownable2Step.owner()).to.equal(this.accountA.address); | ||
expect(await this.ownable2Step.pendingOwner()).to.equal(ethers.ZeroAddress); | ||
}); | ||
|
||
it('guards transfer against invalid user', async function () { | ||
await this.ownable2Step.transferOwnership(accountA, { from: owner }); | ||
await expectRevertCustomError( | ||
this.ownable2Step.acceptOwnership({ from: accountB }), | ||
'OwnableUnauthorizedAccount', | ||
[accountB], | ||
); | ||
await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address); | ||
|
||
await expect(this.ownable2Step.connect(this.accountB).acceptOwnership()) | ||
.to.be.revertedWithCustomError(this.ownable2Step, 'OwnableUnauthorizedAccount') | ||
.withArgs(this.accountB.address); | ||
}); | ||
}); | ||
|
||
describe('renouncing ownership', async function () { | ||
it('changes owner after renouncing ownership', async function () { | ||
await this.ownable2Step.renounceOwnership({ from: owner }); | ||
await expect(this.ownable2Step.connect(this.owner).renounceOwnership()) | ||
.to.emit(this.ownable2Step, 'OwnershipTransferred') | ||
.withArgs(this.owner.address, ethers.ZeroAddress); | ||
|
||
// If renounceOwnership is removed from parent an alternative is needed ... | ||
// without it is difficult to cleanly renounce with the two step process | ||
// see: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3620#discussion_r957930388 | ||
expect(await this.ownable2Step.owner()).to.equal(ZERO_ADDRESS); | ||
expect(await this.ownable2Step.owner()).to.equal(ethers.ZeroAddress); | ||
}); | ||
|
||
it('pending owner resets after renouncing ownership', async function () { | ||
await this.ownable2Step.transferOwnership(accountA, { from: owner }); | ||
expect(await this.ownable2Step.pendingOwner()).to.equal(accountA); | ||
await this.ownable2Step.renounceOwnership({ from: owner }); | ||
expect(await this.ownable2Step.pendingOwner()).to.equal(ZERO_ADDRESS); | ||
await expectRevertCustomError( | ||
this.ownable2Step.acceptOwnership({ from: accountA }), | ||
'OwnableUnauthorizedAccount', | ||
[accountA], | ||
); | ||
await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address); | ||
expect(await this.ownable2Step.pendingOwner()).to.equal(this.accountA.address); | ||
|
||
await this.ownable2Step.connect(this.owner).renounceOwnership(); | ||
expect(await this.ownable2Step.pendingOwner()).to.equal(ethers.ZeroAddress); | ||
|
||
await expect(this.ownable2Step.connect(this.accountA).acceptOwnership()) | ||
.to.be.revertedWithCustomError(this.ownable2Step, 'OwnableUnauthorizedAccount') | ||
.withArgs(this.accountA.address); | ||
}); | ||
|
||
it('allows to recover access using the internal _transferOwnership', async function () { | ||
await this.ownable2Step.renounceOwnership({ from: owner }); | ||
const receipt = await this.ownable2Step.$_transferOwnership(accountA); | ||
expectEvent(receipt, 'OwnershipTransferred'); | ||
await this.ownable2Step.connect(this.owner).renounceOwnership(); | ||
|
||
await expect(this.ownable2Step.$_transferOwnership(this.accountA.address)) | ||
.to.emit(this.ownable2Step, 'OwnershipTransferred') | ||
.withArgs(ethers.ZeroAddress, this.accountA.address); | ||
|
||
expect(await this.ownable2Step.owner()).to.equal(accountA); | ||
expect(await this.ownable2Step.owner()).to.equal(this.accountA.address); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,6 @@ | ||
const RLP = require('rlp'); | ||
|
||
function computeCreateAddress(deployer, nonce) { | ||
return web3.utils.toChecksumAddress(web3.utils.sha3(RLP.encode([deployer.address ?? deployer, nonce])).slice(-40)); | ||
} | ||
|
||
function computeCreate2Address(saltHex, bytecode, deployer) { | ||
return web3.utils.toChecksumAddress( | ||
web3.utils | ||
.sha3( | ||
`0x${['ff', deployer.address ?? deployer, saltHex, web3.utils.soliditySha3(bytecode)] | ||
.map(x => x.replace(/0x/, '')) | ||
.join('')}`, | ||
) | ||
.slice(-40), | ||
); | ||
} | ||
const { ethers } = require('hardhat'); | ||
|
||
module.exports = { | ||
computeCreateAddress, | ||
computeCreate2Address, | ||
computeCreateAddress: (from, nonce) => ethers.getCreateAddress({ from, nonce }), | ||
computeCreate2Address: (salt, bytecode, from) => ethers.getCreate2Address(from, salt, ethers.keccak256(bytecode)), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could cache it in a global helper. But it should run only once per contract test suite anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is easily cacheable in
env.contract.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know how this will behave with ethers?
Would it work if in the tests we do
this.accounts.shift()
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking in something like:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And we would get the signers as an argument of
contract("...", accounts => { ... });
?
That makes sens, but it will also break the existing truffle tests, which we don't want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too maintenance heavy (and error prone) IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If loadFixture does a "per contract suite" snapshot, I don't see why we would have to also do a "cross contract suite" snapshot. That is making everything slower, and I really don't see what it brings in exchange. The only usecase I see for that, is if we wanted our test to all start with
blockNumber = 0
... and I don't think that is an assumption we should make. IMO our tests (each test.js file) should work regardless of the initial state of the chain.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could do
And then do
and
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping the high-level snapshot makes sense if not all the contract suites implement a fixture. Let's try removing the high-level snapshot once we finish with the migration as well.
I like this, let's implement it that way 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Side note, I also added a comment in #4658 to keep track of things we should do to consolidate the migration once done