Skip to content
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

Merged
merged 31 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cf3c377
migrate Ownable tests to ethers
Amxx Oct 5, 2023
673ff79
fix lint
Amxx Oct 5, 2023
c9b4e8d
Update hardhat.config.js
Amxx Oct 5, 2023
c46a45e
add Ownable2Step
Amxx Oct 5, 2023
328a344
remove deploy helper
Amxx Oct 5, 2023
7c12232
add deprecation notices
Amxx Oct 5, 2023
517ad87
up
Amxx Oct 5, 2023
9002e5f
up
Amxx Oct 5, 2023
74558f8
remove .address when doing ethers call that support addressable
Amxx Oct 6, 2023
9ea2db1
Fix flaky test in ERC2981.behavior
ernestognw Oct 10, 2023
fd7100a
Update test/access/Ownable.test.js
Amxx Oct 10, 2023
1a9a046
Update test/access/Ownable.test.js
Amxx Oct 10, 2023
b569ca8
Fix lint
ernestognw Oct 10, 2023
b24fec4
Fix upgradeable tests
ernestognw Oct 10, 2023
5e96021
redesign signers/fixture
Amxx Oct 10, 2023
f382329
Fix vanilla tests by overriding require and readArtifact with sync an…
ernestognw Oct 10, 2023
95be46d
Revert "redesign signers/fixture"
Amxx Oct 10, 2023
55b0406
Optimize ethers.getSigners call by passing a promise to use within fi…
ernestognw Oct 10, 2023
e45e482
Slice ethers accounts
ernestognw Oct 11, 2023
b391c2f
rename
Amxx Oct 11, 2023
74bab81
override hre.ethers.getSigners
Amxx Oct 11, 2023
98e5ecd
unify coding style/naming between env-contracts.js and env-artifacts.js
Amxx Oct 11, 2023
4db1800
Attempt to fix tests by avoid overriding `this` in Truffle require
ernestognw Oct 12, 2023
0a8a69d
Revert "Attempt to fix tests by avoid overriding `this` in Truffle re…
Amxx Oct 12, 2023
ec3bfc5
Merge branch 'master' into test/migration/ownable
Amxx Oct 12, 2023
5d5a150
Force compile on upgradeable and coverage workflows
Amxx Oct 12, 2023
300fa1e
use cached getSigners() in fixtures
Amxx Oct 13, 2023
ae4381e
use describe instead of contract for ethers test that don't need the …
Amxx Oct 13, 2023
b02770e
add enviornment sanity check
Amxx Oct 16, 2023
4e6a1ca
skip signer slice when running coverage
Amxx Oct 16, 2023
69b91a0
up
Amxx Oct 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ const argv = require('yargs/yargs')()
},
}).argv;

require('@nomiclabs/hardhat-truffle5');
require('@nomiclabs/hardhat-truffle5'); // deprecated
require('@nomicfoundation/hardhat-toolbox');
require('@nomicfoundation/hardhat-ethers');
require('hardhat-ignore-warnings');
require('hardhat-exposed');
require('solidity-docgen');
Expand Down
1,086 changes: 1,037 additions & 49 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@
"@changesets/cli": "^2.26.0",
"@changesets/pre": "^1.0.14",
"@changesets/read": "^0.5.9",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.2",
"@nomicfoundation/hardhat-ethers": "^3.0.4",
"@nomicfoundation/hardhat-foundry": "^1.1.1",
"@nomicfoundation/hardhat-network-helpers": "^1.0.3",
"@nomicfoundation/hardhat-toolbox": "^3.0.0",
"@nomiclabs/hardhat-truffle5": "^2.0.5",
"@nomiclabs/hardhat-web3": "^2.0.0",
"@openzeppelin/docs-utils": "^0.1.5",
Expand All @@ -68,9 +71,10 @@
"eth-sig-util": "^3.0.0",
"ethereumjs-util": "^7.0.7",
"ethereumjs-wallet": "^1.0.1",
"ethers": "^6.7.1",
"glob": "^10.3.5",
"graphlib": "^2.1.8",
"hardhat": "^2.9.1",
"hardhat": "^2.17.4",
"hardhat-exposed": "^0.3.13",
"hardhat-gas-reporter": "^1.0.9",
"hardhat-ignore-warnings": "^0.2.0",
Expand Down
77 changes: 42 additions & 35 deletions test/access/Ownable.test.js
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();
Copy link
Contributor

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.

Copy link
Member

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

Copy link
Collaborator Author

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() ?

Copy link
Member

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:

extendEnvironment(env => {
  const { contract } = env;

  env.contract = function (name, body) {
    const { takeSnapshot } = require('@nomicfoundation/hardhat-network-helpers');

    contract(name, accounts => {
      // reset the state of the chain in between contract test suites
      let snapshot;
      let signers;

      before(async function () {
        signers = signers ?? await ethers.getSigners();
        snapshot = await takeSnapshot();
      });

      after(async function () {
        await snapshot.restore();
      });

      // remove the default account from the accounts list used in tests, in order
      // to protect tests against accidentally passing due to the contract
      // deployer being used subsequently as function caller
      // The signers array is also cloned using the spread operator to avoid changes
      // to the original array.
      body([...signers].slice(1));
    });
  };
});

Copy link
Collaborator Author

@Amxx Amxx Oct 10, 2023

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think of keeping a registry of already migrated contract suites?

Too maintenance heavy (and error prone) IMO.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We couldn't confirm such theory, but if that's the case, we should keep loadFixtures per contract suite and also this snapshot.

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.

Copy link
Collaborator Author

@Amxx Amxx Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could do

body(accounts.slice(1), signers.slice(1));

And then do

// truffle, no change needed
contract("...", accounts => { ... });

and

// ethers
contract("...", (_, signers) => { ... });

Copy link
Member

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.

we could do

body(accounts.slice(1), signers.slice(1));

I like this, let's implement it that way 😄

Copy link
Member

@ernestognw ernestognw Oct 10, 2023

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

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);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using .address everywhere will be a pain. There is an issue about it:

Are there any workarounds? Is it worth it to contribute to Hardhat to improve this?

Copy link
Collaborator Author

@Amxx Amxx Oct 5, 2023

Choose a reason for hiding this comment

The 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 ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ethers.resolveAddress already does it even better ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I'd love to simplify the code by removing .address when its better supported, but I don't think we should wait for that. We should migrate with .address and then remove them when possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with migrating using .address without waiting for Hardhat support.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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);
});
});
});
89 changes: 51 additions & 38 deletions test/access/Ownable2Step.test.js
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);
});
});
});
2 changes: 2 additions & 0 deletions test/helpers/chainid.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ async function getChainId() {

module.exports = {
getChainId,
// TODO: when tests are ready to support bigint chainId
// getChainId: ethers.provider.getNetwork().then(network => network.chainId),
};
22 changes: 3 additions & 19 deletions test/helpers/create.js
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)),
};
2 changes: 2 additions & 0 deletions test/helpers/customError.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// DEPRECATED: replace with hardhat-toolbox chai matchers.

const { expect } = require('chai');

/** Revert handler that supports custom errors. */
Expand Down
Loading