mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 19:26:33 +00:00
Fix .gitignore: stop tracking ignored files
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
## Testing
|
||||
|
||||
Unit test are critical to OpenZeppelin Contracts. They help ensure code quality and mitigate against security vulnerabilities. The directory structure within the `/test` directory corresponds to the `/contracts` directory.
|
||||
@@ -0,0 +1,867 @@
|
||||
const { expectEvent, expectRevert, constants, BN } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { time } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior');
|
||||
const { network } = require('hardhat');
|
||||
const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants');
|
||||
|
||||
const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
||||
const ROLE = web3.utils.soliditySha3('ROLE');
|
||||
const OTHER_ROLE = web3.utils.soliditySha3('OTHER_ROLE');
|
||||
const ZERO = web3.utils.toBN(0);
|
||||
|
||||
function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, otherAdmin) {
|
||||
shouldSupportInterfaces(['AccessControl']);
|
||||
|
||||
describe('default admin', function () {
|
||||
it('deployer has default admin role', async function () {
|
||||
expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, admin)).to.equal(true);
|
||||
});
|
||||
|
||||
it("other roles's admin is the default admin role", async function () {
|
||||
expect(await this.accessControl.getRoleAdmin(ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
|
||||
});
|
||||
|
||||
it("default admin role's admin is itself", async function () {
|
||||
expect(await this.accessControl.getRoleAdmin(DEFAULT_ADMIN_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('granting', function () {
|
||||
beforeEach(async function () {
|
||||
await this.accessControl.grantRole(ROLE, authorized, { from: admin });
|
||||
});
|
||||
|
||||
it('non-admin cannot grant role to other accounts', async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.grantRole(ROLE, authorized, { from: other }),
|
||||
`${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('accounts can be granted a role multiple times', async function () {
|
||||
await this.accessControl.grantRole(ROLE, authorized, { from: admin });
|
||||
const receipt = await this.accessControl.grantRole(ROLE, authorized, { from: admin });
|
||||
expectEvent.notEmitted(receipt, 'RoleGranted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('revoking', function () {
|
||||
it('roles that are not had can be revoked', async function () {
|
||||
expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(false);
|
||||
|
||||
const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: admin });
|
||||
expectEvent.notEmitted(receipt, 'RoleRevoked');
|
||||
});
|
||||
|
||||
context('with granted role', function () {
|
||||
beforeEach(async function () {
|
||||
await this.accessControl.grantRole(ROLE, authorized, { from: admin });
|
||||
});
|
||||
|
||||
it('admin can revoke role', async function () {
|
||||
const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: admin });
|
||||
expectEvent(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: admin });
|
||||
|
||||
expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(false);
|
||||
});
|
||||
|
||||
it('non-admin cannot revoke role', async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.revokeRole(ROLE, authorized, { from: other }),
|
||||
`${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('a role can be revoked multiple times', async function () {
|
||||
await this.accessControl.revokeRole(ROLE, authorized, { from: admin });
|
||||
|
||||
const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: admin });
|
||||
expectEvent.notEmitted(receipt, 'RoleRevoked');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renouncing', function () {
|
||||
it('roles that are not had can be renounced', async function () {
|
||||
const receipt = await this.accessControl.renounceRole(ROLE, authorized, { from: authorized });
|
||||
expectEvent.notEmitted(receipt, 'RoleRevoked');
|
||||
});
|
||||
|
||||
context('with granted role', function () {
|
||||
beforeEach(async function () {
|
||||
await this.accessControl.grantRole(ROLE, authorized, { from: admin });
|
||||
});
|
||||
|
||||
it('bearer can renounce role', async function () {
|
||||
const receipt = await this.accessControl.renounceRole(ROLE, authorized, { from: authorized });
|
||||
expectEvent(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: authorized });
|
||||
|
||||
expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(false);
|
||||
});
|
||||
|
||||
it('only the sender can renounce their roles', async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.renounceRole(ROLE, authorized, { from: admin }),
|
||||
`${errorPrefix}: can only renounce roles for self`,
|
||||
);
|
||||
});
|
||||
|
||||
it('a role can be renounced multiple times', async function () {
|
||||
await this.accessControl.renounceRole(ROLE, authorized, { from: authorized });
|
||||
|
||||
const receipt = await this.accessControl.renounceRole(ROLE, authorized, { from: authorized });
|
||||
expectEvent.notEmitted(receipt, 'RoleRevoked');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setting role admin', function () {
|
||||
beforeEach(async function () {
|
||||
const receipt = await this.accessControl.$_setRoleAdmin(ROLE, OTHER_ROLE);
|
||||
expectEvent(receipt, 'RoleAdminChanged', {
|
||||
role: ROLE,
|
||||
previousAdminRole: DEFAULT_ADMIN_ROLE,
|
||||
newAdminRole: OTHER_ROLE,
|
||||
});
|
||||
|
||||
await this.accessControl.grantRole(OTHER_ROLE, otherAdmin, { from: admin });
|
||||
});
|
||||
|
||||
it("a role's admin role can be changed", async function () {
|
||||
expect(await this.accessControl.getRoleAdmin(ROLE)).to.equal(OTHER_ROLE);
|
||||
});
|
||||
|
||||
it('the new admin can grant roles', async function () {
|
||||
const receipt = await this.accessControl.grantRole(ROLE, authorized, { from: otherAdmin });
|
||||
expectEvent(receipt, 'RoleGranted', { account: authorized, role: ROLE, sender: otherAdmin });
|
||||
});
|
||||
|
||||
it('the new admin can revoke roles', async function () {
|
||||
await this.accessControl.grantRole(ROLE, authorized, { from: otherAdmin });
|
||||
const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: otherAdmin });
|
||||
expectEvent(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: otherAdmin });
|
||||
});
|
||||
|
||||
it("a role's previous admins no longer grant roles", async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.grantRole(ROLE, authorized, { from: admin }),
|
||||
`${errorPrefix}: account ${admin.toLowerCase()} is missing role ${OTHER_ROLE}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("a role's previous admins no longer revoke roles", async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.revokeRole(ROLE, authorized, { from: admin }),
|
||||
`${errorPrefix}: account ${admin.toLowerCase()} is missing role ${OTHER_ROLE}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onlyRole modifier', function () {
|
||||
beforeEach(async function () {
|
||||
await this.accessControl.grantRole(ROLE, authorized, { from: admin });
|
||||
});
|
||||
|
||||
it('do not revert if sender has role', async function () {
|
||||
await this.accessControl.methods['$_checkRole(bytes32)'](ROLE, { from: authorized });
|
||||
});
|
||||
|
||||
it("revert if sender doesn't have role #1", async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.methods['$_checkRole(bytes32)'](ROLE, { from: other }),
|
||||
`${errorPrefix}: account ${other.toLowerCase()} is missing role ${ROLE}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("revert if sender doesn't have role #2", async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.methods['$_checkRole(bytes32)'](OTHER_ROLE, { from: authorized }),
|
||||
`${errorPrefix}: account ${authorized.toLowerCase()} is missing role ${OTHER_ROLE}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeAccessControlEnumerable(errorPrefix, admin, authorized, other, otherAdmin, otherAuthorized) {
|
||||
shouldSupportInterfaces(['AccessControlEnumerable']);
|
||||
|
||||
describe('enumerating', function () {
|
||||
it('role bearers can be enumerated', async function () {
|
||||
await this.accessControl.grantRole(ROLE, authorized, { from: admin });
|
||||
await this.accessControl.grantRole(ROLE, other, { from: admin });
|
||||
await this.accessControl.grantRole(ROLE, otherAuthorized, { from: admin });
|
||||
await this.accessControl.revokeRole(ROLE, other, { from: admin });
|
||||
|
||||
const memberCount = await this.accessControl.getRoleMemberCount(ROLE);
|
||||
expect(memberCount).to.bignumber.equal('2');
|
||||
|
||||
const bearers = [];
|
||||
for (let i = 0; i < memberCount; ++i) {
|
||||
bearers.push(await this.accessControl.getRoleMember(ROLE, i));
|
||||
}
|
||||
|
||||
expect(bearers).to.have.members([authorized, otherAuthorized]);
|
||||
});
|
||||
it('role enumeration should be in sync after renounceRole call', async function () {
|
||||
expect(await this.accessControl.getRoleMemberCount(ROLE)).to.bignumber.equal('0');
|
||||
await this.accessControl.grantRole(ROLE, admin, { from: admin });
|
||||
expect(await this.accessControl.getRoleMemberCount(ROLE)).to.bignumber.equal('1');
|
||||
await this.accessControl.renounceRole(ROLE, admin, { from: admin });
|
||||
expect(await this.accessControl.getRoleMemberCount(ROLE)).to.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defaultAdmin, newDefaultAdmin, other) {
|
||||
shouldSupportInterfaces(['AccessControlDefaultAdminRules']);
|
||||
|
||||
function expectNoEvent(receipt, eventName) {
|
||||
try {
|
||||
expectEvent(receipt, eventName);
|
||||
throw new Error(`${eventName} event found`);
|
||||
} catch (err) {
|
||||
expect(err.message).to.eq(`No '${eventName}' events found: expected false to equal true`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const getter of ['owner', 'defaultAdmin']) {
|
||||
describe(`${getter}()`, function () {
|
||||
it('has a default set to the initial default admin', async function () {
|
||||
const value = await this.accessControl[getter]();
|
||||
expect(value).to.equal(defaultAdmin);
|
||||
expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, value)).to.be.true;
|
||||
});
|
||||
|
||||
it('changes if the default admin changes', async function () {
|
||||
// Starts an admin transfer
|
||||
await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin });
|
||||
|
||||
// Wait for acceptance
|
||||
const acceptSchedule = web3.utils.toBN(await time.latest()).add(delay);
|
||||
await time.setNextBlockTimestamp(acceptSchedule.addn(1));
|
||||
await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin });
|
||||
|
||||
const value = await this.accessControl[getter]();
|
||||
expect(value).to.equal(newDefaultAdmin);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('pendingDefaultAdmin()', function () {
|
||||
it('returns 0 if no pending default admin transfer', async function () {
|
||||
const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.eq(ZERO_ADDRESS);
|
||||
expect(schedule).to.be.bignumber.eq(ZERO);
|
||||
});
|
||||
|
||||
describe('when there is a scheduled default admin transfer', function () {
|
||||
beforeEach('begins admin transfer', async function () {
|
||||
await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin });
|
||||
});
|
||||
|
||||
for (const [fromSchedule, tag] of [
|
||||
[-1, 'before'],
|
||||
[0, 'exactly when'],
|
||||
[1, 'after'],
|
||||
]) {
|
||||
it(`returns pending admin and schedule ${tag} it passes if not accepted`, async function () {
|
||||
// Wait until schedule + fromSchedule
|
||||
const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule);
|
||||
await network.provider.send('evm_mine'); // Mine a block to force the timestamp
|
||||
|
||||
const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.eq(newDefaultAdmin);
|
||||
expect(schedule).to.be.bignumber.eq(firstSchedule);
|
||||
});
|
||||
}
|
||||
|
||||
it('returns 0 after schedule passes and the transfer was accepted', async function () {
|
||||
// Wait after schedule
|
||||
const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
await time.setNextBlockTimestamp(firstSchedule.addn(1));
|
||||
|
||||
// Accepts
|
||||
await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin });
|
||||
|
||||
const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.eq(ZERO_ADDRESS);
|
||||
expect(schedule).to.be.bignumber.eq(ZERO);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultAdminDelay()', function () {
|
||||
it('returns the current delay', async function () {
|
||||
expect(await this.accessControl.defaultAdminDelay()).to.be.bignumber.eq(delay);
|
||||
});
|
||||
|
||||
describe('when there is a scheduled delay change', function () {
|
||||
const newDelay = web3.utils.toBN(0xdead); // Any change
|
||||
|
||||
beforeEach('begins delay change', async function () {
|
||||
await this.accessControl.changeDefaultAdminDelay(newDelay, { from: defaultAdmin });
|
||||
});
|
||||
|
||||
for (const [fromSchedule, tag, expectedDelay, delayTag] of [
|
||||
[-1, 'before', delay, 'old'],
|
||||
[0, 'exactly when', delay, 'old'],
|
||||
[1, 'after', newDelay, 'new'],
|
||||
]) {
|
||||
it(`returns ${delayTag} delay ${tag} delay schedule passes`, async function () {
|
||||
// Wait until schedule + fromSchedule
|
||||
const { schedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
await time.setNextBlockTimestamp(schedule.toNumber() + fromSchedule);
|
||||
await network.provider.send('evm_mine'); // Mine a block to force the timestamp
|
||||
|
||||
const currentDelay = await this.accessControl.defaultAdminDelay();
|
||||
expect(currentDelay).to.be.bignumber.eq(expectedDelay);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('pendingDefaultAdminDelay()', function () {
|
||||
it('returns 0 if not set', async function () {
|
||||
const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
expect(newDelay).to.be.bignumber.eq(ZERO);
|
||||
expect(schedule).to.be.bignumber.eq(ZERO);
|
||||
});
|
||||
|
||||
describe('when there is a scheduled delay change', function () {
|
||||
const newDelay = web3.utils.toBN(0xdead); // Any change
|
||||
|
||||
beforeEach('begins admin transfer', async function () {
|
||||
await this.accessControl.changeDefaultAdminDelay(newDelay, { from: defaultAdmin });
|
||||
});
|
||||
|
||||
for (const [fromSchedule, tag, expectedDelay, delayTag, expectZeroSchedule] of [
|
||||
[-1, 'before', newDelay, 'new'],
|
||||
[0, 'exactly when', newDelay, 'new'],
|
||||
[1, 'after', ZERO, 'zero', true],
|
||||
]) {
|
||||
it(`returns ${delayTag} delay ${tag} delay schedule passes`, async function () {
|
||||
// Wait until schedule + fromSchedule
|
||||
const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule);
|
||||
await network.provider.send('evm_mine'); // Mine a block to force the timestamp
|
||||
|
||||
const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
expect(newDelay).to.be.bignumber.eq(expectedDelay);
|
||||
expect(schedule).to.be.bignumber.eq(expectZeroSchedule ? ZERO : firstSchedule);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultAdminDelayIncreaseWait()', function () {
|
||||
it('should return 5 days (default)', async function () {
|
||||
expect(await this.accessControl.defaultAdminDelayIncreaseWait()).to.be.bignumber.eq(
|
||||
web3.utils.toBN(time.duration.days(5)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should revert if granting default admin role', async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }),
|
||||
`${errorPrefix}: can't directly grant default admin role`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should revert if revoking default admin role', async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }),
|
||||
`${errorPrefix}: can't directly revoke default admin role`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should revert if defaultAdmin's admin is changed", async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.$_setRoleAdmin(DEFAULT_ADMIN_ROLE, defaultAdmin),
|
||||
`${errorPrefix}: can't violate default admin rules`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not grant the default admin role twice', async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin),
|
||||
`${errorPrefix}: default admin already granted`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('begins a default admin transfer', function () {
|
||||
let receipt;
|
||||
let acceptSchedule;
|
||||
|
||||
it('reverts if called by non default admin accounts', async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: other }),
|
||||
`${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('when there is no pending delay nor pending admin transfer', function () {
|
||||
beforeEach('begins admin transfer', async function () {
|
||||
receipt = await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin });
|
||||
acceptSchedule = web3.utils.toBN(await time.latest()).add(delay);
|
||||
});
|
||||
|
||||
it('should set pending default admin and schedule', async function () {
|
||||
const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.equal(newDefaultAdmin);
|
||||
expect(schedule).to.be.bignumber.equal(acceptSchedule);
|
||||
expectEvent(receipt, 'DefaultAdminTransferScheduled', {
|
||||
newAdmin,
|
||||
acceptSchedule,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is a pending admin transfer', function () {
|
||||
beforeEach('sets a pending default admin transfer', async function () {
|
||||
await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin });
|
||||
acceptSchedule = web3.utils.toBN(await time.latest()).add(delay);
|
||||
});
|
||||
|
||||
for (const [fromSchedule, tag] of [
|
||||
[-1, 'before'],
|
||||
[0, 'exactly when'],
|
||||
[1, 'after'],
|
||||
]) {
|
||||
it(`should be able to begin a transfer again ${tag} acceptSchedule passes`, async function () {
|
||||
// Wait until schedule + fromSchedule
|
||||
await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule);
|
||||
|
||||
// defaultAdmin changes its mind and begin again to another address
|
||||
const receipt = await this.accessControl.beginDefaultAdminTransfer(other, { from: defaultAdmin });
|
||||
const newSchedule = web3.utils.toBN(await time.latest()).add(delay);
|
||||
const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.equal(other);
|
||||
expect(schedule).to.be.bignumber.equal(newSchedule);
|
||||
|
||||
// Cancellation is always emitted since it was never accepted
|
||||
expectEvent(receipt, 'DefaultAdminTransferCanceled');
|
||||
});
|
||||
}
|
||||
|
||||
it('should not emit a cancellation event if the new default admin accepted', async function () {
|
||||
// Wait until the acceptSchedule has passed
|
||||
await time.setNextBlockTimestamp(acceptSchedule.addn(1));
|
||||
|
||||
// Accept and restart
|
||||
await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin });
|
||||
const receipt = await this.accessControl.beginDefaultAdminTransfer(other, { from: newDefaultAdmin });
|
||||
|
||||
expectNoEvent(receipt, 'DefaultAdminTransferCanceled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is a pending delay', function () {
|
||||
const newDelay = web3.utils.toBN(time.duration.hours(3));
|
||||
|
||||
beforeEach('schedule a delay change', async function () {
|
||||
await this.accessControl.changeDefaultAdminDelay(newDelay, { from: defaultAdmin });
|
||||
const pendingDefaultAdminDelay = await this.accessControl.pendingDefaultAdminDelay();
|
||||
acceptSchedule = pendingDefaultAdminDelay.schedule;
|
||||
});
|
||||
|
||||
for (const [fromSchedule, schedulePassed, expectedDelay, delayTag] of [
|
||||
[-1, 'before', delay, 'old'],
|
||||
[0, 'exactly when', delay, 'old'],
|
||||
[1, 'after', newDelay, 'new'],
|
||||
]) {
|
||||
it(`should set the ${delayTag} delay and apply it to next default admin transfer schedule ${schedulePassed} acceptSchedule passed`, async function () {
|
||||
// Wait until the expected fromSchedule time
|
||||
await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule);
|
||||
|
||||
// Start the new default admin transfer and get its schedule
|
||||
const receipt = await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin });
|
||||
const expectedAcceptSchedule = web3.utils.toBN(await time.latest()).add(expectedDelay);
|
||||
|
||||
// Check that the schedule corresponds with the new delay
|
||||
const { newAdmin, schedule: transferSchedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.equal(newDefaultAdmin);
|
||||
expect(transferSchedule).to.be.bignumber.equal(expectedAcceptSchedule);
|
||||
|
||||
expectEvent(receipt, 'DefaultAdminTransferScheduled', {
|
||||
newAdmin,
|
||||
acceptSchedule: expectedAcceptSchedule,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('accepts transfer admin', function () {
|
||||
let acceptSchedule;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin });
|
||||
acceptSchedule = web3.utils.toBN(await time.latest()).add(delay);
|
||||
});
|
||||
|
||||
it('should revert if caller is not pending default admin', async function () {
|
||||
await time.setNextBlockTimestamp(acceptSchedule.addn(1));
|
||||
await expectRevert(
|
||||
this.accessControl.acceptDefaultAdminTransfer({ from: other }),
|
||||
`${errorPrefix}: pending admin must accept`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('when caller is pending default admin and delay has passed', function () {
|
||||
beforeEach(async function () {
|
||||
await time.setNextBlockTimestamp(acceptSchedule.addn(1));
|
||||
});
|
||||
|
||||
it('accepts a transfer and changes default admin', async function () {
|
||||
const receipt = await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin });
|
||||
|
||||
// Storage changes
|
||||
expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.false;
|
||||
expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, newDefaultAdmin)).to.be.true;
|
||||
expect(await this.accessControl.owner()).to.equal(newDefaultAdmin);
|
||||
|
||||
// Emit events
|
||||
expectEvent(receipt, 'RoleRevoked', {
|
||||
role: DEFAULT_ADMIN_ROLE,
|
||||
account: defaultAdmin,
|
||||
});
|
||||
expectEvent(receipt, 'RoleGranted', {
|
||||
role: DEFAULT_ADMIN_ROLE,
|
||||
account: newDefaultAdmin,
|
||||
});
|
||||
|
||||
// Resets pending default admin and schedule
|
||||
const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.equal(constants.ZERO_ADDRESS);
|
||||
expect(schedule).to.be.bignumber.equal(ZERO);
|
||||
});
|
||||
});
|
||||
|
||||
describe('schedule not passed', function () {
|
||||
for (const [fromSchedule, tag] of [
|
||||
[-1, 'less'],
|
||||
[0, 'equal'],
|
||||
]) {
|
||||
it(`should revert if block.timestamp is ${tag} to schedule`, async function () {
|
||||
await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule);
|
||||
await expectRevert(
|
||||
this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }),
|
||||
`${errorPrefix}: transfer delay not passed`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancels a default admin transfer', function () {
|
||||
it('reverts if called by non default admin accounts', async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.cancelDefaultAdminTransfer({ from: other }),
|
||||
`${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('when there is a pending default admin transfer', function () {
|
||||
let acceptSchedule;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin });
|
||||
acceptSchedule = web3.utils.toBN(await time.latest()).add(delay);
|
||||
});
|
||||
|
||||
for (const [fromSchedule, tag] of [
|
||||
[-1, 'before'],
|
||||
[0, 'exactly when'],
|
||||
[1, 'after'],
|
||||
]) {
|
||||
it(`resets pending default admin and schedule ${tag} transfer schedule passes`, async function () {
|
||||
// Advance until passed delay
|
||||
await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule);
|
||||
|
||||
const receipt = await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin });
|
||||
|
||||
const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.equal(constants.ZERO_ADDRESS);
|
||||
expect(schedule).to.be.bignumber.equal(ZERO);
|
||||
|
||||
expectEvent(receipt, 'DefaultAdminTransferCanceled');
|
||||
});
|
||||
}
|
||||
|
||||
it('should revert if the previous default admin tries to accept', async function () {
|
||||
await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin });
|
||||
|
||||
// Advance until passed delay
|
||||
await time.setNextBlockTimestamp(acceptSchedule.addn(1));
|
||||
|
||||
// Previous pending default admin should not be able to accept after cancellation.
|
||||
await expectRevert(
|
||||
this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }),
|
||||
`${errorPrefix}: pending admin must accept`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is no pending default admin transfer', async function () {
|
||||
it('should succeed without changes', async function () {
|
||||
const receipt = await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin });
|
||||
|
||||
const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.equal(constants.ZERO_ADDRESS);
|
||||
expect(schedule).to.be.bignumber.equal(ZERO);
|
||||
|
||||
expectNoEvent(receipt, 'DefaultAdminTransferCanceled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renounces admin', function () {
|
||||
let expectedSchedule;
|
||||
let delayPassed;
|
||||
let delayNotPassed;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.accessControl.beginDefaultAdminTransfer(constants.ZERO_ADDRESS, { from: defaultAdmin });
|
||||
expectedSchedule = web3.utils.toBN(await time.latest()).add(delay);
|
||||
delayNotPassed = expectedSchedule;
|
||||
delayPassed = expectedSchedule.addn(1);
|
||||
});
|
||||
|
||||
it('reverts if caller is not default admin', async function () {
|
||||
await time.setNextBlockTimestamp(delayPassed);
|
||||
await expectRevert(
|
||||
this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: defaultAdmin }),
|
||||
`${errorPrefix}: can only renounce roles for self`,
|
||||
);
|
||||
});
|
||||
|
||||
it("renouncing the admin role when not an admin doesn't affect the schedule", async function () {
|
||||
await time.setNextBlockTimestamp(delayPassed);
|
||||
await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: other });
|
||||
|
||||
const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.equal(constants.ZERO_ADDRESS);
|
||||
expect(schedule).to.be.bignumber.equal(expectedSchedule);
|
||||
});
|
||||
|
||||
it('keeps defaultAdmin consistent with hasRole if another non-defaultAdmin user renounces the DEFAULT_ADMIN_ROLE', async function () {
|
||||
await time.setNextBlockTimestamp(delayPassed);
|
||||
|
||||
// This passes because it's a noop
|
||||
await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: other });
|
||||
|
||||
expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.true;
|
||||
expect(await this.accessControl.defaultAdmin()).to.be.equal(defaultAdmin);
|
||||
});
|
||||
|
||||
it('renounces role', async function () {
|
||||
await time.setNextBlockTimestamp(delayPassed);
|
||||
const receipt = await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin });
|
||||
|
||||
expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.false;
|
||||
expect(await this.accessControl.defaultAdmin()).to.be.equal(constants.ZERO_ADDRESS);
|
||||
expectEvent(receipt, 'RoleRevoked', {
|
||||
role: DEFAULT_ADMIN_ROLE,
|
||||
account: defaultAdmin,
|
||||
});
|
||||
expect(await this.accessControl.owner()).to.equal(constants.ZERO_ADDRESS);
|
||||
const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin();
|
||||
expect(newAdmin).to.eq(ZERO_ADDRESS);
|
||||
expect(schedule).to.be.bignumber.eq(ZERO);
|
||||
});
|
||||
|
||||
it('allows to recover access using the internal _grantRole', async function () {
|
||||
await time.setNextBlockTimestamp(delayPassed);
|
||||
await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin });
|
||||
|
||||
const grantRoleReceipt = await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, other);
|
||||
expectEvent(grantRoleReceipt, 'RoleGranted', {
|
||||
role: DEFAULT_ADMIN_ROLE,
|
||||
account: other,
|
||||
});
|
||||
});
|
||||
|
||||
describe('schedule not passed', function () {
|
||||
for (const [fromSchedule, tag] of [
|
||||
[-1, 'less'],
|
||||
[0, 'equal'],
|
||||
]) {
|
||||
it(`reverts if block.timestamp is ${tag} to schedule`, async function () {
|
||||
await time.setNextBlockTimestamp(delayNotPassed.toNumber() + fromSchedule);
|
||||
await expectRevert(
|
||||
this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }),
|
||||
`${errorPrefix}: only can renounce in two delayed steps`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('changes delay', function () {
|
||||
it('reverts if called by non default admin accounts', async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.changeDefaultAdminDelay(time.duration.hours(4), {
|
||||
from: other,
|
||||
}),
|
||||
`${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`,
|
||||
);
|
||||
});
|
||||
|
||||
for (const [newDefaultAdminDelay, delayChangeType] of [
|
||||
[web3.utils.toBN(delay).subn(time.duration.hours(1)), 'decreased'],
|
||||
[web3.utils.toBN(delay).addn(time.duration.hours(1)), 'increased'],
|
||||
[web3.utils.toBN(delay).addn(time.duration.days(5)), 'increased to more than 5 days'],
|
||||
]) {
|
||||
describe(`when the delay is ${delayChangeType}`, function () {
|
||||
it('begins the delay change to the new delay', async function () {
|
||||
// Begins the change
|
||||
const receipt = await this.accessControl.changeDefaultAdminDelay(newDefaultAdminDelay, {
|
||||
from: defaultAdmin,
|
||||
});
|
||||
|
||||
// Calculate expected values
|
||||
const cap = await this.accessControl.defaultAdminDelayIncreaseWait();
|
||||
const changeDelay = newDefaultAdminDelay.lte(delay)
|
||||
? delay.sub(newDefaultAdminDelay)
|
||||
: BN.min(newDefaultAdminDelay, cap);
|
||||
const timestamp = web3.utils.toBN(await time.latest());
|
||||
const effectSchedule = timestamp.add(changeDelay);
|
||||
|
||||
// Assert
|
||||
const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
expect(newDelay).to.be.bignumber.eq(newDefaultAdminDelay);
|
||||
expect(schedule).to.be.bignumber.eq(effectSchedule);
|
||||
expectEvent(receipt, 'DefaultAdminDelayChangeScheduled', {
|
||||
newDelay,
|
||||
effectSchedule,
|
||||
});
|
||||
});
|
||||
|
||||
describe('scheduling again', function () {
|
||||
beforeEach('schedule once', async function () {
|
||||
await this.accessControl.changeDefaultAdminDelay(newDefaultAdminDelay, { from: defaultAdmin });
|
||||
});
|
||||
|
||||
for (const [fromSchedule, tag] of [
|
||||
[-1, 'before'],
|
||||
[0, 'exactly when'],
|
||||
[1, 'after'],
|
||||
]) {
|
||||
const passed = fromSchedule > 0;
|
||||
|
||||
it(`succeeds ${tag} the delay schedule passes`, async function () {
|
||||
// Wait until schedule + fromSchedule
|
||||
const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule);
|
||||
|
||||
// Default admin changes its mind and begins another delay change
|
||||
const anotherNewDefaultAdminDelay = newDefaultAdminDelay.addn(time.duration.hours(2));
|
||||
const receipt = await this.accessControl.changeDefaultAdminDelay(anotherNewDefaultAdminDelay, {
|
||||
from: defaultAdmin,
|
||||
});
|
||||
|
||||
// Calculate expected values
|
||||
const cap = await this.accessControl.defaultAdminDelayIncreaseWait();
|
||||
const timestamp = web3.utils.toBN(await time.latest());
|
||||
const effectSchedule = timestamp.add(BN.min(cap, anotherNewDefaultAdminDelay));
|
||||
|
||||
// Assert
|
||||
const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
expect(newDelay).to.be.bignumber.eq(anotherNewDefaultAdminDelay);
|
||||
expect(schedule).to.be.bignumber.eq(effectSchedule);
|
||||
expectEvent(receipt, 'DefaultAdminDelayChangeScheduled', {
|
||||
newDelay,
|
||||
effectSchedule,
|
||||
});
|
||||
});
|
||||
|
||||
const emit = passed ? 'not emit' : 'emit';
|
||||
it(`should ${emit} a cancellation event ${tag} the delay schedule passes`, async function () {
|
||||
// Wait until schedule + fromSchedule
|
||||
const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule);
|
||||
|
||||
// Default admin changes its mind and begins another delay change
|
||||
const anotherNewDefaultAdminDelay = newDefaultAdminDelay.addn(time.duration.hours(2));
|
||||
const receipt = await this.accessControl.changeDefaultAdminDelay(anotherNewDefaultAdminDelay, {
|
||||
from: defaultAdmin,
|
||||
});
|
||||
|
||||
const eventMatcher = passed ? expectNoEvent : expectEvent;
|
||||
eventMatcher(receipt, 'DefaultAdminDelayChangeCanceled');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('rollbacks a delay change', function () {
|
||||
it('reverts if called by non default admin accounts', async function () {
|
||||
await expectRevert(
|
||||
this.accessControl.rollbackDefaultAdminDelay({ from: other }),
|
||||
`${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('when there is a pending delay', function () {
|
||||
beforeEach('set pending delay', async function () {
|
||||
await this.accessControl.changeDefaultAdminDelay(time.duration.hours(12), { from: defaultAdmin });
|
||||
});
|
||||
|
||||
for (const [fromSchedule, tag] of [
|
||||
[-1, 'before'],
|
||||
[0, 'exactly when'],
|
||||
[1, 'after'],
|
||||
]) {
|
||||
const passed = fromSchedule > 0;
|
||||
|
||||
it(`resets pending delay and schedule ${tag} delay change schedule passes`, async function () {
|
||||
// Wait until schedule + fromSchedule
|
||||
const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule);
|
||||
|
||||
await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin });
|
||||
|
||||
const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
expect(newDelay).to.be.bignumber.eq(ZERO);
|
||||
expect(schedule).to.be.bignumber.eq(ZERO);
|
||||
});
|
||||
|
||||
const emit = passed ? 'not emit' : 'emit';
|
||||
it(`should ${emit} a cancellation event ${tag} the delay schedule passes`, async function () {
|
||||
// Wait until schedule + fromSchedule
|
||||
const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule);
|
||||
|
||||
const receipt = await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin });
|
||||
|
||||
const eventMatcher = passed ? expectNoEvent : expectEvent;
|
||||
eventMatcher(receipt, 'DefaultAdminDelayChangeCanceled');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('when there is no pending delay', function () {
|
||||
it('succeeds without changes', async function () {
|
||||
await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin });
|
||||
|
||||
const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay();
|
||||
expect(newDelay).to.be.bignumber.eq(ZERO);
|
||||
expect(schedule).to.be.bignumber.eq(ZERO);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_ADMIN_ROLE,
|
||||
shouldBehaveLikeAccessControl,
|
||||
shouldBehaveLikeAccessControlEnumerable,
|
||||
shouldBehaveLikeAccessControlDefaultAdminRules,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
const { DEFAULT_ADMIN_ROLE, shouldBehaveLikeAccessControl } = require('./AccessControl.behavior.js');
|
||||
|
||||
const AccessControl = artifacts.require('$AccessControl');
|
||||
|
||||
contract('AccessControl', function (accounts) {
|
||||
beforeEach(async function () {
|
||||
this.accessControl = await AccessControl.new({ from: accounts[0] });
|
||||
await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]);
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccessControl('AccessControl', ...accounts);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
const { expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { BridgeHelper } = require('../helpers/crosschain');
|
||||
|
||||
const { DEFAULT_ADMIN_ROLE, shouldBehaveLikeAccessControl } = require('./AccessControl.behavior.js');
|
||||
|
||||
const crossChainRoleAlias = role =>
|
||||
web3.utils.leftPad(
|
||||
web3.utils.toHex(web3.utils.toBN(role).xor(web3.utils.toBN(web3.utils.soliditySha3('CROSSCHAIN_ALIAS')))),
|
||||
64,
|
||||
);
|
||||
|
||||
const AccessControlCrossChainMock = artifacts.require('$AccessControlCrossChainMock');
|
||||
|
||||
const ROLE = web3.utils.soliditySha3('ROLE');
|
||||
|
||||
contract('AccessControl', function (accounts) {
|
||||
before(async function () {
|
||||
this.bridge = await BridgeHelper.deploy();
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
this.accessControl = await AccessControlCrossChainMock.new({ from: accounts[0] });
|
||||
await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]);
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccessControl('AccessControl', ...accounts);
|
||||
|
||||
describe('CrossChain enabled', function () {
|
||||
beforeEach(async function () {
|
||||
await this.accessControl.grantRole(ROLE, accounts[0], { from: accounts[0] });
|
||||
await this.accessControl.grantRole(crossChainRoleAlias(ROLE), accounts[1], { from: accounts[0] });
|
||||
});
|
||||
|
||||
it('check alliassing', async function () {
|
||||
expect(await this.accessControl.$_crossChainRoleAlias(ROLE)).to.be.bignumber.equal(crossChainRoleAlias(ROLE));
|
||||
});
|
||||
|
||||
it('Crosschain calls not authorized to non-aliased addresses', async function () {
|
||||
await expectRevert(
|
||||
this.bridge.call(accounts[0], this.accessControl, '$_checkRole(bytes32)', [ROLE]),
|
||||
`AccessControl: account ${accounts[0].toLowerCase()} is missing role ${crossChainRoleAlias(ROLE)}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('Crosschain calls not authorized to non-aliased addresses', async function () {
|
||||
await this.bridge.call(accounts[1], this.accessControl, '$_checkRole(bytes32)', [ROLE]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
const { time, constants, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const {
|
||||
shouldBehaveLikeAccessControl,
|
||||
shouldBehaveLikeAccessControlDefaultAdminRules,
|
||||
} = require('./AccessControl.behavior.js');
|
||||
|
||||
const AccessControlDefaultAdminRules = artifacts.require('$AccessControlDefaultAdminRules');
|
||||
|
||||
contract('AccessControlDefaultAdminRules', function (accounts) {
|
||||
const delay = web3.utils.toBN(time.duration.hours(10));
|
||||
|
||||
beforeEach(async function () {
|
||||
this.accessControl = await AccessControlDefaultAdminRules.new(delay, accounts[0], { from: accounts[0] });
|
||||
});
|
||||
|
||||
it('initial admin not zero', async function () {
|
||||
await expectRevert(
|
||||
AccessControlDefaultAdminRules.new(delay, constants.ZERO_ADDRESS),
|
||||
'AccessControl: 0 default admin',
|
||||
);
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccessControl('AccessControl', ...accounts);
|
||||
shouldBehaveLikeAccessControlDefaultAdminRules('AccessControl', delay, ...accounts);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
const {
|
||||
DEFAULT_ADMIN_ROLE,
|
||||
shouldBehaveLikeAccessControl,
|
||||
shouldBehaveLikeAccessControlEnumerable,
|
||||
} = require('./AccessControl.behavior.js');
|
||||
|
||||
const AccessControlEnumerable = artifacts.require('$AccessControlEnumerable');
|
||||
|
||||
contract('AccessControl', function (accounts) {
|
||||
beforeEach(async function () {
|
||||
this.accessControl = await AccessControlEnumerable.new({ from: accounts[0] });
|
||||
await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]);
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccessControl('AccessControl', ...accounts);
|
||||
shouldBehaveLikeAccessControlEnumerable('AccessControl', ...accounts);
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const Ownable = artifacts.require('$Ownable');
|
||||
|
||||
contract('Ownable', function (accounts) {
|
||||
const [owner, other] = accounts;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.ownable = await Ownable.new({ from: owner });
|
||||
});
|
||||
|
||||
it('has an owner', async function () {
|
||||
expect(await this.ownable.owner()).to.equal(owner);
|
||||
});
|
||||
|
||||
describe('transfer ownership', function () {
|
||||
it('changes owner after transfer', async function () {
|
||||
const receipt = await this.ownable.transferOwnership(other, { from: owner });
|
||||
expectEvent(receipt, 'OwnershipTransferred');
|
||||
|
||||
expect(await this.ownable.owner()).to.equal(other);
|
||||
});
|
||||
|
||||
it('prevents non-owners from transferring', async function () {
|
||||
await expectRevert(this.ownable.transferOwnership(other, { from: other }), 'Ownable: caller is not the owner');
|
||||
});
|
||||
|
||||
it('guards ownership against stuck state', async function () {
|
||||
await expectRevert(
|
||||
this.ownable.transferOwnership(ZERO_ADDRESS, { from: owner }),
|
||||
'Ownable: new owner is the zero address',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renounce ownership', function () {
|
||||
it('loses ownership after renouncement', async function () {
|
||||
const receipt = await this.ownable.renounceOwnership({ from: owner });
|
||||
expectEvent(receipt, 'OwnershipTransferred');
|
||||
|
||||
expect(await this.ownable.owner()).to.equal(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('prevents non-owners from renouncement', async function () {
|
||||
await expectRevert(this.ownable.renounceOwnership({ from: other }), 'Ownable: caller is not the owner');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
expect(await this.ownable.owner()).to.equal(other);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
const { expect } = require('chai');
|
||||
|
||||
const Ownable2Step = artifacts.require('$Ownable2Step');
|
||||
|
||||
contract('Ownable2Step', function (accounts) {
|
||||
const [owner, accountA, accountB] = accounts;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.ownable2Step = await Ownable2Step.new({ from: owner });
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('guards transfer against invalid user', async function () {
|
||||
await this.ownable2Step.transferOwnership(accountA, { from: owner });
|
||||
await expectRevert(
|
||||
this.ownable2Step.acceptOwnership({ from: accountB }),
|
||||
'Ownable2Step: caller is not the new owner',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('renouncing ownership', async function () {
|
||||
it('changes owner after renouncing ownership', async function () {
|
||||
await this.ownable2Step.renounceOwnership({ from: owner });
|
||||
// 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);
|
||||
});
|
||||
|
||||
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 expectRevert(
|
||||
this.ownable2Step.acceptOwnership({ from: accountA }),
|
||||
'Ownable2Step: caller is not the new owner',
|
||||
);
|
||||
});
|
||||
|
||||
it('allows to recover access using the internal _transferOwnership', async function () {
|
||||
await this.ownable.renounceOwnership({ from: owner });
|
||||
const receipt = await this.ownable.$_transferOwnership(accountA);
|
||||
expectEvent(receipt, 'OwnershipTransferred');
|
||||
|
||||
expect(await this.ownable.owner()).to.equal(accountA);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
const { BridgeHelper } = require('../helpers/crosschain');
|
||||
const { expectRevertCustomError } = require('../helpers/customError');
|
||||
|
||||
function randomAddress() {
|
||||
return web3.utils.toChecksumAddress(web3.utils.randomHex(20));
|
||||
}
|
||||
|
||||
const CrossChainEnabledAMBMock = artifacts.require('CrossChainEnabledAMBMock');
|
||||
const CrossChainEnabledArbitrumL1Mock = artifacts.require('CrossChainEnabledArbitrumL1Mock');
|
||||
const CrossChainEnabledArbitrumL2Mock = artifacts.require('CrossChainEnabledArbitrumL2Mock');
|
||||
const CrossChainEnabledOptimismMock = artifacts.require('CrossChainEnabledOptimismMock');
|
||||
const CrossChainEnabledPolygonChildMock = artifacts.require('CrossChainEnabledPolygonChildMock');
|
||||
|
||||
function shouldBehaveLikeReceiver(sender = randomAddress()) {
|
||||
it('should reject same-chain calls', async function () {
|
||||
await expectRevertCustomError(this.receiver.crossChainRestricted(), 'NotCrossChainCall()');
|
||||
|
||||
await expectRevertCustomError(this.receiver.crossChainOwnerRestricted(), 'NotCrossChainCall()');
|
||||
});
|
||||
|
||||
it('should restrict to cross-chain call from a invalid sender', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.bridge.call(sender, this.receiver, 'crossChainOwnerRestricted()'),
|
||||
`InvalidCrossChainSender("${sender}", "${await this.receiver.owner()}")`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should grant access to cross-chain call from the owner', async function () {
|
||||
await this.bridge.call(await this.receiver.owner(), this.receiver, 'crossChainOwnerRestricted()');
|
||||
});
|
||||
}
|
||||
|
||||
contract('CrossChainEnabled', function () {
|
||||
describe('AMB', function () {
|
||||
beforeEach(async function () {
|
||||
this.bridge = await BridgeHelper.deploy('AMB');
|
||||
this.receiver = await CrossChainEnabledAMBMock.new(this.bridge.address);
|
||||
});
|
||||
|
||||
shouldBehaveLikeReceiver();
|
||||
});
|
||||
|
||||
describe('Arbitrum-L1', function () {
|
||||
beforeEach(async function () {
|
||||
this.bridge = await BridgeHelper.deploy('Arbitrum-L1');
|
||||
this.receiver = await CrossChainEnabledArbitrumL1Mock.new(this.bridge.address);
|
||||
});
|
||||
|
||||
shouldBehaveLikeReceiver();
|
||||
});
|
||||
|
||||
describe('Arbitrum-L2', function () {
|
||||
beforeEach(async function () {
|
||||
this.bridge = await BridgeHelper.deploy('Arbitrum-L2');
|
||||
this.receiver = await CrossChainEnabledArbitrumL2Mock.new();
|
||||
});
|
||||
|
||||
shouldBehaveLikeReceiver();
|
||||
});
|
||||
|
||||
describe('Optimism', function () {
|
||||
beforeEach(async function () {
|
||||
this.bridge = await BridgeHelper.deploy('Optimism');
|
||||
this.receiver = await CrossChainEnabledOptimismMock.new(this.bridge.address);
|
||||
});
|
||||
|
||||
shouldBehaveLikeReceiver();
|
||||
});
|
||||
|
||||
describe('Polygon-Child', function () {
|
||||
beforeEach(async function () {
|
||||
this.bridge = await BridgeHelper.deploy('Polygon-Child');
|
||||
this.receiver = await CrossChainEnabledPolygonChildMock.new(this.bridge.address);
|
||||
});
|
||||
|
||||
shouldBehaveLikeReceiver();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
const { balance, constants, ether, expectEvent, send, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const PaymentSplitter = artifacts.require('PaymentSplitter');
|
||||
const ERC20 = artifacts.require('$ERC20');
|
||||
|
||||
contract('PaymentSplitter', function (accounts) {
|
||||
const [owner, payee1, payee2, payee3, nonpayee1, payer1] = accounts;
|
||||
|
||||
const amount = ether('1');
|
||||
|
||||
it('rejects an empty set of payees', async function () {
|
||||
await expectRevert(PaymentSplitter.new([], []), 'PaymentSplitter: no payees');
|
||||
});
|
||||
|
||||
it('rejects more payees than shares', async function () {
|
||||
await expectRevert(
|
||||
PaymentSplitter.new([payee1, payee2, payee3], [20, 30]),
|
||||
'PaymentSplitter: payees and shares length mismatch',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects more shares than payees', async function () {
|
||||
await expectRevert(
|
||||
PaymentSplitter.new([payee1, payee2], [20, 30, 40]),
|
||||
'PaymentSplitter: payees and shares length mismatch',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects null payees', async function () {
|
||||
await expectRevert(
|
||||
PaymentSplitter.new([payee1, ZERO_ADDRESS], [20, 30]),
|
||||
'PaymentSplitter: account is the zero address',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects zero-valued shares', async function () {
|
||||
await expectRevert(PaymentSplitter.new([payee1, payee2], [20, 0]), 'PaymentSplitter: shares are 0');
|
||||
});
|
||||
|
||||
it('rejects repeated payees', async function () {
|
||||
await expectRevert(PaymentSplitter.new([payee1, payee1], [20, 30]), 'PaymentSplitter: account already has shares');
|
||||
});
|
||||
|
||||
context('once deployed', function () {
|
||||
beforeEach(async function () {
|
||||
this.payees = [payee1, payee2, payee3];
|
||||
this.shares = [20, 10, 70];
|
||||
|
||||
this.contract = await PaymentSplitter.new(this.payees, this.shares);
|
||||
this.token = await ERC20.new('MyToken', 'MT');
|
||||
await this.token.$_mint(owner, ether('1000'));
|
||||
});
|
||||
|
||||
it('has total shares', async function () {
|
||||
expect(await this.contract.totalShares()).to.be.bignumber.equal('100');
|
||||
});
|
||||
|
||||
it('has payees', async function () {
|
||||
await Promise.all(
|
||||
this.payees.map(async (payee, index) => {
|
||||
expect(await this.contract.payee(index)).to.equal(payee);
|
||||
expect(await this.contract.released(payee)).to.be.bignumber.equal('0');
|
||||
expect(await this.contract.releasable(payee)).to.be.bignumber.equal('0');
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('accepts payments', function () {
|
||||
it('Ether', async function () {
|
||||
await send.ether(owner, this.contract.address, amount);
|
||||
|
||||
expect(await balance.current(this.contract.address)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('Token', async function () {
|
||||
await this.token.transfer(this.contract.address, amount, { from: owner });
|
||||
|
||||
expect(await this.token.balanceOf(this.contract.address)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shares', function () {
|
||||
it('stores shares if address is payee', async function () {
|
||||
expect(await this.contract.shares(payee1)).to.be.bignumber.not.equal('0');
|
||||
});
|
||||
|
||||
it('does not store shares if address is not payee', async function () {
|
||||
expect(await this.contract.shares(nonpayee1)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('release', function () {
|
||||
describe('Ether', function () {
|
||||
it('reverts if no funds to claim', async function () {
|
||||
await expectRevert(this.contract.release(payee1), 'PaymentSplitter: account is not due payment');
|
||||
});
|
||||
it('reverts if non-payee want to claim', async function () {
|
||||
await send.ether(payer1, this.contract.address, amount);
|
||||
await expectRevert(this.contract.release(nonpayee1), 'PaymentSplitter: account has no shares');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token', function () {
|
||||
it('reverts if no funds to claim', async function () {
|
||||
await expectRevert(
|
||||
this.contract.release(this.token.address, payee1),
|
||||
'PaymentSplitter: account is not due payment',
|
||||
);
|
||||
});
|
||||
it('reverts if non-payee want to claim', async function () {
|
||||
await this.token.transfer(this.contract.address, amount, { from: owner });
|
||||
await expectRevert(
|
||||
this.contract.release(this.token.address, nonpayee1),
|
||||
'PaymentSplitter: account has no shares',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracks releasable and released', function () {
|
||||
it('Ether', async function () {
|
||||
await send.ether(payer1, this.contract.address, amount);
|
||||
const payment = amount.divn(10);
|
||||
expect(await this.contract.releasable(payee2)).to.be.bignumber.equal(payment);
|
||||
await this.contract.release(payee2);
|
||||
expect(await this.contract.releasable(payee2)).to.be.bignumber.equal('0');
|
||||
expect(await this.contract.released(payee2)).to.be.bignumber.equal(payment);
|
||||
});
|
||||
|
||||
it('Token', async function () {
|
||||
await this.token.transfer(this.contract.address, amount, { from: owner });
|
||||
const payment = amount.divn(10);
|
||||
expect(await this.contract.releasable(this.token.address, payee2, {})).to.be.bignumber.equal(payment);
|
||||
await this.contract.release(this.token.address, payee2);
|
||||
expect(await this.contract.releasable(this.token.address, payee2, {})).to.be.bignumber.equal('0');
|
||||
expect(await this.contract.released(this.token.address, payee2)).to.be.bignumber.equal(payment);
|
||||
});
|
||||
});
|
||||
|
||||
describe('distributes funds to payees', function () {
|
||||
it('Ether', async function () {
|
||||
await send.ether(payer1, this.contract.address, amount);
|
||||
|
||||
// receive funds
|
||||
const initBalance = await balance.current(this.contract.address);
|
||||
expect(initBalance).to.be.bignumber.equal(amount);
|
||||
|
||||
// distribute to payees
|
||||
|
||||
const tracker1 = await balance.tracker(payee1);
|
||||
const receipt1 = await this.contract.release(payee1);
|
||||
const profit1 = await tracker1.delta();
|
||||
expect(profit1).to.be.bignumber.equal(ether('0.20'));
|
||||
expectEvent(receipt1, 'PaymentReleased', { to: payee1, amount: profit1 });
|
||||
|
||||
const tracker2 = await balance.tracker(payee2);
|
||||
const receipt2 = await this.contract.release(payee2);
|
||||
const profit2 = await tracker2.delta();
|
||||
expect(profit2).to.be.bignumber.equal(ether('0.10'));
|
||||
expectEvent(receipt2, 'PaymentReleased', { to: payee2, amount: profit2 });
|
||||
|
||||
const tracker3 = await balance.tracker(payee3);
|
||||
const receipt3 = await this.contract.release(payee3);
|
||||
const profit3 = await tracker3.delta();
|
||||
expect(profit3).to.be.bignumber.equal(ether('0.70'));
|
||||
expectEvent(receipt3, 'PaymentReleased', { to: payee3, amount: profit3 });
|
||||
|
||||
// end balance should be zero
|
||||
expect(await balance.current(this.contract.address)).to.be.bignumber.equal('0');
|
||||
|
||||
// check correct funds released accounting
|
||||
expect(await this.contract.totalReleased()).to.be.bignumber.equal(initBalance);
|
||||
});
|
||||
|
||||
it('Token', async function () {
|
||||
expect(await this.token.balanceOf(payee1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOf(payee2)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOf(payee3)).to.be.bignumber.equal('0');
|
||||
|
||||
await this.token.transfer(this.contract.address, amount, { from: owner });
|
||||
|
||||
expectEvent(await this.contract.release(this.token.address, payee1), 'ERC20PaymentReleased', {
|
||||
token: this.token.address,
|
||||
to: payee1,
|
||||
amount: ether('0.20'),
|
||||
});
|
||||
|
||||
await this.token.transfer(this.contract.address, amount, { from: owner });
|
||||
|
||||
expectEvent(await this.contract.release(this.token.address, payee1), 'ERC20PaymentReleased', {
|
||||
token: this.token.address,
|
||||
to: payee1,
|
||||
amount: ether('0.20'),
|
||||
});
|
||||
|
||||
expectEvent(await this.contract.release(this.token.address, payee2), 'ERC20PaymentReleased', {
|
||||
token: this.token.address,
|
||||
to: payee2,
|
||||
amount: ether('0.20'),
|
||||
});
|
||||
|
||||
expectEvent(await this.contract.release(this.token.address, payee3), 'ERC20PaymentReleased', {
|
||||
token: this.token.address,
|
||||
to: payee3,
|
||||
amount: ether('1.40'),
|
||||
});
|
||||
|
||||
expect(await this.token.balanceOf(payee1)).to.be.bignumber.equal(ether('0.40'));
|
||||
expect(await this.token.balanceOf(payee2)).to.be.bignumber.equal(ether('0.20'));
|
||||
expect(await this.token.balanceOf(payee3)).to.be.bignumber.equal(ether('1.40'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
const { time } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
const { expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
function releasedEvent(token, amount) {
|
||||
return token ? ['ERC20Released', { token: token.address, amount }] : ['EtherReleased', { amount }];
|
||||
}
|
||||
|
||||
function shouldBehaveLikeVesting(beneficiary) {
|
||||
it('check vesting schedule', async function () {
|
||||
const [vestedAmount, releasable, ...args] = this.token
|
||||
? ['vestedAmount(address,uint64)', 'releasable(address)', this.token.address]
|
||||
: ['vestedAmount(uint64)', 'releasable()'];
|
||||
|
||||
for (const timestamp of this.schedule) {
|
||||
await time.increaseTo(timestamp);
|
||||
const vesting = this.vestingFn(timestamp);
|
||||
|
||||
expect(await this.mock.methods[vestedAmount](...args, timestamp)).to.be.bignumber.equal(vesting);
|
||||
|
||||
expect(await this.mock.methods[releasable](...args)).to.be.bignumber.equal(vesting);
|
||||
}
|
||||
});
|
||||
|
||||
it('execute vesting schedule', async function () {
|
||||
const [release, ...args] = this.token ? ['release(address)', this.token.address] : ['release()'];
|
||||
|
||||
let released = web3.utils.toBN(0);
|
||||
const before = await this.getBalance(beneficiary);
|
||||
|
||||
{
|
||||
const receipt = await this.mock.methods[release](...args);
|
||||
|
||||
await expectEvent.inTransaction(receipt.tx, this.mock, ...releasedEvent(this.token, '0'));
|
||||
|
||||
await this.checkRelease(receipt, beneficiary, '0');
|
||||
|
||||
expect(await this.getBalance(beneficiary)).to.be.bignumber.equal(before);
|
||||
}
|
||||
|
||||
for (const timestamp of this.schedule) {
|
||||
await time.setNextBlockTimestamp(timestamp);
|
||||
const vested = this.vestingFn(timestamp);
|
||||
|
||||
const receipt = await this.mock.methods[release](...args);
|
||||
await expectEvent.inTransaction(receipt.tx, this.mock, ...releasedEvent(this.token, vested.sub(released)));
|
||||
|
||||
await this.checkRelease(receipt, beneficiary, vested.sub(released));
|
||||
|
||||
expect(await this.getBalance(beneficiary)).to.be.bignumber.equal(before.add(vested));
|
||||
|
||||
released = vested;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeVesting,
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
const { web3 } = require('@openzeppelin/test-helpers/src/setup');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const VestingWallet = artifacts.require('VestingWallet');
|
||||
const ERC20 = artifacts.require('$ERC20');
|
||||
|
||||
const { shouldBehaveLikeVesting } = require('./VestingWallet.behavior');
|
||||
|
||||
const min = (...args) => args.slice(1).reduce((x, y) => (x.lt(y) ? x : y), args[0]);
|
||||
|
||||
contract('VestingWallet', function (accounts) {
|
||||
const [sender, beneficiary] = accounts;
|
||||
|
||||
const amount = web3.utils.toBN(web3.utils.toWei('100'));
|
||||
const duration = web3.utils.toBN(4 * 365 * 86400); // 4 years
|
||||
|
||||
beforeEach(async function () {
|
||||
this.start = (await time.latest()).addn(3600); // in 1 hour
|
||||
this.mock = await VestingWallet.new(beneficiary, this.start, duration);
|
||||
});
|
||||
|
||||
it('rejects zero address for beneficiary', async function () {
|
||||
await expectRevert(
|
||||
VestingWallet.new(constants.ZERO_ADDRESS, this.start, duration),
|
||||
'VestingWallet: beneficiary is zero address',
|
||||
);
|
||||
});
|
||||
|
||||
it('check vesting contract', async function () {
|
||||
expect(await this.mock.beneficiary()).to.be.equal(beneficiary);
|
||||
expect(await this.mock.start()).to.be.bignumber.equal(this.start);
|
||||
expect(await this.mock.duration()).to.be.bignumber.equal(duration);
|
||||
});
|
||||
|
||||
describe('vesting schedule', function () {
|
||||
beforeEach(async function () {
|
||||
this.schedule = Array(64)
|
||||
.fill()
|
||||
.map((_, i) => web3.utils.toBN(i).mul(duration).divn(60).add(this.start));
|
||||
this.vestingFn = timestamp => min(amount, amount.mul(timestamp.sub(this.start)).div(duration));
|
||||
});
|
||||
|
||||
describe('Eth vesting', function () {
|
||||
beforeEach(async function () {
|
||||
await web3.eth.sendTransaction({ from: sender, to: this.mock.address, value: amount });
|
||||
this.getBalance = account => web3.eth.getBalance(account).then(web3.utils.toBN);
|
||||
this.checkRelease = () => {};
|
||||
});
|
||||
|
||||
shouldBehaveLikeVesting(beneficiary);
|
||||
});
|
||||
|
||||
describe('ERC20 vesting', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20.new('Name', 'Symbol');
|
||||
this.getBalance = account => this.token.balanceOf(account);
|
||||
this.checkRelease = (receipt, to, value) =>
|
||||
expectEvent.inTransaction(receipt.tx, this.token, 'Transfer', { from: this.mock.address, to, value });
|
||||
|
||||
await this.token.$_mint(this.mock.address, amount);
|
||||
});
|
||||
|
||||
shouldBehaveLikeVesting(beneficiary);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,699 @@
|
||||
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const Enums = require('../helpers/enums');
|
||||
const { getDomain, domainType } = require('../helpers/eip712');
|
||||
const { GovernorHelper } = require('../helpers/governance');
|
||||
const { clockFromReceipt } = require('../helpers/time');
|
||||
|
||||
const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior');
|
||||
const { shouldBehaveLikeEIP6372 } = require('./utils/EIP6372.behavior');
|
||||
|
||||
const Governor = artifacts.require('$GovernorMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
const ERC721 = artifacts.require('$ERC721');
|
||||
const ERC1155 = artifacts.require('$ERC1155');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
|
||||
{ Token: artifacts.require('$ERC20VotesLegacyMock'), mode: 'blocknumber' },
|
||||
];
|
||||
|
||||
contract('Governor', function (accounts) {
|
||||
const [owner, proposer, voter1, voter2, voter3, voter4] = accounts;
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = web3.utils.toWei('100');
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.chainId = await web3.eth.getChainId();
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName);
|
||||
this.mock = await Governor.new(
|
||||
name, // name
|
||||
votingDelay, // initialVotingDelay
|
||||
votingPeriod, // initialVotingPeriod
|
||||
0, // initialProposalThreshold
|
||||
this.token.address, // tokenAddress
|
||||
10, // quorumNumeratorValue
|
||||
);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
|
||||
|
||||
await this.token.$_mint(owner, tokenSupply);
|
||||
await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
|
||||
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
data: this.receiver.contract.methods.mockFunction().encodeABI(),
|
||||
value,
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor', 'GovernorWithParams', 'GovernorCancel']);
|
||||
shouldBehaveLikeEIP6372(mode);
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.be.equal(name);
|
||||
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
|
||||
expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=for,abstain');
|
||||
});
|
||||
|
||||
it('nominal workflow', async function () {
|
||||
// Before
|
||||
expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(constants.ZERO_ADDRESS);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
|
||||
expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
|
||||
|
||||
// Run proposal
|
||||
const txPropose = await this.helper.propose({ from: proposer });
|
||||
|
||||
expectEvent(txPropose, 'ProposalCreated', {
|
||||
proposalId: this.proposal.id,
|
||||
proposer,
|
||||
targets: this.proposal.targets,
|
||||
// values: this.proposal.values,
|
||||
signatures: this.proposal.signatures,
|
||||
calldatas: this.proposal.data,
|
||||
voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay),
|
||||
voteEnd: web3.utils
|
||||
.toBN(await clockFromReceipt[mode](txPropose.receipt))
|
||||
.add(votingDelay)
|
||||
.add(votingPeriod),
|
||||
description: this.proposal.description,
|
||||
});
|
||||
|
||||
await this.helper.waitForSnapshot();
|
||||
|
||||
expectEvent(
|
||||
await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }),
|
||||
'VoteCast',
|
||||
{
|
||||
voter: voter1,
|
||||
support: Enums.VoteType.For,
|
||||
reason: 'This is nice',
|
||||
weight: web3.utils.toWei('10'),
|
||||
},
|
||||
);
|
||||
|
||||
expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', {
|
||||
voter: voter2,
|
||||
support: Enums.VoteType.For,
|
||||
weight: web3.utils.toWei('7'),
|
||||
});
|
||||
|
||||
expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', {
|
||||
voter: voter3,
|
||||
support: Enums.VoteType.Against,
|
||||
weight: web3.utils.toWei('5'),
|
||||
});
|
||||
|
||||
expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', {
|
||||
voter: voter4,
|
||||
support: Enums.VoteType.Abstain,
|
||||
weight: web3.utils.toWei('2'),
|
||||
});
|
||||
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
|
||||
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
|
||||
// After
|
||||
expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(proposer);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
|
||||
expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value);
|
||||
});
|
||||
|
||||
it('vote with signature', async function () {
|
||||
const voterBySig = Wallet.generate();
|
||||
const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString());
|
||||
|
||||
const signature = (contract, message) =>
|
||||
getDomain(contract)
|
||||
.then(domain => ({
|
||||
primaryType: 'Ballot',
|
||||
types: {
|
||||
EIP712Domain: domainType(domain),
|
||||
Ballot: [
|
||||
{ name: 'proposalId', type: 'uint256' },
|
||||
{ name: 'support', type: 'uint8' },
|
||||
],
|
||||
},
|
||||
domain,
|
||||
message,
|
||||
}))
|
||||
.then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data }))
|
||||
.then(fromRpcSig);
|
||||
|
||||
await this.token.delegate(voterBySigAddress, { from: voter1 });
|
||||
|
||||
// Run proposal
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
expectEvent(await this.helper.vote({ support: Enums.VoteType.For, signature }), 'VoteCast', {
|
||||
voter: voterBySigAddress,
|
||||
support: Enums.VoteType.For,
|
||||
});
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
// After
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true);
|
||||
});
|
||||
|
||||
it('send ethers', async function () {
|
||||
const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20));
|
||||
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: empty,
|
||||
value,
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
// Before
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
|
||||
expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0');
|
||||
|
||||
// Run proposal
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
// After
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
|
||||
expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value);
|
||||
});
|
||||
|
||||
describe('should revert', function () {
|
||||
describe('on propose', function () {
|
||||
it('if proposal already exists', async function () {
|
||||
await this.helper.propose();
|
||||
await expectRevert(this.helper.propose(), 'Governor: proposal already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on vote', function () {
|
||||
it('if proposal does not exist', async function () {
|
||||
await expectRevert(
|
||||
this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
|
||||
'Governor: unknown proposal id',
|
||||
);
|
||||
});
|
||||
|
||||
it('if voting has not started', async function () {
|
||||
await this.helper.propose();
|
||||
await expectRevert(
|
||||
this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
|
||||
'Governor: vote not currently active',
|
||||
);
|
||||
});
|
||||
|
||||
it('if support value is invalid', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await expectRevert(
|
||||
this.helper.vote({ support: web3.utils.toBN('255') }),
|
||||
'GovernorVotingSimple: invalid value for enum VoteType',
|
||||
);
|
||||
});
|
||||
|
||||
it('if vote was already casted', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await expectRevert(
|
||||
this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
|
||||
'GovernorVotingSimple: vote already cast',
|
||||
);
|
||||
});
|
||||
|
||||
it('if voting is over', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForDeadline();
|
||||
await expectRevert(
|
||||
this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
|
||||
'Governor: vote not currently active',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on execute', function () {
|
||||
it('if proposal does not exist', async function () {
|
||||
await expectRevert(this.helper.execute(), 'Governor: unknown proposal id');
|
||||
});
|
||||
|
||||
it('if quorum is not reached', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 });
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('if score not reached', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('if voting is not over', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('if receiver revert without reason', async function () {
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await expectRevert(this.helper.execute(), 'Governor: call reverted without message');
|
||||
});
|
||||
|
||||
it('if receiver revert with reason', async function () {
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting');
|
||||
});
|
||||
|
||||
it('if proposal was already executed', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('state', function () {
|
||||
it('Unset', async function () {
|
||||
await expectRevert(this.mock.state(this.proposal.id), 'Governor: unknown proposal id');
|
||||
});
|
||||
|
||||
it('Pending & Active', async function () {
|
||||
await this.helper.propose();
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
|
||||
await this.helper.waitForSnapshot();
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
|
||||
await this.helper.waitForSnapshot(+1);
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
|
||||
});
|
||||
|
||||
it('Defeated', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForDeadline();
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
|
||||
await this.helper.waitForDeadline(+1);
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
|
||||
});
|
||||
|
||||
it('Succeeded', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
|
||||
await this.helper.waitForDeadline(+1);
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
|
||||
});
|
||||
|
||||
it('Executed', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', function () {
|
||||
describe('internal', function () {
|
||||
it('before proposal', async function () {
|
||||
await expectRevert(this.helper.cancel('internal'), 'Governor: unknown proposal id');
|
||||
});
|
||||
|
||||
it('after proposal', async function () {
|
||||
await this.helper.propose();
|
||||
|
||||
await this.helper.cancel('internal');
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
|
||||
|
||||
await this.helper.waitForSnapshot();
|
||||
await expectRevert(
|
||||
this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
|
||||
'Governor: vote not currently active',
|
||||
);
|
||||
});
|
||||
|
||||
it('after vote', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
|
||||
await this.helper.cancel('internal');
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
|
||||
|
||||
await this.helper.waitForDeadline();
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('after deadline', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await this.helper.cancel('internal');
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
|
||||
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('after execution', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
await expectRevert(this.helper.cancel('internal'), 'Governor: proposal not active');
|
||||
});
|
||||
});
|
||||
|
||||
describe('public', function () {
|
||||
it('before proposal', async function () {
|
||||
await expectRevert(this.helper.cancel('external'), 'Governor: unknown proposal id');
|
||||
});
|
||||
|
||||
it('after proposal', async function () {
|
||||
await this.helper.propose();
|
||||
|
||||
await this.helper.cancel('external');
|
||||
});
|
||||
|
||||
it('after proposal - restricted to proposer', async function () {
|
||||
await this.helper.propose();
|
||||
|
||||
await expectRevert(this.helper.cancel('external', { from: owner }), 'Governor: only proposer can cancel');
|
||||
});
|
||||
|
||||
it('after vote started', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot(1); // snapshot + 1 block
|
||||
|
||||
await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
|
||||
});
|
||||
|
||||
it('after vote', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
|
||||
await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
|
||||
});
|
||||
|
||||
it('after deadline', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
|
||||
});
|
||||
|
||||
it('after execution', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('proposal length', function () {
|
||||
it('empty', async function () {
|
||||
this.helper.setProposal([], '<proposal description>');
|
||||
await expectRevert(this.helper.propose(), 'Governor: empty proposal');
|
||||
});
|
||||
|
||||
it('mismatch #1', async function () {
|
||||
this.helper.setProposal(
|
||||
{
|
||||
targets: [],
|
||||
values: [web3.utils.toWei('0')],
|
||||
data: [this.receiver.contract.methods.mockFunction().encodeABI()],
|
||||
},
|
||||
'<proposal description>',
|
||||
);
|
||||
await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
|
||||
});
|
||||
|
||||
it('mismatch #2', async function () {
|
||||
this.helper.setProposal(
|
||||
{
|
||||
targets: [this.receiver.address],
|
||||
values: [],
|
||||
data: [this.receiver.contract.methods.mockFunction().encodeABI()],
|
||||
},
|
||||
'<proposal description>',
|
||||
);
|
||||
await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
|
||||
});
|
||||
|
||||
it('mismatch #3', async function () {
|
||||
this.helper.setProposal(
|
||||
{
|
||||
targets: [this.receiver.address],
|
||||
values: [web3.utils.toWei('0')],
|
||||
data: [],
|
||||
},
|
||||
'<proposal description>',
|
||||
);
|
||||
await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onlyGovernance updates', function () {
|
||||
it('setVotingDelay is protected', async function () {
|
||||
await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance');
|
||||
});
|
||||
|
||||
it('setVotingPeriod is protected', async function () {
|
||||
await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance');
|
||||
});
|
||||
|
||||
it('setProposalThreshold is protected', async function () {
|
||||
await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance');
|
||||
});
|
||||
|
||||
it('can setVotingDelay through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods.setVotingDelay('0').encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' });
|
||||
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('can setVotingPeriod through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' });
|
||||
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32');
|
||||
});
|
||||
|
||||
it('cannot setVotingPeriod to 0 through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expectRevert(this.helper.execute(), 'GovernorSettings: voting period too low');
|
||||
});
|
||||
|
||||
it('can setProposalThreshold to 0 through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
expectEvent(await this.helper.execute(), 'ProposalThresholdSet', {
|
||||
oldProposalThreshold: '0',
|
||||
newProposalThreshold: '1000000000000000000',
|
||||
});
|
||||
|
||||
expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('safe receive', function () {
|
||||
describe('ERC721', function () {
|
||||
const name = 'Non Fungible Token';
|
||||
const symbol = 'NFT';
|
||||
const tokenId = web3.utils.toBN(1);
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC721.new(name, symbol);
|
||||
await this.token.$_mint(owner, tokenId);
|
||||
});
|
||||
|
||||
it('can receive an ERC721 safeTransfer', async function () {
|
||||
await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner });
|
||||
});
|
||||
});
|
||||
|
||||
describe('ERC1155', function () {
|
||||
const uri = 'https://token-cdn-domain/{id}.json';
|
||||
const tokenIds = {
|
||||
1: web3.utils.toBN(1000),
|
||||
2: web3.utils.toBN(2000),
|
||||
3: web3.utils.toBN(3000),
|
||||
};
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC1155.new(uri);
|
||||
await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
|
||||
});
|
||||
|
||||
it('can receive ERC1155 safeTransfer', async function () {
|
||||
await this.token.safeTransferFrom(
|
||||
owner,
|
||||
this.mock.address,
|
||||
...Object.entries(tokenIds)[0], // id + amount
|
||||
'0x',
|
||||
{ from: owner },
|
||||
);
|
||||
});
|
||||
|
||||
it('can receive ERC1155 safeBatchTransfer', async function () {
|
||||
await this.token.safeBatchTransferFrom(
|
||||
owner,
|
||||
this.mock.address,
|
||||
Object.keys(tokenIds),
|
||||
Object.values(tokenIds),
|
||||
'0x',
|
||||
{ from: owner },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+283
@@ -0,0 +1,283 @@
|
||||
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const RLP = require('rlp');
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
const { clockFromReceipt } = require('../../helpers/time');
|
||||
|
||||
const Timelock = artifacts.require('CompTimelock');
|
||||
const Governor = artifacts.require('$GovernorCompatibilityBravoMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
|
||||
const { shouldBehaveLikeEIP6372 } = require('../utils/EIP6372.behavior');
|
||||
|
||||
function makeContractAddress(creator, nonce) {
|
||||
return web3.utils.toChecksumAddress(
|
||||
web3.utils
|
||||
.sha3(RLP.encode([creator, nonce]))
|
||||
.slice(12)
|
||||
.substring(14),
|
||||
);
|
||||
}
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' },
|
||||
];
|
||||
|
||||
contract('GovernorCompatibilityBravo', function (accounts) {
|
||||
const [owner, proposer, voter1, voter2, voter3, voter4, other] = accounts;
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
// const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = web3.utils.toWei('100');
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const proposalThreshold = web3.utils.toWei('10');
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
const [deployer] = await web3.eth.getAccounts();
|
||||
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName);
|
||||
|
||||
// Need to predict governance address to set it as timelock admin with a delayed transfer
|
||||
const nonce = await web3.eth.getTransactionCount(deployer);
|
||||
const predictGovernor = makeContractAddress(deployer, nonce + 1);
|
||||
|
||||
this.timelock = await Timelock.new(predictGovernor, 2 * 86400);
|
||||
this.mock = await Governor.new(
|
||||
name,
|
||||
votingDelay,
|
||||
votingPeriod,
|
||||
proposalThreshold,
|
||||
this.timelock.address,
|
||||
this.token.address,
|
||||
);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
|
||||
|
||||
await this.token.$_mint(owner, tokenSupply);
|
||||
await this.helper.delegate({ token: this.token, to: proposer, value: proposalThreshold }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
value,
|
||||
signature: 'mockFunction()',
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
shouldBehaveLikeEIP6372(mode);
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.be.equal(name);
|
||||
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
|
||||
expect(await this.mock.quorumVotes()).to.be.bignumber.equal('0');
|
||||
expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=bravo');
|
||||
});
|
||||
|
||||
it('nominal workflow', async function () {
|
||||
// Before
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
|
||||
expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(value);
|
||||
expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
|
||||
|
||||
// Run proposal
|
||||
const txPropose = await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
// After
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
|
||||
expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal('0');
|
||||
expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value);
|
||||
|
||||
const proposal = await this.mock.proposals(this.proposal.id);
|
||||
expect(proposal.id).to.be.bignumber.equal(this.proposal.id);
|
||||
expect(proposal.proposer).to.be.equal(proposer);
|
||||
expect(proposal.eta).to.be.bignumber.equal(await this.mock.proposalEta(this.proposal.id));
|
||||
expect(proposal.startBlock).to.be.bignumber.equal(await this.mock.proposalSnapshot(this.proposal.id));
|
||||
expect(proposal.endBlock).to.be.bignumber.equal(await this.mock.proposalDeadline(this.proposal.id));
|
||||
expect(proposal.canceled).to.be.equal(false);
|
||||
expect(proposal.executed).to.be.equal(true);
|
||||
|
||||
const action = await this.mock.getActions(this.proposal.id);
|
||||
expect(action.targets).to.be.deep.equal(this.proposal.targets);
|
||||
// expect(action.values).to.be.deep.equal(this.proposal.values);
|
||||
expect(action.signatures).to.be.deep.equal(this.proposal.signatures);
|
||||
expect(action.calldatas).to.be.deep.equal(this.proposal.data);
|
||||
|
||||
const voteReceipt1 = await this.mock.getReceipt(this.proposal.id, voter1);
|
||||
expect(voteReceipt1.hasVoted).to.be.equal(true);
|
||||
expect(voteReceipt1.support).to.be.bignumber.equal(Enums.VoteType.For);
|
||||
expect(voteReceipt1.votes).to.be.bignumber.equal(web3.utils.toWei('10'));
|
||||
|
||||
const voteReceipt2 = await this.mock.getReceipt(this.proposal.id, voter2);
|
||||
expect(voteReceipt2.hasVoted).to.be.equal(true);
|
||||
expect(voteReceipt2.support).to.be.bignumber.equal(Enums.VoteType.For);
|
||||
expect(voteReceipt2.votes).to.be.bignumber.equal(web3.utils.toWei('7'));
|
||||
|
||||
const voteReceipt3 = await this.mock.getReceipt(this.proposal.id, voter3);
|
||||
expect(voteReceipt3.hasVoted).to.be.equal(true);
|
||||
expect(voteReceipt3.support).to.be.bignumber.equal(Enums.VoteType.Against);
|
||||
expect(voteReceipt3.votes).to.be.bignumber.equal(web3.utils.toWei('5'));
|
||||
|
||||
const voteReceipt4 = await this.mock.getReceipt(this.proposal.id, voter4);
|
||||
expect(voteReceipt4.hasVoted).to.be.equal(true);
|
||||
expect(voteReceipt4.support).to.be.bignumber.equal(Enums.VoteType.Abstain);
|
||||
expect(voteReceipt4.votes).to.be.bignumber.equal(web3.utils.toWei('2'));
|
||||
|
||||
expectEvent(txPropose, 'ProposalCreated', {
|
||||
proposalId: this.proposal.id,
|
||||
proposer,
|
||||
targets: this.proposal.targets,
|
||||
// values: this.proposal.values,
|
||||
signatures: this.proposal.signatures.map(() => ''), // this event doesn't contain the proposal detail
|
||||
calldatas: this.proposal.fulldata,
|
||||
voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay),
|
||||
voteEnd: web3.utils
|
||||
.toBN(await clockFromReceipt[mode](txPropose.receipt))
|
||||
.add(votingDelay)
|
||||
.add(votingPeriod),
|
||||
description: this.proposal.description,
|
||||
});
|
||||
expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
it('double voting is forbidden', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await expectRevert(
|
||||
this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
|
||||
'GovernorCompatibilityBravo: vote already cast',
|
||||
);
|
||||
});
|
||||
|
||||
it('with function selector and arguments', async function () {
|
||||
const target = this.receiver.address;
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{ target, data: this.receiver.contract.methods.mockFunction().encodeABI() },
|
||||
{ target, data: this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI() },
|
||||
{ target, signature: 'mockFunctionNonPayable()' },
|
||||
{
|
||||
target,
|
||||
signature: 'mockFunctionWithArgs(uint256,uint256)',
|
||||
data: web3.eth.abi.encodeParameters(['uint256', 'uint256'], [18, 43]),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', {
|
||||
a: '17',
|
||||
b: '42',
|
||||
});
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', {
|
||||
a: '18',
|
||||
b: '43',
|
||||
});
|
||||
});
|
||||
|
||||
it('with inconsistent array size for selector and arguments', async function () {
|
||||
const target = this.receiver.address;
|
||||
this.helper.setProposal(
|
||||
{
|
||||
targets: [target, target],
|
||||
values: [0, 0],
|
||||
signatures: ['mockFunction()'], // One signature
|
||||
data: ['0x', this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI()], // Two data entries
|
||||
},
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await expectRevert(this.helper.propose({ from: proposer }), 'GovernorBravo: invalid signatures length');
|
||||
});
|
||||
|
||||
describe('should revert', function () {
|
||||
describe('on propose', function () {
|
||||
it('if proposal does not meet proposalThreshold', async function () {
|
||||
await expectRevert(
|
||||
this.helper.propose({ from: other }),
|
||||
'Governor: proposer votes below proposal threshold',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on vote', function () {
|
||||
it('if vote type is invalide', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await expectRevert(
|
||||
this.helper.vote({ support: 5 }, { from: voter1 }),
|
||||
'GovernorCompatibilityBravo: invalid vote type',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', function () {
|
||||
it('proposer can cancel', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.cancel('external', { from: proposer });
|
||||
});
|
||||
|
||||
it('anyone can cancel if proposer drop below threshold', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer });
|
||||
await this.helper.cancel('external');
|
||||
});
|
||||
|
||||
it('cannot cancel is proposer is still above threshold', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await expectRevert(this.helper.cancel('external'), 'GovernorBravo: proposer above threshold');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
const { expect } = require('chai');
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
|
||||
const Governor = artifacts.require('$GovernorCompMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' },
|
||||
];
|
||||
|
||||
contract('GovernorComp', function (accounts) {
|
||||
const [owner, voter1, voter2, voter3, voter4] = accounts;
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
// const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = web3.utils.toWei('100');
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.owner = owner;
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName);
|
||||
this.mock = await Governor.new(name, this.token.address);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
|
||||
|
||||
await this.token.$_mint(owner, tokenSupply);
|
||||
await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
value,
|
||||
data: this.receiver.contract.methods.mockFunction().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.be.equal(name);
|
||||
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('voting with comp token', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true);
|
||||
|
||||
await this.mock.proposalVotes(this.proposal.id).then(results => {
|
||||
expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17'));
|
||||
expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5'));
|
||||
expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
const { expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
|
||||
const Governor = artifacts.require('$GovernorVoteMocks');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC721Votes'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC721VotesTimestampMock'), mode: 'timestamp' },
|
||||
];
|
||||
|
||||
contract('GovernorERC721', function (accounts) {
|
||||
const [owner, voter1, voter2, voter3, voter4] = accounts;
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
// const version = '1';
|
||||
const tokenName = 'MockNFToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const NFT0 = web3.utils.toBN(0);
|
||||
const NFT1 = web3.utils.toBN(1);
|
||||
const NFT2 = web3.utils.toBN(2);
|
||||
const NFT3 = web3.utils.toBN(3);
|
||||
const NFT4 = web3.utils.toBN(4);
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.owner = owner;
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName, '1');
|
||||
this.mock = await Governor.new(name, this.token.address);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
|
||||
|
||||
await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => this.token.$_mint(owner, tokenId)));
|
||||
await this.helper.delegate({ token: this.token, to: voter1, tokenId: NFT0 }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT1 }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT2 }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, tokenId: NFT3 }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, tokenId: NFT4 }, { from: owner });
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
value,
|
||||
data: this.receiver.contract.methods.mockFunction().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.be.equal(name);
|
||||
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('voting with ERC721 token', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
|
||||
expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), 'VoteCast', {
|
||||
voter: voter1,
|
||||
support: Enums.VoteType.For,
|
||||
weight: '1',
|
||||
});
|
||||
|
||||
expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', {
|
||||
voter: voter2,
|
||||
support: Enums.VoteType.For,
|
||||
weight: '2',
|
||||
});
|
||||
|
||||
expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', {
|
||||
voter: voter3,
|
||||
support: Enums.VoteType.Against,
|
||||
weight: '1',
|
||||
});
|
||||
|
||||
expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', {
|
||||
voter: voter4,
|
||||
support: Enums.VoteType.Abstain,
|
||||
weight: '1',
|
||||
});
|
||||
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true);
|
||||
|
||||
await this.mock.proposalVotes(this.proposal.id).then(results => {
|
||||
expect(results.forVotes).to.be.bignumber.equal('3');
|
||||
expect(results.againstVotes).to.be.bignumber.equal('1');
|
||||
expect(results.abstainVotes).to.be.bignumber.equal('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
const { clockFromReceipt } = require('../../helpers/time');
|
||||
|
||||
const Governor = artifacts.require('$GovernorPreventLateQuorumMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
|
||||
];
|
||||
|
||||
contract('GovernorPreventLateQuorum', function (accounts) {
|
||||
const [owner, proposer, voter1, voter2, voter3, voter4] = accounts;
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
// const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = web3.utils.toWei('100');
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const lateQuorumVoteExtension = web3.utils.toBN(8);
|
||||
const quorum = web3.utils.toWei('1');
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.owner = owner;
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName);
|
||||
this.mock = await Governor.new(
|
||||
name,
|
||||
votingDelay,
|
||||
votingPeriod,
|
||||
0,
|
||||
this.token.address,
|
||||
lateQuorumVoteExtension,
|
||||
quorum,
|
||||
);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
|
||||
|
||||
await this.token.$_mint(owner, tokenSupply);
|
||||
await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
value,
|
||||
data: this.receiver.contract.methods.mockFunction().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.be.equal(name);
|
||||
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.be.bignumber.equal(quorum);
|
||||
expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension);
|
||||
});
|
||||
|
||||
it('nominal workflow unaffected', async function () {
|
||||
const txPropose = await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true);
|
||||
|
||||
await this.mock.proposalVotes(this.proposal.id).then(results => {
|
||||
expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17'));
|
||||
expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5'));
|
||||
expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2'));
|
||||
});
|
||||
|
||||
const voteStart = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay);
|
||||
const voteEnd = web3.utils
|
||||
.toBN(await clockFromReceipt[mode](txPropose.receipt))
|
||||
.add(votingDelay)
|
||||
.add(votingPeriod);
|
||||
expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(voteStart);
|
||||
expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(voteEnd);
|
||||
|
||||
expectEvent(txPropose, 'ProposalCreated', {
|
||||
proposalId: this.proposal.id,
|
||||
proposer,
|
||||
targets: this.proposal.targets,
|
||||
// values: this.proposal.values.map(value => web3.utils.toBN(value)),
|
||||
signatures: this.proposal.signatures,
|
||||
calldatas: this.proposal.data,
|
||||
voteStart,
|
||||
voteEnd,
|
||||
description: this.proposal.description,
|
||||
});
|
||||
});
|
||||
|
||||
it('Delay is extended to prevent last minute take-over', async function () {
|
||||
const txPropose = await this.helper.propose({ from: proposer });
|
||||
|
||||
// compute original schedule
|
||||
const startBlock = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay);
|
||||
const endBlock = web3.utils
|
||||
.toBN(await clockFromReceipt[mode](txPropose.receipt))
|
||||
.add(votingDelay)
|
||||
.add(votingPeriod);
|
||||
expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock);
|
||||
expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock);
|
||||
|
||||
// wait for the last minute to vote
|
||||
await this.helper.waitForDeadline(-1);
|
||||
const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
|
||||
// cannot execute yet
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
|
||||
|
||||
// compute new extended schedule
|
||||
const extendedDeadline = web3.utils
|
||||
.toBN(await clockFromReceipt[mode](txVote.receipt))
|
||||
.add(lateQuorumVoteExtension);
|
||||
expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock);
|
||||
expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(extendedDeadline);
|
||||
|
||||
// still possible to vote
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
|
||||
|
||||
await this.helper.waitForDeadline();
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
|
||||
await this.helper.waitForDeadline(+1);
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
|
||||
|
||||
// check extension event
|
||||
expectEvent(txVote, 'ProposalExtended', { proposalId: this.proposal.id, extendedDeadline });
|
||||
});
|
||||
|
||||
describe('onlyGovernance updates', function () {
|
||||
it('setLateQuorumVoteExtension is protected', async function () {
|
||||
await expectRevert(this.mock.setLateQuorumVoteExtension(0), 'Governor: onlyGovernance');
|
||||
});
|
||||
|
||||
it('can setLateQuorumVoteExtension through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods.setLateQuorumVoteExtension('0').encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
expectEvent(await this.helper.execute(), 'LateQuorumVoteExtensionSet', {
|
||||
oldVoteExtension: lateQuorumVoteExtension,
|
||||
newVoteExtension: '0',
|
||||
});
|
||||
|
||||
expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const RLP = require('rlp');
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
|
||||
const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
const Timelock = artifacts.require('CompTimelock');
|
||||
const Governor = artifacts.require('$GovernorTimelockCompoundMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
|
||||
function makeContractAddress(creator, nonce) {
|
||||
return web3.utils.toChecksumAddress(
|
||||
web3.utils
|
||||
.sha3(RLP.encode([creator, nonce]))
|
||||
.slice(12)
|
||||
.substring(14),
|
||||
);
|
||||
}
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
|
||||
];
|
||||
|
||||
contract('GovernorTimelockCompound', function (accounts) {
|
||||
const [owner, voter1, voter2, voter3, voter4, other] = accounts;
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
// const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = web3.utils.toWei('100');
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
const [deployer] = await web3.eth.getAccounts();
|
||||
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName);
|
||||
|
||||
// Need to predict governance address to set it as timelock admin with a delayed transfer
|
||||
const nonce = await web3.eth.getTransactionCount(deployer);
|
||||
const predictGovernor = makeContractAddress(deployer, nonce + 1);
|
||||
|
||||
this.timelock = await Timelock.new(predictGovernor, 2 * 86400);
|
||||
this.mock = await Governor.new(
|
||||
name,
|
||||
votingDelay,
|
||||
votingPeriod,
|
||||
0,
|
||||
this.timelock.address,
|
||||
this.token.address,
|
||||
0,
|
||||
);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
|
||||
|
||||
await this.token.$_mint(owner, tokenSupply);
|
||||
await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
value,
|
||||
data: this.receiver.contract.methods.mockFunction().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']);
|
||||
|
||||
it("doesn't accept ether transfers", async function () {
|
||||
await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 }));
|
||||
});
|
||||
|
||||
it('post deployment check', async function () {
|
||||
expect(await this.mock.name()).to.be.equal(name);
|
||||
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
|
||||
|
||||
expect(await this.mock.timelock()).to.be.equal(this.timelock.address);
|
||||
expect(await this.timelock.admin()).to.be.equal(this.mock.address);
|
||||
});
|
||||
|
||||
it('nominal', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
|
||||
await this.helper.waitForDeadline();
|
||||
const txQueue = await this.helper.queue();
|
||||
const eta = await this.mock.proposalEta(this.proposal.id);
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id });
|
||||
await expectEvent.inTransaction(txQueue.tx, this.timelock, 'QueueTransaction', { eta });
|
||||
|
||||
expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
|
||||
await expectEvent.inTransaction(txExecute.tx, this.timelock, 'ExecuteTransaction', { eta });
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
describe('should revert', function () {
|
||||
describe('on queue', function () {
|
||||
it('if already queued', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('if proposal contains duplicate calls', async function () {
|
||||
const action = {
|
||||
target: this.token.address,
|
||||
data: this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI(),
|
||||
};
|
||||
this.helper.setProposal([action, action], '<proposal description>');
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await expectRevert(
|
||||
this.helper.queue(),
|
||||
'GovernorTimelockCompound: identical proposal action already queued',
|
||||
);
|
||||
await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on execute', function () {
|
||||
it('if not queued', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline(+1);
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
|
||||
|
||||
await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued');
|
||||
});
|
||||
|
||||
it('if too early', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
|
||||
|
||||
await expectRevert(
|
||||
this.helper.execute(),
|
||||
"Timelock::executeTransaction: Transaction hasn't surpassed time lock",
|
||||
);
|
||||
});
|
||||
|
||||
it('if too late', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta(+30 * 86400);
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired);
|
||||
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('if already executed', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
await this.helper.execute();
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', function () {
|
||||
it('cancel before queue prevents scheduling', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
|
||||
await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('cancel after queue prevents executing', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onlyGovernance', function () {
|
||||
describe('relay', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(this.mock.address, 1);
|
||||
});
|
||||
|
||||
it('is protected', async function () {
|
||||
await expectRevert(
|
||||
this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()),
|
||||
'Governor: onlyGovernance',
|
||||
);
|
||||
});
|
||||
|
||||
it('can be executed through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods
|
||||
.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI())
|
||||
.encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
expect(await this.token.balanceOf(this.mock.address), 1);
|
||||
expect(await this.token.balanceOf(other), 0);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
expect(await this.token.balanceOf(this.mock.address), 0);
|
||||
expect(await this.token.balanceOf(other), 1);
|
||||
|
||||
await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', {
|
||||
from: this.mock.address,
|
||||
to: other,
|
||||
value: '1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTimelock', function () {
|
||||
beforeEach(async function () {
|
||||
this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400);
|
||||
});
|
||||
|
||||
it('is protected', async function () {
|
||||
await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance');
|
||||
});
|
||||
|
||||
it('can be executed through governance to', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.timelock.address,
|
||||
data: this.timelock.contract.methods.setPendingAdmin(owner).encodeABI(),
|
||||
},
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
expectEvent(txExecute, 'TimelockChange', {
|
||||
oldTimelock: this.timelock.address,
|
||||
newTimelock: this.newTimelock.address,
|
||||
});
|
||||
|
||||
expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address);
|
||||
});
|
||||
});
|
||||
|
||||
it('can transfer timelock to new governor', async function () {
|
||||
const newGovernor = await Governor.new(name, 8, 32, 0, this.timelock.address, this.token.address, 0);
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.timelock.address,
|
||||
data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
await expectEvent.inTransaction(txExecute.tx, this.timelock, 'NewPendingAdmin', {
|
||||
newPendingAdmin: newGovernor.address,
|
||||
});
|
||||
|
||||
await newGovernor.__acceptAdmin();
|
||||
expect(await this.timelock.admin()).to.be.bignumber.equal(newGovernor.address);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
|
||||
const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
const Timelock = artifacts.require('TimelockController');
|
||||
const Governor = artifacts.require('$GovernorTimelockControlMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
|
||||
];
|
||||
|
||||
contract('GovernorTimelockControl', function (accounts) {
|
||||
const [owner, voter1, voter2, voter3, voter4, other] = accounts;
|
||||
|
||||
const TIMELOCK_ADMIN_ROLE = web3.utils.soliditySha3('TIMELOCK_ADMIN_ROLE');
|
||||
const PROPOSER_ROLE = web3.utils.soliditySha3('PROPOSER_ROLE');
|
||||
const EXECUTOR_ROLE = web3.utils.soliditySha3('EXECUTOR_ROLE');
|
||||
const CANCELLER_ROLE = web3.utils.soliditySha3('CANCELLER_ROLE');
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
// const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = web3.utils.toWei('100');
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
const [deployer] = await web3.eth.getAccounts();
|
||||
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName);
|
||||
this.timelock = await Timelock.new(3600, [], [], deployer);
|
||||
this.mock = await Governor.new(
|
||||
name,
|
||||
votingDelay,
|
||||
votingPeriod,
|
||||
0,
|
||||
this.timelock.address,
|
||||
this.token.address,
|
||||
0,
|
||||
);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
this.TIMELOCK_ADMIN_ROLE = await this.timelock.TIMELOCK_ADMIN_ROLE();
|
||||
this.PROPOSER_ROLE = await this.timelock.PROPOSER_ROLE();
|
||||
this.EXECUTOR_ROLE = await this.timelock.EXECUTOR_ROLE();
|
||||
this.CANCELLER_ROLE = await this.timelock.CANCELLER_ROLE();
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
|
||||
|
||||
// normal setup: governor is proposer, everyone is executor, timelock is its own admin
|
||||
await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address);
|
||||
await this.timelock.grantRole(PROPOSER_ROLE, owner);
|
||||
await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address);
|
||||
await this.timelock.grantRole(CANCELLER_ROLE, owner);
|
||||
await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS);
|
||||
await this.timelock.revokeRole(TIMELOCK_ADMIN_ROLE, deployer);
|
||||
|
||||
await this.token.$_mint(owner, tokenSupply);
|
||||
await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
value,
|
||||
data: this.receiver.contract.methods.mockFunction().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
this.proposal.timelockid = await this.timelock.hashOperationBatch(
|
||||
...this.proposal.shortProposal.slice(0, 3),
|
||||
'0x0',
|
||||
this.proposal.shortProposal[3],
|
||||
);
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']);
|
||||
|
||||
it("doesn't accept ether transfers", async function () {
|
||||
await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 }));
|
||||
});
|
||||
|
||||
it('post deployment check', async function () {
|
||||
expect(await this.mock.name()).to.be.equal(name);
|
||||
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
|
||||
|
||||
expect(await this.mock.timelock()).to.be.equal(this.timelock.address);
|
||||
});
|
||||
|
||||
it('nominal', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
|
||||
await this.helper.waitForDeadline();
|
||||
const txQueue = await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id });
|
||||
await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid });
|
||||
await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', {
|
||||
id: this.proposal.timelockid,
|
||||
});
|
||||
|
||||
expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
|
||||
await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid });
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
describe('should revert', function () {
|
||||
describe('on queue', function () {
|
||||
it('if already queued', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
|
||||
await this.helper.waitForDeadline();
|
||||
const txQueue = await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id });
|
||||
await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', {
|
||||
id: this.proposal.timelockid,
|
||||
});
|
||||
|
||||
expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
|
||||
await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', {
|
||||
id: this.proposal.timelockid,
|
||||
});
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
describe('should revert', function () {
|
||||
describe('on queue', function () {
|
||||
it('if already queued', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on execute', function () {
|
||||
it('if not queued', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline(+1);
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
|
||||
|
||||
await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready');
|
||||
});
|
||||
|
||||
it('if too early', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
|
||||
|
||||
await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready');
|
||||
});
|
||||
|
||||
it('if already executed', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
await this.helper.execute();
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('if already executed by another proposer', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
|
||||
await this.timelock.executeBatch(
|
||||
...this.proposal.shortProposal.slice(0, 3),
|
||||
'0x0',
|
||||
this.proposal.shortProposal[3],
|
||||
);
|
||||
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', function () {
|
||||
it('cancel before queue prevents scheduling', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
|
||||
await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('cancel after queue prevents executing', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
it('cancel on timelock is reflected on governor', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
|
||||
|
||||
expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', {
|
||||
id: this.proposal.timelockid,
|
||||
});
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onlyGovernance', function () {
|
||||
describe('relay', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(this.mock.address, 1);
|
||||
});
|
||||
|
||||
it('is protected', async function () {
|
||||
await expectRevert(
|
||||
this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()),
|
||||
'Governor: onlyGovernance',
|
||||
);
|
||||
});
|
||||
|
||||
it('can be executed through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods
|
||||
.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI())
|
||||
.encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
expect(await this.token.balanceOf(this.mock.address), 1);
|
||||
expect(await this.token.balanceOf(other), 0);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
expect(await this.token.balanceOf(this.mock.address), 0);
|
||||
expect(await this.token.balanceOf(other), 1);
|
||||
|
||||
await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', {
|
||||
from: this.mock.address,
|
||||
to: other,
|
||||
value: '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('is payable and can transfer eth to EOA', async function () {
|
||||
const t2g = web3.utils.toBN(128); // timelock to governor
|
||||
const g2o = web3.utils.toBN(100); // governor to eoa (other)
|
||||
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
value: t2g,
|
||||
data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0));
|
||||
const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN);
|
||||
const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
await this.helper.execute();
|
||||
|
||||
expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(
|
||||
timelockBalance.sub(t2g),
|
||||
);
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o));
|
||||
expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o));
|
||||
});
|
||||
|
||||
it('protected against other proposers', async function () {
|
||||
await this.timelock.schedule(
|
||||
this.mock.address,
|
||||
web3.utils.toWei('0'),
|
||||
this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(),
|
||||
constants.ZERO_BYTES32,
|
||||
constants.ZERO_BYTES32,
|
||||
3600,
|
||||
{ from: owner },
|
||||
);
|
||||
|
||||
await time.increase(3600);
|
||||
|
||||
await expectRevert(
|
||||
this.timelock.execute(
|
||||
this.mock.address,
|
||||
web3.utils.toWei('0'),
|
||||
this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(),
|
||||
constants.ZERO_BYTES32,
|
||||
constants.ZERO_BYTES32,
|
||||
{ from: owner },
|
||||
),
|
||||
'TimelockController: underlying transaction reverted',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTimelock', function () {
|
||||
beforeEach(async function () {
|
||||
this.newTimelock = await Timelock.new(
|
||||
3600,
|
||||
[this.mock.address],
|
||||
[this.mock.address],
|
||||
constants.ZERO_ADDRESS,
|
||||
);
|
||||
});
|
||||
|
||||
it('is protected', async function () {
|
||||
await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance');
|
||||
});
|
||||
|
||||
it('can be executed through governance to', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
expectEvent(txExecute, 'TimelockChange', {
|
||||
oldTimelock: this.timelock.address,
|
||||
newTimelock: this.newTimelock.address,
|
||||
});
|
||||
|
||||
expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('clear queue of pending governor calls', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
await this.helper.execute();
|
||||
|
||||
// This path clears _governanceCall as part of the afterExecute call,
|
||||
// but we have not way to check that the cleanup actually happened other
|
||||
// then coverage reports.
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
const { expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
const { clock } = require('../../helpers/time');
|
||||
|
||||
const Governor = artifacts.require('$GovernorMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
|
||||
];
|
||||
|
||||
contract('GovernorVotesQuorumFraction', function (accounts) {
|
||||
const [owner, voter1, voter2, voter3, voter4] = accounts;
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
// const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = web3.utils.toBN(web3.utils.toWei('100'));
|
||||
const ratio = web3.utils.toBN(8); // percents
|
||||
const newRatio = web3.utils.toBN(6); // percents
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.owner = owner;
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName);
|
||||
this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.token.address, ratio);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
|
||||
|
||||
await this.token.$_mint(owner, tokenSupply);
|
||||
await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
value,
|
||||
data: this.receiver.contract.methods.mockFunction().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.be.equal(name);
|
||||
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
|
||||
expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(ratio);
|
||||
expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100');
|
||||
expect(await clock[mode]().then(timepoint => this.mock.quorum(timepoint - 1))).to.be.bignumber.equal(
|
||||
tokenSupply.mul(ratio).divn(100),
|
||||
);
|
||||
});
|
||||
|
||||
it('quroum reached', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
});
|
||||
|
||||
it('quroum not reached', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
await this.helper.waitForDeadline();
|
||||
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
|
||||
});
|
||||
|
||||
describe('onlyGovernance updates', function () {
|
||||
it('updateQuorumNumerator is protected', async function () {
|
||||
await expectRevert(this.mock.updateQuorumNumerator(newRatio), 'Governor: onlyGovernance');
|
||||
});
|
||||
|
||||
it('can updateQuorumNumerator through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
expectEvent(await this.helper.execute(), 'QuorumNumeratorUpdated', {
|
||||
oldQuorumNumerator: ratio,
|
||||
newQuorumNumerator: newRatio,
|
||||
});
|
||||
|
||||
expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio);
|
||||
expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100');
|
||||
|
||||
// it takes one block for the new quorum to take effect
|
||||
expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal(
|
||||
tokenSupply.mul(ratio).divn(100),
|
||||
);
|
||||
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal(
|
||||
tokenSupply.mul(newRatio).divn(100),
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot updateQuorumNumerator over the maximum', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.address,
|
||||
data: this.mock.contract.methods.updateQuorumNumerator('101').encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expectRevert(
|
||||
this.helper.execute(),
|
||||
'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
const { expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { getDomain, domainType } = require('../../helpers/eip712');
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
|
||||
const Governor = artifacts.require('$GovernorWithParamsMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
|
||||
const rawParams = {
|
||||
uintParam: web3.utils.toBN('42'),
|
||||
strParam: 'These are my params',
|
||||
};
|
||||
|
||||
const encodedParams = web3.eth.abi.encodeParameters(['uint256', 'string'], Object.values(rawParams));
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
|
||||
];
|
||||
|
||||
contract('GovernorWithParams', function (accounts) {
|
||||
const [owner, proposer, voter1, voter2, voter3, voter4] = accounts;
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = web3.utils.toWei('100');
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.chainId = await web3.eth.getChainId();
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName);
|
||||
this.mock = await Governor.new(name, this.token.address);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
|
||||
|
||||
await this.token.$_mint(owner, tokenSupply);
|
||||
await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
value,
|
||||
data: this.receiver.contract.methods.mockFunction().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.be.equal(name);
|
||||
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||
});
|
||||
|
||||
it('nominal is unaffected', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
|
||||
expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value);
|
||||
});
|
||||
|
||||
it('Voting with params is properly supported', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
|
||||
const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam);
|
||||
|
||||
const tx = await this.helper.vote(
|
||||
{
|
||||
support: Enums.VoteType.For,
|
||||
reason: 'no particular reason',
|
||||
params: encodedParams,
|
||||
},
|
||||
{ from: voter2 },
|
||||
);
|
||||
|
||||
expectEvent(tx, 'CountParams', { ...rawParams });
|
||||
expectEvent(tx, 'VoteCastWithParams', {
|
||||
voter: voter2,
|
||||
proposalId: this.proposal.id,
|
||||
support: Enums.VoteType.For,
|
||||
weight,
|
||||
reason: 'no particular reason',
|
||||
params: encodedParams,
|
||||
});
|
||||
|
||||
const votes = await this.mock.proposalVotes(this.proposal.id);
|
||||
expect(votes.forVotes).to.be.bignumber.equal(weight);
|
||||
});
|
||||
|
||||
it('Voting with params by signature is properly supported', async function () {
|
||||
const voterBySig = Wallet.generate();
|
||||
const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString());
|
||||
|
||||
const signature = (contract, message) =>
|
||||
getDomain(contract)
|
||||
.then(domain => ({
|
||||
primaryType: 'ExtendedBallot',
|
||||
types: {
|
||||
EIP712Domain: domainType(domain),
|
||||
ExtendedBallot: [
|
||||
{ name: 'proposalId', type: 'uint256' },
|
||||
{ name: 'support', type: 'uint8' },
|
||||
{ name: 'reason', type: 'string' },
|
||||
{ name: 'params', type: 'bytes' },
|
||||
],
|
||||
},
|
||||
domain,
|
||||
message,
|
||||
}))
|
||||
.then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data }))
|
||||
.then(fromRpcSig);
|
||||
|
||||
await this.token.delegate(voterBySigAddress, { from: voter2 });
|
||||
|
||||
// Run proposal
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
|
||||
const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam);
|
||||
|
||||
const tx = await this.helper.vote({
|
||||
support: Enums.VoteType.For,
|
||||
reason: 'no particular reason',
|
||||
params: encodedParams,
|
||||
signature,
|
||||
});
|
||||
|
||||
expectEvent(tx, 'CountParams', { ...rawParams });
|
||||
expectEvent(tx, 'VoteCastWithParams', {
|
||||
voter: voterBySigAddress,
|
||||
proposalId: this.proposal.id,
|
||||
support: Enums.VoteType.For,
|
||||
weight,
|
||||
reason: 'no particular reason',
|
||||
params: encodedParams,
|
||||
});
|
||||
|
||||
const votes = await this.mock.proposalVotes(this.proposal.id);
|
||||
expect(votes.forVotes).to.be.bignumber.equal(weight);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
const { clock } = require('../../helpers/time');
|
||||
|
||||
function shouldBehaveLikeEIP6372(mode = 'blocknumber') {
|
||||
describe('should implement EIP6372', function () {
|
||||
beforeEach(async function () {
|
||||
this.mock = this.mock ?? this.token ?? this.votes;
|
||||
});
|
||||
|
||||
it('clock is correct', async function () {
|
||||
expect(await this.mock.clock()).to.be.bignumber.equal(await clock[mode]().then(web3.utils.toBN));
|
||||
});
|
||||
|
||||
it('CLOCK_MODE is correct', async function () {
|
||||
const params = new URLSearchParams(await this.mock.CLOCK_MODE());
|
||||
expect(params.get('mode')).to.be.equal(mode);
|
||||
expect(params.get('from')).to.be.equal(mode == 'blocknumber' ? 'default' : null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeEIP6372,
|
||||
};
|
||||
@@ -0,0 +1,361 @@
|
||||
const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { MAX_UINT256, ZERO_ADDRESS } = constants;
|
||||
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
|
||||
const { shouldBehaveLikeEIP6372 } = require('./EIP6372.behavior');
|
||||
|
||||
const { getDomain, domainType, domainSeparator } = require('../../helpers/eip712');
|
||||
const { clockFromReceipt } = require('../../helpers/time');
|
||||
|
||||
const Delegation = [
|
||||
{ name: 'delegatee', type: 'address' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'expiry', type: 'uint256' },
|
||||
];
|
||||
|
||||
function shouldBehaveLikeVotes(mode = 'blocknumber') {
|
||||
shouldBehaveLikeEIP6372(mode);
|
||||
|
||||
describe('run votes workflow', function () {
|
||||
it('initial nonce is 0', async function () {
|
||||
expect(await this.votes.nonces(this.account1)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('domain separator', async function () {
|
||||
expect(await this.votes.DOMAIN_SEPARATOR()).to.equal(domainSeparator(await getDomain(this.votes)));
|
||||
});
|
||||
|
||||
describe('delegation with signature', function () {
|
||||
const delegator = Wallet.generate();
|
||||
const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString());
|
||||
const nonce = 0;
|
||||
|
||||
const buildAndSignData = async (contract, message, pk) => {
|
||||
const data = await getDomain(contract).then(domain => ({
|
||||
primaryType: 'Delegation',
|
||||
types: { EIP712Domain: domainType(domain), Delegation },
|
||||
domain,
|
||||
message,
|
||||
}));
|
||||
return fromRpcSig(ethSigUtil.signTypedMessage(pk, { data }));
|
||||
};
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.votes.$_mint(delegatorAddress, this.NFT0);
|
||||
});
|
||||
|
||||
it('accept signed delegation', async function () {
|
||||
const { v, r, s } = await buildAndSignData(
|
||||
this.votes,
|
||||
{
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
},
|
||||
delegator.getPrivateKey(),
|
||||
);
|
||||
|
||||
expect(await this.votes.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: delegatorAddress,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: delegatorAddress,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: delegatorAddress,
|
||||
previousBalance: '0',
|
||||
newBalance: '1',
|
||||
});
|
||||
|
||||
expect(await this.votes.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
|
||||
|
||||
expect(await this.votes.getVotes(delegatorAddress)).to.be.bignumber.equal('1');
|
||||
expect(await this.votes.getPastVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.votes.getPastVotes(delegatorAddress, timepoint)).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('rejects reused signature', async function () {
|
||||
const { v, r, s } = await buildAndSignData(
|
||||
this.votes,
|
||||
{
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
},
|
||||
delegator.getPrivateKey(),
|
||||
);
|
||||
|
||||
await this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
|
||||
|
||||
await expectRevert(
|
||||
this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
|
||||
'Votes: invalid nonce',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects bad delegatee', async function () {
|
||||
const { v, r, s } = await buildAndSignData(
|
||||
this.votes,
|
||||
{
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
},
|
||||
delegator.getPrivateKey(),
|
||||
);
|
||||
|
||||
const receipt = await this.votes.delegateBySig(this.account1Delegatee, nonce, MAX_UINT256, v, r, s);
|
||||
const { args } = receipt.logs.find(({ event }) => event === 'DelegateChanged');
|
||||
expect(args.delegator).to.not.be.equal(delegatorAddress);
|
||||
expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
|
||||
expect(args.toDelegate).to.be.equal(this.account1Delegatee);
|
||||
});
|
||||
|
||||
it('rejects bad nonce', async function () {
|
||||
const { v, r, s } = await buildAndSignData(
|
||||
this.votes,
|
||||
{
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
},
|
||||
delegator.getPrivateKey(),
|
||||
);
|
||||
|
||||
await expectRevert(
|
||||
this.votes.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
|
||||
'Votes: invalid nonce',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects expired permit', async function () {
|
||||
const expiry = (await time.latest()) - time.duration.weeks(1);
|
||||
|
||||
const { v, r, s } = await buildAndSignData(
|
||||
this.votes,
|
||||
{
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry,
|
||||
},
|
||||
delegator.getPrivateKey(),
|
||||
);
|
||||
|
||||
await expectRevert(
|
||||
this.votes.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
|
||||
'Votes: signature expired',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('set delegation', function () {
|
||||
describe('call', function () {
|
||||
it('delegation with tokens', async function () {
|
||||
await this.votes.$_mint(this.account1, this.NFT0);
|
||||
expect(await this.votes.delegates(this.account1)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: this.account1,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: this.account1,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: this.account1,
|
||||
previousBalance: '0',
|
||||
newBalance: '1',
|
||||
});
|
||||
|
||||
expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1);
|
||||
|
||||
expect(await this.votes.getVotes(this.account1)).to.be.bignumber.equal('1');
|
||||
expect(await this.votes.getPastVotes(this.account1, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.votes.getPastVotes(this.account1, timepoint)).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('delegation without tokens', async function () {
|
||||
expect(await this.votes.delegates(this.account1)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 });
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: this.account1,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: this.account1,
|
||||
});
|
||||
expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
|
||||
|
||||
expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('change delegation', function () {
|
||||
beforeEach(async function () {
|
||||
await this.votes.$_mint(this.account1, this.NFT0);
|
||||
await this.votes.delegate(this.account1, { from: this.account1 });
|
||||
});
|
||||
|
||||
it('call', async function () {
|
||||
expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1);
|
||||
|
||||
const { receipt } = await this.votes.delegate(this.account1Delegatee, { from: this.account1 });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: this.account1,
|
||||
fromDelegate: this.account1,
|
||||
toDelegate: this.account1Delegatee,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: this.account1,
|
||||
previousBalance: '1',
|
||||
newBalance: '0',
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: this.account1Delegatee,
|
||||
previousBalance: '0',
|
||||
newBalance: '1',
|
||||
});
|
||||
|
||||
expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1Delegatee);
|
||||
|
||||
expect(await this.votes.getVotes(this.account1)).to.be.bignumber.equal('0');
|
||||
expect(await this.votes.getVotes(this.account1Delegatee)).to.be.bignumber.equal('1');
|
||||
expect(await this.votes.getPastVotes(this.account1, timepoint - 1)).to.be.bignumber.equal('1');
|
||||
expect(await this.votes.getPastVotes(this.account1Delegatee, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.votes.getPastVotes(this.account1, timepoint)).to.be.bignumber.equal('0');
|
||||
expect(await this.votes.getPastVotes(this.account1Delegatee, timepoint)).to.be.bignumber.equal('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPastTotalSupply', function () {
|
||||
beforeEach(async function () {
|
||||
await this.votes.delegate(this.account1, { from: this.account1 });
|
||||
});
|
||||
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.votes.getPastTotalSupply(5e10), 'future lookup');
|
||||
});
|
||||
|
||||
it('returns 0 if there are no checkpoints', async function () {
|
||||
expect(await this.votes.getPastTotalSupply(0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns the latest block if >= last checkpoint block', async function () {
|
||||
const { receipt } = await this.votes.$_mint(this.account1, this.NFT0);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.votes.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.votes.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('returns zero if < first checkpoint block', async function () {
|
||||
await time.advanceBlock();
|
||||
const { receipt } = await this.votes.$_mint(this.account1, this.NFT1);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.votes.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.votes.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('generally returns the voting balance at the appropriate checkpoint', async function () {
|
||||
const t1 = await this.votes.$_mint(this.account1, this.NFT1);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t2 = await this.votes.$_burn(this.NFT1);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t3 = await this.votes.$_mint(this.account1, this.NFT2);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t4 = await this.votes.$_burn(this.NFT2);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t5 = await this.votes.$_mint(this.account1, this.NFT3);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
t5.timepoint = await clockFromReceipt[mode](t5.receipt);
|
||||
|
||||
expect(await this.votes.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.votes.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('1');
|
||||
expect(await this.votes.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal('1');
|
||||
expect(await this.votes.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('0');
|
||||
expect(await this.votes.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.votes.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('1');
|
||||
expect(await this.votes.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal('1');
|
||||
expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('0');
|
||||
expect(await this.votes.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.be.bignumber.equal('1');
|
||||
expect(await this.votes.getPastTotalSupply(t5.timepoint + 1)).to.be.bignumber.equal('1');
|
||||
});
|
||||
});
|
||||
|
||||
// The following tests are a adaptation of
|
||||
// https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
|
||||
describe('Compound test suite', function () {
|
||||
beforeEach(async function () {
|
||||
await this.votes.$_mint(this.account1, this.NFT0);
|
||||
await this.votes.$_mint(this.account1, this.NFT1);
|
||||
await this.votes.$_mint(this.account1, this.NFT2);
|
||||
await this.votes.$_mint(this.account1, this.NFT3);
|
||||
});
|
||||
|
||||
describe('getPastVotes', function () {
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.votes.getPastVotes(this.account2, 5e10), 'future lookup');
|
||||
});
|
||||
|
||||
it('returns 0 if there are no checkpoints', async function () {
|
||||
expect(await this.votes.getPastVotes(this.account2, 0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns the latest block if >= last checkpoint block', async function () {
|
||||
const { receipt } = await this.votes.delegate(this.account2, { from: this.account1 });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
const latest = await this.votes.getVotes(this.account2);
|
||||
expect(await this.votes.getPastVotes(this.account2, timepoint)).to.be.bignumber.equal(latest);
|
||||
expect(await this.votes.getPastVotes(this.account2, timepoint + 1)).to.be.bignumber.equal(latest);
|
||||
});
|
||||
|
||||
it('returns zero if < first checkpoint block', async function () {
|
||||
await time.advanceBlock();
|
||||
const { receipt } = await this.votes.delegate(this.account2, { from: this.account1 });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.votes.getPastVotes(this.account2, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeVotes,
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
const { expectRevert, BN } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { getChainId } = require('../../helpers/chainid');
|
||||
const { clockFromReceipt } = require('../../helpers/time');
|
||||
|
||||
const { shouldBehaveLikeVotes } = require('./Votes.behavior');
|
||||
|
||||
const MODES = {
|
||||
blocknumber: artifacts.require('$VotesMock'),
|
||||
timestamp: artifacts.require('$VotesTimestampMock'),
|
||||
};
|
||||
|
||||
contract('Votes', function (accounts) {
|
||||
const [account1, account2, account3] = accounts;
|
||||
|
||||
for (const [mode, artifact] of Object.entries(MODES)) {
|
||||
describe(`vote with ${mode}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.name = 'My Vote';
|
||||
this.votes = await artifact.new(this.name, '1');
|
||||
});
|
||||
|
||||
it('starts with zero votes', async function () {
|
||||
expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
describe('performs voting operations', function () {
|
||||
beforeEach(async function () {
|
||||
this.tx1 = await this.votes.$_mint(account1, 1);
|
||||
this.tx2 = await this.votes.$_mint(account2, 1);
|
||||
this.tx3 = await this.votes.$_mint(account3, 1);
|
||||
this.tx1.timepoint = await clockFromReceipt[mode](this.tx1.receipt);
|
||||
this.tx2.timepoint = await clockFromReceipt[mode](this.tx2.receipt);
|
||||
this.tx3.timepoint = await clockFromReceipt[mode](this.tx3.receipt);
|
||||
});
|
||||
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.votes.getPastTotalSupply(this.tx3.timepoint + 1), 'Votes: future lookup');
|
||||
});
|
||||
|
||||
it('delegates', async function () {
|
||||
await this.votes.delegate(account3, account2);
|
||||
|
||||
expect(await this.votes.delegates(account3)).to.be.equal(account2);
|
||||
});
|
||||
|
||||
it('returns total amount of votes', async function () {
|
||||
expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('performs voting workflow', function () {
|
||||
beforeEach(async function () {
|
||||
this.chainId = await getChainId();
|
||||
this.account1 = account1;
|
||||
this.account2 = account2;
|
||||
this.account1Delegatee = account2;
|
||||
this.NFT0 = new BN('10000000000000000000000000');
|
||||
this.NFT1 = new BN('10');
|
||||
this.NFT2 = new BN('20');
|
||||
this.NFT3 = new BN('30');
|
||||
});
|
||||
|
||||
// includes EIP6372 behavior check
|
||||
shouldBehaveLikeVotes(mode);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
const hre = require('hardhat');
|
||||
|
||||
async function getChainId() {
|
||||
const chainIdHex = await hre.network.provider.send('eth_chainId', []);
|
||||
return new hre.web3.utils.BN(chainIdHex, 'hex');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getChainId,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
function computeCreate2Address(saltHex, bytecode, deployer) {
|
||||
return web3.utils.toChecksumAddress(
|
||||
`0x${web3.utils
|
||||
.sha3(`0x${['ff', deployer, saltHex, web3.utils.soliditySha3(bytecode)].map(x => x.replace(/0x/, '')).join('')}`)
|
||||
.slice(-40)}`,
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
computeCreate2Address,
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
const { promisify } = require('util');
|
||||
|
||||
const BridgeAMBMock = artifacts.require('BridgeAMBMock');
|
||||
const BridgeArbitrumL1Mock = artifacts.require('BridgeArbitrumL1Mock');
|
||||
const BridgeArbitrumL2Mock = artifacts.require('BridgeArbitrumL2Mock');
|
||||
const BridgeOptimismMock = artifacts.require('BridgeOptimismMock');
|
||||
const BridgePolygonChildMock = artifacts.require('BridgePolygonChildMock');
|
||||
|
||||
class BridgeHelper {
|
||||
static async deploy(type) {
|
||||
return new BridgeHelper(await deployBridge(type));
|
||||
}
|
||||
|
||||
constructor(bridge) {
|
||||
this.bridge = bridge;
|
||||
this.address = bridge.address;
|
||||
}
|
||||
|
||||
call(from, target, selector = undefined, args = []) {
|
||||
return this.bridge.relayAs(
|
||||
target.address || target,
|
||||
selector ? target.contract.methods[selector](...args).encodeABI() : '0x',
|
||||
from,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function deployBridge(type = 'Arbitrum-L2') {
|
||||
switch (type) {
|
||||
case 'AMB':
|
||||
return BridgeAMBMock.new();
|
||||
|
||||
case 'Arbitrum-L1':
|
||||
return BridgeArbitrumL1Mock.new();
|
||||
|
||||
case 'Arbitrum-L2': {
|
||||
const instance = await BridgeArbitrumL2Mock.new();
|
||||
const code = await web3.eth.getCode(instance.address);
|
||||
await promisify(web3.currentProvider.send.bind(web3.currentProvider))({
|
||||
jsonrpc: '2.0',
|
||||
method: 'hardhat_setCode',
|
||||
params: ['0x0000000000000000000000000000000000000064', code],
|
||||
id: new Date().getTime(),
|
||||
});
|
||||
return BridgeArbitrumL2Mock.at('0x0000000000000000000000000000000000000064');
|
||||
}
|
||||
|
||||
case 'Optimism':
|
||||
return BridgeOptimismMock.new();
|
||||
|
||||
case 'Polygon-Child':
|
||||
return BridgePolygonChildMock.new();
|
||||
|
||||
default:
|
||||
throw new Error(`CrossChain: ${type} is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BridgeHelper,
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const { config } = require('hardhat');
|
||||
|
||||
const optimizationsEnabled = config.solidity.compilers.some(c => c.settings.optimizer.enabled);
|
||||
|
||||
/** Revert handler that supports custom errors. */
|
||||
async function expectRevertCustomError(promise, reason) {
|
||||
try {
|
||||
await promise;
|
||||
expect.fail("Expected promise to throw but it didn't");
|
||||
} catch (revert) {
|
||||
if (reason) {
|
||||
if (optimizationsEnabled) {
|
||||
// Optimizations currently mess with Hardhat's decoding of custom errors
|
||||
expect(revert.message).to.include.oneOf([reason, 'unrecognized return data or custom error']);
|
||||
} else {
|
||||
expect(revert.message).to.include(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
expectRevertCustomError,
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const keccak256 = require('keccak256');
|
||||
|
||||
const EIP712Domain = [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'version', type: 'string' },
|
||||
{ name: 'chainId', type: 'uint256' },
|
||||
{ name: 'verifyingContract', type: 'address' },
|
||||
{ name: 'salt', type: 'bytes32' },
|
||||
];
|
||||
|
||||
const Permit = [
|
||||
{ name: 'owner', type: 'address' },
|
||||
{ name: 'spender', type: 'address' },
|
||||
{ name: 'value', type: 'uint256' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
];
|
||||
|
||||
function bufferToHexString(buffer) {
|
||||
return '0x' + buffer.toString('hex');
|
||||
}
|
||||
|
||||
function hexStringToBuffer(hexstr) {
|
||||
return Buffer.from(hexstr.replace(/^0x/, ''), 'hex');
|
||||
}
|
||||
|
||||
async function getDomain(contract) {
|
||||
const { fields, name, version, chainId, verifyingContract, salt, extensions } = await contract.eip712Domain();
|
||||
|
||||
if (extensions.length > 0) {
|
||||
throw Error('Extensions not implemented');
|
||||
}
|
||||
|
||||
const domain = { name, version, chainId, verifyingContract, salt };
|
||||
for (const [i, { name }] of EIP712Domain.entries()) {
|
||||
if (!(fields & (1 << i))) {
|
||||
delete domain[name];
|
||||
}
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
function domainType(domain) {
|
||||
return EIP712Domain.filter(({ name }) => domain[name] !== undefined);
|
||||
}
|
||||
|
||||
function domainSeparator(domain) {
|
||||
return bufferToHexString(
|
||||
ethSigUtil.TypedDataUtils.hashStruct('EIP712Domain', domain, { EIP712Domain: domainType(domain) }),
|
||||
);
|
||||
}
|
||||
|
||||
function hashTypedData(domain, structHash) {
|
||||
return bufferToHexString(
|
||||
keccak256(Buffer.concat(['0x1901', domainSeparator(domain), structHash].map(str => hexStringToBuffer(str)))),
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Permit,
|
||||
getDomain,
|
||||
domainType,
|
||||
domainSeparator,
|
||||
hashTypedData,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
const { BN } = require('@openzeppelin/test-helpers');
|
||||
|
||||
function Enum(...options) {
|
||||
return Object.fromEntries(options.map((key, i) => [key, new BN(i)]));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Enum,
|
||||
ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'),
|
||||
VoteType: Enum('Against', 'For', 'Abstain'),
|
||||
Rounding: Enum('Down', 'Up', 'Zero'),
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const ImplementationLabel = 'eip1967.proxy.implementation';
|
||||
const AdminLabel = 'eip1967.proxy.admin';
|
||||
const BeaconLabel = 'eip1967.proxy.beacon';
|
||||
|
||||
function labelToSlot(label) {
|
||||
return '0x' + web3.utils.toBN(web3.utils.keccak256(label)).subn(1).toString(16);
|
||||
}
|
||||
|
||||
function getSlot(address, slot) {
|
||||
return web3.eth.getStorageAt(
|
||||
web3.utils.isAddress(address) ? address : address.address,
|
||||
web3.utils.isHex(slot) ? slot : labelToSlot(slot),
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ImplementationLabel,
|
||||
AdminLabel,
|
||||
BeaconLabel,
|
||||
ImplementationSlot: labelToSlot(ImplementationLabel),
|
||||
AdminSlot: labelToSlot(AdminLabel),
|
||||
BeaconSlot: labelToSlot(BeaconLabel),
|
||||
getSlot,
|
||||
};
|
||||
@@ -0,0 +1,201 @@
|
||||
const { forward } = require('../helpers/time');
|
||||
|
||||
function zip(...args) {
|
||||
return Array(Math.max(...args.map(array => array.length)))
|
||||
.fill()
|
||||
.map((_, i) => args.map(array => array[i]));
|
||||
}
|
||||
|
||||
function concatHex(...args) {
|
||||
return web3.utils.bytesToHex([].concat(...args.map(h => web3.utils.hexToBytes(h || '0x'))));
|
||||
}
|
||||
|
||||
function concatOpts(args, opts = null) {
|
||||
return opts ? args.concat(opts) : args;
|
||||
}
|
||||
|
||||
class GovernorHelper {
|
||||
constructor(governor, mode = 'blocknumber') {
|
||||
this.governor = governor;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
delegate(delegation = {}, opts = null) {
|
||||
return Promise.all([
|
||||
delegation.token.delegate(delegation.to, { from: delegation.to }),
|
||||
delegation.value && delegation.token.transfer(...concatOpts([delegation.to, delegation.value]), opts),
|
||||
delegation.tokenId &&
|
||||
delegation.token
|
||||
.ownerOf(delegation.tokenId)
|
||||
.then(owner =>
|
||||
delegation.token.transferFrom(...concatOpts([owner, delegation.to, delegation.tokenId], opts)),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
propose(opts = null) {
|
||||
const proposal = this.currentProposal;
|
||||
|
||||
return this.governor.methods[
|
||||
proposal.useCompatibilityInterface
|
||||
? 'propose(address[],uint256[],string[],bytes[],string)'
|
||||
: 'propose(address[],uint256[],bytes[],string)'
|
||||
](...concatOpts(proposal.fullProposal, opts));
|
||||
}
|
||||
|
||||
queue(opts = null) {
|
||||
const proposal = this.currentProposal;
|
||||
|
||||
return proposal.useCompatibilityInterface
|
||||
? this.governor.methods['queue(uint256)'](...concatOpts([proposal.id], opts))
|
||||
: this.governor.methods['queue(address[],uint256[],bytes[],bytes32)'](
|
||||
...concatOpts(proposal.shortProposal, opts),
|
||||
);
|
||||
}
|
||||
|
||||
execute(opts = null) {
|
||||
const proposal = this.currentProposal;
|
||||
|
||||
return proposal.useCompatibilityInterface
|
||||
? this.governor.methods['execute(uint256)'](...concatOpts([proposal.id], opts))
|
||||
: this.governor.methods['execute(address[],uint256[],bytes[],bytes32)'](
|
||||
...concatOpts(proposal.shortProposal, opts),
|
||||
);
|
||||
}
|
||||
|
||||
cancel(visibility = 'external', opts = null) {
|
||||
const proposal = this.currentProposal;
|
||||
|
||||
switch (visibility) {
|
||||
case 'external':
|
||||
if (proposal.useCompatibilityInterface) {
|
||||
return this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts));
|
||||
} else {
|
||||
return this.governor.methods['cancel(address[],uint256[],bytes[],bytes32)'](
|
||||
...concatOpts(proposal.shortProposal, opts),
|
||||
);
|
||||
}
|
||||
case 'internal':
|
||||
return this.governor.methods['$_cancel(address[],uint256[],bytes[],bytes32)'](
|
||||
...concatOpts(proposal.shortProposal, opts),
|
||||
);
|
||||
default:
|
||||
throw new Error(`unsuported visibility "${visibility}"`);
|
||||
}
|
||||
}
|
||||
|
||||
vote(vote = {}, opts = null) {
|
||||
const proposal = this.currentProposal;
|
||||
|
||||
return vote.signature
|
||||
? // if signature, and either params or reason →
|
||||
vote.params || vote.reason
|
||||
? vote
|
||||
.signature(this.governor, {
|
||||
proposalId: proposal.id,
|
||||
support: vote.support,
|
||||
reason: vote.reason || '',
|
||||
params: vote.params || '',
|
||||
})
|
||||
.then(({ v, r, s }) =>
|
||||
this.governor.castVoteWithReasonAndParamsBySig(
|
||||
...concatOpts([proposal.id, vote.support, vote.reason || '', vote.params || '', v, r, s], opts),
|
||||
),
|
||||
)
|
||||
: vote
|
||||
.signature(this.governor, {
|
||||
proposalId: proposal.id,
|
||||
support: vote.support,
|
||||
})
|
||||
.then(({ v, r, s }) =>
|
||||
this.governor.castVoteBySig(...concatOpts([proposal.id, vote.support, v, r, s], opts)),
|
||||
)
|
||||
: vote.params
|
||||
? // otherwise if params
|
||||
this.governor.castVoteWithReasonAndParams(
|
||||
...concatOpts([proposal.id, vote.support, vote.reason || '', vote.params], opts),
|
||||
)
|
||||
: vote.reason
|
||||
? // otherwise if reason
|
||||
this.governor.castVoteWithReason(...concatOpts([proposal.id, vote.support, vote.reason], opts))
|
||||
: this.governor.castVote(...concatOpts([proposal.id, vote.support], opts));
|
||||
}
|
||||
|
||||
async waitForSnapshot(offset = 0) {
|
||||
const proposal = this.currentProposal;
|
||||
const timepoint = await this.governor.proposalSnapshot(proposal.id);
|
||||
return forward[this.mode](timepoint.addn(offset));
|
||||
}
|
||||
|
||||
async waitForDeadline(offset = 0) {
|
||||
const proposal = this.currentProposal;
|
||||
const timepoint = await this.governor.proposalDeadline(proposal.id);
|
||||
return forward[this.mode](timepoint.addn(offset));
|
||||
}
|
||||
|
||||
async waitForEta(offset = 0) {
|
||||
const proposal = this.currentProposal;
|
||||
const timestamp = await this.governor.proposalEta(proposal.id);
|
||||
return forward.timestamp(timestamp.addn(offset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a proposal either as
|
||||
* 1) an array of objects [{ target, value, data, signature? }]
|
||||
* 2) an object of arrays { targets: [], values: [], data: [], signatures?: [] }
|
||||
*/
|
||||
setProposal(actions, description) {
|
||||
let targets, values, signatures, data, useCompatibilityInterface;
|
||||
|
||||
if (Array.isArray(actions)) {
|
||||
useCompatibilityInterface = actions.some(a => 'signature' in a);
|
||||
targets = actions.map(a => a.target);
|
||||
values = actions.map(a => a.value || '0');
|
||||
signatures = actions.map(a => a.signature || '');
|
||||
data = actions.map(a => a.data || '0x');
|
||||
} else {
|
||||
useCompatibilityInterface = Array.isArray(actions.signatures);
|
||||
({ targets, values, signatures = [], data } = actions);
|
||||
}
|
||||
|
||||
const fulldata = zip(
|
||||
signatures.map(s => s && web3.eth.abi.encodeFunctionSignature(s)),
|
||||
data,
|
||||
).map(hexs => concatHex(...hexs));
|
||||
|
||||
const descriptionHash = web3.utils.keccak256(description);
|
||||
|
||||
// condensed version for queueing end executing
|
||||
const shortProposal = [targets, values, fulldata, descriptionHash];
|
||||
|
||||
// full version for proposing
|
||||
const fullProposal = [targets, values, ...(useCompatibilityInterface ? [signatures] : []), data, description];
|
||||
|
||||
// proposal id
|
||||
const id = web3.utils.toBN(
|
||||
web3.utils.keccak256(
|
||||
web3.eth.abi.encodeParameters(['address[]', 'uint256[]', 'bytes[]', 'bytes32'], shortProposal),
|
||||
),
|
||||
);
|
||||
|
||||
this.currentProposal = {
|
||||
id,
|
||||
targets,
|
||||
values,
|
||||
signatures,
|
||||
data,
|
||||
fulldata,
|
||||
description,
|
||||
descriptionHash,
|
||||
shortProposal,
|
||||
fullProposal,
|
||||
useCompatibilityInterface,
|
||||
};
|
||||
|
||||
return this.currentProposal;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GovernorHelper,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
function mapValues(obj, fn) {
|
||||
return Object.fromEntries([...Object.entries(obj)].map(([k, v]) => [k, fn(v)]));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mapValues,
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
function toEthSignedMessageHash(messageHex) {
|
||||
const messageBuffer = Buffer.from(messageHex.substring(2), 'hex');
|
||||
const prefix = Buffer.from(`\u0019Ethereum Signed Message:\n${messageBuffer.length}`);
|
||||
return web3.utils.sha3(Buffer.concat([prefix, messageBuffer]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a signed data with intended validator according to the version 0 of EIP-191
|
||||
* @param validatorAddress The address of the validator
|
||||
* @param dataHex The data to be concatenated with the prefix and signed
|
||||
*/
|
||||
function toDataWithIntendedValidatorHash(validatorAddress, dataHex) {
|
||||
const validatorBuffer = Buffer.from(web3.utils.hexToBytes(validatorAddress));
|
||||
const dataBuffer = Buffer.from(web3.utils.hexToBytes(dataHex));
|
||||
const preambleBuffer = Buffer.from('\x19');
|
||||
const versionBuffer = Buffer.from('\x00');
|
||||
const ethMessage = Buffer.concat([preambleBuffer, versionBuffer, validatorBuffer, dataBuffer]);
|
||||
|
||||
return web3.utils.sha3(ethMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a signer between a contract and a signer for a voucher of method, args, and redeemer
|
||||
* Note that `method` is the web3 method, not the truffle-contract method
|
||||
* @param contract TruffleContract
|
||||
* @param signer address
|
||||
* @param redeemer address
|
||||
* @param methodName string
|
||||
* @param methodArgs any[]
|
||||
*/
|
||||
const getSignFor =
|
||||
(contract, signer) =>
|
||||
(redeemer, methodName, methodArgs = []) => {
|
||||
const parts = [contract.address, redeemer];
|
||||
|
||||
const REAL_SIGNATURE_SIZE = 2 * 65; // 65 bytes in hexadecimal string length
|
||||
const PADDED_SIGNATURE_SIZE = 2 * 96; // 96 bytes in hexadecimal string length
|
||||
const DUMMY_SIGNATURE = `0x${web3.utils.padLeft('', REAL_SIGNATURE_SIZE)}`;
|
||||
|
||||
// if we have a method, add it to the parts that we're signing
|
||||
if (methodName) {
|
||||
if (methodArgs.length > 0) {
|
||||
parts.push(
|
||||
contract.contract.methods[methodName](...methodArgs.concat([DUMMY_SIGNATURE]))
|
||||
.encodeABI()
|
||||
.slice(0, -1 * PADDED_SIGNATURE_SIZE),
|
||||
);
|
||||
} else {
|
||||
const abi = contract.abi.find(abi => abi.name === methodName);
|
||||
parts.push(abi.signature);
|
||||
}
|
||||
}
|
||||
|
||||
// return the signature of the "Ethereum Signed Message" hash of the hash of `parts`
|
||||
const messageHex = web3.utils.soliditySha3(...parts);
|
||||
return web3.eth.sign(messageHex, signer);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
toEthSignedMessageHash,
|
||||
toDataWithIntendedValidatorHash,
|
||||
getSignFor,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
const ozHelpers = require('@openzeppelin/test-helpers');
|
||||
const helpers = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
module.exports = {
|
||||
clock: {
|
||||
blocknumber: () => helpers.time.latestBlock(),
|
||||
timestamp: () => helpers.time.latest(),
|
||||
},
|
||||
clockFromReceipt: {
|
||||
blocknumber: receipt => Promise.resolve(receipt.blockNumber),
|
||||
timestamp: receipt => web3.eth.getBlock(receipt.blockNumber).then(block => block.timestamp),
|
||||
},
|
||||
forward: {
|
||||
blocknumber: ozHelpers.time.advanceBlockTo,
|
||||
timestamp: helpers.time.increaseTo,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
const { network } = require('hardhat');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const queue = promisify(setImmediate);
|
||||
|
||||
async function countPendingTransactions() {
|
||||
return parseInt(await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']));
|
||||
}
|
||||
|
||||
async function batchInBlock(txs) {
|
||||
try {
|
||||
// disable auto-mining
|
||||
await network.provider.send('evm_setAutomine', [false]);
|
||||
// send all transactions
|
||||
const promises = txs.map(fn => fn());
|
||||
// wait for node to have all pending transactions
|
||||
while (txs.length > (await countPendingTransactions())) {
|
||||
await queue();
|
||||
}
|
||||
// mine one block
|
||||
await network.provider.send('evm_mine');
|
||||
// fetch receipts
|
||||
const receipts = await Promise.all(promises);
|
||||
// Sanity check, all tx should be in the same block
|
||||
const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber));
|
||||
expect(minedBlocks.size).to.equal(1);
|
||||
|
||||
return receipts;
|
||||
} finally {
|
||||
// enable auto-mining
|
||||
await network.provider.send('evm_setAutomine', [true]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
countPendingTransactions,
|
||||
batchInBlock,
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
const { getDomain, domainType } = require('../helpers/eip712');
|
||||
|
||||
const { expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC2771ContextMock = artifacts.require('ERC2771ContextMock');
|
||||
const MinimalForwarder = artifacts.require('MinimalForwarder');
|
||||
const ContextMockCaller = artifacts.require('ContextMockCaller');
|
||||
|
||||
const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior');
|
||||
|
||||
contract('ERC2771Context', function (accounts) {
|
||||
beforeEach(async function () {
|
||||
this.forwarder = await MinimalForwarder.new();
|
||||
this.recipient = await ERC2771ContextMock.new(this.forwarder.address);
|
||||
|
||||
this.domain = await getDomain(this.forwarder);
|
||||
this.types = {
|
||||
EIP712Domain: domainType(this.domain),
|
||||
ForwardRequest: [
|
||||
{ name: 'from', type: 'address' },
|
||||
{ name: 'to', type: 'address' },
|
||||
{ name: 'value', type: 'uint256' },
|
||||
{ name: 'gas', type: 'uint256' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'data', type: 'bytes' },
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('recognize trusted forwarder', async function () {
|
||||
expect(await this.recipient.isTrustedForwarder(this.forwarder.address));
|
||||
});
|
||||
|
||||
context('when called directly', function () {
|
||||
beforeEach(async function () {
|
||||
this.context = this.recipient; // The Context behavior expects the contract in this.context
|
||||
this.caller = await ContextMockCaller.new();
|
||||
});
|
||||
|
||||
shouldBehaveLikeRegularContext(...accounts);
|
||||
});
|
||||
|
||||
context('when receiving a relayed call', function () {
|
||||
beforeEach(async function () {
|
||||
this.wallet = Wallet.generate();
|
||||
this.sender = web3.utils.toChecksumAddress(this.wallet.getAddressString());
|
||||
this.data = {
|
||||
types: this.types,
|
||||
domain: this.domain,
|
||||
primaryType: 'ForwardRequest',
|
||||
};
|
||||
});
|
||||
|
||||
describe('msgSender', function () {
|
||||
it('returns the relayed transaction original sender', async function () {
|
||||
const data = this.recipient.contract.methods.msgSender().encodeABI();
|
||||
|
||||
const req = {
|
||||
from: this.sender,
|
||||
to: this.recipient.address,
|
||||
value: '0',
|
||||
gas: '100000',
|
||||
nonce: (await this.forwarder.getNonce(this.sender)).toString(),
|
||||
data,
|
||||
};
|
||||
|
||||
const sign = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { data: { ...this.data, message: req } });
|
||||
expect(await this.forwarder.verify(req, sign)).to.equal(true);
|
||||
|
||||
const { tx } = await this.forwarder.execute(req, sign);
|
||||
await expectEvent.inTransaction(tx, ERC2771ContextMock, 'Sender', { sender: this.sender });
|
||||
});
|
||||
});
|
||||
|
||||
describe('msgData', function () {
|
||||
it('returns the relayed transaction original data', async function () {
|
||||
const integerValue = '42';
|
||||
const stringValue = 'OpenZeppelin';
|
||||
const data = this.recipient.contract.methods.msgData(integerValue, stringValue).encodeABI();
|
||||
|
||||
const req = {
|
||||
from: this.sender,
|
||||
to: this.recipient.address,
|
||||
value: '0',
|
||||
gas: '100000',
|
||||
nonce: (await this.forwarder.getNonce(this.sender)).toString(),
|
||||
data,
|
||||
};
|
||||
|
||||
const sign = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { data: { ...this.data, message: req } });
|
||||
expect(await this.forwarder.verify(req, sign)).to.equal(true);
|
||||
|
||||
const { tx } = await this.forwarder.execute(req, sign);
|
||||
await expectEvent.inTransaction(tx, ERC2771ContextMock, 'Data', { data, integerValue, stringValue });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
const { getDomain, domainType } = require('../helpers/eip712');
|
||||
|
||||
const { expectRevert, constants } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const MinimalForwarder = artifacts.require('MinimalForwarder');
|
||||
const CallReceiverMock = artifacts.require('CallReceiverMock');
|
||||
|
||||
contract('MinimalForwarder', function (accounts) {
|
||||
beforeEach(async function () {
|
||||
this.forwarder = await MinimalForwarder.new();
|
||||
|
||||
this.domain = await getDomain(this.forwarder);
|
||||
this.types = {
|
||||
EIP712Domain: domainType(this.domain),
|
||||
ForwardRequest: [
|
||||
{ name: 'from', type: 'address' },
|
||||
{ name: 'to', type: 'address' },
|
||||
{ name: 'value', type: 'uint256' },
|
||||
{ name: 'gas', type: 'uint256' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'data', type: 'bytes' },
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
context('with message', function () {
|
||||
beforeEach(async function () {
|
||||
this.wallet = Wallet.generate();
|
||||
this.sender = web3.utils.toChecksumAddress(this.wallet.getAddressString());
|
||||
this.req = {
|
||||
from: this.sender,
|
||||
to: constants.ZERO_ADDRESS,
|
||||
value: '0',
|
||||
gas: '100000',
|
||||
nonce: Number(await this.forwarder.getNonce(this.sender)),
|
||||
data: '0x',
|
||||
};
|
||||
this.sign = () =>
|
||||
ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), {
|
||||
data: {
|
||||
types: this.types,
|
||||
domain: this.domain,
|
||||
primaryType: 'ForwardRequest',
|
||||
message: this.req,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('verify', function () {
|
||||
context('valid signature', function () {
|
||||
beforeEach(async function () {
|
||||
expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(web3.utils.toBN(this.req.nonce));
|
||||
});
|
||||
|
||||
it('success', async function () {
|
||||
expect(await this.forwarder.verify(this.req, this.sign())).to.be.equal(true);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(web3.utils.toBN(this.req.nonce));
|
||||
});
|
||||
});
|
||||
|
||||
context('invalid signature', function () {
|
||||
it('tampered from', async function () {
|
||||
expect(await this.forwarder.verify({ ...this.req, from: accounts[0] }, this.sign())).to.be.equal(false);
|
||||
});
|
||||
it('tampered to', async function () {
|
||||
expect(await this.forwarder.verify({ ...this.req, to: accounts[0] }, this.sign())).to.be.equal(false);
|
||||
});
|
||||
it('tampered value', async function () {
|
||||
expect(await this.forwarder.verify({ ...this.req, value: web3.utils.toWei('1') }, this.sign())).to.be.equal(
|
||||
false,
|
||||
);
|
||||
});
|
||||
it('tampered nonce', async function () {
|
||||
expect(await this.forwarder.verify({ ...this.req, nonce: this.req.nonce + 1 }, this.sign())).to.be.equal(
|
||||
false,
|
||||
);
|
||||
});
|
||||
it('tampered data', async function () {
|
||||
expect(await this.forwarder.verify({ ...this.req, data: '0x1742' }, this.sign())).to.be.equal(false);
|
||||
});
|
||||
it('tampered signature', async function () {
|
||||
const tamperedsign = web3.utils.hexToBytes(this.sign());
|
||||
tamperedsign[42] ^= 0xff;
|
||||
expect(await this.forwarder.verify(this.req, web3.utils.bytesToHex(tamperedsign))).to.be.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('execute', function () {
|
||||
context('valid signature', function () {
|
||||
beforeEach(async function () {
|
||||
expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(web3.utils.toBN(this.req.nonce));
|
||||
});
|
||||
|
||||
it('success', async function () {
|
||||
await this.forwarder.execute(this.req, this.sign()); // expect to not revert
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(
|
||||
web3.utils.toBN(this.req.nonce + 1),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('invalid signature', function () {
|
||||
it('tampered from', async function () {
|
||||
await expectRevert(
|
||||
this.forwarder.execute({ ...this.req, from: accounts[0] }, this.sign()),
|
||||
'MinimalForwarder: signature does not match request',
|
||||
);
|
||||
});
|
||||
it('tampered to', async function () {
|
||||
await expectRevert(
|
||||
this.forwarder.execute({ ...this.req, to: accounts[0] }, this.sign()),
|
||||
'MinimalForwarder: signature does not match request',
|
||||
);
|
||||
});
|
||||
it('tampered value', async function () {
|
||||
await expectRevert(
|
||||
this.forwarder.execute({ ...this.req, value: web3.utils.toWei('1') }, this.sign()),
|
||||
'MinimalForwarder: signature does not match request',
|
||||
);
|
||||
});
|
||||
it('tampered nonce', async function () {
|
||||
await expectRevert(
|
||||
this.forwarder.execute({ ...this.req, nonce: this.req.nonce + 1 }, this.sign()),
|
||||
'MinimalForwarder: signature does not match request',
|
||||
);
|
||||
});
|
||||
it('tampered data', async function () {
|
||||
await expectRevert(
|
||||
this.forwarder.execute({ ...this.req, data: '0x1742' }, this.sign()),
|
||||
'MinimalForwarder: signature does not match request',
|
||||
);
|
||||
});
|
||||
it('tampered signature', async function () {
|
||||
const tamperedsign = web3.utils.hexToBytes(this.sign());
|
||||
tamperedsign[42] ^= 0xff;
|
||||
await expectRevert(
|
||||
this.forwarder.execute(this.req, web3.utils.bytesToHex(tamperedsign)),
|
||||
'MinimalForwarder: signature does not match request',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('bubble out of gas', async function () {
|
||||
const receiver = await CallReceiverMock.new();
|
||||
const gasAvailable = 100000;
|
||||
this.req.to = receiver.address;
|
||||
this.req.data = receiver.contract.methods.mockFunctionOutOfGas().encodeABI();
|
||||
this.req.gas = 1000000;
|
||||
|
||||
await expectRevert.assertion(this.forwarder.execute(this.req, this.sign(), { gas: gasAvailable }));
|
||||
|
||||
const { transactions } = await web3.eth.getBlock('latest');
|
||||
const { gasUsed } = await web3.eth.getTransactionReceipt(transactions[0]);
|
||||
|
||||
expect(gasUsed).to.be.equal(gasAvailable);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
const path = require('path');
|
||||
const {
|
||||
promises: fs,
|
||||
constants: { F_OK },
|
||||
} = require('fs');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { pathUpdates, updateImportPaths, getUpgradeablePath } = require('../scripts/migrate-imports.js');
|
||||
|
||||
describe('migrate-imports.js', function () {
|
||||
it('every new path exists', async function () {
|
||||
for (const p of Object.values(pathUpdates)) {
|
||||
try {
|
||||
await fs.access(path.join('contracts', p), F_OK);
|
||||
} catch (e) {
|
||||
if (p.startsWith('proxy/')) continue; // excluded from transpilation of upgradeable contracts
|
||||
await fs.access(path.join('contracts', getUpgradeablePath(p)), F_OK);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('replaces import paths in a file', async function () {
|
||||
const source = `
|
||||
import '@openzeppelin/contracts/math/Math.sol';
|
||||
import '@openzeppelin/contracts-upgradeable/math/MathUpgradeable.sol';
|
||||
`;
|
||||
const expected = `
|
||||
import '@openzeppelin/contracts/utils/math/Math.sol';
|
||||
import '@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol';
|
||||
`;
|
||||
expect(updateImportPaths(source)).to.equal(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
const { expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const DummyImplementation = artifacts.require('DummyImplementation');
|
||||
|
||||
module.exports = function shouldBehaveLikeClone(createClone) {
|
||||
before('deploy implementation', async function () {
|
||||
this.implementation = web3.utils.toChecksumAddress((await DummyImplementation.new()).address);
|
||||
});
|
||||
|
||||
const assertProxyInitialization = function ({ value, balance }) {
|
||||
it('initializes the proxy', async function () {
|
||||
const dummy = new DummyImplementation(this.proxy);
|
||||
expect(await dummy.value()).to.be.bignumber.equal(value.toString());
|
||||
});
|
||||
|
||||
it('has expected balance', async function () {
|
||||
expect(await web3.eth.getBalance(this.proxy)).to.be.bignumber.equal(balance.toString());
|
||||
});
|
||||
};
|
||||
|
||||
describe('initialization without parameters', function () {
|
||||
describe('non payable', function () {
|
||||
const expectedInitializedValue = 10;
|
||||
const initializeData = new DummyImplementation('').contract.methods['initializeNonPayable()']().encodeABI();
|
||||
|
||||
describe('when not sending balance', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (await createClone(this.implementation, initializeData)).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert.unspecified(createClone(this.implementation, initializeData, { value }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('payable', function () {
|
||||
const expectedInitializedValue = 100;
|
||||
const initializeData = new DummyImplementation('').contract.methods['initializePayable()']().encodeABI();
|
||||
|
||||
describe('when not sending balance', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (await createClone(this.implementation, initializeData)).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (await createClone(this.implementation, initializeData, { value })).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialization with parameters', function () {
|
||||
describe('non payable', function () {
|
||||
const expectedInitializedValue = 10;
|
||||
const initializeData = new DummyImplementation('').contract.methods
|
||||
.initializeNonPayableWithValue(expectedInitializedValue)
|
||||
.encodeABI();
|
||||
|
||||
describe('when not sending balance', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (await createClone(this.implementation, initializeData)).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert.unspecified(createClone(this.implementation, initializeData, { value }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('payable', function () {
|
||||
const expectedInitializedValue = 42;
|
||||
const initializeData = new DummyImplementation('').contract.methods
|
||||
.initializePayableWithValue(expectedInitializedValue)
|
||||
.encodeABI();
|
||||
|
||||
describe('when not sending balance', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (await createClone(this.implementation, initializeData)).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (await createClone(this.implementation, initializeData, { value })).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { computeCreate2Address } = require('../helpers/create2');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const shouldBehaveLikeClone = require('./Clones.behaviour');
|
||||
|
||||
const Clones = artifacts.require('$Clones');
|
||||
|
||||
contract('Clones', function (accounts) {
|
||||
const [deployer] = accounts;
|
||||
|
||||
describe('clone', function () {
|
||||
shouldBehaveLikeClone(async (implementation, initData, opts = {}) => {
|
||||
const factory = await Clones.new();
|
||||
const receipt = await factory.$clone(implementation);
|
||||
const address = receipt.logs.find(({ event }) => event === 'return$clone').args.instance;
|
||||
await web3.eth.sendTransaction({ from: deployer, to: address, value: opts.value, data: initData });
|
||||
return { address };
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloneDeterministic', function () {
|
||||
shouldBehaveLikeClone(async (implementation, initData, opts = {}) => {
|
||||
const salt = web3.utils.randomHex(32);
|
||||
const factory = await Clones.new();
|
||||
const receipt = await factory.$cloneDeterministic(implementation, salt);
|
||||
const address = receipt.logs.find(({ event }) => event === 'return$cloneDeterministic').args.instance;
|
||||
await web3.eth.sendTransaction({ from: deployer, to: address, value: opts.value, data: initData });
|
||||
return { address };
|
||||
});
|
||||
|
||||
it('address already used', async function () {
|
||||
const implementation = web3.utils.randomHex(20);
|
||||
const salt = web3.utils.randomHex(32);
|
||||
const factory = await Clones.new();
|
||||
// deploy once
|
||||
expectEvent(await factory.$cloneDeterministic(implementation, salt), 'return$cloneDeterministic');
|
||||
// deploy twice
|
||||
await expectRevert(factory.$cloneDeterministic(implementation, salt), 'ERC1167: create2 failed');
|
||||
});
|
||||
|
||||
it('address prediction', async function () {
|
||||
const implementation = web3.utils.randomHex(20);
|
||||
const salt = web3.utils.randomHex(32);
|
||||
const factory = await Clones.new();
|
||||
const predicted = await factory.$predictDeterministicAddress(implementation, salt);
|
||||
|
||||
const creationCode = [
|
||||
'0x3d602d80600a3d3981f3363d3d373d3d3d363d73',
|
||||
implementation.replace(/0x/, '').toLowerCase(),
|
||||
'5af43d82803e903d91602b57fd5bf3',
|
||||
].join('');
|
||||
|
||||
expect(computeCreate2Address(salt, creationCode, factory.address)).to.be.equal(predicted);
|
||||
|
||||
expectEvent(await factory.$cloneDeterministic(implementation, salt), 'return$cloneDeterministic', {
|
||||
instance: predicted,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
const shouldBehaveLikeProxy = require('../Proxy.behaviour');
|
||||
|
||||
const ERC1967Proxy = artifacts.require('ERC1967Proxy');
|
||||
|
||||
contract('ERC1967Proxy', function (accounts) {
|
||||
const [proxyAdminOwner] = accounts;
|
||||
|
||||
const createProxy = async function (implementation, _admin, initData, opts) {
|
||||
return ERC1967Proxy.new(implementation, initData, opts);
|
||||
};
|
||||
|
||||
shouldBehaveLikeProxy(createProxy, undefined, proxyAdminOwner);
|
||||
});
|
||||
@@ -0,0 +1,225 @@
|
||||
const { expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { getSlot, ImplementationSlot } = require('../helpers/erc1967');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const DummyImplementation = artifacts.require('DummyImplementation');
|
||||
|
||||
module.exports = function shouldBehaveLikeProxy(createProxy, proxyAdminAddress, proxyCreator) {
|
||||
it('cannot be initialized with a non-contract address', async function () {
|
||||
const nonContractAddress = proxyCreator;
|
||||
const initializeData = Buffer.from('');
|
||||
await expectRevert.unspecified(
|
||||
createProxy(nonContractAddress, proxyAdminAddress, initializeData, {
|
||||
from: proxyCreator,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
before('deploy implementation', async function () {
|
||||
this.implementation = web3.utils.toChecksumAddress((await DummyImplementation.new()).address);
|
||||
});
|
||||
|
||||
const assertProxyInitialization = function ({ value, balance }) {
|
||||
it('sets the implementation address', async function () {
|
||||
const implementationSlot = await getSlot(this.proxy, ImplementationSlot);
|
||||
const implementationAddress = web3.utils.toChecksumAddress(implementationSlot.substr(-40));
|
||||
expect(implementationAddress).to.be.equal(this.implementation);
|
||||
});
|
||||
|
||||
it('initializes the proxy', async function () {
|
||||
const dummy = new DummyImplementation(this.proxy);
|
||||
expect(await dummy.value()).to.be.bignumber.equal(value.toString());
|
||||
});
|
||||
|
||||
it('has expected balance', async function () {
|
||||
expect(await web3.eth.getBalance(this.proxy)).to.be.bignumber.equal(balance.toString());
|
||||
});
|
||||
};
|
||||
|
||||
describe('without initialization', function () {
|
||||
const initializeData = Buffer.from('');
|
||||
|
||||
describe('when not sending balance', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (
|
||||
await createProxy(this.implementation, proxyAdminAddress, initializeData, {
|
||||
from: proxyCreator,
|
||||
})
|
||||
).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({ value: 0, balance: 0 });
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (
|
||||
await createProxy(this.implementation, proxyAdminAddress, initializeData, {
|
||||
from: proxyCreator,
|
||||
value,
|
||||
})
|
||||
).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({ value: 0, balance: value });
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialization without parameters', function () {
|
||||
describe('non payable', function () {
|
||||
const expectedInitializedValue = 10;
|
||||
const initializeData = new DummyImplementation('').contract.methods['initializeNonPayable()']().encodeABI();
|
||||
|
||||
describe('when not sending balance', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (
|
||||
await createProxy(this.implementation, proxyAdminAddress, initializeData, {
|
||||
from: proxyCreator,
|
||||
})
|
||||
).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert.unspecified(
|
||||
createProxy(this.implementation, proxyAdminAddress, initializeData, { from: proxyCreator, value }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('payable', function () {
|
||||
const expectedInitializedValue = 100;
|
||||
const initializeData = new DummyImplementation('').contract.methods['initializePayable()']().encodeABI();
|
||||
|
||||
describe('when not sending balance', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (
|
||||
await createProxy(this.implementation, proxyAdminAddress, initializeData, {
|
||||
from: proxyCreator,
|
||||
})
|
||||
).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (
|
||||
await createProxy(this.implementation, proxyAdminAddress, initializeData, {
|
||||
from: proxyCreator,
|
||||
value,
|
||||
})
|
||||
).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialization with parameters', function () {
|
||||
describe('non payable', function () {
|
||||
const expectedInitializedValue = 10;
|
||||
const initializeData = new DummyImplementation('').contract.methods
|
||||
.initializeNonPayableWithValue(expectedInitializedValue)
|
||||
.encodeABI();
|
||||
|
||||
describe('when not sending balance', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (
|
||||
await createProxy(this.implementation, proxyAdminAddress, initializeData, {
|
||||
from: proxyCreator,
|
||||
})
|
||||
).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert.unspecified(
|
||||
createProxy(this.implementation, proxyAdminAddress, initializeData, { from: proxyCreator, value }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('payable', function () {
|
||||
const expectedInitializedValue = 42;
|
||||
const initializeData = new DummyImplementation('').contract.methods
|
||||
.initializePayableWithValue(expectedInitializedValue)
|
||||
.encodeABI();
|
||||
|
||||
describe('when not sending balance', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (
|
||||
await createProxy(this.implementation, proxyAdminAddress, initializeData, {
|
||||
from: proxyCreator,
|
||||
})
|
||||
).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = (
|
||||
await createProxy(this.implementation, proxyAdminAddress, initializeData, {
|
||||
from: proxyCreator,
|
||||
value,
|
||||
})
|
||||
).address;
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reverting initialization', function () {
|
||||
const initializeData = new DummyImplementation('').contract.methods.reverts().encodeABI();
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
createProxy(this.implementation, proxyAdminAddress, initializeData, { from: proxyCreator }),
|
||||
'DummyImplementation reverted',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
const { expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { getSlot, BeaconSlot } = require('../../helpers/erc1967');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const UpgradeableBeacon = artifacts.require('UpgradeableBeacon');
|
||||
const BeaconProxy = artifacts.require('BeaconProxy');
|
||||
const DummyImplementation = artifacts.require('DummyImplementation');
|
||||
const DummyImplementationV2 = artifacts.require('DummyImplementationV2');
|
||||
const BadBeaconNoImpl = artifacts.require('BadBeaconNoImpl');
|
||||
const BadBeaconNotContract = artifacts.require('BadBeaconNotContract');
|
||||
|
||||
contract('BeaconProxy', function (accounts) {
|
||||
const [anotherAccount] = accounts;
|
||||
|
||||
describe('bad beacon is not accepted', async function () {
|
||||
it('non-contract beacon', async function () {
|
||||
await expectRevert(BeaconProxy.new(anotherAccount, '0x'), 'ERC1967: new beacon is not a contract');
|
||||
});
|
||||
|
||||
it('non-compliant beacon', async function () {
|
||||
const beacon = await BadBeaconNoImpl.new();
|
||||
await expectRevert.unspecified(BeaconProxy.new(beacon.address, '0x'));
|
||||
});
|
||||
|
||||
it('non-contract implementation', async function () {
|
||||
const beacon = await BadBeaconNotContract.new();
|
||||
await expectRevert(BeaconProxy.new(beacon.address, '0x'), 'ERC1967: beacon implementation is not a contract');
|
||||
});
|
||||
});
|
||||
|
||||
before('deploy implementation', async function () {
|
||||
this.implementationV0 = await DummyImplementation.new();
|
||||
this.implementationV1 = await DummyImplementationV2.new();
|
||||
});
|
||||
|
||||
describe('initialization', function () {
|
||||
before(function () {
|
||||
this.assertInitialized = async ({ value, balance }) => {
|
||||
const beaconSlot = await getSlot(this.proxy, BeaconSlot);
|
||||
const beaconAddress = web3.utils.toChecksumAddress(beaconSlot.substr(-40));
|
||||
expect(beaconAddress).to.equal(this.beacon.address);
|
||||
|
||||
const dummy = new DummyImplementation(this.proxy.address);
|
||||
expect(await dummy.value()).to.bignumber.eq(value);
|
||||
|
||||
expect(await web3.eth.getBalance(this.proxy.address)).to.bignumber.eq(balance);
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach('deploy beacon', async function () {
|
||||
this.beacon = await UpgradeableBeacon.new(this.implementationV0.address);
|
||||
});
|
||||
|
||||
it('no initialization', async function () {
|
||||
const data = Buffer.from('');
|
||||
const balance = '10';
|
||||
this.proxy = await BeaconProxy.new(this.beacon.address, data, { value: balance });
|
||||
await this.assertInitialized({ value: '0', balance });
|
||||
});
|
||||
|
||||
it('non-payable initialization', async function () {
|
||||
const value = '55';
|
||||
const data = this.implementationV0.contract.methods.initializeNonPayableWithValue(value).encodeABI();
|
||||
this.proxy = await BeaconProxy.new(this.beacon.address, data);
|
||||
await this.assertInitialized({ value, balance: '0' });
|
||||
});
|
||||
|
||||
it('payable initialization', async function () {
|
||||
const value = '55';
|
||||
const data = this.implementationV0.contract.methods.initializePayableWithValue(value).encodeABI();
|
||||
const balance = '100';
|
||||
this.proxy = await BeaconProxy.new(this.beacon.address, data, { value: balance });
|
||||
await this.assertInitialized({ value, balance });
|
||||
});
|
||||
|
||||
it('reverting initialization', async function () {
|
||||
const data = this.implementationV0.contract.methods.reverts().encodeABI();
|
||||
await expectRevert(BeaconProxy.new(this.beacon.address, data), 'DummyImplementation reverted');
|
||||
});
|
||||
});
|
||||
|
||||
it('upgrade a proxy by upgrading its beacon', async function () {
|
||||
const beacon = await UpgradeableBeacon.new(this.implementationV0.address);
|
||||
|
||||
const value = '10';
|
||||
const data = this.implementationV0.contract.methods.initializeNonPayableWithValue(value).encodeABI();
|
||||
const proxy = await BeaconProxy.new(beacon.address, data);
|
||||
|
||||
const dummy = new DummyImplementation(proxy.address);
|
||||
|
||||
// test initial values
|
||||
expect(await dummy.value()).to.bignumber.eq(value);
|
||||
|
||||
// test initial version
|
||||
expect(await dummy.version()).to.eq('V1');
|
||||
|
||||
// upgrade beacon
|
||||
await beacon.upgradeTo(this.implementationV1.address);
|
||||
|
||||
// test upgraded version
|
||||
expect(await dummy.version()).to.eq('V2');
|
||||
});
|
||||
|
||||
it('upgrade 2 proxies by upgrading shared beacon', async function () {
|
||||
const value1 = '10';
|
||||
const value2 = '42';
|
||||
|
||||
const beacon = await UpgradeableBeacon.new(this.implementationV0.address);
|
||||
|
||||
const proxy1InitializeData = this.implementationV0.contract.methods
|
||||
.initializeNonPayableWithValue(value1)
|
||||
.encodeABI();
|
||||
const proxy1 = await BeaconProxy.new(beacon.address, proxy1InitializeData);
|
||||
|
||||
const proxy2InitializeData = this.implementationV0.contract.methods
|
||||
.initializeNonPayableWithValue(value2)
|
||||
.encodeABI();
|
||||
const proxy2 = await BeaconProxy.new(beacon.address, proxy2InitializeData);
|
||||
|
||||
const dummy1 = new DummyImplementation(proxy1.address);
|
||||
const dummy2 = new DummyImplementation(proxy2.address);
|
||||
|
||||
// test initial values
|
||||
expect(await dummy1.value()).to.bignumber.eq(value1);
|
||||
expect(await dummy2.value()).to.bignumber.eq(value2);
|
||||
|
||||
// test initial version
|
||||
expect(await dummy1.version()).to.eq('V1');
|
||||
expect(await dummy2.version()).to.eq('V1');
|
||||
|
||||
// upgrade beacon
|
||||
await beacon.upgradeTo(this.implementationV1.address);
|
||||
|
||||
// test upgraded version
|
||||
expect(await dummy1.version()).to.eq('V2');
|
||||
expect(await dummy2.version()).to.eq('V2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
const { expectRevert, expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const UpgradeableBeacon = artifacts.require('UpgradeableBeacon');
|
||||
const Implementation1 = artifacts.require('Implementation1');
|
||||
const Implementation2 = artifacts.require('Implementation2');
|
||||
|
||||
contract('UpgradeableBeacon', function (accounts) {
|
||||
const [owner, other] = accounts;
|
||||
|
||||
it('cannot be created with non-contract implementation', async function () {
|
||||
await expectRevert(UpgradeableBeacon.new(accounts[0]), 'UpgradeableBeacon: implementation is not a contract');
|
||||
});
|
||||
|
||||
context('once deployed', async function () {
|
||||
beforeEach('deploying beacon', async function () {
|
||||
this.v1 = await Implementation1.new();
|
||||
this.beacon = await UpgradeableBeacon.new(this.v1.address, { from: owner });
|
||||
});
|
||||
|
||||
it('returns implementation', async function () {
|
||||
expect(await this.beacon.implementation()).to.equal(this.v1.address);
|
||||
});
|
||||
|
||||
it('can be upgraded by the owner', async function () {
|
||||
const v2 = await Implementation2.new();
|
||||
const receipt = await this.beacon.upgradeTo(v2.address, { from: owner });
|
||||
expectEvent(receipt, 'Upgraded', { implementation: v2.address });
|
||||
expect(await this.beacon.implementation()).to.equal(v2.address);
|
||||
});
|
||||
|
||||
it('cannot be upgraded to a non-contract', async function () {
|
||||
await expectRevert(
|
||||
this.beacon.upgradeTo(other, { from: owner }),
|
||||
'UpgradeableBeacon: implementation is not a contract',
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot be upgraded by other account', async function () {
|
||||
const v2 = await Implementation2.new();
|
||||
await expectRevert(this.beacon.upgradeTo(v2.address, { from: other }), 'Ownable: caller is not the owner');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
const { expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ImplV1 = artifacts.require('DummyImplementation');
|
||||
const ImplV2 = artifacts.require('DummyImplementationV2');
|
||||
const ProxyAdmin = artifacts.require('ProxyAdmin');
|
||||
const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy');
|
||||
const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy');
|
||||
|
||||
contract('ProxyAdmin', function (accounts) {
|
||||
const [proxyAdminOwner, newAdmin, anotherAccount] = accounts;
|
||||
|
||||
before('set implementations', async function () {
|
||||
this.implementationV1 = await ImplV1.new();
|
||||
this.implementationV2 = await ImplV2.new();
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
const initializeData = Buffer.from('');
|
||||
this.proxyAdmin = await ProxyAdmin.new({ from: proxyAdminOwner });
|
||||
const proxy = await TransparentUpgradeableProxy.new(
|
||||
this.implementationV1.address,
|
||||
this.proxyAdmin.address,
|
||||
initializeData,
|
||||
{ from: proxyAdminOwner },
|
||||
);
|
||||
this.proxy = await ITransparentUpgradeableProxy.at(proxy.address);
|
||||
});
|
||||
|
||||
it('has an owner', async function () {
|
||||
expect(await this.proxyAdmin.owner()).to.equal(proxyAdminOwner);
|
||||
});
|
||||
|
||||
describe('#getProxyAdmin', function () {
|
||||
it('returns proxyAdmin as admin of the proxy', async function () {
|
||||
const admin = await this.proxyAdmin.getProxyAdmin(this.proxy.address);
|
||||
expect(admin).to.be.equal(this.proxyAdmin.address);
|
||||
});
|
||||
|
||||
it('call to invalid proxy', async function () {
|
||||
await expectRevert.unspecified(this.proxyAdmin.getProxyAdmin(this.implementationV1.address));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#changeProxyAdmin', function () {
|
||||
it('fails to change proxy admin if its not the proxy owner', async function () {
|
||||
await expectRevert(
|
||||
this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: anotherAccount }),
|
||||
'caller is not the owner',
|
||||
);
|
||||
});
|
||||
|
||||
it('changes proxy admin', async function () {
|
||||
await this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: proxyAdminOwner });
|
||||
expect(await this.proxy.admin.call({ from: newAdmin })).to.eq(newAdmin);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getProxyImplementation', function () {
|
||||
it('returns proxy implementation address', async function () {
|
||||
const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address);
|
||||
expect(implementationAddress).to.be.equal(this.implementationV1.address);
|
||||
});
|
||||
|
||||
it('call to invalid proxy', async function () {
|
||||
await expectRevert.unspecified(this.proxyAdmin.getProxyImplementation(this.implementationV1.address));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upgrade', function () {
|
||||
context('with unauthorized account', function () {
|
||||
it('fails to upgrade', async function () {
|
||||
await expectRevert(
|
||||
this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: anotherAccount }),
|
||||
'caller is not the owner',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with authorized account', function () {
|
||||
it('upgrades implementation', async function () {
|
||||
await this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: proxyAdminOwner });
|
||||
const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address);
|
||||
expect(implementationAddress).to.be.equal(this.implementationV2.address);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upgradeAndCall', function () {
|
||||
context('with unauthorized account', function () {
|
||||
it('fails to upgrade', async function () {
|
||||
const callData = new ImplV1('').contract.methods.initializeNonPayableWithValue(1337).encodeABI();
|
||||
await expectRevert(
|
||||
this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, {
|
||||
from: anotherAccount,
|
||||
}),
|
||||
'caller is not the owner',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with authorized account', function () {
|
||||
context('with invalid callData', function () {
|
||||
it('fails to upgrade', async function () {
|
||||
const callData = '0x12345678';
|
||||
await expectRevert.unspecified(
|
||||
this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, {
|
||||
from: proxyAdminOwner,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with valid callData', function () {
|
||||
it('upgrades implementation', async function () {
|
||||
const callData = new ImplV1('').contract.methods.initializeNonPayableWithValue(1337).encodeABI();
|
||||
await this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, {
|
||||
from: proxyAdminOwner,
|
||||
});
|
||||
const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address);
|
||||
expect(implementationAddress).to.be.equal(this.implementationV2.address);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+433
@@ -0,0 +1,433 @@
|
||||
const { BN, expectRevert, expectEvent, constants } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
const { getSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967');
|
||||
|
||||
const { expect } = require('chai');
|
||||
const { web3 } = require('hardhat');
|
||||
|
||||
const Implementation1 = artifacts.require('Implementation1');
|
||||
const Implementation2 = artifacts.require('Implementation2');
|
||||
const Implementation3 = artifacts.require('Implementation3');
|
||||
const Implementation4 = artifacts.require('Implementation4');
|
||||
const MigratableMockV1 = artifacts.require('MigratableMockV1');
|
||||
const MigratableMockV2 = artifacts.require('MigratableMockV2');
|
||||
const MigratableMockV3 = artifacts.require('MigratableMockV3');
|
||||
const InitializableMock = artifacts.require('InitializableMock');
|
||||
const DummyImplementation = artifacts.require('DummyImplementation');
|
||||
const ClashingImplementation = artifacts.require('ClashingImplementation');
|
||||
|
||||
module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProxy, accounts) {
|
||||
const [proxyAdminAddress, proxyAdminOwner, anotherAccount] = accounts;
|
||||
|
||||
before(async function () {
|
||||
this.implementationV0 = (await DummyImplementation.new()).address;
|
||||
this.implementationV1 = (await DummyImplementation.new()).address;
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
const initializeData = Buffer.from('');
|
||||
this.proxy = await createProxy(this.implementationV0, proxyAdminAddress, initializeData, {
|
||||
from: proxyAdminOwner,
|
||||
});
|
||||
this.proxyAddress = this.proxy.address;
|
||||
});
|
||||
|
||||
describe('implementation', function () {
|
||||
it('returns the current implementation address', async function () {
|
||||
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
|
||||
|
||||
expect(implementation).to.be.equal(this.implementationV0);
|
||||
});
|
||||
|
||||
it('delegates to the implementation', async function () {
|
||||
const dummy = new DummyImplementation(this.proxyAddress);
|
||||
const value = await dummy.get();
|
||||
|
||||
expect(value).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgradeTo', function () {
|
||||
describe('when the sender is the admin', function () {
|
||||
const from = proxyAdminAddress;
|
||||
|
||||
describe('when the given implementation is different from the current one', function () {
|
||||
it('upgrades to the requested implementation', async function () {
|
||||
await this.proxy.upgradeTo(this.implementationV1, { from });
|
||||
|
||||
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
|
||||
expect(implementation).to.be.equal(this.implementationV1);
|
||||
});
|
||||
|
||||
it('emits an event', async function () {
|
||||
expectEvent(await this.proxy.upgradeTo(this.implementationV1, { from }), 'Upgraded', {
|
||||
implementation: this.implementationV1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given implementation is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.proxy.upgradeTo(ZERO_ADDRESS, { from }),
|
||||
'ERC1967: new implementation is not a contract',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender is not the admin', function () {
|
||||
const from = anotherAccount;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert.unspecified(this.proxy.upgradeTo(this.implementationV1, { from }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgradeToAndCall', function () {
|
||||
describe('without migrations', function () {
|
||||
beforeEach(async function () {
|
||||
this.behavior = await InitializableMock.new();
|
||||
});
|
||||
|
||||
describe('when the call does not fail', function () {
|
||||
const initializeData = new InitializableMock('').contract.methods['initializeWithX(uint256)'](42).encodeABI();
|
||||
|
||||
describe('when the sender is the admin', function () {
|
||||
const from = proxyAdminAddress;
|
||||
const value = 1e5;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from, value });
|
||||
});
|
||||
|
||||
it('upgrades to the requested implementation', async function () {
|
||||
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
|
||||
expect(implementation).to.be.equal(this.behavior.address);
|
||||
});
|
||||
|
||||
it('emits an event', function () {
|
||||
expectEvent(this.receipt, 'Upgraded', { implementation: this.behavior.address });
|
||||
});
|
||||
|
||||
it('calls the initializer function', async function () {
|
||||
const migratable = new InitializableMock(this.proxyAddress);
|
||||
const x = await migratable.x();
|
||||
expect(x).to.be.bignumber.equal('42');
|
||||
});
|
||||
|
||||
it('sends given value to the proxy', async function () {
|
||||
const balance = await web3.eth.getBalance(this.proxyAddress);
|
||||
expect(balance.toString()).to.be.bignumber.equal(value.toString());
|
||||
});
|
||||
|
||||
it('uses the storage of the proxy', async function () {
|
||||
// storage layout should look as follows:
|
||||
// - 0: Initializable storage ++ initializerRan ++ onlyInitializingRan
|
||||
// - 1: x
|
||||
const storedValue = await web3.eth.getStorageAt(this.proxyAddress, 1);
|
||||
expect(parseInt(storedValue)).to.eq(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender is not the admin', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert.unspecified(
|
||||
this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: anotherAccount }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the call does fail', function () {
|
||||
const initializeData = new InitializableMock('').contract.methods.fail().encodeABI();
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert.unspecified(
|
||||
this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: proxyAdminAddress }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with migrations', function () {
|
||||
describe('when the sender is the admin', function () {
|
||||
const from = proxyAdminAddress;
|
||||
const value = 1e5;
|
||||
|
||||
describe('when upgrading to V1', function () {
|
||||
const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI();
|
||||
|
||||
beforeEach(async function () {
|
||||
this.behaviorV1 = await MigratableMockV1.new();
|
||||
this.balancePreviousV1 = new BN(await web3.eth.getBalance(this.proxyAddress));
|
||||
this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV1.address, v1MigrationData, { from, value });
|
||||
});
|
||||
|
||||
it('upgrades to the requested version and emits an event', async function () {
|
||||
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
|
||||
expect(implementation).to.be.equal(this.behaviorV1.address);
|
||||
expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV1.address });
|
||||
});
|
||||
|
||||
it("calls the 'initialize' function and sends given value to the proxy", async function () {
|
||||
const migratable = new MigratableMockV1(this.proxyAddress);
|
||||
|
||||
const x = await migratable.x();
|
||||
expect(x).to.be.bignumber.equal('42');
|
||||
|
||||
const balance = await web3.eth.getBalance(this.proxyAddress);
|
||||
expect(new BN(balance)).to.be.bignumber.equal(this.balancePreviousV1.addn(value));
|
||||
});
|
||||
|
||||
describe('when upgrading to V2', function () {
|
||||
const v2MigrationData = new MigratableMockV2('').contract.methods.migrate(10, 42).encodeABI();
|
||||
|
||||
beforeEach(async function () {
|
||||
this.behaviorV2 = await MigratableMockV2.new();
|
||||
this.balancePreviousV2 = new BN(await web3.eth.getBalance(this.proxyAddress));
|
||||
this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV2.address, v2MigrationData, {
|
||||
from,
|
||||
value,
|
||||
});
|
||||
});
|
||||
|
||||
it('upgrades to the requested version and emits an event', async function () {
|
||||
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
|
||||
expect(implementation).to.be.equal(this.behaviorV2.address);
|
||||
expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV2.address });
|
||||
});
|
||||
|
||||
it("calls the 'migrate' function and sends given value to the proxy", async function () {
|
||||
const migratable = new MigratableMockV2(this.proxyAddress);
|
||||
|
||||
const x = await migratable.x();
|
||||
expect(x).to.be.bignumber.equal('10');
|
||||
|
||||
const y = await migratable.y();
|
||||
expect(y).to.be.bignumber.equal('42');
|
||||
|
||||
const balance = new BN(await web3.eth.getBalance(this.proxyAddress));
|
||||
expect(balance).to.be.bignumber.equal(this.balancePreviousV2.addn(value));
|
||||
});
|
||||
|
||||
describe('when upgrading to V3', function () {
|
||||
const v3MigrationData = new MigratableMockV3('').contract.methods['migrate()']().encodeABI();
|
||||
|
||||
beforeEach(async function () {
|
||||
this.behaviorV3 = await MigratableMockV3.new();
|
||||
this.balancePreviousV3 = new BN(await web3.eth.getBalance(this.proxyAddress));
|
||||
this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV3.address, v3MigrationData, {
|
||||
from,
|
||||
value,
|
||||
});
|
||||
});
|
||||
|
||||
it('upgrades to the requested version and emits an event', async function () {
|
||||
const implementation = await this.proxy.implementation({ from: proxyAdminAddress });
|
||||
expect(implementation).to.be.equal(this.behaviorV3.address);
|
||||
expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV3.address });
|
||||
});
|
||||
|
||||
it("calls the 'migrate' function and sends given value to the proxy", async function () {
|
||||
const migratable = new MigratableMockV3(this.proxyAddress);
|
||||
|
||||
const x = await migratable.x();
|
||||
expect(x).to.be.bignumber.equal('42');
|
||||
|
||||
const y = await migratable.y();
|
||||
expect(y).to.be.bignumber.equal('10');
|
||||
|
||||
const balance = new BN(await web3.eth.getBalance(this.proxyAddress));
|
||||
expect(balance).to.be.bignumber.equal(this.balancePreviousV3.addn(value));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender is not the admin', function () {
|
||||
const from = anotherAccount;
|
||||
|
||||
it('reverts', async function () {
|
||||
const behaviorV1 = await MigratableMockV1.new();
|
||||
const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI();
|
||||
await expectRevert.unspecified(this.proxy.upgradeToAndCall(behaviorV1.address, v1MigrationData, { from }));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeAdmin', function () {
|
||||
describe('when the new proposed admin is not the zero address', function () {
|
||||
const newAdmin = anotherAccount;
|
||||
|
||||
describe('when the sender is the admin', function () {
|
||||
beforeEach('transferring', async function () {
|
||||
this.receipt = await this.proxy.changeAdmin(newAdmin, { from: proxyAdminAddress });
|
||||
});
|
||||
|
||||
it('assigns new proxy admin', async function () {
|
||||
const newProxyAdmin = await this.proxy.admin({ from: newAdmin });
|
||||
expect(newProxyAdmin).to.be.equal(anotherAccount);
|
||||
});
|
||||
|
||||
it('emits an event', function () {
|
||||
expectEvent(this.receipt, 'AdminChanged', {
|
||||
previousAdmin: proxyAdminAddress,
|
||||
newAdmin: newAdmin,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender is not the admin', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert.unspecified(this.proxy.changeAdmin(newAdmin, { from: anotherAccount }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the new proposed admin is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.proxy.changeAdmin(ZERO_ADDRESS, { from: proxyAdminAddress }),
|
||||
'ERC1967: new admin is the zero address',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('storage', function () {
|
||||
it('should store the implementation address in specified location', async function () {
|
||||
const implementationSlot = await getSlot(this.proxy, ImplementationSlot);
|
||||
const implementationAddress = web3.utils.toChecksumAddress(implementationSlot.substr(-40));
|
||||
expect(implementationAddress).to.be.equal(this.implementationV0);
|
||||
});
|
||||
|
||||
it('should store the admin proxy in specified location', async function () {
|
||||
const proxyAdminSlot = await getSlot(this.proxy, AdminSlot);
|
||||
const proxyAdminAddress = web3.utils.toChecksumAddress(proxyAdminSlot.substr(-40));
|
||||
expect(proxyAdminAddress).to.be.equal(proxyAdminAddress);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transparent proxy', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
const initializeData = Buffer.from('');
|
||||
this.impl = await ClashingImplementation.new();
|
||||
this.proxy = await createProxy(this.impl.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner });
|
||||
|
||||
this.clashing = new ClashingImplementation(this.proxy.address);
|
||||
});
|
||||
|
||||
it('proxy admin cannot call delegated functions', async function () {
|
||||
await expectRevert(
|
||||
this.clashing.delegatedFunction({ from: proxyAdminAddress }),
|
||||
'TransparentUpgradeableProxy: admin cannot fallback to proxy target',
|
||||
);
|
||||
});
|
||||
|
||||
describe('when function names clash', function () {
|
||||
it('when sender is proxy admin should run the proxy function', async function () {
|
||||
const value = await this.proxy.admin({ from: proxyAdminAddress, value: 0 });
|
||||
expect(value).to.be.equal(proxyAdminAddress);
|
||||
});
|
||||
|
||||
it('when sender is other should delegate to implementation', async function () {
|
||||
const value = await this.proxy.admin({ from: anotherAccount, value: 0 });
|
||||
expect(value).to.be.equal('0x0000000000000000000000000000000011111142');
|
||||
});
|
||||
|
||||
it('when sender is proxy admin value should not be accepted', async function () {
|
||||
await expectRevert.unspecified(this.proxy.admin({ from: proxyAdminAddress, value: 1 }));
|
||||
});
|
||||
|
||||
it('when sender is other value should be accepted', async function () {
|
||||
const value = await this.proxy.admin({ from: anotherAccount, value: 1 });
|
||||
expect(value).to.be.equal('0x0000000000000000000000000000000011111142');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('regression', () => {
|
||||
const initializeData = Buffer.from('');
|
||||
|
||||
it('should add new function', async () => {
|
||||
const instance1 = await Implementation1.new();
|
||||
const proxy = await createProxy(instance1.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner });
|
||||
|
||||
const proxyInstance1 = new Implementation1(proxy.address);
|
||||
await proxyInstance1.setValue(42);
|
||||
|
||||
const instance2 = await Implementation2.new();
|
||||
await proxy.upgradeTo(instance2.address, { from: proxyAdminAddress });
|
||||
|
||||
const proxyInstance2 = new Implementation2(proxy.address);
|
||||
const res = await proxyInstance2.getValue();
|
||||
expect(res.toString()).to.eq('42');
|
||||
});
|
||||
|
||||
it('should remove function', async () => {
|
||||
const instance2 = await Implementation2.new();
|
||||
const proxy = await createProxy(instance2.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner });
|
||||
|
||||
const proxyInstance2 = new Implementation2(proxy.address);
|
||||
await proxyInstance2.setValue(42);
|
||||
const res = await proxyInstance2.getValue();
|
||||
expect(res.toString()).to.eq('42');
|
||||
|
||||
const instance1 = await Implementation1.new();
|
||||
await proxy.upgradeTo(instance1.address, { from: proxyAdminAddress });
|
||||
|
||||
const proxyInstance1 = new Implementation2(proxy.address);
|
||||
await expectRevert.unspecified(proxyInstance1.getValue());
|
||||
});
|
||||
|
||||
it('should change function signature', async () => {
|
||||
const instance1 = await Implementation1.new();
|
||||
const proxy = await createProxy(instance1.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner });
|
||||
|
||||
const proxyInstance1 = new Implementation1(proxy.address);
|
||||
await proxyInstance1.setValue(42);
|
||||
|
||||
const instance3 = await Implementation3.new();
|
||||
await proxy.upgradeTo(instance3.address, { from: proxyAdminAddress });
|
||||
const proxyInstance3 = new Implementation3(proxy.address);
|
||||
|
||||
const res = await proxyInstance3.getValue(8);
|
||||
expect(res.toString()).to.eq('50');
|
||||
});
|
||||
|
||||
it('should add fallback function', async () => {
|
||||
const initializeData = Buffer.from('');
|
||||
const instance1 = await Implementation1.new();
|
||||
const proxy = await createProxy(instance1.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner });
|
||||
|
||||
const instance4 = await Implementation4.new();
|
||||
await proxy.upgradeTo(instance4.address, { from: proxyAdminAddress });
|
||||
const proxyInstance4 = new Implementation4(proxy.address);
|
||||
|
||||
const data = '0x';
|
||||
await web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data });
|
||||
|
||||
const res = await proxyInstance4.getValue();
|
||||
expect(res.toString()).to.eq('1');
|
||||
});
|
||||
|
||||
it('should remove fallback function', async () => {
|
||||
const instance4 = await Implementation4.new();
|
||||
const proxy = await createProxy(instance4.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner });
|
||||
|
||||
const instance2 = await Implementation2.new();
|
||||
await proxy.upgradeTo(instance2.address, { from: proxyAdminAddress });
|
||||
|
||||
const data = '0x';
|
||||
await expectRevert.unspecified(web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data }));
|
||||
|
||||
const proxyInstance2 = new Implementation2(proxy.address);
|
||||
const res = await proxyInstance2.getValue();
|
||||
expect(res.toString()).to.eq('0');
|
||||
});
|
||||
});
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
const shouldBehaveLikeProxy = require('../Proxy.behaviour');
|
||||
const shouldBehaveLikeTransparentUpgradeableProxy = require('./TransparentUpgradeableProxy.behaviour');
|
||||
|
||||
const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy');
|
||||
const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy');
|
||||
|
||||
contract('TransparentUpgradeableProxy', function (accounts) {
|
||||
const [proxyAdminAddress, proxyAdminOwner] = accounts;
|
||||
|
||||
const createProxy = async function (logic, admin, initData, opts) {
|
||||
const { address } = await TransparentUpgradeableProxy.new(logic, admin, initData, opts);
|
||||
return ITransparentUpgradeableProxy.at(address);
|
||||
};
|
||||
|
||||
shouldBehaveLikeProxy(createProxy, proxyAdminAddress, proxyAdminOwner);
|
||||
shouldBehaveLikeTransparentUpgradeableProxy(createProxy, accounts);
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const InitializableMock = artifacts.require('InitializableMock');
|
||||
const ConstructorInitializableMock = artifacts.require('ConstructorInitializableMock');
|
||||
const ChildConstructorInitializableMock = artifacts.require('ChildConstructorInitializableMock');
|
||||
const ReinitializerMock = artifacts.require('ReinitializerMock');
|
||||
const SampleChild = artifacts.require('SampleChild');
|
||||
const DisableBad1 = artifacts.require('DisableBad1');
|
||||
const DisableBad2 = artifacts.require('DisableBad2');
|
||||
const DisableOk = artifacts.require('DisableOk');
|
||||
|
||||
contract('Initializable', function () {
|
||||
describe('basic testing without inheritance', function () {
|
||||
beforeEach('deploying', async function () {
|
||||
this.contract = await InitializableMock.new();
|
||||
});
|
||||
|
||||
describe('before initialize', function () {
|
||||
it('initializer has not run', async function () {
|
||||
expect(await this.contract.initializerRan()).to.equal(false);
|
||||
});
|
||||
|
||||
it('_initializing returns false before initialization', async function () {
|
||||
expect(await this.contract.isInitializing()).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after initialize', function () {
|
||||
beforeEach('initializing', async function () {
|
||||
await this.contract.initialize();
|
||||
});
|
||||
|
||||
it('initializer has run', async function () {
|
||||
expect(await this.contract.initializerRan()).to.equal(true);
|
||||
});
|
||||
|
||||
it('_initializing returns false after initialization', async function () {
|
||||
expect(await this.contract.isInitializing()).to.equal(false);
|
||||
});
|
||||
|
||||
it('initializer does not run again', async function () {
|
||||
await expectRevert(this.contract.initialize(), 'Initializable: contract is already initialized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested under an initializer', function () {
|
||||
it('initializer modifier reverts', async function () {
|
||||
await expectRevert(this.contract.initializerNested(), 'Initializable: contract is already initialized');
|
||||
});
|
||||
|
||||
it('onlyInitializing modifier succeeds', async function () {
|
||||
await this.contract.onlyInitializingNested();
|
||||
expect(await this.contract.onlyInitializingRan()).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot call onlyInitializable function outside the scope of an initializable function', async function () {
|
||||
await expectRevert(this.contract.initializeOnlyInitializing(), 'Initializable: contract is not initializing');
|
||||
});
|
||||
});
|
||||
|
||||
it('nested initializer can run during construction', async function () {
|
||||
const contract2 = await ConstructorInitializableMock.new();
|
||||
expect(await contract2.initializerRan()).to.equal(true);
|
||||
expect(await contract2.onlyInitializingRan()).to.equal(true);
|
||||
});
|
||||
|
||||
it('multiple constructor levels can be initializers', async function () {
|
||||
const contract2 = await ChildConstructorInitializableMock.new();
|
||||
expect(await contract2.initializerRan()).to.equal(true);
|
||||
expect(await contract2.childInitializerRan()).to.equal(true);
|
||||
expect(await contract2.onlyInitializingRan()).to.equal(true);
|
||||
});
|
||||
|
||||
describe('reinitialization', function () {
|
||||
beforeEach('deploying', async function () {
|
||||
this.contract = await ReinitializerMock.new();
|
||||
});
|
||||
|
||||
it('can reinitialize', async function () {
|
||||
expect(await this.contract.counter()).to.be.bignumber.equal('0');
|
||||
await this.contract.initialize();
|
||||
expect(await this.contract.counter()).to.be.bignumber.equal('1');
|
||||
await this.contract.reinitialize(2);
|
||||
expect(await this.contract.counter()).to.be.bignumber.equal('2');
|
||||
await this.contract.reinitialize(3);
|
||||
expect(await this.contract.counter()).to.be.bignumber.equal('3');
|
||||
});
|
||||
|
||||
it('can jump multiple steps', async function () {
|
||||
expect(await this.contract.counter()).to.be.bignumber.equal('0');
|
||||
await this.contract.initialize();
|
||||
expect(await this.contract.counter()).to.be.bignumber.equal('1');
|
||||
await this.contract.reinitialize(128);
|
||||
expect(await this.contract.counter()).to.be.bignumber.equal('2');
|
||||
});
|
||||
|
||||
it('cannot nest reinitializers', async function () {
|
||||
expect(await this.contract.counter()).to.be.bignumber.equal('0');
|
||||
await expectRevert(this.contract.nestedReinitialize(2, 2), 'Initializable: contract is already initialized');
|
||||
await expectRevert(this.contract.nestedReinitialize(2, 3), 'Initializable: contract is already initialized');
|
||||
await expectRevert(this.contract.nestedReinitialize(3, 2), 'Initializable: contract is already initialized');
|
||||
});
|
||||
|
||||
it('can chain reinitializers', async function () {
|
||||
expect(await this.contract.counter()).to.be.bignumber.equal('0');
|
||||
await this.contract.chainReinitialize(2, 3);
|
||||
expect(await this.contract.counter()).to.be.bignumber.equal('2');
|
||||
});
|
||||
|
||||
it('_getInitializedVersion returns right version', async function () {
|
||||
await this.contract.initialize();
|
||||
expect(await this.contract.getInitializedVersion()).to.be.bignumber.equal('1');
|
||||
await this.contract.reinitialize(12);
|
||||
expect(await this.contract.getInitializedVersion()).to.be.bignumber.equal('12');
|
||||
});
|
||||
|
||||
describe('contract locking', function () {
|
||||
it('prevents initialization', async function () {
|
||||
await this.contract.disableInitializers();
|
||||
await expectRevert(this.contract.initialize(), 'Initializable: contract is already initialized');
|
||||
});
|
||||
|
||||
it('prevents re-initialization', async function () {
|
||||
await this.contract.disableInitializers();
|
||||
await expectRevert(this.contract.reinitialize(255), 'Initializable: contract is already initialized');
|
||||
});
|
||||
|
||||
it('can lock contract after initialization', async function () {
|
||||
await this.contract.initialize();
|
||||
await this.contract.disableInitializers();
|
||||
await expectRevert(this.contract.reinitialize(255), 'Initializable: contract is already initialized');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', function () {
|
||||
it('constructor initialization emits event', async function () {
|
||||
const contract = await ConstructorInitializableMock.new();
|
||||
|
||||
await expectEvent.inTransaction(contract.transactionHash, contract, 'Initialized', { version: '1' });
|
||||
});
|
||||
|
||||
it('initialization emits event', async function () {
|
||||
const contract = await ReinitializerMock.new();
|
||||
|
||||
const { receipt } = await contract.initialize();
|
||||
expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(1);
|
||||
expectEvent(receipt, 'Initialized', { version: '1' });
|
||||
});
|
||||
|
||||
it('reinitialization emits event', async function () {
|
||||
const contract = await ReinitializerMock.new();
|
||||
|
||||
const { receipt } = await contract.reinitialize(128);
|
||||
expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(1);
|
||||
expectEvent(receipt, 'Initialized', { version: '128' });
|
||||
});
|
||||
|
||||
it('chained reinitialization emits multiple events', async function () {
|
||||
const contract = await ReinitializerMock.new();
|
||||
|
||||
const { receipt } = await contract.chainReinitialize(2, 3);
|
||||
expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(2);
|
||||
expectEvent(receipt, 'Initialized', { version: '2' });
|
||||
expectEvent(receipt, 'Initialized', { version: '3' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('complex testing with inheritance', function () {
|
||||
const mother = '12';
|
||||
const gramps = '56';
|
||||
const father = '34';
|
||||
const child = '78';
|
||||
|
||||
beforeEach('deploying', async function () {
|
||||
this.contract = await SampleChild.new();
|
||||
});
|
||||
|
||||
beforeEach('initializing', async function () {
|
||||
await this.contract.initialize(mother, gramps, father, child);
|
||||
});
|
||||
|
||||
it('initializes human', async function () {
|
||||
expect(await this.contract.isHuman()).to.be.equal(true);
|
||||
});
|
||||
|
||||
it('initializes mother', async function () {
|
||||
expect(await this.contract.mother()).to.be.bignumber.equal(mother);
|
||||
});
|
||||
|
||||
it('initializes gramps', async function () {
|
||||
expect(await this.contract.gramps()).to.be.bignumber.equal(gramps);
|
||||
});
|
||||
|
||||
it('initializes father', async function () {
|
||||
expect(await this.contract.father()).to.be.bignumber.equal(father);
|
||||
});
|
||||
|
||||
it('initializes child', async function () {
|
||||
expect(await this.contract.child()).to.be.bignumber.equal(child);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disabling initialization', function () {
|
||||
it('old and new patterns in bad sequence', async function () {
|
||||
await expectRevert(DisableBad1.new(), 'Initializable: contract is already initialized');
|
||||
await expectRevert(DisableBad2.new(), 'Initializable: contract is initializing');
|
||||
});
|
||||
|
||||
it('old and new patterns in good sequence', async function () {
|
||||
const ok = await DisableOk.new();
|
||||
await expectEvent.inConstruction(ok, 'Initialized', { version: '1' });
|
||||
await expectEvent.inConstruction(ok, 'Initialized', { version: '255' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { web3 } = require('@openzeppelin/test-helpers/src/setup');
|
||||
const { getSlot, ImplementationSlot } = require('../../helpers/erc1967');
|
||||
|
||||
const ERC1967Proxy = artifacts.require('ERC1967Proxy');
|
||||
const UUPSUpgradeableMock = artifacts.require('UUPSUpgradeableMock');
|
||||
const UUPSUpgradeableUnsafeMock = artifacts.require('UUPSUpgradeableUnsafeMock');
|
||||
const UUPSUpgradeableLegacyMock = artifacts.require('UUPSUpgradeableLegacyMock');
|
||||
const NonUpgradeableMock = artifacts.require('NonUpgradeableMock');
|
||||
|
||||
contract('UUPSUpgradeable', function () {
|
||||
before(async function () {
|
||||
this.implInitial = await UUPSUpgradeableMock.new();
|
||||
this.implUpgradeOk = await UUPSUpgradeableMock.new();
|
||||
this.implUpgradeUnsafe = await UUPSUpgradeableUnsafeMock.new();
|
||||
this.implUpgradeNonUUPS = await NonUpgradeableMock.new();
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x');
|
||||
this.instance = await UUPSUpgradeableMock.at(address);
|
||||
});
|
||||
|
||||
it('upgrade to upgradeable implementation', async function () {
|
||||
const { receipt } = await this.instance.upgradeTo(this.implUpgradeOk.address);
|
||||
expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1);
|
||||
expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address });
|
||||
});
|
||||
|
||||
it('upgrade to upgradeable implementation with call', async function () {
|
||||
expect(await this.instance.current()).to.be.bignumber.equal('0');
|
||||
|
||||
const { receipt } = await this.instance.upgradeToAndCall(
|
||||
this.implUpgradeOk.address,
|
||||
this.implUpgradeOk.contract.methods.increment().encodeABI(),
|
||||
);
|
||||
expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1);
|
||||
expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address });
|
||||
|
||||
expect(await this.instance.current()).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('upgrade to and unsafe upgradeable implementation', async function () {
|
||||
const { receipt } = await this.instance.upgradeTo(this.implUpgradeUnsafe.address);
|
||||
expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeUnsafe.address });
|
||||
});
|
||||
|
||||
// delegate to a non existing upgradeTo function causes a low level revert
|
||||
it('reject upgrade to non uups implementation', async function () {
|
||||
await expectRevert(
|
||||
this.instance.upgradeTo(this.implUpgradeNonUUPS.address),
|
||||
'ERC1967Upgrade: new implementation is not UUPS',
|
||||
);
|
||||
});
|
||||
|
||||
it('reject proxy address as implementation', async function () {
|
||||
const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x');
|
||||
const otherInstance = await UUPSUpgradeableMock.at(address);
|
||||
|
||||
await expectRevert(
|
||||
this.instance.upgradeTo(otherInstance.address),
|
||||
'ERC1967Upgrade: new implementation is not UUPS',
|
||||
);
|
||||
});
|
||||
|
||||
it('can upgrade from legacy implementations', async function () {
|
||||
const legacyImpl = await UUPSUpgradeableLegacyMock.new();
|
||||
const legacyInstance = await ERC1967Proxy.new(legacyImpl.address, '0x').then(({ address }) =>
|
||||
UUPSUpgradeableLegacyMock.at(address),
|
||||
);
|
||||
|
||||
const receipt = await legacyInstance.upgradeTo(this.implInitial.address);
|
||||
|
||||
const UpgradedEvents = receipt.logs.filter(
|
||||
({ address, event }) => address === legacyInstance.address && event === 'Upgraded',
|
||||
);
|
||||
expect(UpgradedEvents.length).to.be.equal(1);
|
||||
|
||||
expectEvent(receipt, 'Upgraded', { implementation: this.implInitial.address });
|
||||
|
||||
const implementationSlot = await getSlot(legacyInstance, ImplementationSlot);
|
||||
const implementationAddress = web3.utils.toChecksumAddress(implementationSlot.substr(-40));
|
||||
expect(implementationAddress).to.be.equal(this.implInitial.address);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const PausableMock = artifacts.require('PausableMock');
|
||||
|
||||
contract('Pausable', function (accounts) {
|
||||
const [pauser] = accounts;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.pausable = await PausableMock.new();
|
||||
});
|
||||
|
||||
context('when unpaused', function () {
|
||||
beforeEach(async function () {
|
||||
expect(await this.pausable.paused()).to.equal(false);
|
||||
});
|
||||
|
||||
it('can perform normal process in non-pause', async function () {
|
||||
expect(await this.pausable.count()).to.be.bignumber.equal('0');
|
||||
|
||||
await this.pausable.normalProcess();
|
||||
expect(await this.pausable.count()).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('cannot take drastic measure in non-pause', async function () {
|
||||
await expectRevert(this.pausable.drasticMeasure(), 'Pausable: not paused');
|
||||
expect(await this.pausable.drasticMeasureTaken()).to.equal(false);
|
||||
});
|
||||
|
||||
context('when paused', function () {
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.pausable.pause({ from: pauser });
|
||||
});
|
||||
|
||||
it('emits a Paused event', function () {
|
||||
expectEvent(this.receipt, 'Paused', { account: pauser });
|
||||
});
|
||||
|
||||
it('cannot perform normal process in pause', async function () {
|
||||
await expectRevert(this.pausable.normalProcess(), 'Pausable: paused');
|
||||
});
|
||||
|
||||
it('can take a drastic measure in a pause', async function () {
|
||||
await this.pausable.drasticMeasure();
|
||||
expect(await this.pausable.drasticMeasureTaken()).to.equal(true);
|
||||
});
|
||||
|
||||
it('reverts when re-pausing', async function () {
|
||||
await expectRevert(this.pausable.pause(), 'Pausable: paused');
|
||||
});
|
||||
|
||||
describe('unpausing', function () {
|
||||
it('is unpausable by the pauser', async function () {
|
||||
await this.pausable.unpause();
|
||||
expect(await this.pausable.paused()).to.equal(false);
|
||||
});
|
||||
|
||||
context('when unpaused', function () {
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.pausable.unpause({ from: pauser });
|
||||
});
|
||||
|
||||
it('emits an Unpaused event', function () {
|
||||
expectEvent(this.receipt, 'Unpaused', { account: pauser });
|
||||
});
|
||||
|
||||
it('should resume allowing normal process', async function () {
|
||||
expect(await this.pausable.count()).to.be.bignumber.equal('0');
|
||||
await this.pausable.normalProcess();
|
||||
expect(await this.pausable.count()).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('should prevent drastic measure', async function () {
|
||||
await expectRevert(this.pausable.drasticMeasure(), 'Pausable: not paused');
|
||||
});
|
||||
|
||||
it('reverts when re-unpausing', async function () {
|
||||
await expectRevert(this.pausable.unpause(), 'Pausable: not paused');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
const { balance, ether } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const PullPaymentMock = artifacts.require('PullPaymentMock');
|
||||
|
||||
contract('PullPayment', function (accounts) {
|
||||
const [payer, payee1, payee2] = accounts;
|
||||
|
||||
const amount = ether('17');
|
||||
|
||||
beforeEach(async function () {
|
||||
this.contract = await PullPaymentMock.new({ value: amount });
|
||||
});
|
||||
|
||||
describe('payments', function () {
|
||||
it('can record an async payment correctly', async function () {
|
||||
await this.contract.callTransfer(payee1, 100, { from: payer });
|
||||
expect(await this.contract.payments(payee1)).to.be.bignumber.equal('100');
|
||||
});
|
||||
|
||||
it('can add multiple balances on one account', async function () {
|
||||
await this.contract.callTransfer(payee1, 200, { from: payer });
|
||||
await this.contract.callTransfer(payee1, 300, { from: payer });
|
||||
expect(await this.contract.payments(payee1)).to.be.bignumber.equal('500');
|
||||
});
|
||||
|
||||
it('can add balances on multiple accounts', async function () {
|
||||
await this.contract.callTransfer(payee1, 200, { from: payer });
|
||||
await this.contract.callTransfer(payee2, 300, { from: payer });
|
||||
|
||||
expect(await this.contract.payments(payee1)).to.be.bignumber.equal('200');
|
||||
|
||||
expect(await this.contract.payments(payee2)).to.be.bignumber.equal('300');
|
||||
});
|
||||
});
|
||||
|
||||
describe('withdrawPayments', function () {
|
||||
it('can withdraw payment', async function () {
|
||||
const balanceTracker = await balance.tracker(payee1);
|
||||
|
||||
await this.contract.callTransfer(payee1, amount, { from: payer });
|
||||
expect(await this.contract.payments(payee1)).to.be.bignumber.equal(amount);
|
||||
|
||||
await this.contract.withdrawPayments(payee1);
|
||||
|
||||
expect(await balanceTracker.delta()).to.be.bignumber.equal(amount);
|
||||
expect(await this.contract.payments(payee1)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
const { expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ReentrancyMock = artifacts.require('ReentrancyMock');
|
||||
const ReentrancyAttack = artifacts.require('ReentrancyAttack');
|
||||
|
||||
contract('ReentrancyGuard', function () {
|
||||
beforeEach(async function () {
|
||||
this.reentrancyMock = await ReentrancyMock.new();
|
||||
expect(await this.reentrancyMock.counter()).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('nonReentrant function can be called', async function () {
|
||||
expect(await this.reentrancyMock.counter()).to.be.bignumber.equal('0');
|
||||
await this.reentrancyMock.callback();
|
||||
expect(await this.reentrancyMock.counter()).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('does not allow remote callback', async function () {
|
||||
const attacker = await ReentrancyAttack.new();
|
||||
await expectRevert(this.reentrancyMock.countAndCall(attacker.address), 'ReentrancyAttack: failed call');
|
||||
});
|
||||
|
||||
it('_reentrancyGuardEntered should be true when guarded', async function () {
|
||||
await this.reentrancyMock.guardedCheckEntered();
|
||||
});
|
||||
|
||||
it('_reentrancyGuardEntered should be false when unguarded', async function () {
|
||||
await this.reentrancyMock.unguardedCheckNotEntered();
|
||||
});
|
||||
|
||||
// The following are more side-effects than intended behavior:
|
||||
// I put them here as documentation, and to monitor any changes
|
||||
// in the side-effects.
|
||||
it('does not allow local recursion', async function () {
|
||||
await expectRevert(this.reentrancyMock.countLocalRecursive(10), 'ReentrancyGuard: reentrant call');
|
||||
});
|
||||
|
||||
it('does not allow indirect local recursion', async function () {
|
||||
await expectRevert(this.reentrancyMock.countThisRecursive(10), 'ReentrancyMock: failed call');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,767 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
const ERC1155ReceiverMock = artifacts.require('ERC1155ReceiverMock');
|
||||
|
||||
function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, multiTokenHolder, recipient, proxy]) {
|
||||
const firstTokenId = new BN(1);
|
||||
const secondTokenId = new BN(2);
|
||||
const unknownTokenId = new BN(3);
|
||||
|
||||
const firstAmount = new BN(1000);
|
||||
const secondAmount = new BN(2000);
|
||||
|
||||
const RECEIVER_SINGLE_MAGIC_VALUE = '0xf23a6e61';
|
||||
const RECEIVER_BATCH_MAGIC_VALUE = '0xbc197c81';
|
||||
|
||||
describe('like an ERC1155', function () {
|
||||
describe('balanceOf', function () {
|
||||
it('reverts when queried about the zero address', async function () {
|
||||
await expectRevert(
|
||||
this.token.balanceOf(ZERO_ADDRESS, firstTokenId),
|
||||
'ERC1155: address zero is not a valid owner',
|
||||
);
|
||||
});
|
||||
|
||||
context("when accounts don't own tokens", function () {
|
||||
it('returns zero for given addresses', async function () {
|
||||
expect(await this.token.balanceOf(firstTokenHolder, firstTokenId)).to.be.bignumber.equal('0');
|
||||
|
||||
expect(await this.token.balanceOf(secondTokenHolder, secondTokenId)).to.be.bignumber.equal('0');
|
||||
|
||||
expect(await this.token.balanceOf(firstTokenHolder, unknownTokenId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('when accounts own some tokens', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(firstTokenHolder, firstTokenId, firstAmount, '0x', {
|
||||
from: minter,
|
||||
});
|
||||
await this.token.$_mint(secondTokenHolder, secondTokenId, secondAmount, '0x', {
|
||||
from: minter,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the amount of tokens owned by the given addresses', async function () {
|
||||
expect(await this.token.balanceOf(firstTokenHolder, firstTokenId)).to.be.bignumber.equal(firstAmount);
|
||||
|
||||
expect(await this.token.balanceOf(secondTokenHolder, secondTokenId)).to.be.bignumber.equal(secondAmount);
|
||||
|
||||
expect(await this.token.balanceOf(firstTokenHolder, unknownTokenId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('balanceOfBatch', function () {
|
||||
it("reverts when input arrays don't match up", async function () {
|
||||
await expectRevert(
|
||||
this.token.balanceOfBatch(
|
||||
[firstTokenHolder, secondTokenHolder, firstTokenHolder, secondTokenHolder],
|
||||
[firstTokenId, secondTokenId, unknownTokenId],
|
||||
),
|
||||
'ERC1155: accounts and ids length mismatch',
|
||||
);
|
||||
|
||||
await expectRevert(
|
||||
this.token.balanceOfBatch(
|
||||
[firstTokenHolder, secondTokenHolder],
|
||||
[firstTokenId, secondTokenId, unknownTokenId],
|
||||
),
|
||||
'ERC1155: accounts and ids length mismatch',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when one of the addresses is the zero address', async function () {
|
||||
await expectRevert(
|
||||
this.token.balanceOfBatch(
|
||||
[firstTokenHolder, secondTokenHolder, ZERO_ADDRESS],
|
||||
[firstTokenId, secondTokenId, unknownTokenId],
|
||||
),
|
||||
'ERC1155: address zero is not a valid owner',
|
||||
);
|
||||
});
|
||||
|
||||
context("when accounts don't own tokens", function () {
|
||||
it('returns zeros for each account', async function () {
|
||||
const result = await this.token.balanceOfBatch(
|
||||
[firstTokenHolder, secondTokenHolder, firstTokenHolder],
|
||||
[firstTokenId, secondTokenId, unknownTokenId],
|
||||
);
|
||||
expect(result).to.be.an('array');
|
||||
expect(result[0]).to.be.a.bignumber.equal('0');
|
||||
expect(result[1]).to.be.a.bignumber.equal('0');
|
||||
expect(result[2]).to.be.a.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('when accounts own some tokens', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(firstTokenHolder, firstTokenId, firstAmount, '0x', {
|
||||
from: minter,
|
||||
});
|
||||
await this.token.$_mint(secondTokenHolder, secondTokenId, secondAmount, '0x', {
|
||||
from: minter,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns amounts owned by each account in order passed', async function () {
|
||||
const result = await this.token.balanceOfBatch(
|
||||
[secondTokenHolder, firstTokenHolder, firstTokenHolder],
|
||||
[secondTokenId, firstTokenId, unknownTokenId],
|
||||
);
|
||||
expect(result).to.be.an('array');
|
||||
expect(result[0]).to.be.a.bignumber.equal(secondAmount);
|
||||
expect(result[1]).to.be.a.bignumber.equal(firstAmount);
|
||||
expect(result[2]).to.be.a.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns multiple times the balance of the same address when asked', async function () {
|
||||
const result = await this.token.balanceOfBatch(
|
||||
[firstTokenHolder, secondTokenHolder, firstTokenHolder],
|
||||
[firstTokenId, secondTokenId, firstTokenId],
|
||||
);
|
||||
expect(result).to.be.an('array');
|
||||
expect(result[0]).to.be.a.bignumber.equal(result[2]);
|
||||
expect(result[0]).to.be.a.bignumber.equal(firstAmount);
|
||||
expect(result[1]).to.be.a.bignumber.equal(secondAmount);
|
||||
expect(result[2]).to.be.a.bignumber.equal(firstAmount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setApprovalForAll', function () {
|
||||
let receipt;
|
||||
beforeEach(async function () {
|
||||
receipt = await this.token.setApprovalForAll(proxy, true, { from: multiTokenHolder });
|
||||
});
|
||||
|
||||
it('sets approval status which can be queried via isApprovedForAll', async function () {
|
||||
expect(await this.token.isApprovedForAll(multiTokenHolder, proxy)).to.be.equal(true);
|
||||
});
|
||||
|
||||
it('emits an ApprovalForAll log', function () {
|
||||
expectEvent(receipt, 'ApprovalForAll', { account: multiTokenHolder, operator: proxy, approved: true });
|
||||
});
|
||||
|
||||
it('can unset approval for an operator', async function () {
|
||||
await this.token.setApprovalForAll(proxy, false, { from: multiTokenHolder });
|
||||
expect(await this.token.isApprovedForAll(multiTokenHolder, proxy)).to.be.equal(false);
|
||||
});
|
||||
|
||||
it('reverts if attempting to approve self as an operator', async function () {
|
||||
await expectRevert(
|
||||
this.token.setApprovalForAll(multiTokenHolder, true, { from: multiTokenHolder }),
|
||||
'ERC1155: setting approval status for self',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeTransferFrom', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(multiTokenHolder, firstTokenId, firstAmount, '0x', {
|
||||
from: minter,
|
||||
});
|
||||
await this.token.$_mint(multiTokenHolder, secondTokenId, secondAmount, '0x', {
|
||||
from: minter,
|
||||
});
|
||||
});
|
||||
|
||||
it('reverts when transferring more than balance', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount.addn(1), '0x', {
|
||||
from: multiTokenHolder,
|
||||
}),
|
||||
'ERC1155: insufficient balance for transfer',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when transferring to zero address', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(multiTokenHolder, ZERO_ADDRESS, firstTokenId, firstAmount, '0x', {
|
||||
from: multiTokenHolder,
|
||||
}),
|
||||
'ERC1155: transfer to the zero address',
|
||||
);
|
||||
});
|
||||
|
||||
function transferWasSuccessful({ operator, from, id, value }) {
|
||||
it('debits transferred balance from sender', async function () {
|
||||
const newBalance = await this.token.balanceOf(from, id);
|
||||
expect(newBalance).to.be.a.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('credits transferred balance to receiver', async function () {
|
||||
const newBalance = await this.token.balanceOf(this.toWhom, id);
|
||||
expect(newBalance).to.be.a.bignumber.equal(value);
|
||||
});
|
||||
|
||||
it('emits a TransferSingle log', function () {
|
||||
expectEvent(this.transferLogs, 'TransferSingle', {
|
||||
operator,
|
||||
from,
|
||||
to: this.toWhom,
|
||||
id,
|
||||
value,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
context('when called by the multiTokenHolder', async function () {
|
||||
beforeEach(async function () {
|
||||
this.toWhom = recipient;
|
||||
this.transferLogs = await this.token.safeTransferFrom(
|
||||
multiTokenHolder,
|
||||
recipient,
|
||||
firstTokenId,
|
||||
firstAmount,
|
||||
'0x',
|
||||
{
|
||||
from: multiTokenHolder,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
transferWasSuccessful.call(this, {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
id: firstTokenId,
|
||||
value: firstAmount,
|
||||
});
|
||||
|
||||
it('preserves existing balances which are not transferred by multiTokenHolder', async function () {
|
||||
const balance1 = await this.token.balanceOf(multiTokenHolder, secondTokenId);
|
||||
expect(balance1).to.be.a.bignumber.equal(secondAmount);
|
||||
|
||||
const balance2 = await this.token.balanceOf(recipient, secondTokenId);
|
||||
expect(balance2).to.be.a.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('when called by an operator on behalf of the multiTokenHolder', function () {
|
||||
context('when operator is not approved by multiTokenHolder', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.setApprovalForAll(proxy, false, { from: multiTokenHolder });
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount, '0x', {
|
||||
from: proxy,
|
||||
}),
|
||||
'ERC1155: caller is not token owner or approved',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when operator is approved by multiTokenHolder', function () {
|
||||
beforeEach(async function () {
|
||||
this.toWhom = recipient;
|
||||
await this.token.setApprovalForAll(proxy, true, { from: multiTokenHolder });
|
||||
this.transferLogs = await this.token.safeTransferFrom(
|
||||
multiTokenHolder,
|
||||
recipient,
|
||||
firstTokenId,
|
||||
firstAmount,
|
||||
'0x',
|
||||
{
|
||||
from: proxy,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
transferWasSuccessful.call(this, {
|
||||
operator: proxy,
|
||||
from: multiTokenHolder,
|
||||
id: firstTokenId,
|
||||
value: firstAmount,
|
||||
});
|
||||
|
||||
it("preserves operator's balances not involved in the transfer", async function () {
|
||||
const balance1 = await this.token.balanceOf(proxy, firstTokenId);
|
||||
expect(balance1).to.be.a.bignumber.equal('0');
|
||||
|
||||
const balance2 = await this.token.balanceOf(proxy, secondTokenId);
|
||||
expect(balance2).to.be.a.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('when sending to a valid receiver', function () {
|
||||
beforeEach(async function () {
|
||||
this.receiver = await ERC1155ReceiverMock.new(
|
||||
RECEIVER_SINGLE_MAGIC_VALUE,
|
||||
false,
|
||||
RECEIVER_BATCH_MAGIC_VALUE,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
context('without data', function () {
|
||||
beforeEach(async function () {
|
||||
this.toWhom = this.receiver.address;
|
||||
this.transferReceipt = await this.token.safeTransferFrom(
|
||||
multiTokenHolder,
|
||||
this.receiver.address,
|
||||
firstTokenId,
|
||||
firstAmount,
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
);
|
||||
this.transferLogs = this.transferReceipt;
|
||||
});
|
||||
|
||||
transferWasSuccessful.call(this, {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
id: firstTokenId,
|
||||
value: firstAmount,
|
||||
});
|
||||
|
||||
it('calls onERC1155Received', async function () {
|
||||
await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'Received', {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
id: firstTokenId,
|
||||
value: firstAmount,
|
||||
data: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('with data', function () {
|
||||
const data = '0xf00dd00d';
|
||||
beforeEach(async function () {
|
||||
this.toWhom = this.receiver.address;
|
||||
this.transferReceipt = await this.token.safeTransferFrom(
|
||||
multiTokenHolder,
|
||||
this.receiver.address,
|
||||
firstTokenId,
|
||||
firstAmount,
|
||||
data,
|
||||
{ from: multiTokenHolder },
|
||||
);
|
||||
this.transferLogs = this.transferReceipt;
|
||||
});
|
||||
|
||||
transferWasSuccessful.call(this, {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
id: firstTokenId,
|
||||
value: firstAmount,
|
||||
});
|
||||
|
||||
it('calls onERC1155Received', async function () {
|
||||
await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'Received', {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
id: firstTokenId,
|
||||
value: firstAmount,
|
||||
data,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('to a receiver contract returning unexpected value', function () {
|
||||
beforeEach(async function () {
|
||||
this.receiver = await ERC1155ReceiverMock.new('0x00c0ffee', false, RECEIVER_BATCH_MAGIC_VALUE, false);
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', {
|
||||
from: multiTokenHolder,
|
||||
}),
|
||||
'ERC1155: ERC1155Receiver rejected tokens',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('to a receiver contract that reverts', function () {
|
||||
beforeEach(async function () {
|
||||
this.receiver = await ERC1155ReceiverMock.new(
|
||||
RECEIVER_SINGLE_MAGIC_VALUE,
|
||||
true,
|
||||
RECEIVER_BATCH_MAGIC_VALUE,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', {
|
||||
from: multiTokenHolder,
|
||||
}),
|
||||
'ERC1155ReceiverMock: reverting on receive',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('to a contract that does not implement the required function', function () {
|
||||
it('reverts', async function () {
|
||||
const invalidReceiver = this.token;
|
||||
await expectRevert.unspecified(
|
||||
this.token.safeTransferFrom(multiTokenHolder, invalidReceiver.address, firstTokenId, firstAmount, '0x', {
|
||||
from: multiTokenHolder,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeBatchTransferFrom', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(multiTokenHolder, firstTokenId, firstAmount, '0x', {
|
||||
from: minter,
|
||||
});
|
||||
await this.token.$_mint(multiTokenHolder, secondTokenId, secondAmount, '0x', {
|
||||
from: minter,
|
||||
});
|
||||
});
|
||||
|
||||
it('reverts when transferring amount more than any of balances', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
recipient,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount.addn(1)],
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
),
|
||||
'ERC1155: insufficient balance for transfer',
|
||||
);
|
||||
});
|
||||
|
||||
it("reverts when ids array length doesn't match amounts array length", async function () {
|
||||
await expectRevert(
|
||||
this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
recipient,
|
||||
[firstTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
),
|
||||
'ERC1155: ids and amounts length mismatch',
|
||||
);
|
||||
|
||||
await expectRevert(
|
||||
this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
recipient,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount],
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
),
|
||||
'ERC1155: ids and amounts length mismatch',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when transferring to zero address', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
ZERO_ADDRESS,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
),
|
||||
'ERC1155: transfer to the zero address',
|
||||
);
|
||||
});
|
||||
|
||||
function batchTransferWasSuccessful({ operator, from, ids, values }) {
|
||||
it('debits transferred balances from sender', async function () {
|
||||
const newBalances = await this.token.balanceOfBatch(new Array(ids.length).fill(from), ids);
|
||||
for (const newBalance of newBalances) {
|
||||
expect(newBalance).to.be.a.bignumber.equal('0');
|
||||
}
|
||||
});
|
||||
|
||||
it('credits transferred balances to receiver', async function () {
|
||||
const newBalances = await this.token.balanceOfBatch(new Array(ids.length).fill(this.toWhom), ids);
|
||||
for (let i = 0; i < newBalances.length; i++) {
|
||||
expect(newBalances[i]).to.be.a.bignumber.equal(values[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it('emits a TransferBatch log', function () {
|
||||
expectEvent(this.transferLogs, 'TransferBatch', {
|
||||
operator,
|
||||
from,
|
||||
to: this.toWhom,
|
||||
// ids,
|
||||
// values,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
context('when called by the multiTokenHolder', async function () {
|
||||
beforeEach(async function () {
|
||||
this.toWhom = recipient;
|
||||
this.transferLogs = await this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
recipient,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
);
|
||||
});
|
||||
|
||||
batchTransferWasSuccessful.call(this, {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
ids: [firstTokenId, secondTokenId],
|
||||
values: [firstAmount, secondAmount],
|
||||
});
|
||||
});
|
||||
|
||||
context('when called by an operator on behalf of the multiTokenHolder', function () {
|
||||
context('when operator is not approved by multiTokenHolder', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.setApprovalForAll(proxy, false, { from: multiTokenHolder });
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
recipient,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
'0x',
|
||||
{ from: proxy },
|
||||
),
|
||||
'ERC1155: caller is not token owner or approved',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when operator is approved by multiTokenHolder', function () {
|
||||
beforeEach(async function () {
|
||||
this.toWhom = recipient;
|
||||
await this.token.setApprovalForAll(proxy, true, { from: multiTokenHolder });
|
||||
this.transferLogs = await this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
recipient,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
'0x',
|
||||
{ from: proxy },
|
||||
);
|
||||
});
|
||||
|
||||
batchTransferWasSuccessful.call(this, {
|
||||
operator: proxy,
|
||||
from: multiTokenHolder,
|
||||
ids: [firstTokenId, secondTokenId],
|
||||
values: [firstAmount, secondAmount],
|
||||
});
|
||||
|
||||
it("preserves operator's balances not involved in the transfer", async function () {
|
||||
const balance1 = await this.token.balanceOf(proxy, firstTokenId);
|
||||
expect(balance1).to.be.a.bignumber.equal('0');
|
||||
const balance2 = await this.token.balanceOf(proxy, secondTokenId);
|
||||
expect(balance2).to.be.a.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('when sending to a valid receiver', function () {
|
||||
beforeEach(async function () {
|
||||
this.receiver = await ERC1155ReceiverMock.new(
|
||||
RECEIVER_SINGLE_MAGIC_VALUE,
|
||||
false,
|
||||
RECEIVER_BATCH_MAGIC_VALUE,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
context('without data', function () {
|
||||
beforeEach(async function () {
|
||||
this.toWhom = this.receiver.address;
|
||||
this.transferReceipt = await this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
this.receiver.address,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
);
|
||||
this.transferLogs = this.transferReceipt;
|
||||
});
|
||||
|
||||
batchTransferWasSuccessful.call(this, {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
ids: [firstTokenId, secondTokenId],
|
||||
values: [firstAmount, secondAmount],
|
||||
});
|
||||
|
||||
it('calls onERC1155BatchReceived', async function () {
|
||||
await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
// ids: [firstTokenId, secondTokenId],
|
||||
// values: [firstAmount, secondAmount],
|
||||
data: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('with data', function () {
|
||||
const data = '0xf00dd00d';
|
||||
beforeEach(async function () {
|
||||
this.toWhom = this.receiver.address;
|
||||
this.transferReceipt = await this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
this.receiver.address,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
data,
|
||||
{ from: multiTokenHolder },
|
||||
);
|
||||
this.transferLogs = this.transferReceipt;
|
||||
});
|
||||
|
||||
batchTransferWasSuccessful.call(this, {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
ids: [firstTokenId, secondTokenId],
|
||||
values: [firstAmount, secondAmount],
|
||||
});
|
||||
|
||||
it('calls onERC1155Received', async function () {
|
||||
await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
// ids: [firstTokenId, secondTokenId],
|
||||
// values: [firstAmount, secondAmount],
|
||||
data,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('to a receiver contract returning unexpected value', function () {
|
||||
beforeEach(async function () {
|
||||
this.receiver = await ERC1155ReceiverMock.new(
|
||||
RECEIVER_SINGLE_MAGIC_VALUE,
|
||||
false,
|
||||
RECEIVER_SINGLE_MAGIC_VALUE,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
this.receiver.address,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
),
|
||||
'ERC1155: ERC1155Receiver rejected tokens',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('to a receiver contract that reverts', function () {
|
||||
beforeEach(async function () {
|
||||
this.receiver = await ERC1155ReceiverMock.new(
|
||||
RECEIVER_SINGLE_MAGIC_VALUE,
|
||||
false,
|
||||
RECEIVER_BATCH_MAGIC_VALUE,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
this.receiver.address,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
),
|
||||
'ERC1155ReceiverMock: reverting on batch receive',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('to a receiver contract that reverts only on single transfers', function () {
|
||||
beforeEach(async function () {
|
||||
this.receiver = await ERC1155ReceiverMock.new(
|
||||
RECEIVER_SINGLE_MAGIC_VALUE,
|
||||
true,
|
||||
RECEIVER_BATCH_MAGIC_VALUE,
|
||||
false,
|
||||
);
|
||||
|
||||
this.toWhom = this.receiver.address;
|
||||
this.transferReceipt = await this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
this.receiver.address,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
);
|
||||
this.transferLogs = this.transferReceipt;
|
||||
});
|
||||
|
||||
batchTransferWasSuccessful.call(this, {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
ids: [firstTokenId, secondTokenId],
|
||||
values: [firstAmount, secondAmount],
|
||||
});
|
||||
|
||||
it('calls onERC1155BatchReceived', async function () {
|
||||
await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', {
|
||||
operator: multiTokenHolder,
|
||||
from: multiTokenHolder,
|
||||
// ids: [firstTokenId, secondTokenId],
|
||||
// values: [firstAmount, secondAmount],
|
||||
data: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('to a contract that does not implement the required function', function () {
|
||||
it('reverts', async function () {
|
||||
const invalidReceiver = this.token;
|
||||
await expectRevert.unspecified(
|
||||
this.token.safeBatchTransferFrom(
|
||||
multiTokenHolder,
|
||||
invalidReceiver.address,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstAmount, secondAmount],
|
||||
'0x',
|
||||
{ from: multiTokenHolder },
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC165', 'ERC1155']);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC1155,
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { shouldBehaveLikeERC1155 } = require('./ERC1155.behavior');
|
||||
const ERC1155Mock = artifacts.require('$ERC1155');
|
||||
|
||||
contract('ERC1155', function (accounts) {
|
||||
const [operator, tokenHolder, tokenBatchHolder, ...otherAccounts] = accounts;
|
||||
|
||||
const initialURI = 'https://token-cdn-domain/{id}.json';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC1155Mock.new(initialURI);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC1155(otherAccounts);
|
||||
|
||||
describe('internal functions', function () {
|
||||
const tokenId = new BN(1990);
|
||||
const mintAmount = new BN(9001);
|
||||
const burnAmount = new BN(3000);
|
||||
|
||||
const tokenBatchIds = [new BN(2000), new BN(2010), new BN(2020)];
|
||||
const mintAmounts = [new BN(5000), new BN(10000), new BN(42195)];
|
||||
const burnAmounts = [new BN(5000), new BN(9001), new BN(195)];
|
||||
|
||||
const data = '0x12345678';
|
||||
|
||||
describe('_mint', function () {
|
||||
it('reverts with a zero destination address', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_mint(ZERO_ADDRESS, tokenId, mintAmount, data),
|
||||
'ERC1155: mint to the zero address',
|
||||
);
|
||||
});
|
||||
|
||||
context('with minted tokens', function () {
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.token.$_mint(tokenHolder, tokenId, mintAmount, data, { from: operator });
|
||||
});
|
||||
|
||||
it('emits a TransferSingle event', function () {
|
||||
expectEvent(this.receipt, 'TransferSingle', {
|
||||
operator,
|
||||
from: ZERO_ADDRESS,
|
||||
to: tokenHolder,
|
||||
id: tokenId,
|
||||
value: mintAmount,
|
||||
});
|
||||
});
|
||||
|
||||
it('credits the minted amount of tokens', async function () {
|
||||
expect(await this.token.balanceOf(tokenHolder, tokenId)).to.be.bignumber.equal(mintAmount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_mintBatch', function () {
|
||||
it('reverts with a zero destination address', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_mintBatch(ZERO_ADDRESS, tokenBatchIds, mintAmounts, data),
|
||||
'ERC1155: mint to the zero address',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts if length of inputs do not match', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds, mintAmounts.slice(1), data),
|
||||
'ERC1155: ids and amounts length mismatch',
|
||||
);
|
||||
|
||||
await expectRevert(
|
||||
this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds.slice(1), mintAmounts, data),
|
||||
'ERC1155: ids and amounts length mismatch',
|
||||
);
|
||||
});
|
||||
|
||||
context('with minted batch of tokens', function () {
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds, mintAmounts, data, {
|
||||
from: operator,
|
||||
});
|
||||
});
|
||||
|
||||
it('emits a TransferBatch event', function () {
|
||||
expectEvent(this.receipt, 'TransferBatch', {
|
||||
operator,
|
||||
from: ZERO_ADDRESS,
|
||||
to: tokenBatchHolder,
|
||||
});
|
||||
});
|
||||
|
||||
it('credits the minted batch of tokens', async function () {
|
||||
const holderBatchBalances = await this.token.balanceOfBatch(
|
||||
new Array(tokenBatchIds.length).fill(tokenBatchHolder),
|
||||
tokenBatchIds,
|
||||
);
|
||||
|
||||
for (let i = 0; i < holderBatchBalances.length; i++) {
|
||||
expect(holderBatchBalances[i]).to.be.bignumber.equal(mintAmounts[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_burn', function () {
|
||||
it("reverts when burning the zero account's tokens", async function () {
|
||||
await expectRevert(this.token.$_burn(ZERO_ADDRESS, tokenId, mintAmount), 'ERC1155: burn from the zero address');
|
||||
});
|
||||
|
||||
it('reverts when burning a non-existent token id', async function () {
|
||||
await expectRevert(this.token.$_burn(tokenHolder, tokenId, mintAmount), 'ERC1155: burn amount exceeds balance');
|
||||
});
|
||||
|
||||
it('reverts when burning more than available tokens', async function () {
|
||||
await this.token.$_mint(tokenHolder, tokenId, mintAmount, data, { from: operator });
|
||||
|
||||
await expectRevert(
|
||||
this.token.$_burn(tokenHolder, tokenId, mintAmount.addn(1)),
|
||||
'ERC1155: burn amount exceeds balance',
|
||||
);
|
||||
});
|
||||
|
||||
context('with minted-then-burnt tokens', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(tokenHolder, tokenId, mintAmount, data);
|
||||
this.receipt = await this.token.$_burn(tokenHolder, tokenId, burnAmount, { from: operator });
|
||||
});
|
||||
|
||||
it('emits a TransferSingle event', function () {
|
||||
expectEvent(this.receipt, 'TransferSingle', {
|
||||
operator,
|
||||
from: tokenHolder,
|
||||
to: ZERO_ADDRESS,
|
||||
id: tokenId,
|
||||
value: burnAmount,
|
||||
});
|
||||
});
|
||||
|
||||
it('accounts for both minting and burning', async function () {
|
||||
expect(await this.token.balanceOf(tokenHolder, tokenId)).to.be.bignumber.equal(mintAmount.sub(burnAmount));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_burnBatch', function () {
|
||||
it("reverts when burning the zero account's tokens", async function () {
|
||||
await expectRevert(
|
||||
this.token.$_burnBatch(ZERO_ADDRESS, tokenBatchIds, burnAmounts),
|
||||
'ERC1155: burn from the zero address',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts if length of inputs do not match', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts.slice(1)),
|
||||
'ERC1155: ids and amounts length mismatch',
|
||||
);
|
||||
|
||||
await expectRevert(
|
||||
this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds.slice(1), burnAmounts),
|
||||
'ERC1155: ids and amounts length mismatch',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when burning a non-existent token id', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts),
|
||||
'ERC1155: burn amount exceeds balance',
|
||||
);
|
||||
});
|
||||
|
||||
context('with minted-then-burnt tokens', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds, mintAmounts, data);
|
||||
this.receipt = await this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts, { from: operator });
|
||||
});
|
||||
|
||||
it('emits a TransferBatch event', function () {
|
||||
expectEvent(this.receipt, 'TransferBatch', {
|
||||
operator,
|
||||
from: tokenBatchHolder,
|
||||
to: ZERO_ADDRESS,
|
||||
// ids: tokenBatchIds,
|
||||
// values: burnAmounts,
|
||||
});
|
||||
});
|
||||
|
||||
it('accounts for both minting and burning', async function () {
|
||||
const holderBatchBalances = await this.token.balanceOfBatch(
|
||||
new Array(tokenBatchIds.length).fill(tokenBatchHolder),
|
||||
tokenBatchIds,
|
||||
);
|
||||
|
||||
for (let i = 0; i < holderBatchBalances.length; i++) {
|
||||
expect(holderBatchBalances[i]).to.be.bignumber.equal(mintAmounts[i].sub(burnAmounts[i]));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ERC1155MetadataURI', function () {
|
||||
const firstTokenID = new BN('42');
|
||||
const secondTokenID = new BN('1337');
|
||||
|
||||
it('emits no URI event in constructor', async function () {
|
||||
await expectEvent.notEmitted.inConstruction(this.token, 'URI');
|
||||
});
|
||||
|
||||
it('sets the initial URI for all token types', async function () {
|
||||
expect(await this.token.uri(firstTokenID)).to.be.equal(initialURI);
|
||||
expect(await this.token.uri(secondTokenID)).to.be.equal(initialURI);
|
||||
});
|
||||
|
||||
describe('_setURI', function () {
|
||||
const newURI = 'https://token-cdn-domain/{locale}/{id}.json';
|
||||
|
||||
it('emits no URI event', async function () {
|
||||
const receipt = await this.token.$_setURI(newURI);
|
||||
|
||||
expectEvent.notEmitted(receipt, 'URI');
|
||||
});
|
||||
|
||||
it('sets the new URI for all token types', async function () {
|
||||
await this.token.$_setURI(newURI);
|
||||
|
||||
expect(await this.token.uri(firstTokenID)).to.be.equal(newURI);
|
||||
expect(await this.token.uri(secondTokenID)).to.be.equal(newURI);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
const { BN, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC1155Burnable = artifacts.require('$ERC1155Burnable');
|
||||
|
||||
contract('ERC1155Burnable', function (accounts) {
|
||||
const [holder, operator, other] = accounts;
|
||||
|
||||
const uri = 'https://token.com';
|
||||
|
||||
const tokenIds = [new BN('42'), new BN('1137')];
|
||||
const amounts = [new BN('3000'), new BN('9902')];
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC1155Burnable.new(uri);
|
||||
|
||||
await this.token.$_mint(holder, tokenIds[0], amounts[0], '0x');
|
||||
await this.token.$_mint(holder, tokenIds[1], amounts[1], '0x');
|
||||
});
|
||||
|
||||
describe('burn', function () {
|
||||
it('holder can burn their tokens', async function () {
|
||||
await this.token.burn(holder, tokenIds[0], amounts[0].subn(1), { from: holder });
|
||||
|
||||
expect(await this.token.balanceOf(holder, tokenIds[0])).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it("approved operators can burn the holder's tokens", async function () {
|
||||
await this.token.setApprovalForAll(operator, true, { from: holder });
|
||||
await this.token.burn(holder, tokenIds[0], amounts[0].subn(1), { from: operator });
|
||||
|
||||
expect(await this.token.balanceOf(holder, tokenIds[0])).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it("unapproved accounts cannot burn the holder's tokens", async function () {
|
||||
await expectRevert(
|
||||
this.token.burn(holder, tokenIds[0], amounts[0].subn(1), { from: other }),
|
||||
'ERC1155: caller is not token owner or approved',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('burnBatch', function () {
|
||||
it('holder can burn their tokens', async function () {
|
||||
await this.token.burnBatch(holder, tokenIds, [amounts[0].subn(1), amounts[1].subn(2)], { from: holder });
|
||||
|
||||
expect(await this.token.balanceOf(holder, tokenIds[0])).to.be.bignumber.equal('1');
|
||||
expect(await this.token.balanceOf(holder, tokenIds[1])).to.be.bignumber.equal('2');
|
||||
});
|
||||
|
||||
it("approved operators can burn the holder's tokens", async function () {
|
||||
await this.token.setApprovalForAll(operator, true, { from: holder });
|
||||
await this.token.burnBatch(holder, tokenIds, [amounts[0].subn(1), amounts[1].subn(2)], { from: operator });
|
||||
|
||||
expect(await this.token.balanceOf(holder, tokenIds[0])).to.be.bignumber.equal('1');
|
||||
expect(await this.token.balanceOf(holder, tokenIds[1])).to.be.bignumber.equal('2');
|
||||
});
|
||||
|
||||
it("unapproved accounts cannot burn the holder's tokens", async function () {
|
||||
await expectRevert(
|
||||
this.token.burnBatch(holder, tokenIds, [amounts[0].subn(1), amounts[1].subn(2)], { from: other }),
|
||||
'ERC1155: caller is not token owner or approved',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
const { BN, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC1155Pausable = artifacts.require('$ERC1155Pausable');
|
||||
|
||||
contract('ERC1155Pausable', function (accounts) {
|
||||
const [holder, operator, receiver, other] = accounts;
|
||||
|
||||
const uri = 'https://token.com';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC1155Pausable.new(uri);
|
||||
});
|
||||
|
||||
context('when token is paused', function () {
|
||||
const firstTokenId = new BN('37');
|
||||
const firstTokenAmount = new BN('42');
|
||||
|
||||
const secondTokenId = new BN('19842');
|
||||
const secondTokenAmount = new BN('23');
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.setApprovalForAll(operator, true, { from: holder });
|
||||
await this.token.$_mint(holder, firstTokenId, firstTokenAmount, '0x');
|
||||
|
||||
await this.token.$_pause();
|
||||
});
|
||||
|
||||
it('reverts when trying to safeTransferFrom from holder', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(holder, receiver, firstTokenId, firstTokenAmount, '0x', { from: holder }),
|
||||
'ERC1155Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to safeTransferFrom from operator', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(holder, receiver, firstTokenId, firstTokenAmount, '0x', { from: operator }),
|
||||
'ERC1155Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to safeBatchTransferFrom from holder', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeBatchTransferFrom(holder, receiver, [firstTokenId], [firstTokenAmount], '0x', { from: holder }),
|
||||
'ERC1155Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to safeBatchTransferFrom from operator', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeBatchTransferFrom(holder, receiver, [firstTokenId], [firstTokenAmount], '0x', {
|
||||
from: operator,
|
||||
}),
|
||||
'ERC1155Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to mint', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_mint(holder, secondTokenId, secondTokenAmount, '0x'),
|
||||
'ERC1155Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to mintBatch', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_mintBatch(holder, [secondTokenId], [secondTokenAmount], '0x'),
|
||||
'ERC1155Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to burn', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_burn(holder, firstTokenId, firstTokenAmount),
|
||||
'ERC1155Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to burnBatch', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_burnBatch(holder, [firstTokenId], [firstTokenAmount]),
|
||||
'ERC1155Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
describe('setApprovalForAll', function () {
|
||||
it('approves an operator', async function () {
|
||||
await this.token.setApprovalForAll(other, true, { from: holder });
|
||||
expect(await this.token.isApprovedForAll(holder, other)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
it('returns the amount of tokens owned by the given address', async function () {
|
||||
const balance = await this.token.balanceOf(holder, firstTokenId);
|
||||
expect(balance).to.be.bignumber.equal(firstTokenAmount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isApprovedForAll', function () {
|
||||
it('returns the approval of the operator', async function () {
|
||||
expect(await this.token.isApprovedForAll(holder, operator)).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
const { BN } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC1155Supply = artifacts.require('$ERC1155Supply');
|
||||
|
||||
contract('ERC1155Supply', function (accounts) {
|
||||
const [holder] = accounts;
|
||||
|
||||
const uri = 'https://token.com';
|
||||
|
||||
const firstTokenId = new BN('37');
|
||||
const firstTokenAmount = new BN('42');
|
||||
|
||||
const secondTokenId = new BN('19842');
|
||||
const secondTokenAmount = new BN('23');
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC1155Supply.new(uri);
|
||||
});
|
||||
|
||||
context('before mint', function () {
|
||||
it('exist', async function () {
|
||||
expect(await this.token.exists(firstTokenId)).to.be.equal(false);
|
||||
});
|
||||
|
||||
it('totalSupply', async function () {
|
||||
expect(await this.token.totalSupply(firstTokenId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('after mint', function () {
|
||||
context('single', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, firstTokenId, firstTokenAmount, '0x');
|
||||
});
|
||||
|
||||
it('exist', async function () {
|
||||
expect(await this.token.exists(firstTokenId)).to.be.equal(true);
|
||||
});
|
||||
|
||||
it('totalSupply', async function () {
|
||||
expect(await this.token.totalSupply(firstTokenId)).to.be.bignumber.equal(firstTokenAmount);
|
||||
});
|
||||
});
|
||||
|
||||
context('batch', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mintBatch(
|
||||
holder,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenAmount, secondTokenAmount],
|
||||
'0x',
|
||||
);
|
||||
});
|
||||
|
||||
it('exist', async function () {
|
||||
expect(await this.token.exists(firstTokenId)).to.be.equal(true);
|
||||
expect(await this.token.exists(secondTokenId)).to.be.equal(true);
|
||||
});
|
||||
|
||||
it('totalSupply', async function () {
|
||||
expect(await this.token.totalSupply(firstTokenId)).to.be.bignumber.equal(firstTokenAmount);
|
||||
expect(await this.token.totalSupply(secondTokenId)).to.be.bignumber.equal(secondTokenAmount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('after burn', function () {
|
||||
context('single', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, firstTokenId, firstTokenAmount, '0x');
|
||||
await this.token.$_burn(holder, firstTokenId, firstTokenAmount);
|
||||
});
|
||||
|
||||
it('exist', async function () {
|
||||
expect(await this.token.exists(firstTokenId)).to.be.equal(false);
|
||||
});
|
||||
|
||||
it('totalSupply', async function () {
|
||||
expect(await this.token.totalSupply(firstTokenId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('batch', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mintBatch(
|
||||
holder,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenAmount, secondTokenAmount],
|
||||
'0x',
|
||||
);
|
||||
await this.token.$_burnBatch(holder, [firstTokenId, secondTokenId], [firstTokenAmount, secondTokenAmount]);
|
||||
});
|
||||
|
||||
it('exist', async function () {
|
||||
expect(await this.token.exists(firstTokenId)).to.be.equal(false);
|
||||
expect(await this.token.exists(secondTokenId)).to.be.equal(false);
|
||||
});
|
||||
|
||||
it('totalSupply', async function () {
|
||||
expect(await this.token.totalSupply(firstTokenId)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.totalSupply(secondTokenId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
const { BN, expectEvent } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
const { artifacts } = require('hardhat');
|
||||
|
||||
const ERC1155URIStorage = artifacts.require('$ERC1155URIStorage');
|
||||
|
||||
contract(['ERC1155URIStorage'], function (accounts) {
|
||||
const [holder] = accounts;
|
||||
|
||||
const erc1155Uri = 'https://token.com/nfts/';
|
||||
const baseUri = 'https://token.com/';
|
||||
|
||||
const tokenId = new BN('1');
|
||||
const amount = new BN('3000');
|
||||
|
||||
describe('with base uri set', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC1155URIStorage.new(erc1155Uri);
|
||||
await this.token.$_setBaseURI(baseUri);
|
||||
|
||||
await this.token.$_mint(holder, tokenId, amount, '0x');
|
||||
});
|
||||
|
||||
it('can request the token uri, returning the erc1155 uri if no token uri was set', async function () {
|
||||
const receivedTokenUri = await this.token.uri(tokenId);
|
||||
|
||||
expect(receivedTokenUri).to.be.equal(erc1155Uri);
|
||||
});
|
||||
|
||||
it('can request the token uri, returning the concatenated uri if a token uri was set', async function () {
|
||||
const tokenUri = '1234/';
|
||||
const receipt = await this.token.$_setURI(tokenId, tokenUri);
|
||||
|
||||
const receivedTokenUri = await this.token.uri(tokenId);
|
||||
|
||||
const expectedUri = `${baseUri}${tokenUri}`;
|
||||
expect(receivedTokenUri).to.be.equal(expectedUri);
|
||||
expectEvent(receipt, 'URI', { value: expectedUri, id: tokenId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('with base uri set to the empty string', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC1155URIStorage.new('');
|
||||
|
||||
await this.token.$_mint(holder, tokenId, amount, '0x');
|
||||
});
|
||||
|
||||
it('can request the token uri, returning an empty string if no token uri was set', async function () {
|
||||
const receivedTokenUri = await this.token.uri(tokenId);
|
||||
|
||||
expect(receivedTokenUri).to.be.equal('');
|
||||
});
|
||||
|
||||
it('can request the token uri, returning the token uri if a token uri was set', async function () {
|
||||
const tokenUri = 'ipfs://1234/';
|
||||
const receipt = await this.token.$_setURI(tokenId, tokenUri);
|
||||
|
||||
const receivedTokenUri = await this.token.uri(tokenId);
|
||||
|
||||
expect(receivedTokenUri).to.be.equal(tokenUri);
|
||||
expectEvent(receipt, 'URI', { value: tokenUri, id: tokenId });
|
||||
});
|
||||
});
|
||||
});
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC1155PresetMinterPauser = artifacts.require('ERC1155PresetMinterPauser');
|
||||
|
||||
contract('ERC1155PresetMinterPauser', function (accounts) {
|
||||
const [deployer, other] = accounts;
|
||||
|
||||
const firstTokenId = new BN('845');
|
||||
const firstTokenIdAmount = new BN('5000');
|
||||
|
||||
const secondTokenId = new BN('48324');
|
||||
const secondTokenIdAmount = new BN('77875');
|
||||
|
||||
const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
||||
const MINTER_ROLE = web3.utils.soliditySha3('MINTER_ROLE');
|
||||
const PAUSER_ROLE = web3.utils.soliditySha3('PAUSER_ROLE');
|
||||
|
||||
const uri = 'https://token.com';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC1155PresetMinterPauser.new(uri, { from: deployer });
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC1155', 'AccessControl', 'AccessControlEnumerable']);
|
||||
|
||||
it('deployer has the default admin role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(DEFAULT_ADMIN_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(DEFAULT_ADMIN_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('deployer has the minter role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(MINTER_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(MINTER_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('deployer has the pauser role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(PAUSER_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(PAUSER_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('minter and pauser role admin is the default admin', async function () {
|
||||
expect(await this.token.getRoleAdmin(MINTER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
|
||||
expect(await this.token.getRoleAdmin(PAUSER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
|
||||
});
|
||||
|
||||
describe('minting', function () {
|
||||
it('deployer can mint tokens', async function () {
|
||||
const receipt = await this.token.mint(other, firstTokenId, firstTokenIdAmount, '0x', { from: deployer });
|
||||
expectEvent(receipt, 'TransferSingle', {
|
||||
operator: deployer,
|
||||
from: ZERO_ADDRESS,
|
||||
to: other,
|
||||
value: firstTokenIdAmount,
|
||||
id: firstTokenId,
|
||||
});
|
||||
|
||||
expect(await this.token.balanceOf(other, firstTokenId)).to.be.bignumber.equal(firstTokenIdAmount);
|
||||
});
|
||||
|
||||
it('other accounts cannot mint tokens', async function () {
|
||||
await expectRevert(
|
||||
this.token.mint(other, firstTokenId, firstTokenIdAmount, '0x', { from: other }),
|
||||
'ERC1155PresetMinterPauser: must have minter role to mint',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('batched minting', function () {
|
||||
it('deployer can batch mint tokens', async function () {
|
||||
const receipt = await this.token.mintBatch(
|
||||
other,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenIdAmount, secondTokenIdAmount],
|
||||
'0x',
|
||||
{ from: deployer },
|
||||
);
|
||||
|
||||
expectEvent(receipt, 'TransferBatch', { operator: deployer, from: ZERO_ADDRESS, to: other });
|
||||
|
||||
expect(await this.token.balanceOf(other, firstTokenId)).to.be.bignumber.equal(firstTokenIdAmount);
|
||||
});
|
||||
|
||||
it('other accounts cannot batch mint tokens', async function () {
|
||||
await expectRevert(
|
||||
this.token.mintBatch(other, [firstTokenId, secondTokenId], [firstTokenIdAmount, secondTokenIdAmount], '0x', {
|
||||
from: other,
|
||||
}),
|
||||
'ERC1155PresetMinterPauser: must have minter role to mint',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pausing', function () {
|
||||
it('deployer can pause', async function () {
|
||||
const receipt = await this.token.pause({ from: deployer });
|
||||
expectEvent(receipt, 'Paused', { account: deployer });
|
||||
|
||||
expect(await this.token.paused()).to.equal(true);
|
||||
});
|
||||
|
||||
it('deployer can unpause', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
const receipt = await this.token.unpause({ from: deployer });
|
||||
expectEvent(receipt, 'Unpaused', { account: deployer });
|
||||
|
||||
expect(await this.token.paused()).to.equal(false);
|
||||
});
|
||||
|
||||
it('cannot mint while paused', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
await expectRevert(
|
||||
this.token.mint(other, firstTokenId, firstTokenIdAmount, '0x', { from: deployer }),
|
||||
'ERC1155Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('other accounts cannot pause', async function () {
|
||||
await expectRevert(
|
||||
this.token.pause({ from: other }),
|
||||
'ERC1155PresetMinterPauser: must have pauser role to pause',
|
||||
);
|
||||
});
|
||||
|
||||
it('other accounts cannot unpause', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
await expectRevert(
|
||||
this.token.unpause({ from: other }),
|
||||
'ERC1155PresetMinterPauser: must have pauser role to unpause',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('burning', function () {
|
||||
it('holders can burn their tokens', async function () {
|
||||
await this.token.mint(other, firstTokenId, firstTokenIdAmount, '0x', { from: deployer });
|
||||
|
||||
const receipt = await this.token.burn(other, firstTokenId, firstTokenIdAmount.subn(1), { from: other });
|
||||
expectEvent(receipt, 'TransferSingle', {
|
||||
operator: other,
|
||||
from: other,
|
||||
to: ZERO_ADDRESS,
|
||||
value: firstTokenIdAmount.subn(1),
|
||||
id: firstTokenId,
|
||||
});
|
||||
|
||||
expect(await this.token.balanceOf(other, firstTokenId)).to.be.bignumber.equal('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
const { BN } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const ERC1155Holder = artifacts.require('ERC1155Holder');
|
||||
const ERC1155 = artifacts.require('$ERC1155');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
contract('ERC1155Holder', function (accounts) {
|
||||
const [creator] = accounts;
|
||||
const uri = 'https://token-cdn-domain/{id}.json';
|
||||
const multiTokenIds = [new BN(1), new BN(2), new BN(3)];
|
||||
const multiTokenAmounts = [new BN(1000), new BN(2000), new BN(3000)];
|
||||
const transferData = '0x12345678';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.multiToken = await ERC1155.new(uri);
|
||||
this.holder = await ERC1155Holder.new();
|
||||
await this.multiToken.$_mintBatch(creator, multiTokenIds, multiTokenAmounts, '0x');
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC165', 'ERC1155Receiver']);
|
||||
|
||||
it('receives ERC1155 tokens from a single ID', async function () {
|
||||
await this.multiToken.safeTransferFrom(
|
||||
creator,
|
||||
this.holder.address,
|
||||
multiTokenIds[0],
|
||||
multiTokenAmounts[0],
|
||||
transferData,
|
||||
{ from: creator },
|
||||
);
|
||||
|
||||
expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[0])).to.be.bignumber.equal(
|
||||
multiTokenAmounts[0],
|
||||
);
|
||||
|
||||
for (let i = 1; i < multiTokenIds.length; i++) {
|
||||
expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[i])).to.be.bignumber.equal(new BN(0));
|
||||
}
|
||||
});
|
||||
|
||||
it('receives ERC1155 tokens from a multiple IDs', async function () {
|
||||
for (let i = 0; i < multiTokenIds.length; i++) {
|
||||
expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[i])).to.be.bignumber.equal(new BN(0));
|
||||
}
|
||||
|
||||
await this.multiToken.safeBatchTransferFrom(
|
||||
creator,
|
||||
this.holder.address,
|
||||
multiTokenIds,
|
||||
multiTokenAmounts,
|
||||
transferData,
|
||||
{ from: creator },
|
||||
);
|
||||
|
||||
for (let i = 0; i < multiTokenIds.length; i++) {
|
||||
expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[i])).to.be.bignumber.equal(
|
||||
multiTokenAmounts[i],
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,322 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ZERO_ADDRESS, MAX_UINT256 } = constants;
|
||||
|
||||
function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipient, anotherAccount) {
|
||||
describe('total supply', function () {
|
||||
it('returns the total amount of tokens', async function () {
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
describe('when the requested account has no tokens', function () {
|
||||
it('returns zero', async function () {
|
||||
expect(await this.token.balanceOf(anotherAccount)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the requested account has some tokens', function () {
|
||||
it('returns the total amount of tokens', async function () {
|
||||
expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfer', function () {
|
||||
shouldBehaveLikeERC20Transfer(errorPrefix, initialHolder, recipient, initialSupply, function (from, to, value) {
|
||||
return this.token.transfer(to, value, { from });
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfer from', function () {
|
||||
const spender = recipient;
|
||||
|
||||
describe('when the token owner is not the zero address', function () {
|
||||
const tokenOwner = initialHolder;
|
||||
|
||||
describe('when the recipient is not the zero address', function () {
|
||||
const to = anotherAccount;
|
||||
|
||||
describe('when the spender has enough allowance', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, initialSupply, { from: initialHolder });
|
||||
});
|
||||
|
||||
describe('when the token owner has enough balance', function () {
|
||||
const amount = initialSupply;
|
||||
|
||||
it('transfers the requested amount', async function () {
|
||||
await this.token.transferFrom(tokenOwner, to, amount, { from: spender });
|
||||
|
||||
expect(await this.token.balanceOf(tokenOwner)).to.be.bignumber.equal('0');
|
||||
|
||||
expect(await this.token.balanceOf(to)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('decreases the spender allowance', async function () {
|
||||
await this.token.transferFrom(tokenOwner, to, amount, { from: spender });
|
||||
|
||||
expect(await this.token.allowance(tokenOwner, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
expectEvent(await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'Transfer', {
|
||||
from: tokenOwner,
|
||||
to: to,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'Approval', {
|
||||
owner: tokenOwner,
|
||||
spender: spender,
|
||||
value: await this.token.allowance(tokenOwner, spender),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the token owner does not have enough balance', function () {
|
||||
const amount = initialSupply;
|
||||
|
||||
beforeEach('reducing balance', async function () {
|
||||
await this.token.transfer(to, 1, { from: tokenOwner });
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.transferFrom(tokenOwner, to, amount, { from: spender }),
|
||||
`${errorPrefix}: transfer amount exceeds balance`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender does not have enough allowance', function () {
|
||||
const allowance = initialSupply.subn(1);
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, allowance, { from: tokenOwner });
|
||||
});
|
||||
|
||||
describe('when the token owner has enough balance', function () {
|
||||
const amount = initialSupply;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.transferFrom(tokenOwner, to, amount, { from: spender }),
|
||||
`${errorPrefix}: insufficient allowance`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the token owner does not have enough balance', function () {
|
||||
const amount = allowance;
|
||||
|
||||
beforeEach('reducing balance', async function () {
|
||||
await this.token.transfer(to, 2, { from: tokenOwner });
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.transferFrom(tokenOwner, to, amount, { from: spender }),
|
||||
`${errorPrefix}: transfer amount exceeds balance`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender has unlimited allowance', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, MAX_UINT256, { from: initialHolder });
|
||||
});
|
||||
|
||||
it('does not decrease the spender allowance', async function () {
|
||||
await this.token.transferFrom(tokenOwner, to, 1, { from: spender });
|
||||
|
||||
expect(await this.token.allowance(tokenOwner, spender)).to.be.bignumber.equal(MAX_UINT256);
|
||||
});
|
||||
|
||||
it('does not emit an approval event', async function () {
|
||||
expectEvent.notEmitted(await this.token.transferFrom(tokenOwner, to, 1, { from: spender }), 'Approval');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the recipient is the zero address', function () {
|
||||
const amount = initialSupply;
|
||||
const to = ZERO_ADDRESS;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, amount, { from: tokenOwner });
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.transferFrom(tokenOwner, to, amount, { from: spender }),
|
||||
`${errorPrefix}: transfer to the zero address`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the token owner is the zero address', function () {
|
||||
const amount = 0;
|
||||
const tokenOwner = ZERO_ADDRESS;
|
||||
const to = recipient;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'from the zero address');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('approve', function () {
|
||||
shouldBehaveLikeERC20Approve(
|
||||
errorPrefix,
|
||||
initialHolder,
|
||||
recipient,
|
||||
initialSupply,
|
||||
function (owner, spender, amount) {
|
||||
return this.token.approve(spender, amount, { from: owner });
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC20Transfer(errorPrefix, from, to, balance, transfer) {
|
||||
describe('when the recipient is not the zero address', function () {
|
||||
describe('when the sender does not have enough balance', function () {
|
||||
const amount = balance.addn(1);
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(transfer.call(this, from, to, amount), `${errorPrefix}: transfer amount exceeds balance`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender transfers all balance', function () {
|
||||
const amount = balance;
|
||||
|
||||
it('transfers the requested amount', async function () {
|
||||
await transfer.call(this, from, to, amount);
|
||||
|
||||
expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0');
|
||||
|
||||
expect(await this.token.balanceOf(to)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
expectEvent(await transfer.call(this, from, to, amount), 'Transfer', { from, to, value: amount });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender transfers zero tokens', function () {
|
||||
const amount = new BN('0');
|
||||
|
||||
it('transfers the requested amount', async function () {
|
||||
await transfer.call(this, from, to, amount);
|
||||
|
||||
expect(await this.token.balanceOf(from)).to.be.bignumber.equal(balance);
|
||||
|
||||
expect(await this.token.balanceOf(to)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
expectEvent(await transfer.call(this, from, to, amount), 'Transfer', { from, to, value: amount });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the recipient is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
transfer.call(this, from, ZERO_ADDRESS, balance),
|
||||
`${errorPrefix}: transfer to the zero address`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC20Approve(errorPrefix, owner, spender, supply, approve) {
|
||||
describe('when the spender is not the zero address', function () {
|
||||
describe('when the sender has enough balance', function () {
|
||||
const amount = supply;
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(await approve.call(this, owner, spender, amount), 'Approval', {
|
||||
owner: owner,
|
||||
spender: spender,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there was no approved amount before', function () {
|
||||
it('approves the requested amount', async function () {
|
||||
await approve.call(this, owner, spender, amount);
|
||||
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender had an approved amount', function () {
|
||||
beforeEach(async function () {
|
||||
await approve.call(this, owner, spender, new BN(1));
|
||||
});
|
||||
|
||||
it('approves the requested amount and replaces the previous one', async function () {
|
||||
await approve.call(this, owner, spender, amount);
|
||||
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender does not have enough balance', function () {
|
||||
const amount = supply.addn(1);
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(await approve.call(this, owner, spender, amount), 'Approval', {
|
||||
owner: owner,
|
||||
spender: spender,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there was no approved amount before', function () {
|
||||
it('approves the requested amount', async function () {
|
||||
await approve.call(this, owner, spender, amount);
|
||||
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender had an approved amount', function () {
|
||||
beforeEach(async function () {
|
||||
await approve.call(this, owner, spender, new BN(1));
|
||||
});
|
||||
|
||||
it('approves the requested amount and replaces the previous one', async function () {
|
||||
await approve.call(this, owner, spender, amount);
|
||||
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
approve.call(this, owner, ZERO_ADDRESS, supply),
|
||||
`${errorPrefix}: approve to the zero address`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC20,
|
||||
shouldBehaveLikeERC20Transfer,
|
||||
shouldBehaveLikeERC20Approve,
|
||||
};
|
||||
@@ -0,0 +1,305 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const {
|
||||
shouldBehaveLikeERC20,
|
||||
shouldBehaveLikeERC20Transfer,
|
||||
shouldBehaveLikeERC20Approve,
|
||||
} = require('./ERC20.behavior');
|
||||
|
||||
const ERC20 = artifacts.require('$ERC20');
|
||||
const ERC20Decimals = artifacts.require('$ERC20DecimalsMock');
|
||||
|
||||
contract('ERC20', function (accounts) {
|
||||
const [initialHolder, recipient, anotherAccount] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20.new(name, symbol);
|
||||
await this.token.$_mint(initialHolder, initialSupply);
|
||||
});
|
||||
|
||||
it('has a name', async function () {
|
||||
expect(await this.token.name()).to.equal(name);
|
||||
});
|
||||
|
||||
it('has a symbol', async function () {
|
||||
expect(await this.token.symbol()).to.equal(symbol);
|
||||
});
|
||||
|
||||
it('has 18 decimals', async function () {
|
||||
expect(await this.token.decimals()).to.be.bignumber.equal('18');
|
||||
});
|
||||
|
||||
describe('set decimals', function () {
|
||||
const decimals = new BN(6);
|
||||
|
||||
it('can set decimals during construction', async function () {
|
||||
const token = await ERC20Decimals.new(name, symbol, decimals);
|
||||
expect(await token.decimals()).to.be.bignumber.equal(decimals);
|
||||
});
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount);
|
||||
|
||||
describe('decrease allowance', function () {
|
||||
describe('when the spender is not the zero address', function () {
|
||||
const spender = recipient;
|
||||
|
||||
function shouldDecreaseApproval(amount) {
|
||||
describe('when there was no approved amount before', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.decreaseAllowance(spender, amount, { from: initialHolder }),
|
||||
'ERC20: decreased allowance below zero',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender had an approved amount', function () {
|
||||
const approvedAmount = amount;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, approvedAmount, { from: initialHolder });
|
||||
});
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(
|
||||
await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }),
|
||||
'Approval',
|
||||
{ owner: initialHolder, spender: spender, value: new BN(0) },
|
||||
);
|
||||
});
|
||||
|
||||
it('decreases the spender allowance subtracting the requested amount', async function () {
|
||||
await this.token.decreaseAllowance(spender, approvedAmount.subn(1), { from: initialHolder });
|
||||
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('sets the allowance to zero when all allowance is removed', async function () {
|
||||
await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder });
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('reverts when more than the full allowance is removed', async function () {
|
||||
await expectRevert(
|
||||
this.token.decreaseAllowance(spender, approvedAmount.addn(1), { from: initialHolder }),
|
||||
'ERC20: decreased allowance below zero',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('when the sender has enough balance', function () {
|
||||
const amount = initialSupply;
|
||||
|
||||
shouldDecreaseApproval(amount);
|
||||
});
|
||||
|
||||
describe('when the sender does not have enough balance', function () {
|
||||
const amount = initialSupply.addn(1);
|
||||
|
||||
shouldDecreaseApproval(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender is the zero address', function () {
|
||||
const amount = initialSupply;
|
||||
const spender = ZERO_ADDRESS;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.decreaseAllowance(spender, amount, { from: initialHolder }),
|
||||
'ERC20: decreased allowance below zero',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('increase allowance', function () {
|
||||
const amount = initialSupply;
|
||||
|
||||
describe('when the spender is not the zero address', function () {
|
||||
const spender = recipient;
|
||||
|
||||
describe('when the sender has enough balance', function () {
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', {
|
||||
owner: initialHolder,
|
||||
spender: spender,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there was no approved amount before', function () {
|
||||
it('approves the requested amount', async function () {
|
||||
await this.token.increaseAllowance(spender, amount, { from: initialHolder });
|
||||
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender had an approved amount', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, new BN(1), { from: initialHolder });
|
||||
});
|
||||
|
||||
it('increases the spender allowance adding the requested amount', async function () {
|
||||
await this.token.increaseAllowance(spender, amount, { from: initialHolder });
|
||||
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender does not have enough balance', function () {
|
||||
const amount = initialSupply.addn(1);
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', {
|
||||
owner: initialHolder,
|
||||
spender: spender,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there was no approved amount before', function () {
|
||||
it('approves the requested amount', async function () {
|
||||
await this.token.increaseAllowance(spender, amount, { from: initialHolder });
|
||||
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender had an approved amount', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, new BN(1), { from: initialHolder });
|
||||
});
|
||||
|
||||
it('increases the spender allowance adding the requested amount', async function () {
|
||||
await this.token.increaseAllowance(spender, amount, { from: initialHolder });
|
||||
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender is the zero address', function () {
|
||||
const spender = ZERO_ADDRESS;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.increaseAllowance(spender, amount, { from: initialHolder }),
|
||||
'ERC20: approve to the zero address',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_mint', function () {
|
||||
const amount = new BN(50);
|
||||
it('rejects a null account', async function () {
|
||||
await expectRevert(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20: mint to the zero address');
|
||||
});
|
||||
|
||||
describe('for a non zero account', function () {
|
||||
beforeEach('minting', async function () {
|
||||
this.receipt = await this.token.$_mint(recipient, amount);
|
||||
});
|
||||
|
||||
it('increments totalSupply', async function () {
|
||||
const expectedSupply = initialSupply.add(amount);
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply);
|
||||
});
|
||||
|
||||
it('increments recipient balance', async function () {
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('emits Transfer event', async function () {
|
||||
const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient });
|
||||
|
||||
expect(event.args.value).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_burn', function () {
|
||||
it('rejects a null account', async function () {
|
||||
await expectRevert(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20: burn from the zero address');
|
||||
});
|
||||
|
||||
describe('for a non zero account', function () {
|
||||
it('rejects burning more than balance', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_burn(initialHolder, initialSupply.addn(1)),
|
||||
'ERC20: burn amount exceeds balance',
|
||||
);
|
||||
});
|
||||
|
||||
const describeBurn = function (description, amount) {
|
||||
describe(description, function () {
|
||||
beforeEach('burning', async function () {
|
||||
this.receipt = await this.token.$_burn(initialHolder, amount);
|
||||
});
|
||||
|
||||
it('decrements totalSupply', async function () {
|
||||
const expectedSupply = initialSupply.sub(amount);
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply);
|
||||
});
|
||||
|
||||
it('decrements initialHolder balance', async function () {
|
||||
const expectedBalance = initialSupply.sub(amount);
|
||||
expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance);
|
||||
});
|
||||
|
||||
it('emits Transfer event', async function () {
|
||||
const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS });
|
||||
|
||||
expect(event.args.value).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describeBurn('for entire balance', initialSupply);
|
||||
describeBurn('for less amount than balance', initialSupply.subn(1));
|
||||
});
|
||||
});
|
||||
|
||||
describe('_transfer', function () {
|
||||
shouldBehaveLikeERC20Transfer('ERC20', initialHolder, recipient, initialSupply, function (from, to, amount) {
|
||||
return this.token.$_transfer(from, to, amount);
|
||||
});
|
||||
|
||||
describe('when the sender is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply),
|
||||
'ERC20: transfer from the zero address',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_approve', function () {
|
||||
shouldBehaveLikeERC20Approve('ERC20', initialHolder, recipient, initialSupply, function (owner, spender, amount) {
|
||||
return this.token.$_approve(owner, spender, amount);
|
||||
});
|
||||
|
||||
describe('when the owner is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_approve(ZERO_ADDRESS, recipient, initialSupply),
|
||||
'ERC20: approve from the zero address',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) {
|
||||
describe('burn', function () {
|
||||
describe('when the given amount is not greater than balance of the sender', function () {
|
||||
context('for a zero amount', function () {
|
||||
shouldBurn(new BN(0));
|
||||
});
|
||||
|
||||
context('for a non-zero amount', function () {
|
||||
shouldBurn(new BN(100));
|
||||
});
|
||||
|
||||
function shouldBurn(amount) {
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.token.burn(amount, { from: owner });
|
||||
});
|
||||
|
||||
it('burns the requested amount', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(initialBalance.sub(amount));
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
expectEvent(this.receipt, 'Transfer', {
|
||||
from: owner,
|
||||
to: ZERO_ADDRESS,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('when the given amount is greater than the balance of the sender', function () {
|
||||
const amount = initialBalance.addn(1);
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.burn(amount, { from: owner }), 'ERC20: burn amount exceeds balance');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('burnFrom', function () {
|
||||
describe('on success', function () {
|
||||
context('for a zero amount', function () {
|
||||
shouldBurnFrom(new BN(0));
|
||||
});
|
||||
|
||||
context('for a non-zero amount', function () {
|
||||
shouldBurnFrom(new BN(100));
|
||||
});
|
||||
|
||||
function shouldBurnFrom(amount) {
|
||||
const originalAllowance = amount.muln(3);
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(burner, originalAllowance, { from: owner });
|
||||
this.receipt = await this.token.burnFrom(owner, amount, { from: burner });
|
||||
});
|
||||
|
||||
it('burns the requested amount', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(initialBalance.sub(amount));
|
||||
});
|
||||
|
||||
it('decrements allowance', async function () {
|
||||
expect(await this.token.allowance(owner, burner)).to.be.bignumber.equal(originalAllowance.sub(amount));
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
expectEvent(this.receipt, 'Transfer', {
|
||||
from: owner,
|
||||
to: ZERO_ADDRESS,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('when the given amount is greater than the balance of the sender', function () {
|
||||
const amount = initialBalance.addn(1);
|
||||
|
||||
it('reverts', async function () {
|
||||
await this.token.approve(burner, amount, { from: owner });
|
||||
await expectRevert(this.token.burnFrom(owner, amount, { from: burner }), 'ERC20: burn amount exceeds balance');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given amount is greater than the allowance', function () {
|
||||
const allowance = new BN(100);
|
||||
|
||||
it('reverts', async function () {
|
||||
await this.token.approve(burner, allowance, { from: owner });
|
||||
await expectRevert(
|
||||
this.token.burnFrom(owner, allowance.addn(1), { from: burner }),
|
||||
'ERC20: insufficient allowance',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC20Burnable,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
const { BN } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { shouldBehaveLikeERC20Burnable } = require('./ERC20Burnable.behavior');
|
||||
const ERC20Burnable = artifacts.require('$ERC20Burnable');
|
||||
|
||||
contract('ERC20Burnable', function (accounts) {
|
||||
const [owner, ...otherAccounts] = accounts;
|
||||
|
||||
const initialBalance = new BN(1000);
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20Burnable.new(name, symbol, { from: owner });
|
||||
await this.token.$_mint(owner, initialBalance);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20Burnable(owner, initialBalance, otherAccounts);
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
const { expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
function shouldBehaveLikeERC20Capped(accounts, cap) {
|
||||
describe('capped token', function () {
|
||||
const user = accounts[0];
|
||||
|
||||
it('starts with the correct cap', async function () {
|
||||
expect(await this.token.cap()).to.be.bignumber.equal(cap);
|
||||
});
|
||||
|
||||
it('mints when amount is less than cap', async function () {
|
||||
await this.token.$_mint(user, cap.subn(1));
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(cap.subn(1));
|
||||
});
|
||||
|
||||
it('fails to mint if the amount exceeds the cap', async function () {
|
||||
await this.token.$_mint(user, cap.subn(1));
|
||||
await expectRevert(this.token.$_mint(user, 2), 'ERC20Capped: cap exceeded');
|
||||
});
|
||||
|
||||
it('fails to mint after cap is reached', async function () {
|
||||
await this.token.$_mint(user, cap);
|
||||
await expectRevert(this.token.$_mint(user, 1), 'ERC20Capped: cap exceeded');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC20Capped,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
const { ether, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { shouldBehaveLikeERC20Capped } = require('./ERC20Capped.behavior');
|
||||
|
||||
const ERC20Capped = artifacts.require('$ERC20Capped');
|
||||
|
||||
contract('ERC20Capped', function (accounts) {
|
||||
const cap = ether('1000');
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
it('requires a non-zero cap', async function () {
|
||||
await expectRevert(ERC20Capped.new(name, symbol, 0), 'ERC20Capped: cap is 0');
|
||||
});
|
||||
|
||||
context('once deployed', async function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20Capped.new(name, symbol, cap);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20Capped(accounts, cap);
|
||||
});
|
||||
});
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { MAX_UINT256, ZERO_ADDRESS } = constants;
|
||||
|
||||
const ERC20FlashMintMock = artifacts.require('$ERC20FlashMintMock');
|
||||
const ERC3156FlashBorrowerMock = artifacts.require('ERC3156FlashBorrowerMock');
|
||||
|
||||
contract('ERC20FlashMint', function (accounts) {
|
||||
const [initialHolder, other, anotherAccount] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
const loanAmount = new BN(10000000000000);
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20FlashMintMock.new(name, symbol);
|
||||
await this.token.$_mint(initialHolder, initialSupply);
|
||||
});
|
||||
|
||||
describe('maxFlashLoan', function () {
|
||||
it('token match', async function () {
|
||||
expect(await this.token.maxFlashLoan(this.token.address)).to.be.bignumber.equal(MAX_UINT256.sub(initialSupply));
|
||||
});
|
||||
|
||||
it('token mismatch', async function () {
|
||||
expect(await this.token.maxFlashLoan(ZERO_ADDRESS)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flashFee', function () {
|
||||
it('token match', async function () {
|
||||
expect(await this.token.flashFee(this.token.address, loanAmount)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('token mismatch', async function () {
|
||||
await expectRevert(this.token.flashFee(ZERO_ADDRESS, loanAmount), 'ERC20FlashMint: wrong token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flashFeeReceiver', function () {
|
||||
it('default receiver', async function () {
|
||||
expect(await this.token.$_flashFeeReceiver()).to.be.eq(ZERO_ADDRESS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flashLoan', function () {
|
||||
it('success', async function () {
|
||||
const receiver = await ERC3156FlashBorrowerMock.new(true, true);
|
||||
const { tx } = await this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x');
|
||||
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: receiver.address,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: receiver.address,
|
||||
to: ZERO_ADDRESS,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, receiver, 'BalanceOf', {
|
||||
token: this.token.address,
|
||||
account: receiver.address,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, receiver, 'TotalSupply', {
|
||||
token: this.token.address,
|
||||
value: initialSupply.add(loanAmount),
|
||||
});
|
||||
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
|
||||
expect(await this.token.balanceOf(receiver.address)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.allowance(receiver.address, this.token.address)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('missing return value', async function () {
|
||||
const receiver = await ERC3156FlashBorrowerMock.new(false, true);
|
||||
await expectRevert(
|
||||
this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'),
|
||||
'ERC20FlashMint: invalid return value',
|
||||
);
|
||||
});
|
||||
|
||||
it('missing approval', async function () {
|
||||
const receiver = await ERC3156FlashBorrowerMock.new(true, false);
|
||||
await expectRevert(
|
||||
this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'),
|
||||
'ERC20: insufficient allowance',
|
||||
);
|
||||
});
|
||||
|
||||
it('unavailable funds', async function () {
|
||||
const receiver = await ERC3156FlashBorrowerMock.new(true, true);
|
||||
const data = this.token.contract.methods.transfer(other, 10).encodeABI();
|
||||
await expectRevert(
|
||||
this.token.flashLoan(receiver.address, this.token.address, loanAmount, data),
|
||||
'ERC20: burn amount exceeds balance',
|
||||
);
|
||||
});
|
||||
|
||||
it('more than maxFlashLoan', async function () {
|
||||
const receiver = await ERC3156FlashBorrowerMock.new(true, true);
|
||||
const data = this.token.contract.methods.transfer(other, 10).encodeABI();
|
||||
// _mint overflow reverts using a panic code. No reason string.
|
||||
await expectRevert.unspecified(this.token.flashLoan(receiver.address, this.token.address, MAX_UINT256, data));
|
||||
});
|
||||
|
||||
describe('custom flash fee & custom fee receiver', function () {
|
||||
const receiverInitialBalance = new BN(200000);
|
||||
const flashFee = new BN(5000);
|
||||
|
||||
beforeEach('init receiver balance & set flash fee', async function () {
|
||||
this.receiver = await ERC3156FlashBorrowerMock.new(true, true);
|
||||
const receipt = await this.token.$_mint(this.receiver.address, receiverInitialBalance);
|
||||
await expectEvent(receipt, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: this.receiver.address,
|
||||
value: receiverInitialBalance,
|
||||
});
|
||||
expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(receiverInitialBalance);
|
||||
|
||||
await this.token.setFlashFee(flashFee);
|
||||
expect(await this.token.flashFee(this.token.address, loanAmount)).to.be.bignumber.equal(flashFee);
|
||||
});
|
||||
|
||||
it('default flash fee receiver', async function () {
|
||||
const { tx } = await this.token.flashLoan(this.receiver.address, this.token.address, loanAmount, '0x');
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: this.receiver.address,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: this.receiver.address,
|
||||
to: ZERO_ADDRESS,
|
||||
value: loanAmount.add(flashFee),
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.receiver, 'BalanceOf', {
|
||||
token: this.token.address,
|
||||
account: this.receiver.address,
|
||||
value: receiverInitialBalance.add(loanAmount),
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.receiver, 'TotalSupply', {
|
||||
token: this.token.address,
|
||||
value: initialSupply.add(receiverInitialBalance).add(loanAmount),
|
||||
});
|
||||
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(
|
||||
initialSupply.add(receiverInitialBalance).sub(flashFee),
|
||||
);
|
||||
expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(
|
||||
receiverInitialBalance.sub(flashFee),
|
||||
);
|
||||
expect(await this.token.balanceOf(await this.token.$_flashFeeReceiver())).to.be.bignumber.equal('0');
|
||||
expect(await this.token.allowance(this.receiver.address, this.token.address)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('custom flash fee receiver', async function () {
|
||||
const flashFeeReceiverAddress = anotherAccount;
|
||||
await this.token.setFlashFeeReceiver(flashFeeReceiverAddress);
|
||||
expect(await this.token.$_flashFeeReceiver()).to.be.eq(flashFeeReceiverAddress);
|
||||
|
||||
expect(await this.token.balanceOf(flashFeeReceiverAddress)).to.be.bignumber.equal('0');
|
||||
|
||||
const { tx } = await this.token.flashLoan(this.receiver.address, this.token.address, loanAmount, '0x');
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: this.receiver.address,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: this.receiver.address,
|
||||
to: ZERO_ADDRESS,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: this.receiver.address,
|
||||
to: flashFeeReceiverAddress,
|
||||
value: flashFee,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.receiver, 'BalanceOf', {
|
||||
token: this.token.address,
|
||||
account: this.receiver.address,
|
||||
value: receiverInitialBalance.add(loanAmount),
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.receiver, 'TotalSupply', {
|
||||
token: this.token.address,
|
||||
value: initialSupply.add(receiverInitialBalance).add(loanAmount),
|
||||
});
|
||||
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply.add(receiverInitialBalance));
|
||||
expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(
|
||||
receiverInitialBalance.sub(flashFee),
|
||||
);
|
||||
expect(await this.token.balanceOf(flashFeeReceiverAddress)).to.be.bignumber.equal(flashFee);
|
||||
expect(await this.token.allowance(this.receiver.address, flashFeeReceiverAddress)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
const { BN, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC20Pausable = artifacts.require('$ERC20Pausable');
|
||||
|
||||
contract('ERC20Pausable', function (accounts) {
|
||||
const [holder, recipient, anotherAccount] = accounts;
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20Pausable.new(name, symbol);
|
||||
await this.token.$_mint(holder, initialSupply);
|
||||
});
|
||||
|
||||
describe('pausable token', function () {
|
||||
describe('transfer', function () {
|
||||
it('allows to transfer when unpaused', async function () {
|
||||
await this.token.transfer(recipient, initialSupply, { from: holder });
|
||||
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
it('allows to transfer when paused and then unpaused', async function () {
|
||||
await this.token.$_pause();
|
||||
await this.token.$_unpause();
|
||||
|
||||
await this.token.transfer(recipient, initialSupply, { from: holder });
|
||||
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
it('reverts when trying to transfer when paused', async function () {
|
||||
await this.token.$_pause();
|
||||
|
||||
await expectRevert(
|
||||
this.token.transfer(recipient, initialSupply, { from: holder }),
|
||||
'ERC20Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfer from', function () {
|
||||
const allowance = new BN(40);
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(anotherAccount, allowance, { from: holder });
|
||||
});
|
||||
|
||||
it('allows to transfer from when unpaused', async function () {
|
||||
await this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount });
|
||||
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(allowance);
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(allowance));
|
||||
});
|
||||
|
||||
it('allows to transfer when paused and then unpaused', async function () {
|
||||
await this.token.$_pause();
|
||||
await this.token.$_unpause();
|
||||
|
||||
await this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount });
|
||||
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(allowance);
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(allowance));
|
||||
});
|
||||
|
||||
it('reverts when trying to transfer from when paused', async function () {
|
||||
await this.token.$_pause();
|
||||
|
||||
await expectRevert(
|
||||
this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount }),
|
||||
'ERC20Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mint', function () {
|
||||
const amount = new BN('42');
|
||||
|
||||
it('allows to mint when unpaused', async function () {
|
||||
await this.token.$_mint(recipient, amount);
|
||||
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('allows to mint when paused and then unpaused', async function () {
|
||||
await this.token.$_pause();
|
||||
await this.token.$_unpause();
|
||||
|
||||
await this.token.$_mint(recipient, amount);
|
||||
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('reverts when trying to mint when paused', async function () {
|
||||
await this.token.$_pause();
|
||||
|
||||
await expectRevert(this.token.$_mint(recipient, amount), 'ERC20Pausable: token transfer while paused');
|
||||
});
|
||||
});
|
||||
|
||||
describe('burn', function () {
|
||||
const amount = new BN('42');
|
||||
|
||||
it('allows to burn when unpaused', async function () {
|
||||
await this.token.$_burn(holder, amount);
|
||||
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(amount));
|
||||
});
|
||||
|
||||
it('allows to burn when paused and then unpaused', async function () {
|
||||
await this.token.$_pause();
|
||||
await this.token.$_unpause();
|
||||
|
||||
await this.token.$_burn(holder, amount);
|
||||
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(amount));
|
||||
});
|
||||
|
||||
it('reverts when trying to burn when paused', async function () {
|
||||
await this.token.$_pause();
|
||||
|
||||
await expectRevert(this.token.$_burn(holder, amount), 'ERC20Pausable: token transfer while paused');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const ERC20Snapshot = artifacts.require('$ERC20Snapshot');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
contract('ERC20Snapshot', function (accounts) {
|
||||
const [initialHolder, recipient, other] = accounts;
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20Snapshot.new(name, symbol);
|
||||
await this.token.$_mint(initialHolder, initialSupply);
|
||||
});
|
||||
|
||||
describe('snapshot', function () {
|
||||
it('emits a snapshot event', async function () {
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot');
|
||||
});
|
||||
|
||||
it('creates increasing snapshots ids, starting from 1', async function () {
|
||||
for (const id of ['1', '2', '3', '4', '5']) {
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('totalSupplyAt', function () {
|
||||
it('reverts with a snapshot id of 0', async function () {
|
||||
await expectRevert(this.token.totalSupplyAt(0), 'ERC20Snapshot: id is 0');
|
||||
});
|
||||
|
||||
it('reverts with a not-yet-created snapshot id', async function () {
|
||||
await expectRevert(this.token.totalSupplyAt(1), 'ERC20Snapshot: nonexistent id');
|
||||
});
|
||||
|
||||
context('with initial snapshot', function () {
|
||||
beforeEach(async function () {
|
||||
this.initialSnapshotId = new BN('1');
|
||||
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id: this.initialSnapshotId });
|
||||
});
|
||||
|
||||
context('with no supply changes after the snapshot', function () {
|
||||
it('returns the current total supply', async function () {
|
||||
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
});
|
||||
|
||||
context('with supply changes after the snapshot', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(other, new BN('50'));
|
||||
await this.token.$_burn(initialHolder, new BN('20'));
|
||||
});
|
||||
|
||||
it('returns the total supply before the changes', async function () {
|
||||
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
context('with a second snapshot after supply changes', function () {
|
||||
beforeEach(async function () {
|
||||
this.secondSnapshotId = new BN('2');
|
||||
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id: this.secondSnapshotId });
|
||||
});
|
||||
|
||||
it('snapshots return the supply before and after the changes', async function () {
|
||||
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
|
||||
|
||||
expect(await this.token.totalSupplyAt(this.secondSnapshotId)).to.be.bignumber.equal(
|
||||
await this.token.totalSupply(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with multiple snapshots after supply changes', function () {
|
||||
beforeEach(async function () {
|
||||
this.secondSnapshotIds = ['2', '3', '4'];
|
||||
|
||||
for (const id of this.secondSnapshotIds) {
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id });
|
||||
}
|
||||
});
|
||||
|
||||
it('all posterior snapshots return the supply after the changes', async function () {
|
||||
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
|
||||
|
||||
const currentSupply = await this.token.totalSupply();
|
||||
|
||||
for (const id of this.secondSnapshotIds) {
|
||||
expect(await this.token.totalSupplyAt(id)).to.be.bignumber.equal(currentSupply);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('balanceOfAt', function () {
|
||||
it('reverts with a snapshot id of 0', async function () {
|
||||
await expectRevert(this.token.balanceOfAt(other, 0), 'ERC20Snapshot: id is 0');
|
||||
});
|
||||
|
||||
it('reverts with a not-yet-created snapshot id', async function () {
|
||||
await expectRevert(this.token.balanceOfAt(other, 1), 'ERC20Snapshot: nonexistent id');
|
||||
});
|
||||
|
||||
context('with initial snapshot', function () {
|
||||
beforeEach(async function () {
|
||||
this.initialSnapshotId = new BN('1');
|
||||
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id: this.initialSnapshotId });
|
||||
});
|
||||
|
||||
context('with no balance changes after the snapshot', function () {
|
||||
it('returns the current balance for all accounts', async function () {
|
||||
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
|
||||
initialSupply,
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('with balance changes after the snapshot', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.transfer(recipient, new BN('10'), { from: initialHolder });
|
||||
await this.token.$_mint(other, new BN('50'));
|
||||
await this.token.$_burn(initialHolder, new BN('20'));
|
||||
});
|
||||
|
||||
it('returns the balances before the changes', async function () {
|
||||
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
|
||||
initialSupply,
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
context('with a second snapshot after supply changes', function () {
|
||||
beforeEach(async function () {
|
||||
this.secondSnapshotId = new BN('2');
|
||||
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id: this.secondSnapshotId });
|
||||
});
|
||||
|
||||
it('snapshots return the balances before and after the changes', async function () {
|
||||
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
|
||||
initialSupply,
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
|
||||
expect(await this.token.balanceOfAt(initialHolder, this.secondSnapshotId)).to.be.bignumber.equal(
|
||||
await this.token.balanceOf(initialHolder),
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, this.secondSnapshotId)).to.be.bignumber.equal(
|
||||
await this.token.balanceOf(recipient),
|
||||
);
|
||||
expect(await this.token.balanceOfAt(other, this.secondSnapshotId)).to.be.bignumber.equal(
|
||||
await this.token.balanceOf(other),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with multiple snapshots after supply changes', function () {
|
||||
beforeEach(async function () {
|
||||
this.secondSnapshotIds = ['2', '3', '4'];
|
||||
|
||||
for (const id of this.secondSnapshotIds) {
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id });
|
||||
}
|
||||
});
|
||||
|
||||
it('all posterior snapshots return the supply after the changes', async function () {
|
||||
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
|
||||
initialSupply,
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
|
||||
for (const id of this.secondSnapshotIds) {
|
||||
expect(await this.token.balanceOfAt(initialHolder, id)).to.be.bignumber.equal(
|
||||
await this.token.balanceOf(initialHolder),
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, id)).to.be.bignumber.equal(
|
||||
await this.token.balanceOf(recipient),
|
||||
);
|
||||
expect(await this.token.balanceOfAt(other, id)).to.be.bignumber.equal(await this.token.balanceOf(other));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,578 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { MAX_UINT256, ZERO_ADDRESS } = constants;
|
||||
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
|
||||
const { batchInBlock } = require('../../../helpers/txpool');
|
||||
const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712');
|
||||
const { clock, clockFromReceipt } = require('../../../helpers/time');
|
||||
|
||||
const { shouldBehaveLikeEIP6372 } = require('../../../governance/utils/EIP6372.behavior');
|
||||
|
||||
const Delegation = [
|
||||
{ name: 'delegatee', type: 'address' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'expiry', type: 'uint256' },
|
||||
];
|
||||
|
||||
const MODES = {
|
||||
blocknumber: artifacts.require('$ERC20Votes'),
|
||||
timestamp: artifacts.require('$ERC20VotesTimestampMock'),
|
||||
};
|
||||
|
||||
contract('ERC20Votes', function (accounts) {
|
||||
const [holder, recipient, holderDelegatee, other1, other2] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
const supply = new BN('10000000000000000000000000');
|
||||
|
||||
for (const [mode, artifact] of Object.entries(MODES)) {
|
||||
describe(`vote with ${mode}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await artifact.new(name, symbol, name);
|
||||
});
|
||||
|
||||
shouldBehaveLikeEIP6372(mode);
|
||||
|
||||
it('initial nonce is 0', async function () {
|
||||
expect(await this.token.nonces(holder)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('domain separator', async function () {
|
||||
expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
|
||||
});
|
||||
|
||||
it('minting restriction', async function () {
|
||||
const amount = new BN('2').pow(new BN('224'));
|
||||
await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes');
|
||||
});
|
||||
|
||||
it('recent checkpoints', async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await this.token.$_mint(holder, 1);
|
||||
}
|
||||
const block = await clock[mode]();
|
||||
expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6');
|
||||
// recent
|
||||
expect(await this.token.getPastVotes(holder, block - 1)).to.be.bignumber.equal('5');
|
||||
// non-recent
|
||||
expect(await this.token.getPastVotes(holder, block - 6)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
describe('set delegation', function () {
|
||||
describe('call', function () {
|
||||
it('delegation with balance', async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegate(holder, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: holder,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
|
||||
expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('delegation without balance', async function () {
|
||||
expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegate(holder, { from: holder });
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: holder,
|
||||
});
|
||||
expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with signature', function () {
|
||||
const delegator = Wallet.generate();
|
||||
const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString());
|
||||
const nonce = 0;
|
||||
|
||||
const buildData = (contract, message) =>
|
||||
getDomain(contract).then(domain => ({
|
||||
primaryType: 'Delegation',
|
||||
types: { EIP712Domain: domainType(domain), Delegation },
|
||||
domain,
|
||||
message,
|
||||
}));
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(delegatorAddress, supply);
|
||||
});
|
||||
|
||||
it('accept signed delegation', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: delegatorAddress,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: delegatorAddress,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: delegatorAddress,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
|
||||
|
||||
expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPastVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('rejects reused signature', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
|
||||
'ERC20Votes: invalid nonce',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects bad delegatee', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s);
|
||||
const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged');
|
||||
expect(args.delegator).to.not.be.equal(delegatorAddress);
|
||||
expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
|
||||
expect(args.toDelegate).to.be.equal(holderDelegatee);
|
||||
});
|
||||
|
||||
it('rejects bad nonce', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
|
||||
'ERC20Votes: invalid nonce',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects expired permit', async function () {
|
||||
const expiry = (await time.latest()) - time.duration.weeks(1);
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
|
||||
'ERC20Votes: signature expired',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('change delegation', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
});
|
||||
|
||||
it('call', async function () {
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
|
||||
const { receipt } = await this.token.delegate(holderDelegatee, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: holder,
|
||||
toDelegate: holderDelegatee,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: '0',
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holderDelegatee,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
|
||||
|
||||
expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfers', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
});
|
||||
|
||||
it('no delegation', async function () {
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
|
||||
|
||||
this.holderVotes = '0';
|
||||
this.recipientVotes = '0';
|
||||
});
|
||||
|
||||
it('sender delegation', async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: supply.subn(1),
|
||||
});
|
||||
|
||||
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
|
||||
expect(
|
||||
receipt.logs
|
||||
.filter(({ event }) => event == 'DelegateVotesChanged')
|
||||
.every(({ logIndex }) => transferLogIndex < logIndex),
|
||||
).to.be.equal(true);
|
||||
|
||||
this.holderVotes = supply.subn(1);
|
||||
this.recipientVotes = '0';
|
||||
});
|
||||
|
||||
it('receiver delegation', async function () {
|
||||
await this.token.delegate(recipient, { from: recipient });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
|
||||
|
||||
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
|
||||
expect(
|
||||
receipt.logs
|
||||
.filter(({ event }) => event == 'DelegateVotesChanged')
|
||||
.every(({ logIndex }) => transferLogIndex < logIndex),
|
||||
).to.be.equal(true);
|
||||
|
||||
this.holderVotes = '0';
|
||||
this.recipientVotes = '1';
|
||||
});
|
||||
|
||||
it('full delegation', async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
await this.token.delegate(recipient, { from: recipient });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: supply.subn(1),
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
|
||||
|
||||
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
|
||||
expect(
|
||||
receipt.logs
|
||||
.filter(({ event }) => event == 'DelegateVotesChanged')
|
||||
.every(({ logIndex }) => transferLogIndex < logIndex),
|
||||
).to.be.equal(true);
|
||||
|
||||
this.holderVotes = supply.subn(1);
|
||||
this.recipientVotes = '1';
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes);
|
||||
expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
|
||||
|
||||
// need to advance 2 blocks to see the effect of a transfer on "getPastVotes"
|
||||
const timepoint = await clock[mode]();
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes);
|
||||
expect(await this.token.getPastVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes);
|
||||
});
|
||||
});
|
||||
|
||||
// The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
|
||||
describe('Compound test suite', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
it('grants to initial account', async function () {
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numCheckpoints', function () {
|
||||
it('returns the number of checkpoints for a delegate', async function () {
|
||||
await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
|
||||
|
||||
const t1 = await this.token.delegate(other1, { from: recipient });
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
|
||||
|
||||
const t2 = await this.token.transfer(other2, 10, { from: recipient });
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
|
||||
|
||||
const t3 = await this.token.transfer(other2, 10, { from: recipient });
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3');
|
||||
|
||||
const t4 = await this.token.transfer(recipient, 20, { from: holder });
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4');
|
||||
|
||||
expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']);
|
||||
expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']);
|
||||
expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']);
|
||||
expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']);
|
||||
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal('100');
|
||||
expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal('90');
|
||||
expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal('80');
|
||||
expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal('100');
|
||||
});
|
||||
|
||||
it('does not add more than one checkpoint in a block', async function () {
|
||||
await this.token.transfer(recipient, '100', { from: holder });
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
|
||||
|
||||
const [t1, t2, t3] = await batchInBlock([
|
||||
() => this.token.delegate(other1, { from: recipient, gas: 100000 }),
|
||||
() => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
|
||||
() => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
|
||||
]);
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']);
|
||||
|
||||
const t4 = await this.token.transfer(recipient, 20, { from: holder });
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
|
||||
expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPastVotes', function () {
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.token.getPastVotes(other1, 5e10), 'ERC20Votes: future lookup');
|
||||
});
|
||||
|
||||
it('returns 0 if there are no checkpoints', async function () {
|
||||
expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns the latest block if >= last checkpoint block', async function () {
|
||||
const { receipt } = await this.token.delegate(other1, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastVotes(other1, timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns zero if < first checkpoint block', async function () {
|
||||
await time.advanceBlock();
|
||||
const { receipt } = await this.token.delegate(other1, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastVotes(other1, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('generally returns the voting balance at the appropriate checkpoint', async function () {
|
||||
const t1 = await this.token.delegate(other1, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t2 = await this.token.transfer(other2, 10, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t3 = await this.token.transfer(other2, 10, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t4 = await this.token.transfer(holder, 20, { from: other2 });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.getPastVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPastTotalSupply', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
});
|
||||
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: future lookup');
|
||||
});
|
||||
|
||||
it('returns 0 if there are no checkpoints', async function () {
|
||||
expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns the latest block if >= last checkpoint block', async function () {
|
||||
const { receipt } = await this.token.$_mint(holder, supply);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('returns zero if < first checkpoint block', async function () {
|
||||
await time.advanceBlock();
|
||||
const { receipt } = await this.token.$_mint(holder, supply);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('generally returns the voting balance at the appropriate checkpoint', async function () {
|
||||
const t1 = await this.token.$_mint(holder, supply);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t2 = await this.token.$_burn(holder, 10);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t3 = await this.token.$_burn(holder, 10);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t4 = await this.token.$_mint(holder, 20);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990');
|
||||
expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980');
|
||||
expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
+543
@@ -0,0 +1,543 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { MAX_UINT256, ZERO_ADDRESS } = constants;
|
||||
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
|
||||
const { batchInBlock } = require('../../../helpers/txpool');
|
||||
const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712');
|
||||
const { clock, clockFromReceipt } = require('../../../helpers/time');
|
||||
|
||||
const { shouldBehaveLikeEIP6372 } = require('../../../governance/utils/EIP6372.behavior');
|
||||
|
||||
const Delegation = [
|
||||
{ name: 'delegatee', type: 'address' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'expiry', type: 'uint256' },
|
||||
];
|
||||
|
||||
const MODES = {
|
||||
blocknumber: artifacts.require('$ERC20VotesComp'),
|
||||
// no timestamp mode for ERC20VotesComp yet
|
||||
};
|
||||
|
||||
contract('ERC20VotesComp', function (accounts) {
|
||||
const [holder, recipient, holderDelegatee, other1, other2] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
const supply = new BN('10000000000000000000000000');
|
||||
|
||||
for (const [mode, artifact] of Object.entries(MODES)) {
|
||||
describe(`vote with ${mode}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await artifact.new(name, symbol, name);
|
||||
});
|
||||
|
||||
shouldBehaveLikeEIP6372(mode);
|
||||
|
||||
it('initial nonce is 0', async function () {
|
||||
expect(await this.token.nonces(holder)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('domain separator', async function () {
|
||||
expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
|
||||
});
|
||||
|
||||
it('minting restriction', async function () {
|
||||
const amount = new BN('2').pow(new BN('96'));
|
||||
await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes');
|
||||
});
|
||||
|
||||
describe('set delegation', function () {
|
||||
describe('call', function () {
|
||||
it('delegation with balance', async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegate(holder, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: holder,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
|
||||
expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPriorVotes(holder, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('delegation without balance', async function () {
|
||||
expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegate(holder, { from: holder });
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: holder,
|
||||
});
|
||||
expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with signature', function () {
|
||||
const delegator = Wallet.generate();
|
||||
const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString());
|
||||
const nonce = 0;
|
||||
|
||||
const buildData = (contract, message) =>
|
||||
getDomain(contract).then(domain => ({
|
||||
primaryType: 'Delegation',
|
||||
types: { EIP712Domain: domainType(domain), Delegation },
|
||||
domain,
|
||||
message,
|
||||
}));
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(delegatorAddress, supply);
|
||||
});
|
||||
|
||||
it('accept signed delegation', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: delegatorAddress,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: delegatorAddress,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: delegatorAddress,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
|
||||
|
||||
expect(await this.token.getCurrentVotes(delegatorAddress)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPriorVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPriorVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('rejects reused signature', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
|
||||
'ERC20Votes: invalid nonce',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects bad delegatee', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s);
|
||||
const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged');
|
||||
expect(args.delegator).to.not.be.equal(delegatorAddress);
|
||||
expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
|
||||
expect(args.toDelegate).to.be.equal(holderDelegatee);
|
||||
});
|
||||
|
||||
it('rejects bad nonce', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
|
||||
'ERC20Votes: invalid nonce',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects expired permit', async function () {
|
||||
const expiry = (await time.latest()) - time.duration.weeks(1);
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
|
||||
'ERC20Votes: signature expired',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('change delegation', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
});
|
||||
|
||||
it('call', async function () {
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
|
||||
const { receipt } = await this.token.delegate(holderDelegatee, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: holder,
|
||||
toDelegate: holderDelegatee,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: '0',
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holderDelegatee,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
|
||||
|
||||
expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getCurrentVotes(holderDelegatee)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPriorVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPriorVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPriorVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfers', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
});
|
||||
|
||||
it('no delegation', async function () {
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
|
||||
|
||||
this.holderVotes = '0';
|
||||
this.recipientVotes = '0';
|
||||
});
|
||||
|
||||
it('sender delegation', async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: supply.subn(1),
|
||||
});
|
||||
|
||||
this.holderVotes = supply.subn(1);
|
||||
this.recipientVotes = '0';
|
||||
});
|
||||
|
||||
it('receiver delegation', async function () {
|
||||
await this.token.delegate(recipient, { from: recipient });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
|
||||
|
||||
this.holderVotes = '0';
|
||||
this.recipientVotes = '1';
|
||||
});
|
||||
|
||||
it('full delegation', async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
await this.token.delegate(recipient, { from: recipient });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: supply.subn(1),
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
|
||||
|
||||
this.holderVotes = supply.subn(1);
|
||||
this.recipientVotes = '1';
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(this.holderVotes);
|
||||
expect(await this.token.getCurrentVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
|
||||
|
||||
// need to advance 2 blocks to see the effect of a transfer on "getPriorVotes"
|
||||
const timepoint = await clock[mode]();
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes);
|
||||
expect(await this.token.getPriorVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes);
|
||||
});
|
||||
});
|
||||
|
||||
// The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
|
||||
describe('Compound test suite', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
it('grants to initial account', async function () {
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numCheckpoints', function () {
|
||||
it('returns the number of checkpoints for a delegate', async function () {
|
||||
await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
|
||||
|
||||
const t1 = await this.token.delegate(other1, { from: recipient });
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
|
||||
|
||||
const t2 = await this.token.transfer(other2, 10, { from: recipient });
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
|
||||
|
||||
const t3 = await this.token.transfer(other2, 10, { from: recipient });
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3');
|
||||
|
||||
const t4 = await this.token.transfer(recipient, 20, { from: holder });
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4');
|
||||
|
||||
expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']);
|
||||
expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']);
|
||||
expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']);
|
||||
expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']);
|
||||
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal('100');
|
||||
expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal('90');
|
||||
expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal('80');
|
||||
expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal('100');
|
||||
});
|
||||
|
||||
it('does not add more than one checkpoint in a block', async function () {
|
||||
await this.token.transfer(recipient, '100', { from: holder });
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
|
||||
|
||||
const [t1, t2, t3] = await batchInBlock([
|
||||
() => this.token.delegate(other1, { from: recipient, gas: 100000 }),
|
||||
() => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
|
||||
() => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
|
||||
]);
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']);
|
||||
|
||||
const t4 = await this.token.transfer(recipient, 20, { from: holder });
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
|
||||
expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPriorVotes', function () {
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.token.getPriorVotes(other1, 5e10), 'ERC20Votes: future lookup');
|
||||
});
|
||||
|
||||
it('returns 0 if there are no checkpoints', async function () {
|
||||
expect(await this.token.getPriorVotes(other1, 0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns the latest block if >= last checkpoint block', async function () {
|
||||
const { receipt } = await this.token.delegate(other1, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPriorVotes(other1, timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns zero if < first checkpoint block', async function () {
|
||||
await time.advanceBlock();
|
||||
const { receipt } = await this.token.delegate(other1, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPriorVotes(other1, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('generally returns the voting balance at the appropriate checkpoint', async function () {
|
||||
const t1 = await this.token.delegate(other1, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t2 = await this.token.transfer(other2, 10, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t3 = await this.token.transfer(other2, 10, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t4 = await this.token.transfer(holder, 20, { from: other2 });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.getPriorVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPastTotalSupply', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
});
|
||||
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: future lookup');
|
||||
});
|
||||
|
||||
it('returns 0 if there are no checkpoints', async function () {
|
||||
expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns the latest block if >= last checkpoint block', async function () {
|
||||
const { receipt } = await this.token.$_mint(holder, supply);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('returns zero if < first checkpoint block', async function () {
|
||||
await time.advanceBlock();
|
||||
const { receipt } = await this.token.$_mint(holder, supply);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('generally returns the voting balance at the appropriate checkpoint', async function () {
|
||||
const t1 = await this.token.$_mint(holder, supply);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t2 = await this.token.$_burn(holder, 10);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t3 = await this.token.$_burn(holder, 10);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t4 = await this.token.$_mint(holder, 20);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990');
|
||||
expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980');
|
||||
expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ZERO_ADDRESS, MAX_UINT256 } = constants;
|
||||
|
||||
const { shouldBehaveLikeERC20 } = require('../ERC20.behavior');
|
||||
|
||||
const NotAnERC20 = artifacts.require('CallReceiverMock');
|
||||
const ERC20Decimals = artifacts.require('$ERC20DecimalsMock');
|
||||
const ERC20Wrapper = artifacts.require('$ERC20Wrapper');
|
||||
|
||||
contract('ERC20', function (accounts) {
|
||||
const [initialHolder, recipient, anotherAccount] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
|
||||
beforeEach(async function () {
|
||||
this.underlying = await ERC20Decimals.new(name, symbol, 9);
|
||||
await this.underlying.$_mint(initialHolder, initialSupply);
|
||||
|
||||
this.token = await ERC20Wrapper.new(`Wrapped ${name}`, `W${symbol}`, this.underlying.address);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
expect(await this.underlying.balanceOf(this.token.address)).to.be.bignumber.equal(await this.token.totalSupply());
|
||||
});
|
||||
|
||||
it('has a name', async function () {
|
||||
expect(await this.token.name()).to.equal(`Wrapped ${name}`);
|
||||
});
|
||||
|
||||
it('has a symbol', async function () {
|
||||
expect(await this.token.symbol()).to.equal(`W${symbol}`);
|
||||
});
|
||||
|
||||
it('has the same decimals as the underlying token', async function () {
|
||||
expect(await this.token.decimals()).to.be.bignumber.equal('9');
|
||||
});
|
||||
|
||||
it('decimals default back to 18 if token has no metadata', async function () {
|
||||
const noDecimals = await NotAnERC20.new();
|
||||
const otherToken = await ERC20Wrapper.new(`Wrapped ${name}`, `W${symbol}`, noDecimals.address);
|
||||
expect(await otherToken.decimals()).to.be.bignumber.equal('18');
|
||||
});
|
||||
|
||||
it('has underlying', async function () {
|
||||
expect(await this.token.underlying()).to.be.bignumber.equal(this.underlying.address);
|
||||
});
|
||||
|
||||
describe('deposit', function () {
|
||||
it('valid', async function () {
|
||||
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
|
||||
const { tx } = await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: this.token.address,
|
||||
value: initialSupply,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: initialHolder,
|
||||
value: initialSupply,
|
||||
});
|
||||
});
|
||||
|
||||
it('missing approval', async function () {
|
||||
await expectRevert(
|
||||
this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }),
|
||||
'ERC20: insufficient allowance',
|
||||
);
|
||||
});
|
||||
|
||||
it('missing balance', async function () {
|
||||
await this.underlying.approve(this.token.address, MAX_UINT256, { from: initialHolder });
|
||||
await expectRevert(
|
||||
this.token.depositFor(initialHolder, MAX_UINT256, { from: initialHolder }),
|
||||
'ERC20: transfer amount exceeds balance',
|
||||
);
|
||||
});
|
||||
|
||||
it('to other account', async function () {
|
||||
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
|
||||
const { tx } = await this.token.depositFor(anotherAccount, initialSupply, { from: initialHolder });
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: this.token.address,
|
||||
value: initialSupply,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: anotherAccount,
|
||||
value: initialSupply,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('withdraw', function () {
|
||||
beforeEach(async function () {
|
||||
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
|
||||
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
|
||||
});
|
||||
|
||||
it('missing balance', async function () {
|
||||
await expectRevert(
|
||||
this.token.withdrawTo(initialHolder, MAX_UINT256, { from: initialHolder }),
|
||||
'ERC20: burn amount exceeds balance',
|
||||
);
|
||||
});
|
||||
|
||||
it('valid', async function () {
|
||||
const value = new BN(42);
|
||||
|
||||
const { tx } = await this.token.withdrawTo(initialHolder, value, { from: initialHolder });
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: initialHolder,
|
||||
value: value,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: ZERO_ADDRESS,
|
||||
value: value,
|
||||
});
|
||||
});
|
||||
|
||||
it('entire balance', async function () {
|
||||
const { tx } = await this.token.withdrawTo(initialHolder, initialSupply, { from: initialHolder });
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: initialHolder,
|
||||
value: initialSupply,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: ZERO_ADDRESS,
|
||||
value: initialSupply,
|
||||
});
|
||||
});
|
||||
|
||||
it('to other account', async function () {
|
||||
const { tx } = await this.token.withdrawTo(anotherAccount, initialSupply, { from: initialHolder });
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: anotherAccount,
|
||||
value: initialSupply,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: ZERO_ADDRESS,
|
||||
value: initialSupply,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('recover', function () {
|
||||
it('nothing to recover', async function () {
|
||||
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
|
||||
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.$_recover(anotherAccount);
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: anotherAccount,
|
||||
value: '0',
|
||||
});
|
||||
});
|
||||
|
||||
it('something to recover', async function () {
|
||||
await this.underlying.transfer(this.token.address, initialSupply, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.$_recover(anotherAccount);
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: anotherAccount,
|
||||
value: initialSupply,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('erc20 behaviour', function () {
|
||||
beforeEach(async function () {
|
||||
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
|
||||
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {ERC4626Test} from "erc4626-tests/ERC4626.test.sol";
|
||||
|
||||
import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol";
|
||||
import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol";
|
||||
import {ERC4626} from "openzeppelin/token/ERC20/extensions/ERC4626.sol";
|
||||
|
||||
import {ERC20Mock} from "openzeppelin/mocks/ERC20Mock.sol";
|
||||
import {ERC4626Mock} from "openzeppelin/mocks/ERC4626Mock.sol";
|
||||
import {ERC4626OffsetMock} from "openzeppelin/mocks/token/ERC4626OffsetMock.sol";
|
||||
|
||||
contract ERC4626VaultOffsetMock is ERC4626OffsetMock {
|
||||
constructor(
|
||||
ERC20 underlying_,
|
||||
uint8 offset_
|
||||
) ERC20("My Token Vault", "MTKNV") ERC4626(underlying_) ERC4626OffsetMock(offset_) {}
|
||||
}
|
||||
|
||||
contract ERC4626StdTest is ERC4626Test {
|
||||
ERC20 private _underlying = new ERC20Mock();
|
||||
|
||||
function setUp() public override {
|
||||
_underlying_ = address(_underlying);
|
||||
_vault_ = address(new ERC4626Mock(_underlying_));
|
||||
_delta_ = 0;
|
||||
_vaultMayBeEmpty = true;
|
||||
_unlimitedAmount = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check the case where calculated `decimals` value overflows the `uint8` type.
|
||||
*/
|
||||
function testFuzzDecimalsOverflow(uint8 offset) public {
|
||||
/// @dev Remember that the `_underlying` exhibits a `decimals` value of 18.
|
||||
offset = uint8(bound(uint256(offset), 238, uint256(type(uint8).max)));
|
||||
ERC4626VaultOffsetMock erc4626VaultOffsetMock = new ERC4626VaultOffsetMock(_underlying, offset);
|
||||
vm.expectRevert();
|
||||
erc4626VaultOffsetMock.decimals();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+103
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { BN, constants, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { MAX_UINT256 } = constants;
|
||||
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
|
||||
const ERC20Permit = artifacts.require('$ERC20Permit');
|
||||
|
||||
const { Permit, getDomain, domainType, domainSeparator } = require('../../../helpers/eip712');
|
||||
const { getChainId } = require('../../../helpers/chainid');
|
||||
|
||||
contract('ERC20Permit', function (accounts) {
|
||||
const [initialHolder, spender] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
const version = '1';
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
|
||||
beforeEach(async function () {
|
||||
this.chainId = await getChainId();
|
||||
|
||||
this.token = await ERC20Permit.new(name, symbol, name);
|
||||
await this.token.$_mint(initialHolder, initialSupply);
|
||||
});
|
||||
|
||||
it('initial nonce is 0', async function () {
|
||||
expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('domain separator', async function () {
|
||||
expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
|
||||
});
|
||||
|
||||
describe('permit', function () {
|
||||
const wallet = Wallet.generate();
|
||||
|
||||
const owner = wallet.getAddressString();
|
||||
const value = new BN(42);
|
||||
const nonce = 0;
|
||||
const maxDeadline = MAX_UINT256;
|
||||
|
||||
const buildData = (contract, deadline = maxDeadline) =>
|
||||
getDomain(contract).then(domain => ({
|
||||
primaryType: 'Permit',
|
||||
types: { EIP712Domain: domainType(domain), Permit },
|
||||
domain,
|
||||
message: { owner, spender, value, nonce, deadline },
|
||||
}));
|
||||
|
||||
it('accepts owner signature', async function () {
|
||||
const { v, r, s } = await buildData(this.token)
|
||||
.then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }))
|
||||
.then(fromRpcSig);
|
||||
|
||||
await this.token.permit(owner, spender, value, maxDeadline, v, r, s);
|
||||
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
|
||||
});
|
||||
|
||||
it('rejects reused signature', async function () {
|
||||
const { v, r, s } = await buildData(this.token)
|
||||
.then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }))
|
||||
.then(fromRpcSig);
|
||||
|
||||
await this.token.permit(owner, spender, value, maxDeadline, v, r, s);
|
||||
|
||||
await expectRevert(
|
||||
this.token.permit(owner, spender, value, maxDeadline, v, r, s),
|
||||
'ERC20Permit: invalid signature',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects other signature', async function () {
|
||||
const otherWallet = Wallet.generate();
|
||||
|
||||
const { v, r, s } = await buildData(this.token)
|
||||
.then(data => ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }))
|
||||
.then(fromRpcSig);
|
||||
|
||||
await expectRevert(
|
||||
this.token.permit(owner, spender, value, maxDeadline, v, r, s),
|
||||
'ERC20Permit: invalid signature',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects expired permit', async function () {
|
||||
const deadline = (await time.latest()) - time.duration.weeks(1);
|
||||
|
||||
const { v, r, s } = await buildData(this.token, deadline)
|
||||
.then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }))
|
||||
.then(fromRpcSig);
|
||||
|
||||
await expectRevert(this.token.permit(owner, spender, value, deadline, v, r, s), 'ERC20Permit: expired deadline');
|
||||
});
|
||||
});
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC20PresetFixedSupply = artifacts.require('ERC20PresetFixedSupply');
|
||||
|
||||
contract('ERC20PresetFixedSupply', function (accounts) {
|
||||
const [deployer, owner] = accounts;
|
||||
|
||||
const name = 'PresetFixedSupply';
|
||||
const symbol = 'PFS';
|
||||
|
||||
const initialSupply = new BN('50000');
|
||||
const amount = new BN('10000');
|
||||
|
||||
before(async function () {
|
||||
this.token = await ERC20PresetFixedSupply.new(name, symbol, initialSupply, owner, { from: deployer });
|
||||
});
|
||||
|
||||
it('deployer has the balance equal to initial supply', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
it('total supply is equal to initial supply', async function () {
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
describe('burning', function () {
|
||||
it('holders can burn their tokens', async function () {
|
||||
const remainingBalance = initialSupply.sub(amount);
|
||||
const receipt = await this.token.burn(amount, { from: owner });
|
||||
expectEvent(receipt, 'Transfer', { from: owner, to: ZERO_ADDRESS, value: amount });
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(remainingBalance);
|
||||
});
|
||||
|
||||
it('decrements totalSupply', async function () {
|
||||
const expectedSupply = initialSupply.sub(amount);
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply);
|
||||
});
|
||||
});
|
||||
});
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC20PresetMinterPauser = artifacts.require('ERC20PresetMinterPauser');
|
||||
|
||||
contract('ERC20PresetMinterPauser', function (accounts) {
|
||||
const [deployer, other] = accounts;
|
||||
|
||||
const name = 'MinterPauserToken';
|
||||
const symbol = 'DRT';
|
||||
|
||||
const amount = new BN('5000');
|
||||
|
||||
const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
||||
const MINTER_ROLE = web3.utils.soliditySha3('MINTER_ROLE');
|
||||
const PAUSER_ROLE = web3.utils.soliditySha3('PAUSER_ROLE');
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20PresetMinterPauser.new(name, symbol, { from: deployer });
|
||||
});
|
||||
|
||||
it('deployer has the default admin role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(DEFAULT_ADMIN_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(DEFAULT_ADMIN_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('deployer has the minter role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(MINTER_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(MINTER_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('deployer has the pauser role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(PAUSER_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(PAUSER_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('minter and pauser role admin is the default admin', async function () {
|
||||
expect(await this.token.getRoleAdmin(MINTER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
|
||||
expect(await this.token.getRoleAdmin(PAUSER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
|
||||
});
|
||||
|
||||
describe('minting', function () {
|
||||
it('deployer can mint tokens', async function () {
|
||||
const receipt = await this.token.mint(other, amount, { from: deployer });
|
||||
expectEvent(receipt, 'Transfer', { from: ZERO_ADDRESS, to: other, value: amount });
|
||||
|
||||
expect(await this.token.balanceOf(other)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('other accounts cannot mint tokens', async function () {
|
||||
await expectRevert(
|
||||
this.token.mint(other, amount, { from: other }),
|
||||
'ERC20PresetMinterPauser: must have minter role to mint',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pausing', function () {
|
||||
it('deployer can pause', async function () {
|
||||
const receipt = await this.token.pause({ from: deployer });
|
||||
expectEvent(receipt, 'Paused', { account: deployer });
|
||||
|
||||
expect(await this.token.paused()).to.equal(true);
|
||||
});
|
||||
|
||||
it('deployer can unpause', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
const receipt = await this.token.unpause({ from: deployer });
|
||||
expectEvent(receipt, 'Unpaused', { account: deployer });
|
||||
|
||||
expect(await this.token.paused()).to.equal(false);
|
||||
});
|
||||
|
||||
it('cannot mint while paused', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
await expectRevert(
|
||||
this.token.mint(other, amount, { from: deployer }),
|
||||
'ERC20Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('other accounts cannot pause', async function () {
|
||||
await expectRevert(this.token.pause({ from: other }), 'ERC20PresetMinterPauser: must have pauser role to pause');
|
||||
});
|
||||
|
||||
it('other accounts cannot unpause', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
await expectRevert(
|
||||
this.token.unpause({ from: other }),
|
||||
'ERC20PresetMinterPauser: must have pauser role to unpause',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('burning', function () {
|
||||
it('holders can burn their tokens', async function () {
|
||||
await this.token.mint(other, amount, { from: deployer });
|
||||
|
||||
const receipt = await this.token.burn(amount.subn(1), { from: other });
|
||||
expectEvent(receipt, 'Transfer', { from: other, to: ZERO_ADDRESS, value: amount.subn(1) });
|
||||
|
||||
expect(await this.token.balanceOf(other)).to.be.bignumber.equal('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,350 @@
|
||||
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const SafeERC20 = artifacts.require('$SafeERC20');
|
||||
const ERC20ReturnFalseMock = artifacts.require('$ERC20ReturnFalseMock');
|
||||
const ERC20ReturnTrueMock = artifacts.require('$ERC20'); // default implementation returns true
|
||||
const ERC20NoReturnMock = artifacts.require('$ERC20NoReturnMock');
|
||||
const ERC20PermitNoRevertMock = artifacts.require('$ERC20PermitNoRevertMock');
|
||||
const ERC20ForceApproveMock = artifacts.require('$ERC20ForceApproveMock');
|
||||
|
||||
const { getDomain, domainType, Permit } = require('../../../helpers/eip712');
|
||||
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
|
||||
const name = 'ERC20Mock';
|
||||
const symbol = 'ERC20Mock';
|
||||
|
||||
contract('SafeERC20', function (accounts) {
|
||||
const [hasNoCode] = accounts;
|
||||
|
||||
before(async function () {
|
||||
this.mock = await SafeERC20.new();
|
||||
});
|
||||
|
||||
describe('with address that has no contract code', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = { address: hasNoCode };
|
||||
});
|
||||
|
||||
shouldRevertOnAllCalls(accounts, 'Address: call to non-contract');
|
||||
});
|
||||
|
||||
describe('with token that returns false on all calls', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20ReturnFalseMock.new(name, symbol);
|
||||
});
|
||||
|
||||
shouldRevertOnAllCalls(accounts, 'SafeERC20: ERC20 operation did not succeed');
|
||||
});
|
||||
|
||||
describe('with token that returns true on all calls', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20ReturnTrueMock.new(name, symbol);
|
||||
});
|
||||
|
||||
shouldOnlyRevertOnErrors(accounts);
|
||||
});
|
||||
|
||||
describe('with token that returns no boolean values', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20NoReturnMock.new(name, symbol);
|
||||
});
|
||||
|
||||
shouldOnlyRevertOnErrors(accounts);
|
||||
});
|
||||
|
||||
describe("with token that doesn't revert on invalid permit", function () {
|
||||
const wallet = Wallet.generate();
|
||||
const owner = wallet.getAddressString();
|
||||
const spender = hasNoCode;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20PermitNoRevertMock.new(name, symbol, name);
|
||||
|
||||
this.data = await getDomain(this.token).then(domain => ({
|
||||
primaryType: 'Permit',
|
||||
types: { EIP712Domain: domainType(domain), Permit },
|
||||
domain,
|
||||
message: { owner, spender, value: '42', nonce: '0', deadline: constants.MAX_UINT256 },
|
||||
}));
|
||||
|
||||
this.signature = fromRpcSig(ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data: this.data }));
|
||||
});
|
||||
|
||||
it('accepts owner signature', async function () {
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal('0');
|
||||
|
||||
await this.mock.$safePermit(
|
||||
this.token.address,
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
this.signature.v,
|
||||
this.signature.r,
|
||||
this.signature.s,
|
||||
);
|
||||
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(this.data.message.value);
|
||||
});
|
||||
|
||||
it('revert on reused signature', async function () {
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('0');
|
||||
// use valid signature and consume nounce
|
||||
await this.mock.$safePermit(
|
||||
this.token.address,
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
this.signature.v,
|
||||
this.signature.r,
|
||||
this.signature.s,
|
||||
);
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
|
||||
// invalid call does not revert for this token implementation
|
||||
await this.token.permit(
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
this.signature.v,
|
||||
this.signature.r,
|
||||
this.signature.s,
|
||||
);
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
|
||||
// invalid call revert when called through the SafeERC20 library
|
||||
await expectRevert(
|
||||
this.mock.$safePermit(
|
||||
this.token.address,
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
this.signature.v,
|
||||
this.signature.r,
|
||||
this.signature.s,
|
||||
),
|
||||
'SafeERC20: permit did not succeed',
|
||||
);
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('revert on invalid signature', async function () {
|
||||
// signature that is not valid for owner
|
||||
const invalidSignature = {
|
||||
v: 27,
|
||||
r: '0x71753dc5ecb5b4bfc0e3bc530d79ce5988760ed3f3a234c86a5546491f540775',
|
||||
s: '0x0049cedee5aed990aabed5ad6a9f6e3c565b63379894b5fa8b512eb2b79e485d',
|
||||
};
|
||||
|
||||
// invalid call does not revert for this token implementation
|
||||
await this.token.permit(
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
invalidSignature.v,
|
||||
invalidSignature.r,
|
||||
invalidSignature.s,
|
||||
);
|
||||
|
||||
// invalid call revert when called through the SafeERC20 library
|
||||
await expectRevert(
|
||||
this.mock.$safePermit(
|
||||
this.token.address,
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
invalidSignature.v,
|
||||
invalidSignature.r,
|
||||
invalidSignature.s,
|
||||
),
|
||||
'SafeERC20: permit did not succeed',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with usdt approval beaviour', function () {
|
||||
const spender = hasNoCode;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20ForceApproveMock.new(name, symbol);
|
||||
});
|
||||
|
||||
describe('with initial approval', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_approve(this.mock.address, spender, 100);
|
||||
});
|
||||
|
||||
it('safeApprove fails to update approval to non-zero', async function () {
|
||||
await expectRevert(
|
||||
this.mock.$safeApprove(this.token.address, spender, 200),
|
||||
'SafeERC20: approve from non-zero to non-zero allowance',
|
||||
);
|
||||
});
|
||||
|
||||
it('safeApprove can update approval to zero', async function () {
|
||||
await this.mock.$safeApprove(this.token.address, spender, 0);
|
||||
});
|
||||
|
||||
it('safeApprove can increase approval', async function () {
|
||||
await expectRevert(this.mock.$safeIncreaseAllowance(this.token.address, spender, 10), 'USDT approval failure');
|
||||
});
|
||||
|
||||
it('safeApprove can decrease approval', async function () {
|
||||
await expectRevert(this.mock.$safeDecreaseAllowance(this.token.address, spender, 10), 'USDT approval failure');
|
||||
});
|
||||
|
||||
it('forceApprove works', async function () {
|
||||
await this.mock.$forceApprove(this.token.address, spender, 200);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function shouldRevertOnAllCalls([receiver, spender], reason) {
|
||||
it('reverts on transfer', async function () {
|
||||
await expectRevert(this.mock.$safeTransfer(this.token.address, receiver, 0), reason);
|
||||
});
|
||||
|
||||
it('reverts on transferFrom', async function () {
|
||||
await expectRevert(this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), reason);
|
||||
});
|
||||
|
||||
it('reverts on approve', async function () {
|
||||
await expectRevert(this.mock.$safeApprove(this.token.address, spender, 0), reason);
|
||||
});
|
||||
|
||||
it('reverts on increaseAllowance', async function () {
|
||||
// [TODO] make sure it's reverting for the right reason
|
||||
await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0));
|
||||
});
|
||||
|
||||
it('reverts on decreaseAllowance', async function () {
|
||||
// [TODO] make sure it's reverting for the right reason
|
||||
await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, spender, 0));
|
||||
});
|
||||
|
||||
it('reverts on forceApprove', async function () {
|
||||
await expectRevert(this.mock.$forceApprove(this.token.address, spender, 0), reason);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldOnlyRevertOnErrors([owner, receiver, spender]) {
|
||||
describe('transfers', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(owner, 100);
|
||||
await this.token.$_mint(this.mock.address, 100);
|
||||
await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner });
|
||||
});
|
||||
|
||||
it("doesn't revert on transfer", async function () {
|
||||
const { tx } = await this.mock.$safeTransfer(this.token.address, receiver, 10);
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: this.mock.address,
|
||||
to: receiver,
|
||||
value: '10',
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't revert on transferFrom", async function () {
|
||||
const { tx } = await this.mock.$safeTransferFrom(this.token.address, owner, receiver, 10);
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: owner,
|
||||
to: receiver,
|
||||
value: '10',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('approvals', function () {
|
||||
context('with zero allowance', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_approve(this.mock.address, spender, 0);
|
||||
});
|
||||
|
||||
it("doesn't revert when approving a non-zero allowance", async function () {
|
||||
await this.mock.$safeApprove(this.token.address, spender, 100);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100');
|
||||
});
|
||||
|
||||
it("doesn't revert when approving a zero allowance", async function () {
|
||||
await this.mock.$safeApprove(this.token.address, spender, 0);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it("doesn't revert when force approving a non-zero allowance", async function () {
|
||||
await this.mock.$forceApprove(this.token.address, spender, 100);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100');
|
||||
});
|
||||
|
||||
it("doesn't revert when force approving a zero allowance", async function () {
|
||||
await this.mock.$forceApprove(this.token.address, spender, 0);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it("doesn't revert when increasing the allowance", async function () {
|
||||
await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('10');
|
||||
});
|
||||
|
||||
it('reverts when decreasing the allowance', async function () {
|
||||
await expectRevert(
|
||||
this.mock.$safeDecreaseAllowance(this.token.address, spender, 10),
|
||||
'SafeERC20: decreased allowance below zero',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with non-zero allowance', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_approve(this.mock.address, spender, 100);
|
||||
});
|
||||
|
||||
it('reverts when approving a non-zero allowance', async function () {
|
||||
await expectRevert(
|
||||
this.mock.$safeApprove(this.token.address, spender, 20),
|
||||
'SafeERC20: approve from non-zero to non-zero allowance',
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't revert when approving a zero allowance", async function () {
|
||||
await this.mock.$safeApprove(this.token.address, spender, 0);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it("doesn't revert when force approving a non-zero allowance", async function () {
|
||||
await this.mock.$forceApprove(this.token.address, spender, 20);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('20');
|
||||
});
|
||||
|
||||
it("doesn't revert when force approving a zero allowance", async function () {
|
||||
await this.mock.$forceApprove(this.token.address, spender, 0);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it("doesn't revert when increasing the allowance", async function () {
|
||||
await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('110');
|
||||
});
|
||||
|
||||
it("doesn't revert when decreasing the allowance to a positive value", async function () {
|
||||
await this.mock.$safeDecreaseAllowance(this.token.address, spender, 50);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('50');
|
||||
});
|
||||
|
||||
it('reverts when decreasing the allowance to a negative value', async function () {
|
||||
await expectRevert(
|
||||
this.mock.$safeDecreaseAllowance(this.token.address, spender, 200),
|
||||
'SafeERC20: decreased allowance below zero',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
const { BN, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC20 = artifacts.require('$ERC20');
|
||||
const TokenTimelock = artifacts.require('TokenTimelock');
|
||||
|
||||
contract('TokenTimelock', function (accounts) {
|
||||
const [beneficiary] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
const amount = new BN(100);
|
||||
|
||||
context('with token', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20.new(name, symbol);
|
||||
});
|
||||
|
||||
it('rejects a release time in the past', async function () {
|
||||
const pastReleaseTime = (await time.latest()).sub(time.duration.years(1));
|
||||
await expectRevert(
|
||||
TokenTimelock.new(this.token.address, beneficiary, pastReleaseTime),
|
||||
'TokenTimelock: release time is before current time',
|
||||
);
|
||||
});
|
||||
|
||||
context('once deployed', function () {
|
||||
beforeEach(async function () {
|
||||
this.releaseTime = (await time.latest()).add(time.duration.years(1));
|
||||
this.timelock = await TokenTimelock.new(this.token.address, beneficiary, this.releaseTime);
|
||||
await this.token.$_mint(this.timelock.address, amount);
|
||||
});
|
||||
|
||||
it('can get state', async function () {
|
||||
expect(await this.timelock.token()).to.equal(this.token.address);
|
||||
expect(await this.timelock.beneficiary()).to.equal(beneficiary);
|
||||
expect(await this.timelock.releaseTime()).to.be.bignumber.equal(this.releaseTime);
|
||||
});
|
||||
|
||||
it('cannot be released before time limit', async function () {
|
||||
await expectRevert(this.timelock.release(), 'TokenTimelock: current time is before release time');
|
||||
});
|
||||
|
||||
it('cannot be released just before time limit', async function () {
|
||||
await time.increaseTo(this.releaseTime.sub(time.duration.seconds(3)));
|
||||
await expectRevert(this.timelock.release(), 'TokenTimelock: current time is before release time');
|
||||
});
|
||||
|
||||
it('can be released just after limit', async function () {
|
||||
await time.increaseTo(this.releaseTime.add(time.duration.seconds(1)));
|
||||
await this.timelock.release();
|
||||
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('can be released after time limit', async function () {
|
||||
await time.increaseTo(this.releaseTime.add(time.duration.years(1)));
|
||||
await this.timelock.release();
|
||||
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('cannot be released twice', async function () {
|
||||
await time.increaseTo(this.releaseTime.add(time.duration.years(1)));
|
||||
await this.timelock.release();
|
||||
await expectRevert(this.timelock.release(), 'TokenTimelock: no tokens to release');
|
||||
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,893 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
const ERC721ReceiverMock = artifacts.require('ERC721ReceiverMock');
|
||||
const NonERC721ReceiverMock = artifacts.require('CallReceiverMock');
|
||||
|
||||
const Error = ['None', 'RevertWithMessage', 'RevertWithoutMessage', 'Panic'].reduce(
|
||||
(acc, entry, idx) => Object.assign({ [entry]: idx }, acc),
|
||||
{},
|
||||
);
|
||||
|
||||
const firstTokenId = new BN('5042');
|
||||
const secondTokenId = new BN('79217');
|
||||
const nonExistentTokenId = new BN('13');
|
||||
const fourthTokenId = new BN(4);
|
||||
const baseURI = 'https://api.example.com/v1/';
|
||||
|
||||
const RECEIVER_MAGIC_VALUE = '0x150b7a02';
|
||||
|
||||
function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherApproved, operator, other) {
|
||||
shouldSupportInterfaces(['ERC165', 'ERC721']);
|
||||
|
||||
context('with minted tokens', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(owner, firstTokenId);
|
||||
await this.token.$_mint(owner, secondTokenId);
|
||||
this.toWhom = other; // default to other for toWhom in context-dependent tests
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
context('when the given address owns some tokens', function () {
|
||||
it('returns the amount of tokens owned by the given address', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('2');
|
||||
});
|
||||
});
|
||||
|
||||
context('when the given address does not own any tokens', function () {
|
||||
it('returns 0', async function () {
|
||||
expect(await this.token.balanceOf(other)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('when querying the zero address', function () {
|
||||
it('throws', async function () {
|
||||
await expectRevert(this.token.balanceOf(ZERO_ADDRESS), 'ERC721: address zero is not a valid owner');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ownerOf', function () {
|
||||
context('when the given token ID was tracked by this token', function () {
|
||||
const tokenId = firstTokenId;
|
||||
|
||||
it('returns the owner of the given token ID', async function () {
|
||||
expect(await this.token.ownerOf(tokenId)).to.be.equal(owner);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the given token ID was not tracked by this token', function () {
|
||||
const tokenId = nonExistentTokenId;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfers', function () {
|
||||
const tokenId = firstTokenId;
|
||||
const data = '0x42';
|
||||
|
||||
let receipt = null;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(approved, tokenId, { from: owner });
|
||||
await this.token.setApprovalForAll(operator, true, { from: owner });
|
||||
});
|
||||
|
||||
const transferWasSuccessful = function ({ owner, tokenId }) {
|
||||
it('transfers the ownership of the given token ID to the given address', async function () {
|
||||
expect(await this.token.ownerOf(tokenId)).to.be.equal(this.toWhom);
|
||||
});
|
||||
|
||||
it('emits a Transfer event', async function () {
|
||||
expectEvent(receipt, 'Transfer', { from: owner, to: this.toWhom, tokenId: tokenId });
|
||||
});
|
||||
|
||||
it('clears the approval for the token ID', async function () {
|
||||
expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('adjusts owners balances', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('adjusts owners tokens by index', async function () {
|
||||
if (!this.token.tokenOfOwnerByIndex) return;
|
||||
|
||||
expect(await this.token.tokenOfOwnerByIndex(this.toWhom, 0)).to.be.bignumber.equal(tokenId);
|
||||
|
||||
expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.not.equal(tokenId);
|
||||
});
|
||||
};
|
||||
|
||||
const shouldTransferTokensByUsers = function (transferFunction) {
|
||||
context('when called by the owner', function () {
|
||||
beforeEach(async function () {
|
||||
receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: owner });
|
||||
});
|
||||
transferWasSuccessful({ owner, tokenId, approved });
|
||||
});
|
||||
|
||||
context('when called by the approved individual', function () {
|
||||
beforeEach(async function () {
|
||||
receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: approved });
|
||||
});
|
||||
transferWasSuccessful({ owner, tokenId, approved });
|
||||
});
|
||||
|
||||
context('when called by the operator', function () {
|
||||
beforeEach(async function () {
|
||||
receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: operator });
|
||||
});
|
||||
transferWasSuccessful({ owner, tokenId, approved });
|
||||
});
|
||||
|
||||
context('when called by the owner without an approved user', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner });
|
||||
receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: operator });
|
||||
});
|
||||
transferWasSuccessful({ owner, tokenId, approved: null });
|
||||
});
|
||||
|
||||
context('when sent to the owner', function () {
|
||||
beforeEach(async function () {
|
||||
receipt = await transferFunction.call(this, owner, owner, tokenId, { from: owner });
|
||||
});
|
||||
|
||||
it('keeps ownership of the token', async function () {
|
||||
expect(await this.token.ownerOf(tokenId)).to.be.equal(owner);
|
||||
});
|
||||
|
||||
it('clears the approval for the token ID', async function () {
|
||||
expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('emits only a transfer event', async function () {
|
||||
expectEvent(receipt, 'Transfer', {
|
||||
from: owner,
|
||||
to: owner,
|
||||
tokenId: tokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the owner balance', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('2');
|
||||
});
|
||||
|
||||
it('keeps same tokens by index', async function () {
|
||||
if (!this.token.tokenOfOwnerByIndex) return;
|
||||
const tokensListed = await Promise.all([0, 1].map(i => this.token.tokenOfOwnerByIndex(owner, i)));
|
||||
expect(tokensListed.map(t => t.toNumber())).to.have.members([
|
||||
firstTokenId.toNumber(),
|
||||
secondTokenId.toNumber(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the address of the previous owner is incorrect', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
transferFunction.call(this, other, other, tokenId, { from: owner }),
|
||||
'ERC721: transfer from incorrect owner',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender is not authorized for the token id', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
transferFunction.call(this, owner, other, tokenId, { from: other }),
|
||||
'ERC721: caller is not token owner or approved',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the given token ID does not exist', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
transferFunction.call(this, owner, other, nonExistentTokenId, { from: owner }),
|
||||
'ERC721: invalid token ID',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the address to transfer the token to is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
transferFunction.call(this, owner, ZERO_ADDRESS, tokenId, { from: owner }),
|
||||
'ERC721: transfer to the zero address',
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('via transferFrom', function () {
|
||||
shouldTransferTokensByUsers(function (from, to, tokenId, opts) {
|
||||
return this.token.transferFrom(from, to, tokenId, opts);
|
||||
});
|
||||
});
|
||||
|
||||
describe('via safeTransferFrom', function () {
|
||||
const safeTransferFromWithData = function (from, to, tokenId, opts) {
|
||||
return this.token.methods['safeTransferFrom(address,address,uint256,bytes)'](from, to, tokenId, data, opts);
|
||||
};
|
||||
|
||||
const safeTransferFromWithoutData = function (from, to, tokenId, opts) {
|
||||
return this.token.methods['safeTransferFrom(address,address,uint256)'](from, to, tokenId, opts);
|
||||
};
|
||||
|
||||
const shouldTransferSafely = function (transferFun, data) {
|
||||
describe('to a user account', function () {
|
||||
shouldTransferTokensByUsers(transferFun);
|
||||
});
|
||||
|
||||
describe('to a valid receiver contract', function () {
|
||||
beforeEach(async function () {
|
||||
this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None);
|
||||
this.toWhom = this.receiver.address;
|
||||
});
|
||||
|
||||
shouldTransferTokensByUsers(transferFun);
|
||||
|
||||
it('calls onERC721Received', async function () {
|
||||
const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: owner });
|
||||
|
||||
await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
|
||||
operator: owner,
|
||||
from: owner,
|
||||
tokenId: tokenId,
|
||||
data: data,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onERC721Received from approved', async function () {
|
||||
const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: approved });
|
||||
|
||||
await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
|
||||
operator: approved,
|
||||
from: owner,
|
||||
tokenId: tokenId,
|
||||
data: data,
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an invalid token id', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
transferFun.call(this, owner, this.receiver.address, nonExistentTokenId, { from: owner }),
|
||||
'ERC721: invalid token ID',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('with data', function () {
|
||||
shouldTransferSafely(safeTransferFromWithData, data);
|
||||
});
|
||||
|
||||
describe('without data', function () {
|
||||
shouldTransferSafely(safeTransferFromWithoutData, null);
|
||||
});
|
||||
|
||||
describe('to a receiver contract returning unexpected value', function () {
|
||||
it('reverts', async function () {
|
||||
const invalidReceiver = await ERC721ReceiverMock.new('0x42', Error.None);
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(owner, invalidReceiver.address, tokenId, { from: owner }),
|
||||
'ERC721: transfer to non ERC721Receiver implementer',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('to a receiver contract that reverts with message', function () {
|
||||
it('reverts', async function () {
|
||||
const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithMessage);
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }),
|
||||
'ERC721ReceiverMock: reverting',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('to a receiver contract that reverts without message', function () {
|
||||
it('reverts', async function () {
|
||||
const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithoutMessage);
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }),
|
||||
'ERC721: transfer to non ERC721Receiver implementer',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('to a receiver contract that panics', function () {
|
||||
it('reverts', async function () {
|
||||
const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.Panic);
|
||||
await expectRevert.unspecified(
|
||||
this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('to a contract that does not implement the required function', function () {
|
||||
it('reverts', async function () {
|
||||
const nonReceiver = await NonERC721ReceiverMock.new();
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(owner, nonReceiver.address, tokenId, { from: owner }),
|
||||
'ERC721: transfer to non ERC721Receiver implementer',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('safe mint', function () {
|
||||
const tokenId = fourthTokenId;
|
||||
const data = '0x42';
|
||||
|
||||
describe('via safeMint', function () {
|
||||
// regular minting is tested in ERC721Mintable.test.js and others
|
||||
it('calls onERC721Received — with data', async function () {
|
||||
this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None);
|
||||
const receipt = await this.token.$_safeMint(this.receiver.address, tokenId, data);
|
||||
|
||||
await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
|
||||
from: ZERO_ADDRESS,
|
||||
tokenId: tokenId,
|
||||
data: data,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onERC721Received — without data', async function () {
|
||||
this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None);
|
||||
const receipt = await this.token.$_safeMint(this.receiver.address, tokenId);
|
||||
|
||||
await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
|
||||
from: ZERO_ADDRESS,
|
||||
tokenId: tokenId,
|
||||
});
|
||||
});
|
||||
|
||||
context('to a receiver contract returning unexpected value', function () {
|
||||
it('reverts', async function () {
|
||||
const invalidReceiver = await ERC721ReceiverMock.new('0x42', Error.None);
|
||||
await expectRevert(
|
||||
this.token.$_safeMint(invalidReceiver.address, tokenId),
|
||||
'ERC721: transfer to non ERC721Receiver implementer',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('to a receiver contract that reverts with message', function () {
|
||||
it('reverts', async function () {
|
||||
const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithMessage);
|
||||
await expectRevert(
|
||||
this.token.$_safeMint(revertingReceiver.address, tokenId),
|
||||
'ERC721ReceiverMock: reverting',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('to a receiver contract that reverts without message', function () {
|
||||
it('reverts', async function () {
|
||||
const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithoutMessage);
|
||||
await expectRevert(
|
||||
this.token.$_safeMint(revertingReceiver.address, tokenId),
|
||||
'ERC721: transfer to non ERC721Receiver implementer',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('to a receiver contract that panics', function () {
|
||||
it('reverts', async function () {
|
||||
const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.Panic);
|
||||
await expectRevert.unspecified(this.token.$_safeMint(revertingReceiver.address, tokenId));
|
||||
});
|
||||
});
|
||||
|
||||
context('to a contract that does not implement the required function', function () {
|
||||
it('reverts', async function () {
|
||||
const nonReceiver = await NonERC721ReceiverMock.new();
|
||||
await expectRevert(
|
||||
this.token.$_safeMint(nonReceiver.address, tokenId),
|
||||
'ERC721: transfer to non ERC721Receiver implementer',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('approve', function () {
|
||||
const tokenId = firstTokenId;
|
||||
|
||||
let receipt = null;
|
||||
|
||||
const itClearsApproval = function () {
|
||||
it('clears approval for the token', async function () {
|
||||
expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS);
|
||||
});
|
||||
};
|
||||
|
||||
const itApproves = function (address) {
|
||||
it('sets the approval for the target address', async function () {
|
||||
expect(await this.token.getApproved(tokenId)).to.be.equal(address);
|
||||
});
|
||||
};
|
||||
|
||||
const itEmitsApprovalEvent = function (address) {
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(receipt, 'Approval', {
|
||||
owner: owner,
|
||||
approved: address,
|
||||
tokenId: tokenId,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
context('when clearing approval', function () {
|
||||
context('when there was no prior approval', function () {
|
||||
beforeEach(async function () {
|
||||
receipt = await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner });
|
||||
});
|
||||
|
||||
itClearsApproval();
|
||||
itEmitsApprovalEvent(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
context('when there was a prior approval', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(approved, tokenId, { from: owner });
|
||||
receipt = await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner });
|
||||
});
|
||||
|
||||
itClearsApproval();
|
||||
itEmitsApprovalEvent(ZERO_ADDRESS);
|
||||
});
|
||||
});
|
||||
|
||||
context('when approving a non-zero address', function () {
|
||||
context('when there was no prior approval', function () {
|
||||
beforeEach(async function () {
|
||||
receipt = await this.token.approve(approved, tokenId, { from: owner });
|
||||
});
|
||||
|
||||
itApproves(approved);
|
||||
itEmitsApprovalEvent(approved);
|
||||
});
|
||||
|
||||
context('when there was a prior approval to the same address', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(approved, tokenId, { from: owner });
|
||||
receipt = await this.token.approve(approved, tokenId, { from: owner });
|
||||
});
|
||||
|
||||
itApproves(approved);
|
||||
itEmitsApprovalEvent(approved);
|
||||
});
|
||||
|
||||
context('when there was a prior approval to a different address', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(anotherApproved, tokenId, { from: owner });
|
||||
receipt = await this.token.approve(anotherApproved, tokenId, { from: owner });
|
||||
});
|
||||
|
||||
itApproves(anotherApproved);
|
||||
itEmitsApprovalEvent(anotherApproved);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the address that receives the approval is the owner', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.approve(owner, tokenId, { from: owner }), 'ERC721: approval to current owner');
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender does not own the given token ID', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.approve(approved, tokenId, { from: other }),
|
||||
'ERC721: approve caller is not token owner or approved',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender is approved for the given token ID', function () {
|
||||
it('reverts', async function () {
|
||||
await this.token.approve(approved, tokenId, { from: owner });
|
||||
await expectRevert(
|
||||
this.token.approve(anotherApproved, tokenId, { from: approved }),
|
||||
'ERC721: approve caller is not token owner or approved for all',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender is an operator', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.setApprovalForAll(operator, true, { from: owner });
|
||||
receipt = await this.token.approve(approved, tokenId, { from: operator });
|
||||
});
|
||||
|
||||
itApproves(approved);
|
||||
itEmitsApprovalEvent(approved);
|
||||
});
|
||||
|
||||
context('when the given token ID does not exist', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.approve(approved, nonExistentTokenId, { from: operator }),
|
||||
'ERC721: invalid token ID',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setApprovalForAll', function () {
|
||||
context('when the operator willing to approve is not the owner', function () {
|
||||
context('when there is no operator approval set by the sender', function () {
|
||||
it('approves the operator', async function () {
|
||||
await this.token.setApprovalForAll(operator, true, { from: owner });
|
||||
|
||||
expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true);
|
||||
});
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
const receipt = await this.token.setApprovalForAll(operator, true, { from: owner });
|
||||
|
||||
expectEvent(receipt, 'ApprovalForAll', {
|
||||
owner: owner,
|
||||
operator: operator,
|
||||
approved: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('when the operator was set as not approved', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.setApprovalForAll(operator, false, { from: owner });
|
||||
});
|
||||
|
||||
it('approves the operator', async function () {
|
||||
await this.token.setApprovalForAll(operator, true, { from: owner });
|
||||
|
||||
expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true);
|
||||
});
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
const receipt = await this.token.setApprovalForAll(operator, true, { from: owner });
|
||||
|
||||
expectEvent(receipt, 'ApprovalForAll', {
|
||||
owner: owner,
|
||||
operator: operator,
|
||||
approved: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('can unset the operator approval', async function () {
|
||||
await this.token.setApprovalForAll(operator, false, { from: owner });
|
||||
|
||||
expect(await this.token.isApprovedForAll(owner, operator)).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the operator was already approved', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.setApprovalForAll(operator, true, { from: owner });
|
||||
});
|
||||
|
||||
it('keeps the approval to the given address', async function () {
|
||||
await this.token.setApprovalForAll(operator, true, { from: owner });
|
||||
|
||||
expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true);
|
||||
});
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
const receipt = await this.token.setApprovalForAll(operator, true, { from: owner });
|
||||
|
||||
expectEvent(receipt, 'ApprovalForAll', {
|
||||
owner: owner,
|
||||
operator: operator,
|
||||
approved: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('when the operator is the owner', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.setApprovalForAll(owner, true, { from: owner }), 'ERC721: approve to caller');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApproved', async function () {
|
||||
context('when token is not minted', async function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.getApproved(nonExistentTokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
});
|
||||
|
||||
context('when token has been minted ', async function () {
|
||||
it('should return the zero address', async function () {
|
||||
expect(await this.token.getApproved(firstTokenId)).to.be.equal(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
context('when account has been approved', async function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(approved, firstTokenId, { from: owner });
|
||||
});
|
||||
|
||||
it('returns approved account', async function () {
|
||||
expect(await this.token.getApproved(firstTokenId)).to.be.equal(approved);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_mint(address, uint256)', function () {
|
||||
it('reverts with a null destination address', async function () {
|
||||
await expectRevert(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721: mint to the zero address');
|
||||
});
|
||||
|
||||
context('with minted token', async function () {
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.token.$_mint(owner, firstTokenId);
|
||||
});
|
||||
|
||||
it('emits a Transfer event', function () {
|
||||
expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: owner, tokenId: firstTokenId });
|
||||
});
|
||||
|
||||
it('creates the token', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.ownerOf(firstTokenId)).to.equal(owner);
|
||||
});
|
||||
|
||||
it('reverts when adding a token id that already exists', async function () {
|
||||
await expectRevert(this.token.$_mint(owner, firstTokenId), 'ERC721: token already minted');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_burn', function () {
|
||||
it('reverts when burning a non-existent token id', async function () {
|
||||
await expectRevert(this.token.$_burn(nonExistentTokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
|
||||
context('with minted tokens', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(owner, firstTokenId);
|
||||
await this.token.$_mint(owner, secondTokenId);
|
||||
});
|
||||
|
||||
context('with burnt token', function () {
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.token.$_burn(firstTokenId);
|
||||
});
|
||||
|
||||
it('emits a Transfer event', function () {
|
||||
expectEvent(this.receipt, 'Transfer', { from: owner, to: ZERO_ADDRESS, tokenId: firstTokenId });
|
||||
});
|
||||
|
||||
it('deletes the token', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
|
||||
await expectRevert(this.token.ownerOf(firstTokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
|
||||
it('reverts when burning a token id that has been deleted', async function () {
|
||||
await expectRevert(this.token.$_burn(firstTokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved, anotherApproved, operator, other) {
|
||||
shouldSupportInterfaces(['ERC721Enumerable']);
|
||||
|
||||
context('with minted tokens', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(owner, firstTokenId);
|
||||
await this.token.$_mint(owner, secondTokenId);
|
||||
this.toWhom = other; // default to other for toWhom in context-dependent tests
|
||||
});
|
||||
|
||||
describe('totalSupply', function () {
|
||||
it('returns total token supply', async function () {
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal('2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tokenOfOwnerByIndex', function () {
|
||||
describe('when the given index is lower than the amount of tokens owned by the given address', function () {
|
||||
it('returns the token ID placed at the given index', async function () {
|
||||
expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(firstTokenId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the index is greater than or equal to the total tokens owned by the given address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.tokenOfOwnerByIndex(owner, 2), 'ERC721Enumerable: owner index out of bounds');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given address does not own any token', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.tokenOfOwnerByIndex(other, 0), 'ERC721Enumerable: owner index out of bounds');
|
||||
});
|
||||
});
|
||||
|
||||
describe('after transferring all tokens to another user', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.transferFrom(owner, other, firstTokenId, { from: owner });
|
||||
await this.token.transferFrom(owner, other, secondTokenId, { from: owner });
|
||||
});
|
||||
|
||||
it('returns correct token IDs for target', async function () {
|
||||
expect(await this.token.balanceOf(other)).to.be.bignumber.equal('2');
|
||||
const tokensListed = await Promise.all([0, 1].map(i => this.token.tokenOfOwnerByIndex(other, i)));
|
||||
expect(tokensListed.map(t => t.toNumber())).to.have.members([
|
||||
firstTokenId.toNumber(),
|
||||
secondTokenId.toNumber(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns empty collection for original owner', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0');
|
||||
await expectRevert(this.token.tokenOfOwnerByIndex(owner, 0), 'ERC721Enumerable: owner index out of bounds');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tokenByIndex', function () {
|
||||
it('returns all tokens', async function () {
|
||||
const tokensListed = await Promise.all([0, 1].map(i => this.token.tokenByIndex(i)));
|
||||
expect(tokensListed.map(t => t.toNumber())).to.have.members([
|
||||
firstTokenId.toNumber(),
|
||||
secondTokenId.toNumber(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('reverts if index is greater than supply', async function () {
|
||||
await expectRevert(this.token.tokenByIndex(2), 'ERC721Enumerable: global index out of bounds');
|
||||
});
|
||||
|
||||
[firstTokenId, secondTokenId].forEach(function (tokenId) {
|
||||
it(`returns all tokens after burning token ${tokenId} and minting new tokens`, async function () {
|
||||
const newTokenId = new BN(300);
|
||||
const anotherNewTokenId = new BN(400);
|
||||
|
||||
await this.token.$_burn(tokenId);
|
||||
await this.token.$_mint(newOwner, newTokenId);
|
||||
await this.token.$_mint(newOwner, anotherNewTokenId);
|
||||
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal('3');
|
||||
|
||||
const tokensListed = await Promise.all([0, 1, 2].map(i => this.token.tokenByIndex(i)));
|
||||
const expectedTokens = [firstTokenId, secondTokenId, newTokenId, anotherNewTokenId].filter(
|
||||
x => x !== tokenId,
|
||||
);
|
||||
expect(tokensListed.map(t => t.toNumber())).to.have.members(expectedTokens.map(t => t.toNumber()));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_mint(address, uint256)', function () {
|
||||
it('reverts with a null destination address', async function () {
|
||||
await expectRevert(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721: mint to the zero address');
|
||||
});
|
||||
|
||||
context('with minted token', async function () {
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.token.$_mint(owner, firstTokenId);
|
||||
});
|
||||
|
||||
it('adjusts owner tokens by index', async function () {
|
||||
expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(firstTokenId);
|
||||
});
|
||||
|
||||
it('adjusts all tokens list', async function () {
|
||||
expect(await this.token.tokenByIndex(0)).to.be.bignumber.equal(firstTokenId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_burn', function () {
|
||||
it('reverts when burning a non-existent token id', async function () {
|
||||
await expectRevert(this.token.$_burn(firstTokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
|
||||
context('with minted tokens', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(owner, firstTokenId);
|
||||
await this.token.$_mint(owner, secondTokenId);
|
||||
});
|
||||
|
||||
context('with burnt token', function () {
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.token.$_burn(firstTokenId);
|
||||
});
|
||||
|
||||
it('removes that token from the token list of the owner', async function () {
|
||||
expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(secondTokenId);
|
||||
});
|
||||
|
||||
it('adjusts all tokens list', async function () {
|
||||
expect(await this.token.tokenByIndex(0)).to.be.bignumber.equal(secondTokenId);
|
||||
});
|
||||
|
||||
it('burns all tokens', async function () {
|
||||
await this.token.$_burn(secondTokenId, { from: owner });
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal('0');
|
||||
await expectRevert(this.token.tokenByIndex(0), 'ERC721Enumerable: global index out of bounds');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC721Metadata(errorPrefix, name, symbol, owner) {
|
||||
shouldSupportInterfaces(['ERC721Metadata']);
|
||||
|
||||
describe('metadata', function () {
|
||||
it('has a name', async function () {
|
||||
expect(await this.token.name()).to.be.equal(name);
|
||||
});
|
||||
|
||||
it('has a symbol', async function () {
|
||||
expect(await this.token.symbol()).to.be.equal(symbol);
|
||||
});
|
||||
|
||||
describe('token URI', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(owner, firstTokenId);
|
||||
});
|
||||
|
||||
it('return empty string by default', async function () {
|
||||
expect(await this.token.tokenURI(firstTokenId)).to.be.equal('');
|
||||
});
|
||||
|
||||
it('reverts when queried for non existent token id', async function () {
|
||||
await expectRevert(this.token.tokenURI(nonExistentTokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
|
||||
describe('base URI', function () {
|
||||
beforeEach(function () {
|
||||
if (this.token.setBaseURI === undefined) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('base URI can be set', async function () {
|
||||
await this.token.setBaseURI(baseURI);
|
||||
expect(await this.token.baseURI()).to.equal(baseURI);
|
||||
});
|
||||
|
||||
it('base URI is added as a prefix to the token URI', async function () {
|
||||
await this.token.setBaseURI(baseURI);
|
||||
expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + firstTokenId.toString());
|
||||
});
|
||||
|
||||
it('token URI can be changed by changing the base URI', async function () {
|
||||
await this.token.setBaseURI(baseURI);
|
||||
const newBaseURI = 'https://api.example.com/v2/';
|
||||
await this.token.setBaseURI(newBaseURI);
|
||||
expect(await this.token.tokenURI(firstTokenId)).to.be.equal(newBaseURI + firstTokenId.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC721,
|
||||
shouldBehaveLikeERC721Enumerable,
|
||||
shouldBehaveLikeERC721Metadata,
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
const { shouldBehaveLikeERC721, shouldBehaveLikeERC721Metadata } = require('./ERC721.behavior');
|
||||
|
||||
const ERC721 = artifacts.require('$ERC721');
|
||||
|
||||
contract('ERC721', function (accounts) {
|
||||
const name = 'Non Fungible Token';
|
||||
const symbol = 'NFT';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC721.new(name, symbol);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC721('ERC721', ...accounts);
|
||||
shouldBehaveLikeERC721Metadata('ERC721', name, symbol, ...accounts);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
const {
|
||||
shouldBehaveLikeERC721,
|
||||
shouldBehaveLikeERC721Metadata,
|
||||
shouldBehaveLikeERC721Enumerable,
|
||||
} = require('./ERC721.behavior');
|
||||
|
||||
const ERC721Enumerable = artifacts.require('$ERC721Enumerable');
|
||||
|
||||
contract('ERC721Enumerable', function (accounts) {
|
||||
const name = 'Non Fungible Token';
|
||||
const symbol = 'NFT';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC721Enumerable.new(name, symbol);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC721('ERC721', ...accounts);
|
||||
shouldBehaveLikeERC721Metadata('ERC721', name, symbol, ...accounts);
|
||||
shouldBehaveLikeERC721Enumerable('ERC721', ...accounts);
|
||||
});
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC721Burnable = artifacts.require('$ERC721Burnable');
|
||||
|
||||
contract('ERC721Burnable', function (accounts) {
|
||||
const [owner, approved] = accounts;
|
||||
|
||||
const firstTokenId = new BN(1);
|
||||
const secondTokenId = new BN(2);
|
||||
const unknownTokenId = new BN(3);
|
||||
|
||||
const name = 'Non Fungible Token';
|
||||
const symbol = 'NFT';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC721Burnable.new(name, symbol);
|
||||
});
|
||||
|
||||
describe('like a burnable ERC721', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(owner, firstTokenId);
|
||||
await this.token.$_mint(owner, secondTokenId);
|
||||
});
|
||||
|
||||
describe('burn', function () {
|
||||
const tokenId = firstTokenId;
|
||||
let receipt = null;
|
||||
|
||||
describe('when successful', function () {
|
||||
beforeEach(async function () {
|
||||
receipt = await this.token.burn(tokenId, { from: owner });
|
||||
});
|
||||
|
||||
it('burns the given token ID and adjusts the balance of the owner', async function () {
|
||||
await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID');
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('emits a burn event', async function () {
|
||||
expectEvent(receipt, 'Transfer', {
|
||||
from: owner,
|
||||
to: constants.ZERO_ADDRESS,
|
||||
tokenId: tokenId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is a previous approval burned', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(approved, tokenId, { from: owner });
|
||||
receipt = await this.token.burn(tokenId, { from: owner });
|
||||
});
|
||||
|
||||
context('getApproved', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.getApproved(tokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given token ID was not tracked by this contract', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.burn(unknownTokenId, { from: owner }), 'ERC721: invalid token ID');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
// solhint-disable func-name-mixedcase
|
||||
|
||||
import "../../../../contracts/token/ERC721/extensions/ERC721Consecutive.sol";
|
||||
import "forge-std/Test.sol";
|
||||
|
||||
function toSingleton(address account) pure returns (address[] memory) {
|
||||
address[] memory accounts = new address[](1);
|
||||
accounts[0] = account;
|
||||
return accounts;
|
||||
}
|
||||
|
||||
contract ERC721ConsecutiveTarget is StdUtils, ERC721Consecutive {
|
||||
uint256 public totalMinted = 0;
|
||||
|
||||
constructor(address[] memory receivers, uint256[] memory batches) ERC721("", "") {
|
||||
for (uint256 i = 0; i < batches.length; i++) {
|
||||
address receiver = receivers[i % receivers.length];
|
||||
uint96 batchSize = uint96(bound(batches[i], 0, _maxBatchSize()));
|
||||
_mintConsecutive(receiver, batchSize);
|
||||
totalMinted += batchSize;
|
||||
}
|
||||
}
|
||||
|
||||
function burn(uint256 tokenId) public {
|
||||
_burn(tokenId);
|
||||
}
|
||||
}
|
||||
|
||||
contract ERC721ConsecutiveTest is Test {
|
||||
function test_balance(address receiver, uint256[] calldata batches) public {
|
||||
vm.assume(receiver != address(0));
|
||||
|
||||
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches);
|
||||
|
||||
assertEq(token.balanceOf(receiver), token.totalMinted());
|
||||
}
|
||||
|
||||
function test_ownership(address receiver, uint256[] calldata batches, uint256[2] calldata unboundedTokenId) public {
|
||||
vm.assume(receiver != address(0));
|
||||
|
||||
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches);
|
||||
|
||||
if (token.totalMinted() > 0) {
|
||||
uint256 validTokenId = bound(unboundedTokenId[0], 0, token.totalMinted() - 1);
|
||||
assertEq(token.ownerOf(validTokenId), receiver);
|
||||
}
|
||||
|
||||
uint256 invalidTokenId = bound(unboundedTokenId[1], token.totalMinted(), type(uint256).max);
|
||||
vm.expectRevert();
|
||||
token.ownerOf(invalidTokenId);
|
||||
}
|
||||
|
||||
function test_burn(address receiver, uint256[] calldata batches, uint256 unboundedTokenId) public {
|
||||
vm.assume(receiver != address(0));
|
||||
|
||||
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches);
|
||||
|
||||
// only test if we minted at least one token
|
||||
uint256 supply = token.totalMinted();
|
||||
vm.assume(supply > 0);
|
||||
|
||||
// burn a token in [0; supply[
|
||||
uint256 tokenId = bound(unboundedTokenId, 0, supply - 1);
|
||||
token.burn(tokenId);
|
||||
|
||||
// balance should have decreased
|
||||
assertEq(token.balanceOf(receiver), supply - 1);
|
||||
|
||||
// token should be burnt
|
||||
vm.expectRevert();
|
||||
token.ownerOf(tokenId);
|
||||
}
|
||||
|
||||
function test_transfer(
|
||||
address[2] calldata accounts,
|
||||
uint256[2] calldata unboundedBatches,
|
||||
uint256[2] calldata unboundedTokenId
|
||||
) public {
|
||||
vm.assume(accounts[0] != address(0));
|
||||
vm.assume(accounts[1] != address(0));
|
||||
vm.assume(accounts[0] != accounts[1]);
|
||||
|
||||
address[] memory receivers = new address[](2);
|
||||
receivers[0] = accounts[0];
|
||||
receivers[1] = accounts[1];
|
||||
|
||||
// We assume _maxBatchSize is 5000 (the default). This test will break otherwise.
|
||||
uint256[] memory batches = new uint256[](2);
|
||||
batches[0] = bound(unboundedBatches[0], 1, 5000);
|
||||
batches[1] = bound(unboundedBatches[1], 1, 5000);
|
||||
|
||||
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(receivers, batches);
|
||||
|
||||
uint256 tokenId0 = bound(unboundedTokenId[0], 0, batches[0] - 1);
|
||||
uint256 tokenId1 = bound(unboundedTokenId[1], 0, batches[1] - 1) + batches[0];
|
||||
|
||||
assertEq(token.ownerOf(tokenId0), accounts[0]);
|
||||
assertEq(token.ownerOf(tokenId1), accounts[1]);
|
||||
assertEq(token.balanceOf(accounts[0]), batches[0]);
|
||||
assertEq(token.balanceOf(accounts[1]), batches[1]);
|
||||
|
||||
vm.prank(accounts[0]);
|
||||
token.transferFrom(accounts[0], accounts[1], tokenId0);
|
||||
|
||||
assertEq(token.ownerOf(tokenId0), accounts[1]);
|
||||
assertEq(token.ownerOf(tokenId1), accounts[1]);
|
||||
assertEq(token.balanceOf(accounts[0]), batches[0] - 1);
|
||||
assertEq(token.balanceOf(accounts[1]), batches[1] + 1);
|
||||
|
||||
vm.prank(accounts[1]);
|
||||
token.transferFrom(accounts[1], accounts[0], tokenId1);
|
||||
|
||||
assertEq(token.ownerOf(tokenId0), accounts[1]);
|
||||
assertEq(token.ownerOf(tokenId1), accounts[0]);
|
||||
assertEq(token.balanceOf(accounts[0]), batches[0]);
|
||||
assertEq(token.balanceOf(accounts[1]), batches[1]);
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC721ConsecutiveMock = artifacts.require('$ERC721ConsecutiveMock');
|
||||
const ERC721ConsecutiveEnumerableMock = artifacts.require('$ERC721ConsecutiveEnumerableMock');
|
||||
const ERC721ConsecutiveNoConstructorMintMock = artifacts.require('$ERC721ConsecutiveNoConstructorMintMock');
|
||||
|
||||
contract('ERC721Consecutive', function (accounts) {
|
||||
const [user1, user2, user3, receiver] = accounts;
|
||||
|
||||
const name = 'Non Fungible Token';
|
||||
const symbol = 'NFT';
|
||||
const batches = [
|
||||
{ receiver: user1, amount: 0 },
|
||||
{ receiver: user1, amount: 1 },
|
||||
{ receiver: user1, amount: 2 },
|
||||
{ receiver: user2, amount: 5 },
|
||||
{ receiver: user3, amount: 0 },
|
||||
{ receiver: user1, amount: 7 },
|
||||
];
|
||||
const delegates = [user1, user3];
|
||||
|
||||
describe('with valid batches', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC721ConsecutiveMock.new(
|
||||
name,
|
||||
symbol,
|
||||
delegates,
|
||||
batches.map(({ receiver }) => receiver),
|
||||
batches.map(({ amount }) => amount),
|
||||
);
|
||||
});
|
||||
|
||||
describe('minting during construction', function () {
|
||||
it('events are emitted at construction', async function () {
|
||||
let first = 0;
|
||||
|
||||
for (const batch of batches) {
|
||||
if (batch.amount > 0) {
|
||||
await expectEvent.inConstruction(this.token, 'ConsecutiveTransfer', {
|
||||
fromTokenId: web3.utils.toBN(first),
|
||||
toTokenId: web3.utils.toBN(first + batch.amount - 1),
|
||||
fromAddress: constants.ZERO_ADDRESS,
|
||||
toAddress: batch.receiver,
|
||||
});
|
||||
} else {
|
||||
// expectEvent.notEmitted.inConstruction only looks at event name, and doesn't check the parameters
|
||||
}
|
||||
first += batch.amount;
|
||||
}
|
||||
});
|
||||
|
||||
it('ownership is set', async function () {
|
||||
const owners = batches.flatMap(({ receiver, amount }) => Array(amount).fill(receiver));
|
||||
|
||||
for (const tokenId in owners) {
|
||||
expect(await this.token.ownerOf(tokenId)).to.be.equal(owners[tokenId]);
|
||||
}
|
||||
});
|
||||
|
||||
it('balance & voting power are set', async function () {
|
||||
for (const account of accounts) {
|
||||
const balance = batches
|
||||
.filter(({ receiver }) => receiver === account)
|
||||
.map(({ amount }) => amount)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
expect(await this.token.balanceOf(account)).to.be.bignumber.equal(web3.utils.toBN(balance));
|
||||
|
||||
// If not delegated at construction, check before + do delegation
|
||||
if (!delegates.includes(account)) {
|
||||
expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(0));
|
||||
|
||||
await this.token.delegate(account, { from: account });
|
||||
}
|
||||
|
||||
// At this point all accounts should have delegated
|
||||
expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(balance));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('minting after construction', function () {
|
||||
it('consecutive minting is not possible after construction', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_mintConsecutive(user1, 10),
|
||||
'ERC721Consecutive: batch minting restricted to constructor',
|
||||
);
|
||||
});
|
||||
|
||||
it('simple minting is possible after construction', async function () {
|
||||
const tokenId = batches.reduce((acc, { amount }) => acc + amount, 0);
|
||||
|
||||
expect(await this.token.$_exists(tokenId)).to.be.equal(false);
|
||||
|
||||
expectEvent(await this.token.$_mint(user1, tokenId), 'Transfer', {
|
||||
from: constants.ZERO_ADDRESS,
|
||||
to: user1,
|
||||
tokenId: tokenId.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot mint a token that has been batched minted', async function () {
|
||||
const tokenId = batches.reduce((acc, { amount }) => acc + amount, 0) - 1;
|
||||
|
||||
expect(await this.token.$_exists(tokenId)).to.be.equal(true);
|
||||
|
||||
await expectRevert(this.token.$_mint(user1, tokenId), 'ERC721: token already minted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ERC721 behavior', function () {
|
||||
it('core takes over ownership on transfer', async function () {
|
||||
await this.token.transferFrom(user1, receiver, 1, { from: user1 });
|
||||
|
||||
expect(await this.token.ownerOf(1)).to.be.equal(receiver);
|
||||
});
|
||||
|
||||
it('tokens can be burned and re-minted #1', async function () {
|
||||
expectEvent(await this.token.$_burn(1, { from: user1 }), 'Transfer', {
|
||||
from: user1,
|
||||
to: constants.ZERO_ADDRESS,
|
||||
tokenId: '1',
|
||||
});
|
||||
|
||||
await expectRevert(this.token.ownerOf(1), 'ERC721: invalid token ID');
|
||||
|
||||
expectEvent(await this.token.$_mint(user2, 1), 'Transfer', {
|
||||
from: constants.ZERO_ADDRESS,
|
||||
to: user2,
|
||||
tokenId: '1',
|
||||
});
|
||||
|
||||
expect(await this.token.ownerOf(1)).to.be.equal(user2);
|
||||
});
|
||||
|
||||
it('tokens can be burned and re-minted #2', async function () {
|
||||
const tokenId = batches.reduce((acc, { amount }) => acc.addn(amount), web3.utils.toBN(0));
|
||||
|
||||
expect(await this.token.$_exists(tokenId)).to.be.equal(false);
|
||||
await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID');
|
||||
|
||||
// mint
|
||||
await this.token.$_mint(user1, tokenId);
|
||||
|
||||
expect(await this.token.$_exists(tokenId)).to.be.equal(true);
|
||||
expect(await this.token.ownerOf(tokenId), user1);
|
||||
|
||||
// burn
|
||||
expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', {
|
||||
from: user1,
|
||||
to: constants.ZERO_ADDRESS,
|
||||
tokenId,
|
||||
});
|
||||
|
||||
expect(await this.token.$_exists(tokenId)).to.be.equal(false);
|
||||
await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID');
|
||||
|
||||
// re-mint
|
||||
expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', {
|
||||
from: constants.ZERO_ADDRESS,
|
||||
to: user2,
|
||||
tokenId,
|
||||
});
|
||||
|
||||
expect(await this.token.$_exists(tokenId)).to.be.equal(true);
|
||||
expect(await this.token.ownerOf(tokenId), user2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid use', function () {
|
||||
it('cannot mint a batch larger than 5000', async function () {
|
||||
await expectRevert(
|
||||
ERC721ConsecutiveMock.new(name, symbol, [], [user1], ['5001']),
|
||||
'ERC721Consecutive: batch too large',
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot use single minting during construction', async function () {
|
||||
await expectRevert(
|
||||
ERC721ConsecutiveNoConstructorMintMock.new(name, symbol),
|
||||
"ERC721Consecutive: can't mint during construction",
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot use single minting during construction', async function () {
|
||||
await expectRevert(
|
||||
ERC721ConsecutiveNoConstructorMintMock.new(name, symbol),
|
||||
"ERC721Consecutive: can't mint during construction",
|
||||
);
|
||||
});
|
||||
|
||||
it('consecutive mint not compatible with enumerability', async function () {
|
||||
await expectRevert(
|
||||
ERC721ConsecutiveEnumerableMock.new(
|
||||
name,
|
||||
symbol,
|
||||
batches.map(({ receiver }) => receiver),
|
||||
batches.map(({ amount }) => amount),
|
||||
),
|
||||
'ERC721Enumerable: consecutive transfers not supported',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC721Pausable = artifacts.require('$ERC721Pausable');
|
||||
|
||||
contract('ERC721Pausable', function (accounts) {
|
||||
const [owner, receiver, operator] = accounts;
|
||||
|
||||
const name = 'Non Fungible Token';
|
||||
const symbol = 'NFT';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC721Pausable.new(name, symbol);
|
||||
});
|
||||
|
||||
context('when token is paused', function () {
|
||||
const firstTokenId = new BN(1);
|
||||
const secondTokenId = new BN(1337);
|
||||
|
||||
const mockData = '0x42';
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(owner, firstTokenId, { from: owner });
|
||||
await this.token.$_pause();
|
||||
});
|
||||
|
||||
it('reverts when trying to transferFrom', async function () {
|
||||
await expectRevert(
|
||||
this.token.transferFrom(owner, receiver, firstTokenId, { from: owner }),
|
||||
'ERC721Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to safeTransferFrom', async function () {
|
||||
await expectRevert(
|
||||
this.token.safeTransferFrom(owner, receiver, firstTokenId, { from: owner }),
|
||||
'ERC721Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to safeTransferFrom with data', async function () {
|
||||
await expectRevert(
|
||||
this.token.methods['safeTransferFrom(address,address,uint256,bytes)'](owner, receiver, firstTokenId, mockData, {
|
||||
from: owner,
|
||||
}),
|
||||
'ERC721Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to mint', async function () {
|
||||
await expectRevert(this.token.$_mint(receiver, secondTokenId), 'ERC721Pausable: token transfer while paused');
|
||||
});
|
||||
|
||||
it('reverts when trying to burn', async function () {
|
||||
await expectRevert(this.token.$_burn(firstTokenId), 'ERC721Pausable: token transfer while paused');
|
||||
});
|
||||
|
||||
describe('getApproved', function () {
|
||||
it('returns approved address', async function () {
|
||||
const approvedAccount = await this.token.getApproved(firstTokenId);
|
||||
expect(approvedAccount).to.equal(constants.ZERO_ADDRESS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
it('returns the amount of tokens owned by the given address', async function () {
|
||||
const balance = await this.token.balanceOf(owner);
|
||||
expect(balance).to.be.bignumber.equal('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ownerOf', function () {
|
||||
it('returns the amount of tokens owned by the given address', async function () {
|
||||
const ownerOfToken = await this.token.ownerOf(firstTokenId);
|
||||
expect(ownerOfToken).to.equal(owner);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exists', function () {
|
||||
it('returns token existence', async function () {
|
||||
expect(await this.token.$_exists(firstTokenId)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isApprovedForAll', function () {
|
||||
it('returns the approval of the operator', async function () {
|
||||
expect(await this.token.isApprovedForAll(owner, operator)).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
const { BN, constants } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { shouldBehaveLikeERC2981 } = require('../../common/ERC2981.behavior');
|
||||
|
||||
const ERC721Royalty = artifacts.require('$ERC721Royalty');
|
||||
|
||||
contract('ERC721Royalty', function (accounts) {
|
||||
const [account1, account2] = accounts;
|
||||
const tokenId1 = new BN('1');
|
||||
const tokenId2 = new BN('2');
|
||||
const royalty = new BN('200');
|
||||
const salePrice = new BN('1000');
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC721Royalty.new('My Token', 'TKN');
|
||||
|
||||
await this.token.$_mint(account1, tokenId1);
|
||||
await this.token.$_mint(account1, tokenId2);
|
||||
this.account1 = account1;
|
||||
this.account2 = account2;
|
||||
this.tokenId1 = tokenId1;
|
||||
this.tokenId2 = tokenId2;
|
||||
this.salePrice = salePrice;
|
||||
});
|
||||
|
||||
describe('token specific functions', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_setTokenRoyalty(tokenId1, account1, royalty);
|
||||
});
|
||||
|
||||
it('removes royalty information after burn', async function () {
|
||||
await this.token.$_burn(tokenId1);
|
||||
const tokenInfo = await this.token.royaltyInfo(tokenId1, salePrice);
|
||||
|
||||
expect(tokenInfo[0]).to.be.equal(constants.ZERO_ADDRESS);
|
||||
expect(tokenInfo[1]).to.be.bignumber.equal(new BN('0'));
|
||||
});
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC2981();
|
||||
});
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
const ERC721URIStorageMock = artifacts.require('$ERC721URIStorageMock');
|
||||
|
||||
contract('ERC721URIStorage', function (accounts) {
|
||||
const [owner] = accounts;
|
||||
|
||||
const name = 'Non Fungible Token';
|
||||
const symbol = 'NFT';
|
||||
|
||||
const firstTokenId = new BN('5042');
|
||||
const nonExistentTokenId = new BN('13');
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC721URIStorageMock.new(name, symbol);
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['0x49064906']);
|
||||
|
||||
describe('token URI', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(owner, firstTokenId);
|
||||
});
|
||||
|
||||
const baseURI = 'https://api.example.com/v1/';
|
||||
const sampleUri = 'mock://mytoken';
|
||||
|
||||
it('it is empty by default', async function () {
|
||||
expect(await this.token.tokenURI(firstTokenId)).to.be.equal('');
|
||||
});
|
||||
|
||||
it('reverts when queried for non existent token id', async function () {
|
||||
await expectRevert(this.token.tokenURI(nonExistentTokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
|
||||
it('can be set for a token id', async function () {
|
||||
await this.token.$_setTokenURI(firstTokenId, sampleUri);
|
||||
expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri);
|
||||
});
|
||||
|
||||
it('setting the uri emits an event', async function () {
|
||||
expectEvent(await this.token.$_setTokenURI(firstTokenId, sampleUri), 'MetadataUpdate', {
|
||||
_tokenId: firstTokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it('reverts when setting for non existent token id', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_setTokenURI(nonExistentTokenId, sampleUri),
|
||||
'ERC721URIStorage: URI set of nonexistent token',
|
||||
);
|
||||
});
|
||||
|
||||
it('base URI can be set', async function () {
|
||||
await this.token.setBaseURI(baseURI);
|
||||
expect(await this.token.$_baseURI()).to.equal(baseURI);
|
||||
});
|
||||
|
||||
it('base URI is added as a prefix to the token URI', async function () {
|
||||
await this.token.setBaseURI(baseURI);
|
||||
await this.token.$_setTokenURI(firstTokenId, sampleUri);
|
||||
|
||||
expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + sampleUri);
|
||||
});
|
||||
|
||||
it('token URI can be changed by changing the base URI', async function () {
|
||||
await this.token.setBaseURI(baseURI);
|
||||
await this.token.$_setTokenURI(firstTokenId, sampleUri);
|
||||
|
||||
const newBaseURI = 'https://api.example.com/v2/';
|
||||
await this.token.setBaseURI(newBaseURI);
|
||||
expect(await this.token.tokenURI(firstTokenId)).to.be.equal(newBaseURI + sampleUri);
|
||||
});
|
||||
|
||||
it('tokenId is appended to base URI for tokens with no URI', async function () {
|
||||
await this.token.setBaseURI(baseURI);
|
||||
|
||||
expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + firstTokenId);
|
||||
});
|
||||
|
||||
it('tokens without URI can be burnt ', async function () {
|
||||
await this.token.$_burn(firstTokenId, { from: owner });
|
||||
|
||||
expect(await this.token.$_exists(firstTokenId)).to.equal(false);
|
||||
await expectRevert(this.token.tokenURI(firstTokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
|
||||
it('tokens with URI can be burnt ', async function () {
|
||||
await this.token.$_setTokenURI(firstTokenId, sampleUri);
|
||||
|
||||
await this.token.$_burn(firstTokenId, { from: owner });
|
||||
|
||||
expect(await this.token.$_exists(firstTokenId)).to.equal(false);
|
||||
await expectRevert(this.token.tokenURI(firstTokenId), 'ERC721: invalid token ID');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { BN, expectEvent, time } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { getChainId } = require('../../../helpers/chainid');
|
||||
|
||||
const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior');
|
||||
|
||||
const ERC721Votes = artifacts.require('$ERC721Votes');
|
||||
|
||||
contract('ERC721Votes', function (accounts) {
|
||||
const [account1, account2, account1Delegatee, other1, other2] = accounts;
|
||||
|
||||
const name = 'My Vote';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.chainId = await getChainId();
|
||||
|
||||
this.votes = await ERC721Votes.new(name, symbol, name, '1');
|
||||
|
||||
this.NFT0 = new BN('10000000000000000000000000');
|
||||
this.NFT1 = new BN('10');
|
||||
this.NFT2 = new BN('20');
|
||||
this.NFT3 = new BN('30');
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
beforeEach(async function () {
|
||||
await this.votes.$_mint(account1, this.NFT0);
|
||||
await this.votes.$_mint(account1, this.NFT1);
|
||||
await this.votes.$_mint(account1, this.NFT2);
|
||||
await this.votes.$_mint(account1, this.NFT3);
|
||||
});
|
||||
|
||||
it('grants to initial account', async function () {
|
||||
expect(await this.votes.balanceOf(account1)).to.be.bignumber.equal('4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfers', function () {
|
||||
beforeEach(async function () {
|
||||
await this.votes.$_mint(account1, this.NFT0);
|
||||
});
|
||||
|
||||
it('no delegation', async function () {
|
||||
const { receipt } = await this.votes.transferFrom(account1, account2, this.NFT0, { from: account1 });
|
||||
expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: this.NFT0 });
|
||||
expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
|
||||
|
||||
this.account1Votes = '0';
|
||||
this.account2Votes = '0';
|
||||
});
|
||||
|
||||
it('sender delegation', async function () {
|
||||
await this.votes.delegate(account1, { from: account1 });
|
||||
|
||||
const { receipt } = await this.votes.transferFrom(account1, account2, this.NFT0, { from: account1 });
|
||||
expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: this.NFT0 });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousBalance: '1', newBalance: '0' });
|
||||
|
||||
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
|
||||
expect(
|
||||
receipt.logs
|
||||
.filter(({ event }) => event == 'DelegateVotesChanged')
|
||||
.every(({ logIndex }) => transferLogIndex < logIndex),
|
||||
).to.be.equal(true);
|
||||
|
||||
this.account1Votes = '0';
|
||||
this.account2Votes = '0';
|
||||
});
|
||||
|
||||
it('receiver delegation', async function () {
|
||||
await this.votes.delegate(account2, { from: account2 });
|
||||
|
||||
const { receipt } = await this.votes.transferFrom(account1, account2, this.NFT0, { from: account1 });
|
||||
expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: this.NFT0 });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, previousBalance: '0', newBalance: '1' });
|
||||
|
||||
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
|
||||
expect(
|
||||
receipt.logs
|
||||
.filter(({ event }) => event == 'DelegateVotesChanged')
|
||||
.every(({ logIndex }) => transferLogIndex < logIndex),
|
||||
).to.be.equal(true);
|
||||
|
||||
this.account1Votes = '0';
|
||||
this.account2Votes = '1';
|
||||
});
|
||||
|
||||
it('full delegation', async function () {
|
||||
await this.votes.delegate(account1, { from: account1 });
|
||||
await this.votes.delegate(account2, { from: account2 });
|
||||
|
||||
const { receipt } = await this.votes.transferFrom(account1, account2, this.NFT0, { from: account1 });
|
||||
expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: this.NFT0 });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousBalance: '1', newBalance: '0' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, previousBalance: '0', newBalance: '1' });
|
||||
|
||||
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
|
||||
expect(
|
||||
receipt.logs
|
||||
.filter(({ event }) => event == 'DelegateVotesChanged')
|
||||
.every(({ logIndex }) => transferLogIndex < logIndex),
|
||||
).to.be.equal(true);
|
||||
|
||||
this.account1Votes = '0';
|
||||
this.account2Votes = '1';
|
||||
});
|
||||
|
||||
it('returns the same total supply on transfers', async function () {
|
||||
await this.votes.delegate(account1, { from: account1 });
|
||||
|
||||
const { receipt } = await this.votes.transferFrom(account1, account2, this.NFT0, { from: account1 });
|
||||
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.votes.getPastTotalSupply(receipt.blockNumber - 1)).to.be.bignumber.equal('1');
|
||||
expect(await this.votes.getPastTotalSupply(receipt.blockNumber + 1)).to.be.bignumber.equal('1');
|
||||
|
||||
this.account1Votes = '0';
|
||||
this.account2Votes = '0';
|
||||
});
|
||||
|
||||
it('generally returns the voting balance at the appropriate checkpoint', async function () {
|
||||
await this.votes.$_mint(account1, this.NFT1);
|
||||
await this.votes.$_mint(account1, this.NFT2);
|
||||
await this.votes.$_mint(account1, this.NFT3);
|
||||
|
||||
const total = await this.votes.balanceOf(account1);
|
||||
|
||||
const t1 = await this.votes.delegate(other1, { from: account1 });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t2 = await this.votes.transferFrom(account1, other2, this.NFT0, { from: account1 });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t3 = await this.votes.transferFrom(account1, other2, this.NFT2, { from: account1 });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t4 = await this.votes.transferFrom(other2, account1, this.NFT2, { from: other2 });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.votes.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.votes.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total);
|
||||
expect(await this.votes.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total);
|
||||
expect(await this.votes.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3');
|
||||
expect(await this.votes.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3');
|
||||
expect(await this.votes.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2');
|
||||
expect(await this.votes.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2');
|
||||
expect(await this.votes.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3');
|
||||
expect(await this.votes.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3');
|
||||
|
||||
this.account1Votes = '0';
|
||||
this.account2Votes = '0';
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(this.account1Votes);
|
||||
expect(await this.votes.getVotes(account2)).to.be.bignumber.equal(this.account2Votes);
|
||||
|
||||
// need to advance 2 blocks to see the effect of a transfer on "getPastVotes"
|
||||
const blockNumber = await time.latestBlock();
|
||||
await time.advanceBlock();
|
||||
expect(await this.votes.getPastVotes(account1, blockNumber)).to.be.bignumber.equal(this.account1Votes);
|
||||
expect(await this.votes.getPastVotes(account2, blockNumber)).to.be.bignumber.equal(this.account2Votes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Voting workflow', function () {
|
||||
beforeEach(async function () {
|
||||
this.account1 = account1;
|
||||
this.account1Delegatee = account1Delegatee;
|
||||
this.account2 = account2;
|
||||
this.name = 'My Vote';
|
||||
});
|
||||
|
||||
// includes EIP6372 behavior check
|
||||
shouldBehaveLikeVotes();
|
||||
});
|
||||
});
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
const { BN, expectEvent, constants, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { shouldBehaveLikeERC721 } = require('../ERC721.behavior');
|
||||
|
||||
const ERC721 = artifacts.require('$ERC721');
|
||||
const ERC721Wrapper = artifacts.require('$ERC721Wrapper');
|
||||
|
||||
contract('ERC721Wrapper', function (accounts) {
|
||||
const [initialHolder, anotherAccount, approvedAccount] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
const firstTokenId = new BN(1);
|
||||
const secondTokenId = new BN(2);
|
||||
|
||||
beforeEach(async function () {
|
||||
this.underlying = await ERC721.new(name, symbol);
|
||||
this.token = await ERC721Wrapper.new(`Wrapped ${name}`, `W${symbol}`, this.underlying.address);
|
||||
|
||||
await this.underlying.$_safeMint(initialHolder, firstTokenId);
|
||||
await this.underlying.$_safeMint(initialHolder, secondTokenId);
|
||||
});
|
||||
|
||||
it('has a name', async function () {
|
||||
expect(await this.token.name()).to.equal(`Wrapped ${name}`);
|
||||
});
|
||||
|
||||
it('has a symbol', async function () {
|
||||
expect(await this.token.symbol()).to.equal(`W${symbol}`);
|
||||
});
|
||||
|
||||
it('has underlying', async function () {
|
||||
expect(await this.token.underlying()).to.be.bignumber.equal(this.underlying.address);
|
||||
});
|
||||
|
||||
describe('depositFor', function () {
|
||||
it('works with token approval', async function () {
|
||||
await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder });
|
||||
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: this.token.address,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: constants.ZERO_ADDRESS,
|
||||
to: initialHolder,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with approval for all', async function () {
|
||||
await this.underlying.setApprovalForAll(this.token.address, true, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder });
|
||||
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: this.token.address,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: constants.ZERO_ADDRESS,
|
||||
to: initialHolder,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it('works sending to another account', async function () {
|
||||
await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.depositFor(anotherAccount, [firstTokenId], { from: initialHolder });
|
||||
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: this.token.address,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: constants.ZERO_ADDRESS,
|
||||
to: anotherAccount,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with multiple tokens', async function () {
|
||||
await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder });
|
||||
await this.underlying.approve(this.token.address, secondTokenId, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.depositFor(initialHolder, [firstTokenId, secondTokenId], { from: initialHolder });
|
||||
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: this.token.address,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: constants.ZERO_ADDRESS,
|
||||
to: initialHolder,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: this.token.address,
|
||||
tokenId: secondTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: constants.ZERO_ADDRESS,
|
||||
to: initialHolder,
|
||||
tokenId: secondTokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it('reverts with missing approval', async function () {
|
||||
await expectRevert(
|
||||
this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }),
|
||||
'ERC721: caller is not token owner or approved',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('withdrawTo', function () {
|
||||
beforeEach(async function () {
|
||||
await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder });
|
||||
await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder });
|
||||
});
|
||||
|
||||
it('works for an owner', async function () {
|
||||
const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: initialHolder });
|
||||
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: initialHolder,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: constants.ZERO_ADDRESS,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it('works for an approved', async function () {
|
||||
await this.token.approve(approvedAccount, firstTokenId, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: approvedAccount });
|
||||
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: initialHolder,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: constants.ZERO_ADDRESS,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it('works for an approved for all', async function () {
|
||||
await this.token.setApprovalForAll(approvedAccount, true, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: approvedAccount });
|
||||
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: initialHolder,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: constants.ZERO_ADDRESS,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't work for a non-owner nor approved", async function () {
|
||||
await expectRevert(
|
||||
this.token.withdrawTo(initialHolder, [firstTokenId], { from: anotherAccount }),
|
||||
'ERC721Wrapper: caller is not token owner or approved',
|
||||
);
|
||||
});
|
||||
|
||||
it('works with multiple tokens', async function () {
|
||||
await this.underlying.approve(this.token.address, secondTokenId, { from: initialHolder });
|
||||
await this.token.depositFor(initialHolder, [secondTokenId], { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId, secondTokenId], { from: initialHolder });
|
||||
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: initialHolder,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: initialHolder,
|
||||
tokenId: secondTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: constants.ZERO_ADDRESS,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: constants.ZERO_ADDRESS,
|
||||
tokenId: secondTokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it('works to another account', async function () {
|
||||
const { tx } = await this.token.withdrawTo(anotherAccount, [firstTokenId], { from: initialHolder });
|
||||
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: anotherAccount,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: constants.ZERO_ADDRESS,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onERC721Received', function () {
|
||||
it('only allows calls from underlying', async function () {
|
||||
await expectRevert(
|
||||
this.token.onERC721Received(
|
||||
initialHolder,
|
||||
this.token.address,
|
||||
firstTokenId,
|
||||
anotherAccount, // Correct data
|
||||
{ from: anotherAccount },
|
||||
),
|
||||
'ERC721Wrapper: caller is not underlying',
|
||||
);
|
||||
});
|
||||
|
||||
it('mints a token to from', async function () {
|
||||
const { tx } = await this.underlying.safeTransferFrom(initialHolder, this.token.address, firstTokenId, {
|
||||
from: initialHolder,
|
||||
});
|
||||
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: constants.ZERO_ADDRESS,
|
||||
to: initialHolder,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_recover', function () {
|
||||
it('works if there is something to recover', async function () {
|
||||
// Should use `transferFrom` to avoid `onERC721Received` minting
|
||||
await this.underlying.transferFrom(initialHolder, this.token.address, firstTokenId, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.$_recover(anotherAccount, firstTokenId);
|
||||
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: constants.ZERO_ADDRESS,
|
||||
to: anotherAccount,
|
||||
tokenId: firstTokenId,
|
||||
});
|
||||
});
|
||||
|
||||
it('reverts if there is nothing to recover', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_recover(initialHolder, firstTokenId),
|
||||
'ERC721Wrapper: wrapper is not token owner',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ERC712 behavior', function () {
|
||||
shouldBehaveLikeERC721('ERC721', ...accounts);
|
||||
});
|
||||
});
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC721PresetMinterPauserAutoId = artifacts.require('ERC721PresetMinterPauserAutoId');
|
||||
|
||||
contract('ERC721PresetMinterPauserAutoId', function (accounts) {
|
||||
const [deployer, other] = accounts;
|
||||
|
||||
const name = 'MinterAutoIDToken';
|
||||
const symbol = 'MAIT';
|
||||
const baseURI = 'my.app/';
|
||||
|
||||
const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
||||
const MINTER_ROLE = web3.utils.soliditySha3('MINTER_ROLE');
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC721PresetMinterPauserAutoId.new(name, symbol, baseURI, { from: deployer });
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC721', 'ERC721Enumerable', 'AccessControl', 'AccessControlEnumerable']);
|
||||
|
||||
it('token has correct name', async function () {
|
||||
expect(await this.token.name()).to.equal(name);
|
||||
});
|
||||
|
||||
it('token has correct symbol', async function () {
|
||||
expect(await this.token.symbol()).to.equal(symbol);
|
||||
});
|
||||
|
||||
it('deployer has the default admin role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(DEFAULT_ADMIN_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(DEFAULT_ADMIN_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('deployer has the minter role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(MINTER_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(MINTER_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('minter role admin is the default admin', async function () {
|
||||
expect(await this.token.getRoleAdmin(MINTER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
|
||||
});
|
||||
|
||||
describe('minting', function () {
|
||||
it('deployer can mint tokens', async function () {
|
||||
const tokenId = new BN('0');
|
||||
|
||||
const receipt = await this.token.mint(other, { from: deployer });
|
||||
expectEvent(receipt, 'Transfer', { from: ZERO_ADDRESS, to: other, tokenId });
|
||||
|
||||
expect(await this.token.balanceOf(other)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.ownerOf(tokenId)).to.equal(other);
|
||||
|
||||
expect(await this.token.tokenURI(tokenId)).to.equal(baseURI + tokenId);
|
||||
});
|
||||
|
||||
it('other accounts cannot mint tokens', async function () {
|
||||
await expectRevert(
|
||||
this.token.mint(other, { from: other }),
|
||||
'ERC721PresetMinterPauserAutoId: must have minter role to mint',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pausing', function () {
|
||||
it('deployer can pause', async function () {
|
||||
const receipt = await this.token.pause({ from: deployer });
|
||||
expectEvent(receipt, 'Paused', { account: deployer });
|
||||
|
||||
expect(await this.token.paused()).to.equal(true);
|
||||
});
|
||||
|
||||
it('deployer can unpause', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
const receipt = await this.token.unpause({ from: deployer });
|
||||
expectEvent(receipt, 'Unpaused', { account: deployer });
|
||||
|
||||
expect(await this.token.paused()).to.equal(false);
|
||||
});
|
||||
|
||||
it('cannot mint while paused', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
await expectRevert(this.token.mint(other, { from: deployer }), 'ERC721Pausable: token transfer while paused');
|
||||
});
|
||||
|
||||
it('other accounts cannot pause', async function () {
|
||||
await expectRevert(
|
||||
this.token.pause({ from: other }),
|
||||
'ERC721PresetMinterPauserAutoId: must have pauser role to pause',
|
||||
);
|
||||
});
|
||||
|
||||
it('other accounts cannot unpause', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
await expectRevert(
|
||||
this.token.unpause({ from: other }),
|
||||
'ERC721PresetMinterPauserAutoId: must have pauser role to unpause',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('burning', function () {
|
||||
it('holders can burn their tokens', async function () {
|
||||
const tokenId = new BN('0');
|
||||
|
||||
await this.token.mint(other, { from: deployer });
|
||||
|
||||
const receipt = await this.token.burn(tokenId, { from: other });
|
||||
|
||||
expectEvent(receipt, 'Transfer', { from: other, to: ZERO_ADDRESS, tokenId });
|
||||
|
||||
expect(await this.token.balanceOf(other)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC721Holder = artifacts.require('ERC721Holder');
|
||||
const ERC721 = artifacts.require('$ERC721');
|
||||
|
||||
contract('ERC721Holder', function (accounts) {
|
||||
const [owner] = accounts;
|
||||
|
||||
const name = 'Non Fungible Token';
|
||||
const symbol = 'NFT';
|
||||
const tokenId = web3.utils.toBN(1);
|
||||
|
||||
it('receives an ERC721 token', async function () {
|
||||
const token = await ERC721.new(name, symbol);
|
||||
await token.$_mint(owner, tokenId);
|
||||
|
||||
const receiver = await ERC721Holder.new();
|
||||
await token.safeTransferFrom(owner, receiver.address, tokenId, { from: owner });
|
||||
|
||||
expect(await token.ownerOf(tokenId)).to.be.equal(receiver.address);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,597 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC777SenderRecipientMock = artifacts.require('ERC777SenderRecipientMock');
|
||||
|
||||
function shouldBehaveLikeERC777DirectSendBurn(holder, recipient, data) {
|
||||
shouldBehaveLikeERC777DirectSend(holder, recipient, data);
|
||||
shouldBehaveLikeERC777DirectBurn(holder, data);
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777OperatorSendBurn(holder, recipient, operator, data, operatorData) {
|
||||
shouldBehaveLikeERC777OperatorSend(holder, recipient, operator, data, operatorData);
|
||||
shouldBehaveLikeERC777OperatorBurn(holder, operator, data, operatorData);
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, recipient, operator, data, operatorData) {
|
||||
shouldBehaveLikeERC777UnauthorizedOperatorSend(holder, recipient, operator, data, operatorData);
|
||||
shouldBehaveLikeERC777UnauthorizedOperatorBurn(holder, operator, data, operatorData);
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777DirectSend(holder, recipient, data) {
|
||||
describe('direct send', function () {
|
||||
context('when the sender has tokens', function () {
|
||||
shouldDirectSendTokens(holder, recipient, new BN('0'), data);
|
||||
shouldDirectSendTokens(holder, recipient, new BN('1'), data);
|
||||
|
||||
it('reverts when sending more than the balance', async function () {
|
||||
const balance = await this.token.balanceOf(holder);
|
||||
await expectRevert.unspecified(this.token.send(recipient, balance.addn(1), data, { from: holder }));
|
||||
});
|
||||
|
||||
it('reverts when sending to the zero address', async function () {
|
||||
await expectRevert.unspecified(this.token.send(ZERO_ADDRESS, new BN('1'), data, { from: holder }));
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender has no tokens', function () {
|
||||
removeBalance(holder);
|
||||
|
||||
shouldDirectSendTokens(holder, recipient, new BN('0'), data);
|
||||
|
||||
it('reverts when sending a non-zero amount', async function () {
|
||||
await expectRevert.unspecified(this.token.send(recipient, new BN('1'), data, { from: holder }));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777OperatorSend(holder, recipient, operator, data, operatorData) {
|
||||
describe('operator send', function () {
|
||||
context('when the sender has tokens', async function () {
|
||||
shouldOperatorSendTokens(holder, operator, recipient, new BN('0'), data, operatorData);
|
||||
shouldOperatorSendTokens(holder, operator, recipient, new BN('1'), data, operatorData);
|
||||
|
||||
it('reverts when sending more than the balance', async function () {
|
||||
const balance = await this.token.balanceOf(holder);
|
||||
await expectRevert.unspecified(
|
||||
this.token.operatorSend(holder, recipient, balance.addn(1), data, operatorData, { from: operator }),
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when sending to the zero address', async function () {
|
||||
await expectRevert.unspecified(
|
||||
this.token.operatorSend(holder, ZERO_ADDRESS, new BN('1'), data, operatorData, { from: operator }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender has no tokens', function () {
|
||||
removeBalance(holder);
|
||||
|
||||
shouldOperatorSendTokens(holder, operator, recipient, new BN('0'), data, operatorData);
|
||||
|
||||
it('reverts when sending a non-zero amount', async function () {
|
||||
await expectRevert.unspecified(
|
||||
this.token.operatorSend(holder, recipient, new BN('1'), data, operatorData, { from: operator }),
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when sending from the zero address', async function () {
|
||||
// This is not yet reflected in the spec
|
||||
await expectRevert.unspecified(
|
||||
this.token.operatorSend(ZERO_ADDRESS, recipient, new BN('0'), data, operatorData, { from: operator }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777UnauthorizedOperatorSend(holder, recipient, operator, data, operatorData) {
|
||||
describe('operator send', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert.unspecified(this.token.operatorSend(holder, recipient, new BN('0'), data, operatorData));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777DirectBurn(holder, data) {
|
||||
describe('direct burn', function () {
|
||||
context('when the sender has tokens', function () {
|
||||
shouldDirectBurnTokens(holder, new BN('0'), data);
|
||||
shouldDirectBurnTokens(holder, new BN('1'), data);
|
||||
|
||||
it('reverts when burning more than the balance', async function () {
|
||||
const balance = await this.token.balanceOf(holder);
|
||||
await expectRevert.unspecified(this.token.burn(balance.addn(1), data, { from: holder }));
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender has no tokens', function () {
|
||||
removeBalance(holder);
|
||||
|
||||
shouldDirectBurnTokens(holder, new BN('0'), data);
|
||||
|
||||
it('reverts when burning a non-zero amount', async function () {
|
||||
await expectRevert.unspecified(this.token.burn(new BN('1'), data, { from: holder }));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777OperatorBurn(holder, operator, data, operatorData) {
|
||||
describe('operator burn', function () {
|
||||
context('when the sender has tokens', async function () {
|
||||
shouldOperatorBurnTokens(holder, operator, new BN('0'), data, operatorData);
|
||||
shouldOperatorBurnTokens(holder, operator, new BN('1'), data, operatorData);
|
||||
|
||||
it('reverts when burning more than the balance', async function () {
|
||||
const balance = await this.token.balanceOf(holder);
|
||||
await expectRevert.unspecified(
|
||||
this.token.operatorBurn(holder, balance.addn(1), data, operatorData, { from: operator }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender has no tokens', function () {
|
||||
removeBalance(holder);
|
||||
|
||||
shouldOperatorBurnTokens(holder, operator, new BN('0'), data, operatorData);
|
||||
|
||||
it('reverts when burning a non-zero amount', async function () {
|
||||
await expectRevert.unspecified(
|
||||
this.token.operatorBurn(holder, new BN('1'), data, operatorData, { from: operator }),
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when burning from the zero address', async function () {
|
||||
// This is not yet reflected in the spec
|
||||
await expectRevert.unspecified(
|
||||
this.token.operatorBurn(ZERO_ADDRESS, new BN('0'), data, operatorData, { from: operator }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777UnauthorizedOperatorBurn(holder, operator, data, operatorData) {
|
||||
describe('operator burn', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert.unspecified(this.token.operatorBurn(holder, new BN('0'), data, operatorData));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldDirectSendTokens(from, to, amount, data) {
|
||||
shouldSendTokens(from, null, to, amount, data, null);
|
||||
}
|
||||
|
||||
function shouldOperatorSendTokens(from, operator, to, amount, data, operatorData) {
|
||||
shouldSendTokens(from, operator, to, amount, data, operatorData);
|
||||
}
|
||||
|
||||
function shouldSendTokens(from, operator, to, amount, data, operatorData) {
|
||||
const operatorCall = operator !== null;
|
||||
|
||||
it(`${operatorCall ? 'operator ' : ''}can send an amount of ${amount}`, async function () {
|
||||
const initialTotalSupply = await this.token.totalSupply();
|
||||
const initialFromBalance = await this.token.balanceOf(from);
|
||||
const initialToBalance = await this.token.balanceOf(to);
|
||||
|
||||
let receipt;
|
||||
if (!operatorCall) {
|
||||
receipt = await this.token.send(to, amount, data, { from });
|
||||
expectEvent(receipt, 'Sent', {
|
||||
operator: from,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
data,
|
||||
operatorData: null,
|
||||
});
|
||||
} else {
|
||||
receipt = await this.token.operatorSend(from, to, amount, data, operatorData, { from: operator });
|
||||
expectEvent(receipt, 'Sent', {
|
||||
operator,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
});
|
||||
}
|
||||
|
||||
expectEvent(receipt, 'Transfer', {
|
||||
from,
|
||||
to,
|
||||
value: amount,
|
||||
});
|
||||
|
||||
const finalTotalSupply = await this.token.totalSupply();
|
||||
const finalFromBalance = await this.token.balanceOf(from);
|
||||
const finalToBalance = await this.token.balanceOf(to);
|
||||
|
||||
expect(finalTotalSupply).to.be.bignumber.equal(initialTotalSupply);
|
||||
expect(finalToBalance.sub(initialToBalance)).to.be.bignumber.equal(amount);
|
||||
expect(finalFromBalance.sub(initialFromBalance)).to.be.bignumber.equal(amount.neg());
|
||||
});
|
||||
}
|
||||
|
||||
function shouldDirectBurnTokens(from, amount, data) {
|
||||
shouldBurnTokens(from, null, amount, data, null);
|
||||
}
|
||||
|
||||
function shouldOperatorBurnTokens(from, operator, amount, data, operatorData) {
|
||||
shouldBurnTokens(from, operator, amount, data, operatorData);
|
||||
}
|
||||
|
||||
function shouldBurnTokens(from, operator, amount, data, operatorData) {
|
||||
const operatorCall = operator !== null;
|
||||
|
||||
it(`${operatorCall ? 'operator ' : ''}can burn an amount of ${amount}`, async function () {
|
||||
const initialTotalSupply = await this.token.totalSupply();
|
||||
const initialFromBalance = await this.token.balanceOf(from);
|
||||
|
||||
let receipt;
|
||||
if (!operatorCall) {
|
||||
receipt = await this.token.burn(amount, data, { from });
|
||||
expectEvent(receipt, 'Burned', {
|
||||
operator: from,
|
||||
from,
|
||||
amount,
|
||||
data,
|
||||
operatorData: null,
|
||||
});
|
||||
} else {
|
||||
receipt = await this.token.operatorBurn(from, amount, data, operatorData, { from: operator });
|
||||
expectEvent(receipt, 'Burned', {
|
||||
operator,
|
||||
from,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
});
|
||||
}
|
||||
|
||||
expectEvent(receipt, 'Transfer', {
|
||||
from,
|
||||
to: ZERO_ADDRESS,
|
||||
value: amount,
|
||||
});
|
||||
|
||||
const finalTotalSupply = await this.token.totalSupply();
|
||||
const finalFromBalance = await this.token.balanceOf(from);
|
||||
|
||||
expect(finalTotalSupply.sub(initialTotalSupply)).to.be.bignumber.equal(amount.neg());
|
||||
expect(finalFromBalance.sub(initialFromBalance)).to.be.bignumber.equal(amount.neg());
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777InternalMint(recipient, operator, amount, data, operatorData) {
|
||||
shouldInternalMintTokens(operator, recipient, new BN('0'), data, operatorData);
|
||||
shouldInternalMintTokens(operator, recipient, amount, data, operatorData);
|
||||
|
||||
it('reverts when minting tokens for the zero address', async function () {
|
||||
await expectRevert.unspecified(
|
||||
this.token.$_mint(ZERO_ADDRESS, amount, data, operatorData, true, { from: operator }),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldInternalMintTokens(operator, to, amount, data, operatorData) {
|
||||
it(`can (internal) mint an amount of ${amount}`, async function () {
|
||||
const initialTotalSupply = await this.token.totalSupply();
|
||||
const initialToBalance = await this.token.balanceOf(to);
|
||||
|
||||
const receipt = await this.token.$_mint(to, amount, data, operatorData, true, { from: operator });
|
||||
|
||||
expectEvent(receipt, 'Minted', {
|
||||
operator,
|
||||
to,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
});
|
||||
|
||||
expectEvent(receipt, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to,
|
||||
value: amount,
|
||||
});
|
||||
|
||||
const finalTotalSupply = await this.token.totalSupply();
|
||||
const finalToBalance = await this.token.balanceOf(to);
|
||||
|
||||
expect(finalTotalSupply.sub(initialTotalSupply)).to.be.bignumber.equal(amount);
|
||||
expect(finalToBalance.sub(initialToBalance)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData) {
|
||||
context('when TokensRecipient reverts', function () {
|
||||
beforeEach(async function () {
|
||||
await this.tokensRecipientImplementer.setShouldRevertReceive(true);
|
||||
});
|
||||
|
||||
it('send reverts', async function () {
|
||||
await expectRevert.unspecified(sendFromHolder(this.token, this.sender, this.recipient, amount, data));
|
||||
});
|
||||
|
||||
it('operatorSend reverts', async function () {
|
||||
await expectRevert.unspecified(
|
||||
this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }),
|
||||
);
|
||||
});
|
||||
|
||||
it('mint (internal) reverts', async function () {
|
||||
await expectRevert.unspecified(
|
||||
this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when TokensRecipient does not revert', function () {
|
||||
beforeEach(async function () {
|
||||
await this.tokensRecipientImplementer.setShouldRevertSend(false);
|
||||
});
|
||||
|
||||
it('TokensRecipient receives send data and is called after state mutation', async function () {
|
||||
const { tx } = await sendFromHolder(this.token, this.sender, this.recipient, amount, data);
|
||||
|
||||
const postSenderBalance = await this.token.balanceOf(this.sender);
|
||||
const postRecipientBalance = await this.token.balanceOf(this.recipient);
|
||||
|
||||
await assertTokensReceivedCalled(
|
||||
this.token,
|
||||
tx,
|
||||
this.sender,
|
||||
this.sender,
|
||||
this.recipient,
|
||||
amount,
|
||||
data,
|
||||
null,
|
||||
postSenderBalance,
|
||||
postRecipientBalance,
|
||||
);
|
||||
});
|
||||
|
||||
it('TokensRecipient receives operatorSend data and is called after state mutation', async function () {
|
||||
const { tx } = await this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, {
|
||||
from: operator,
|
||||
});
|
||||
|
||||
const postSenderBalance = await this.token.balanceOf(this.sender);
|
||||
const postRecipientBalance = await this.token.balanceOf(this.recipient);
|
||||
|
||||
await assertTokensReceivedCalled(
|
||||
this.token,
|
||||
tx,
|
||||
operator,
|
||||
this.sender,
|
||||
this.recipient,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
postSenderBalance,
|
||||
postRecipientBalance,
|
||||
);
|
||||
});
|
||||
|
||||
it('TokensRecipient receives mint (internal) data and is called after state mutation', async function () {
|
||||
const { tx } = await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator });
|
||||
|
||||
const postRecipientBalance = await this.token.balanceOf(this.recipient);
|
||||
|
||||
await assertTokensReceivedCalled(
|
||||
this.token,
|
||||
tx,
|
||||
operator,
|
||||
ZERO_ADDRESS,
|
||||
this.recipient,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
new BN('0'),
|
||||
postRecipientBalance,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData) {
|
||||
context('when TokensSender reverts', function () {
|
||||
beforeEach(async function () {
|
||||
await this.tokensSenderImplementer.setShouldRevertSend(true);
|
||||
});
|
||||
|
||||
it('send reverts', async function () {
|
||||
await expectRevert.unspecified(sendFromHolder(this.token, this.sender, this.recipient, amount, data));
|
||||
});
|
||||
|
||||
it('operatorSend reverts', async function () {
|
||||
await expectRevert.unspecified(
|
||||
this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }),
|
||||
);
|
||||
});
|
||||
|
||||
it('burn reverts', async function () {
|
||||
await expectRevert.unspecified(burnFromHolder(this.token, this.sender, amount, data));
|
||||
});
|
||||
|
||||
it('operatorBurn reverts', async function () {
|
||||
await expectRevert.unspecified(
|
||||
this.token.operatorBurn(this.sender, amount, data, operatorData, { from: operator }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when TokensSender does not revert', function () {
|
||||
beforeEach(async function () {
|
||||
await this.tokensSenderImplementer.setShouldRevertSend(false);
|
||||
});
|
||||
|
||||
it('TokensSender receives send data and is called before state mutation', async function () {
|
||||
const preSenderBalance = await this.token.balanceOf(this.sender);
|
||||
const preRecipientBalance = await this.token.balanceOf(this.recipient);
|
||||
|
||||
const { tx } = await sendFromHolder(this.token, this.sender, this.recipient, amount, data);
|
||||
|
||||
await assertTokensToSendCalled(
|
||||
this.token,
|
||||
tx,
|
||||
this.sender,
|
||||
this.sender,
|
||||
this.recipient,
|
||||
amount,
|
||||
data,
|
||||
null,
|
||||
preSenderBalance,
|
||||
preRecipientBalance,
|
||||
);
|
||||
});
|
||||
|
||||
it('TokensSender receives operatorSend data and is called before state mutation', async function () {
|
||||
const preSenderBalance = await this.token.balanceOf(this.sender);
|
||||
const preRecipientBalance = await this.token.balanceOf(this.recipient);
|
||||
|
||||
const { tx } = await this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, {
|
||||
from: operator,
|
||||
});
|
||||
|
||||
await assertTokensToSendCalled(
|
||||
this.token,
|
||||
tx,
|
||||
operator,
|
||||
this.sender,
|
||||
this.recipient,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
preSenderBalance,
|
||||
preRecipientBalance,
|
||||
);
|
||||
});
|
||||
|
||||
it('TokensSender receives burn data and is called before state mutation', async function () {
|
||||
const preSenderBalance = await this.token.balanceOf(this.sender);
|
||||
|
||||
const { tx } = await burnFromHolder(this.token, this.sender, amount, data, { from: this.sender });
|
||||
|
||||
await assertTokensToSendCalled(
|
||||
this.token,
|
||||
tx,
|
||||
this.sender,
|
||||
this.sender,
|
||||
ZERO_ADDRESS,
|
||||
amount,
|
||||
data,
|
||||
null,
|
||||
preSenderBalance,
|
||||
);
|
||||
});
|
||||
|
||||
it('TokensSender receives operatorBurn data and is called before state mutation', async function () {
|
||||
const preSenderBalance = await this.token.balanceOf(this.sender);
|
||||
|
||||
const { tx } = await this.token.operatorBurn(this.sender, amount, data, operatorData, { from: operator });
|
||||
|
||||
await assertTokensToSendCalled(
|
||||
this.token,
|
||||
tx,
|
||||
operator,
|
||||
this.sender,
|
||||
ZERO_ADDRESS,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
preSenderBalance,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeBalance(holder) {
|
||||
beforeEach(async function () {
|
||||
await this.token.burn(await this.token.balanceOf(holder), '0x', { from: holder });
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('0');
|
||||
});
|
||||
}
|
||||
|
||||
async function assertTokensReceivedCalled(
|
||||
token,
|
||||
txHash,
|
||||
operator,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
fromBalance,
|
||||
toBalance = '0',
|
||||
) {
|
||||
await expectEvent.inTransaction(txHash, ERC777SenderRecipientMock, 'TokensReceivedCalled', {
|
||||
operator,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
token: token.address,
|
||||
fromBalance,
|
||||
toBalance,
|
||||
});
|
||||
}
|
||||
|
||||
async function assertTokensToSendCalled(
|
||||
token,
|
||||
txHash,
|
||||
operator,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
fromBalance,
|
||||
toBalance = '0',
|
||||
) {
|
||||
await expectEvent.inTransaction(txHash, ERC777SenderRecipientMock, 'TokensToSendCalled', {
|
||||
operator,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
token: token.address,
|
||||
fromBalance,
|
||||
toBalance,
|
||||
});
|
||||
}
|
||||
|
||||
async function sendFromHolder(token, holder, to, amount, data) {
|
||||
if ((await web3.eth.getCode(holder)).length <= '0x'.length) {
|
||||
return token.send(to, amount, data, { from: holder });
|
||||
} else {
|
||||
// assume holder is ERC777SenderRecipientMock contract
|
||||
return (await ERC777SenderRecipientMock.at(holder)).send(token.address, to, amount, data);
|
||||
}
|
||||
}
|
||||
|
||||
async function burnFromHolder(token, holder, amount, data) {
|
||||
if ((await web3.eth.getCode(holder)).length <= '0x'.length) {
|
||||
return token.burn(amount, data, { from: holder });
|
||||
} else {
|
||||
// assume holder is ERC777SenderRecipientMock contract
|
||||
return (await ERC777SenderRecipientMock.at(holder)).burn(token.address, amount, data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC777DirectSendBurn,
|
||||
shouldBehaveLikeERC777OperatorSendBurn,
|
||||
shouldBehaveLikeERC777UnauthorizedOperatorSendBurn,
|
||||
shouldBehaveLikeERC777InternalMint,
|
||||
shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook,
|
||||
shouldBehaveLikeERC777SendBurnWithSendHook,
|
||||
};
|
||||
@@ -0,0 +1,556 @@
|
||||
const { BN, constants, expectEvent, expectRevert, singletons } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const {
|
||||
shouldBehaveLikeERC777DirectSendBurn,
|
||||
shouldBehaveLikeERC777OperatorSendBurn,
|
||||
shouldBehaveLikeERC777UnauthorizedOperatorSendBurn,
|
||||
shouldBehaveLikeERC777InternalMint,
|
||||
shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook,
|
||||
shouldBehaveLikeERC777SendBurnWithSendHook,
|
||||
} = require('./ERC777.behavior');
|
||||
|
||||
const { shouldBehaveLikeERC20, shouldBehaveLikeERC20Approve } = require('../ERC20/ERC20.behavior');
|
||||
|
||||
const ERC777 = artifacts.require('$ERC777Mock');
|
||||
const ERC777SenderRecipientMock = artifacts.require('$ERC777SenderRecipientMock');
|
||||
|
||||
contract('ERC777', function (accounts) {
|
||||
const [registryFunder, holder, defaultOperatorA, defaultOperatorB, newOperator, anyone] = accounts;
|
||||
|
||||
const initialSupply = new BN('10000');
|
||||
const name = 'ERC777Test';
|
||||
const symbol = '777T';
|
||||
const data = web3.utils.sha3('OZ777TestData');
|
||||
const operatorData = web3.utils.sha3('OZ777TestOperatorData');
|
||||
|
||||
const defaultOperators = [defaultOperatorA, defaultOperatorB];
|
||||
|
||||
beforeEach(async function () {
|
||||
this.erc1820 = await singletons.ERC1820Registry(registryFunder);
|
||||
});
|
||||
|
||||
context('with default operators', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC777.new(name, symbol, defaultOperators);
|
||||
await this.token.$_mint(holder, initialSupply, '0x', '0x');
|
||||
});
|
||||
|
||||
describe('as an ERC20 token', function () {
|
||||
shouldBehaveLikeERC20('ERC777', initialSupply, holder, anyone, defaultOperatorA);
|
||||
|
||||
describe('_approve', function () {
|
||||
shouldBehaveLikeERC20Approve('ERC777', holder, anyone, initialSupply, function (owner, spender, amount) {
|
||||
return this.token.$_approve(owner, spender, amount);
|
||||
});
|
||||
|
||||
describe('when the owner is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_approve(ZERO_ADDRESS, anyone, initialSupply),
|
||||
'ERC777: approve from the zero address',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not emit AuthorizedOperator events for default operators', async function () {
|
||||
await expectEvent.notEmitted.inConstruction(this.token, 'AuthorizedOperator');
|
||||
});
|
||||
|
||||
describe('basic information', function () {
|
||||
it('returns the name', async function () {
|
||||
expect(await this.token.name()).to.equal(name);
|
||||
});
|
||||
|
||||
it('returns the symbol', async function () {
|
||||
expect(await this.token.symbol()).to.equal(symbol);
|
||||
});
|
||||
|
||||
it('returns a granularity of 1', async function () {
|
||||
expect(await this.token.granularity()).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('returns the default operators', async function () {
|
||||
expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators);
|
||||
});
|
||||
|
||||
it('default operators are operators for all accounts', async function () {
|
||||
for (const operator of defaultOperators) {
|
||||
expect(await this.token.isOperatorFor(operator, anyone)).to.equal(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns the total supply', async function () {
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
it('returns 18 when decimals is called', async function () {
|
||||
expect(await this.token.decimals()).to.be.bignumber.equal('18');
|
||||
});
|
||||
|
||||
it('the ERC777Token interface is registered in the registry', async function () {
|
||||
expect(
|
||||
await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC777Token')),
|
||||
).to.equal(this.token.address);
|
||||
});
|
||||
|
||||
it('the ERC20Token interface is registered in the registry', async function () {
|
||||
expect(
|
||||
await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC20Token')),
|
||||
).to.equal(this.token.address);
|
||||
});
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
context('for an account with no tokens', function () {
|
||||
it('returns zero', async function () {
|
||||
expect(await this.token.balanceOf(anyone)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('for an account with tokens', function () {
|
||||
it('returns their balance', async function () {
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('with no ERC777TokensSender and no ERC777TokensRecipient implementers', function () {
|
||||
describe('send/burn', function () {
|
||||
shouldBehaveLikeERC777DirectSendBurn(holder, anyone, data);
|
||||
|
||||
context('with self operator', function () {
|
||||
shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, holder, data, operatorData);
|
||||
});
|
||||
|
||||
context('with first default operator', function () {
|
||||
shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorA, data, operatorData);
|
||||
});
|
||||
|
||||
context('with second default operator', function () {
|
||||
shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorB, data, operatorData);
|
||||
});
|
||||
|
||||
context('before authorizing a new operator', function () {
|
||||
shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData);
|
||||
});
|
||||
|
||||
context('with new authorized operator', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.authorizeOperator(newOperator, { from: holder });
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, newOperator, data, operatorData);
|
||||
|
||||
context('with revoked operator', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.revokeOperator(newOperator, { from: holder });
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mint (internal)', function () {
|
||||
const to = anyone;
|
||||
const amount = new BN('5');
|
||||
|
||||
context('with default operator', function () {
|
||||
const operator = defaultOperatorA;
|
||||
|
||||
shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData);
|
||||
});
|
||||
|
||||
context('with non operator', function () {
|
||||
const operator = newOperator;
|
||||
|
||||
shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mint (internal extended)', function () {
|
||||
const amount = new BN('5');
|
||||
|
||||
context('to anyone', function () {
|
||||
beforeEach(async function () {
|
||||
this.recipient = anyone;
|
||||
});
|
||||
|
||||
context('with default operator', function () {
|
||||
const operator = defaultOperatorA;
|
||||
|
||||
it('without requireReceptionAck', async function () {
|
||||
await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
|
||||
});
|
||||
|
||||
it('with requireReceptionAck', async function () {
|
||||
await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator });
|
||||
});
|
||||
});
|
||||
|
||||
context('with non operator', function () {
|
||||
const operator = newOperator;
|
||||
|
||||
it('without requireReceptionAck', async function () {
|
||||
await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
|
||||
});
|
||||
|
||||
it('with requireReceptionAck', async function () {
|
||||
await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('to non ERC777TokensRecipient implementer', function () {
|
||||
beforeEach(async function () {
|
||||
this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
|
||||
this.recipient = this.tokensRecipientImplementer.address;
|
||||
});
|
||||
|
||||
context('with default operator', function () {
|
||||
const operator = defaultOperatorA;
|
||||
|
||||
it('without requireReceptionAck', async function () {
|
||||
await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
|
||||
});
|
||||
|
||||
it('with requireReceptionAck', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }),
|
||||
'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with non operator', function () {
|
||||
const operator = newOperator;
|
||||
|
||||
it('without requireReceptionAck', async function () {
|
||||
await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
|
||||
});
|
||||
|
||||
it('with requireReceptionAck', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }),
|
||||
'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('operator management', function () {
|
||||
it('accounts are their own operator', async function () {
|
||||
expect(await this.token.isOperatorFor(holder, holder)).to.equal(true);
|
||||
});
|
||||
|
||||
it('reverts when self-authorizing', async function () {
|
||||
await expectRevert(
|
||||
this.token.authorizeOperator(holder, { from: holder }),
|
||||
'ERC777: authorizing self as operator',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when self-revoking', async function () {
|
||||
await expectRevert(this.token.revokeOperator(holder, { from: holder }), 'ERC777: revoking self as operator');
|
||||
});
|
||||
|
||||
it('non-operators can be revoked', async function () {
|
||||
expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
|
||||
|
||||
const receipt = await this.token.revokeOperator(newOperator, { from: holder });
|
||||
expectEvent(receipt, 'RevokedOperator', { operator: newOperator, tokenHolder: holder });
|
||||
|
||||
expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
|
||||
});
|
||||
|
||||
it('non-operators can be authorized', async function () {
|
||||
expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
|
||||
|
||||
const receipt = await this.token.authorizeOperator(newOperator, { from: holder });
|
||||
expectEvent(receipt, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder });
|
||||
|
||||
expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(true);
|
||||
});
|
||||
|
||||
describe('new operators', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.authorizeOperator(newOperator, { from: holder });
|
||||
});
|
||||
|
||||
it('are not added to the default operators list', async function () {
|
||||
expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators);
|
||||
});
|
||||
|
||||
it('can be re-authorized', async function () {
|
||||
const receipt = await this.token.authorizeOperator(newOperator, { from: holder });
|
||||
expectEvent(receipt, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder });
|
||||
|
||||
expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(true);
|
||||
});
|
||||
|
||||
it('can be revoked', async function () {
|
||||
const receipt = await this.token.revokeOperator(newOperator, { from: holder });
|
||||
expectEvent(receipt, 'RevokedOperator', { operator: newOperator, tokenHolder: holder });
|
||||
|
||||
expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default operators', function () {
|
||||
it('can be re-authorized', async function () {
|
||||
const receipt = await this.token.authorizeOperator(defaultOperatorA, { from: holder });
|
||||
expectEvent(receipt, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder });
|
||||
|
||||
expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(true);
|
||||
});
|
||||
|
||||
it('can be revoked', async function () {
|
||||
const receipt = await this.token.revokeOperator(defaultOperatorA, { from: holder });
|
||||
expectEvent(receipt, 'RevokedOperator', { operator: defaultOperatorA, tokenHolder: holder });
|
||||
|
||||
expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(false);
|
||||
});
|
||||
|
||||
it('cannot be revoked for themselves', async function () {
|
||||
await expectRevert(
|
||||
this.token.revokeOperator(defaultOperatorA, { from: defaultOperatorA }),
|
||||
'ERC777: revoking self as operator',
|
||||
);
|
||||
});
|
||||
|
||||
context('with revoked default operator', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.revokeOperator(defaultOperatorA, { from: holder });
|
||||
});
|
||||
|
||||
it('default operator is not revoked for other holders', async function () {
|
||||
expect(await this.token.isOperatorFor(defaultOperatorA, anyone)).to.equal(true);
|
||||
});
|
||||
|
||||
it('other default operators are not revoked', async function () {
|
||||
expect(await this.token.isOperatorFor(defaultOperatorB, holder)).to.equal(true);
|
||||
});
|
||||
|
||||
it('default operators list is not modified', async function () {
|
||||
expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators);
|
||||
});
|
||||
|
||||
it('revoked default operator can be re-authorized', async function () {
|
||||
const receipt = await this.token.authorizeOperator(defaultOperatorA, { from: holder });
|
||||
expectEvent(receipt, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder });
|
||||
|
||||
expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('send and receive hooks', function () {
|
||||
const amount = new BN('1');
|
||||
const operator = defaultOperatorA;
|
||||
// sender and recipient are stored inside 'this', since in some tests their addresses are determined dynamically
|
||||
|
||||
describe('tokensReceived', function () {
|
||||
beforeEach(function () {
|
||||
this.sender = holder;
|
||||
});
|
||||
|
||||
context('with no ERC777TokensRecipient implementer', function () {
|
||||
context('with contract recipient', function () {
|
||||
beforeEach(async function () {
|
||||
this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
|
||||
this.recipient = this.tokensRecipientImplementer.address;
|
||||
|
||||
// Note that tokensRecipientImplementer doesn't implement the recipient interface for the recipient
|
||||
});
|
||||
|
||||
it('send reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.send(this.recipient, amount, data, { from: holder }),
|
||||
'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
|
||||
);
|
||||
});
|
||||
|
||||
it('operatorSend reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }),
|
||||
'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
|
||||
);
|
||||
});
|
||||
|
||||
it('mint (internal) reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }),
|
||||
'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
|
||||
);
|
||||
});
|
||||
|
||||
it('(ERC20) transfer succeeds', async function () {
|
||||
await this.token.transfer(this.recipient, amount, { from: holder });
|
||||
});
|
||||
|
||||
it('(ERC20) transferFrom succeeds', async function () {
|
||||
const approved = anyone;
|
||||
await this.token.approve(approved, amount, { from: this.sender });
|
||||
await this.token.transferFrom(this.sender, this.recipient, amount, { from: approved });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('with ERC777TokensRecipient implementer', function () {
|
||||
context('with contract as implementer for an externally owned account', function () {
|
||||
beforeEach(async function () {
|
||||
this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
|
||||
this.recipient = anyone;
|
||||
|
||||
await this.tokensRecipientImplementer.recipientFor(this.recipient);
|
||||
|
||||
await this.erc1820.setInterfaceImplementer(
|
||||
this.recipient,
|
||||
web3.utils.soliditySha3('ERC777TokensRecipient'),
|
||||
this.tokensRecipientImplementer.address,
|
||||
{ from: this.recipient },
|
||||
);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
|
||||
});
|
||||
|
||||
context('with contract as implementer for another contract', function () {
|
||||
beforeEach(async function () {
|
||||
this.recipientContract = await ERC777SenderRecipientMock.new();
|
||||
this.recipient = this.recipientContract.address;
|
||||
|
||||
this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
|
||||
await this.tokensRecipientImplementer.recipientFor(this.recipient);
|
||||
await this.recipientContract.registerRecipient(this.tokensRecipientImplementer.address);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
|
||||
});
|
||||
|
||||
context('with contract as implementer for itself', function () {
|
||||
beforeEach(async function () {
|
||||
this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
|
||||
this.recipient = this.tokensRecipientImplementer.address;
|
||||
|
||||
await this.tokensRecipientImplementer.recipientFor(this.recipient);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tokensToSend', function () {
|
||||
beforeEach(function () {
|
||||
this.recipient = anyone;
|
||||
});
|
||||
|
||||
context('with a contract as implementer for an externally owned account', function () {
|
||||
beforeEach(async function () {
|
||||
this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
|
||||
this.sender = holder;
|
||||
|
||||
await this.tokensSenderImplementer.senderFor(this.sender);
|
||||
|
||||
await this.erc1820.setInterfaceImplementer(
|
||||
this.sender,
|
||||
web3.utils.soliditySha3('ERC777TokensSender'),
|
||||
this.tokensSenderImplementer.address,
|
||||
{ from: this.sender },
|
||||
);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
|
||||
});
|
||||
|
||||
context('with contract as implementer for another contract', function () {
|
||||
beforeEach(async function () {
|
||||
this.senderContract = await ERC777SenderRecipientMock.new();
|
||||
this.sender = this.senderContract.address;
|
||||
|
||||
this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
|
||||
await this.tokensSenderImplementer.senderFor(this.sender);
|
||||
await this.senderContract.registerSender(this.tokensSenderImplementer.address);
|
||||
|
||||
// For the contract to be able to receive tokens (that it can later send), it must also implement the
|
||||
// recipient interface.
|
||||
|
||||
await this.senderContract.recipientFor(this.sender);
|
||||
await this.token.send(this.sender, amount, data, { from: holder });
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
|
||||
});
|
||||
|
||||
context('with a contract as implementer for itself', function () {
|
||||
beforeEach(async function () {
|
||||
this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
|
||||
this.sender = this.tokensSenderImplementer.address;
|
||||
|
||||
await this.tokensSenderImplementer.senderFor(this.sender);
|
||||
|
||||
// For the contract to be able to receive tokens (that it can later send), it must also implement the
|
||||
// recipient interface.
|
||||
|
||||
await this.tokensSenderImplementer.recipientFor(this.sender);
|
||||
await this.token.send(this.sender, amount, data, { from: holder });
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('with no default operators', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC777.new(name, symbol, []);
|
||||
});
|
||||
|
||||
it('default operators list is empty', async function () {
|
||||
expect(await this.token.defaultOperators()).to.deep.equal([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('relative order of hooks', function () {
|
||||
beforeEach(async function () {
|
||||
await singletons.ERC1820Registry(registryFunder);
|
||||
this.sender = await ERC777SenderRecipientMock.new();
|
||||
await this.sender.registerRecipient(this.sender.address);
|
||||
await this.sender.registerSender(this.sender.address);
|
||||
this.token = await ERC777.new(name, symbol, []);
|
||||
await this.token.$_mint(this.sender.address, 1, '0x', '0x');
|
||||
});
|
||||
|
||||
it('send', async function () {
|
||||
const { receipt } = await this.sender.send(this.token.address, anyone, 1, '0x');
|
||||
|
||||
const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer');
|
||||
expect(internalBeforeHook).to.be.gte(0);
|
||||
const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled');
|
||||
expect(externalSendHook).to.be.gte(0);
|
||||
|
||||
expect(externalSendHook).to.be.lt(internalBeforeHook);
|
||||
});
|
||||
|
||||
it('burn', async function () {
|
||||
const { receipt } = await this.sender.burn(this.token.address, 1, '0x');
|
||||
|
||||
const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer');
|
||||
expect(internalBeforeHook).to.be.gte(0);
|
||||
const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled');
|
||||
expect(externalSendHook).to.be.gte(0);
|
||||
|
||||
expect(externalSendHook).to.be.lt(internalBeforeHook);
|
||||
});
|
||||
});
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
const { BN, singletons } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC777PresetFixedSupply = artifacts.require('ERC777PresetFixedSupply');
|
||||
|
||||
contract('ERC777PresetFixedSupply', function (accounts) {
|
||||
const [registryFunder, owner, defaultOperatorA, defaultOperatorB, anyone] = accounts;
|
||||
|
||||
const initialSupply = new BN('10000');
|
||||
const name = 'ERC777Preset';
|
||||
const symbol = '777P';
|
||||
|
||||
const defaultOperators = [defaultOperatorA, defaultOperatorB];
|
||||
|
||||
before(async function () {
|
||||
await singletons.ERC1820Registry(registryFunder);
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC777PresetFixedSupply.new(name, symbol, defaultOperators, initialSupply, owner);
|
||||
});
|
||||
|
||||
it('returns the name', async function () {
|
||||
expect(await this.token.name()).to.equal(name);
|
||||
});
|
||||
|
||||
it('returns the symbol', async function () {
|
||||
expect(await this.token.symbol()).to.equal(symbol);
|
||||
});
|
||||
|
||||
it('returns the default operators', async function () {
|
||||
expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators);
|
||||
});
|
||||
|
||||
it('default operators are operators for all accounts', async function () {
|
||||
for (const operator of defaultOperators) {
|
||||
expect(await this.token.isOperatorFor(operator, anyone)).to.equal(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns the total supply equal to initial supply', async function () {
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
it('returns the balance of owner equal to initial supply', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
function shouldBehaveLikeERC2981() {
|
||||
const royaltyFraction = new BN('10');
|
||||
|
||||
shouldSupportInterfaces(['ERC2981']);
|
||||
|
||||
describe('default royalty', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_setDefaultRoyalty(this.account1, royaltyFraction);
|
||||
});
|
||||
|
||||
it('checks royalty is set', async function () {
|
||||
const royalty = new BN((this.salePrice * royaltyFraction) / 10000);
|
||||
|
||||
const initInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice);
|
||||
|
||||
expect(initInfo[0]).to.be.equal(this.account1);
|
||||
expect(initInfo[1]).to.be.bignumber.equal(royalty);
|
||||
});
|
||||
|
||||
it('updates royalty amount', async function () {
|
||||
const newPercentage = new BN('25');
|
||||
|
||||
// Updated royalty check
|
||||
await this.token.$_setDefaultRoyalty(this.account1, newPercentage);
|
||||
const royalty = new BN((this.salePrice * newPercentage) / 10000);
|
||||
const newInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice);
|
||||
|
||||
expect(newInfo[0]).to.be.equal(this.account1);
|
||||
expect(newInfo[1]).to.be.bignumber.equal(royalty);
|
||||
});
|
||||
|
||||
it('holds same royalty value for different tokens', async function () {
|
||||
const newPercentage = new BN('20');
|
||||
await this.token.$_setDefaultRoyalty(this.account1, newPercentage);
|
||||
|
||||
const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice);
|
||||
const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice);
|
||||
|
||||
expect(token1Info[1]).to.be.bignumber.equal(token2Info[1]);
|
||||
});
|
||||
|
||||
it('Remove royalty information', async function () {
|
||||
const newValue = new BN('0');
|
||||
await this.token.$_deleteDefaultRoyalty();
|
||||
|
||||
const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice);
|
||||
const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice);
|
||||
// Test royalty info is still persistent across all tokens
|
||||
expect(token1Info[0]).to.be.bignumber.equal(token2Info[0]);
|
||||
expect(token1Info[1]).to.be.bignumber.equal(token2Info[1]);
|
||||
// Test information was deleted
|
||||
expect(token1Info[0]).to.be.equal(ZERO_ADDRESS);
|
||||
expect(token1Info[1]).to.be.bignumber.equal(newValue);
|
||||
});
|
||||
|
||||
it('reverts if invalid parameters', async function () {
|
||||
await expectRevert(this.token.$_setDefaultRoyalty(ZERO_ADDRESS, royaltyFraction), 'ERC2981: invalid receiver');
|
||||
|
||||
await expectRevert(
|
||||
this.token.$_setDefaultRoyalty(this.account1, new BN('11000')),
|
||||
'ERC2981: royalty fee will exceed salePrice',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('token based royalty', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_setTokenRoyalty(this.tokenId1, this.account1, royaltyFraction);
|
||||
});
|
||||
|
||||
it('updates royalty amount', async function () {
|
||||
const newPercentage = new BN('25');
|
||||
let royalty = new BN((this.salePrice * royaltyFraction) / 10000);
|
||||
// Initial royalty check
|
||||
const initInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice);
|
||||
|
||||
expect(initInfo[0]).to.be.equal(this.account1);
|
||||
expect(initInfo[1]).to.be.bignumber.equal(royalty);
|
||||
|
||||
// Updated royalty check
|
||||
await this.token.$_setTokenRoyalty(this.tokenId1, this.account1, newPercentage);
|
||||
royalty = new BN((this.salePrice * newPercentage) / 10000);
|
||||
const newInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice);
|
||||
|
||||
expect(newInfo[0]).to.be.equal(this.account1);
|
||||
expect(newInfo[1]).to.be.bignumber.equal(royalty);
|
||||
});
|
||||
|
||||
it('holds different values for different tokens', async function () {
|
||||
const newPercentage = new BN('20');
|
||||
await this.token.$_setTokenRoyalty(this.tokenId2, this.account1, newPercentage);
|
||||
|
||||
const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice);
|
||||
const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice);
|
||||
|
||||
// must be different even at the same this.salePrice
|
||||
expect(token1Info[1]).to.not.be.equal(token2Info.royaltyFraction);
|
||||
});
|
||||
|
||||
it('reverts if invalid parameters', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_setTokenRoyalty(this.tokenId1, ZERO_ADDRESS, royaltyFraction),
|
||||
'ERC2981: Invalid parameters',
|
||||
);
|
||||
|
||||
await expectRevert(
|
||||
this.token.$_setTokenRoyalty(this.tokenId1, this.account1, new BN('11000')),
|
||||
'ERC2981: royalty fee will exceed salePrice',
|
||||
);
|
||||
});
|
||||
|
||||
it('can reset token after setting royalty', async function () {
|
||||
const newPercentage = new BN('30');
|
||||
const royalty = new BN((this.salePrice * newPercentage) / 10000);
|
||||
await this.token.$_setTokenRoyalty(this.tokenId1, this.account2, newPercentage);
|
||||
|
||||
const tokenInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice);
|
||||
|
||||
// Tokens must have own information
|
||||
expect(tokenInfo[1]).to.be.bignumber.equal(royalty);
|
||||
expect(tokenInfo[0]).to.be.equal(this.account2);
|
||||
|
||||
await this.token.$_setTokenRoyalty(this.tokenId2, this.account1, new BN('0'));
|
||||
const result = await this.token.royaltyInfo(this.tokenId2, this.salePrice);
|
||||
// Token must not share default information
|
||||
expect(result[0]).to.be.equal(this.account1);
|
||||
expect(result[1]).to.be.bignumber.equal(new BN('0'));
|
||||
});
|
||||
|
||||
it('can hold default and token royalty information', async function () {
|
||||
const newPercentage = new BN('30');
|
||||
const royalty = new BN((this.salePrice * newPercentage) / 10000);
|
||||
|
||||
await this.token.$_setTokenRoyalty(this.tokenId2, this.account2, newPercentage);
|
||||
|
||||
const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice);
|
||||
const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice);
|
||||
// Tokens must not have same values
|
||||
expect(token1Info[1]).to.not.be.bignumber.equal(token2Info[1]);
|
||||
expect(token1Info[0]).to.not.be.equal(token2Info[0]);
|
||||
|
||||
// Updated token must have new values
|
||||
expect(token2Info[0]).to.be.equal(this.account2);
|
||||
expect(token2Info[1]).to.be.bignumber.equal(royalty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC2981,
|
||||
};
|
||||
@@ -0,0 +1,361 @@
|
||||
const { balance, constants, ether, expectRevert, send, expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const Address = artifacts.require('$Address');
|
||||
const EtherReceiver = artifacts.require('EtherReceiverMock');
|
||||
const CallReceiverMock = artifacts.require('CallReceiverMock');
|
||||
|
||||
contract('Address', function (accounts) {
|
||||
const [recipient, other] = accounts;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.mock = await Address.new();
|
||||
});
|
||||
|
||||
describe('isContract', function () {
|
||||
it('returns false for account address', async function () {
|
||||
expect(await this.mock.$isContract(other)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns true for contract address', async function () {
|
||||
expect(await this.mock.$isContract(this.mock.address)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendValue', function () {
|
||||
beforeEach(async function () {
|
||||
this.recipientTracker = await balance.tracker(recipient);
|
||||
});
|
||||
|
||||
context('when sender contract has no funds', function () {
|
||||
it('sends 0 wei', async function () {
|
||||
await this.mock.$sendValue(other, 0);
|
||||
|
||||
expect(await this.recipientTracker.delta()).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('reverts when sending non-zero amounts', async function () {
|
||||
await expectRevert(this.mock.$sendValue(other, 1), 'Address: insufficient balance');
|
||||
});
|
||||
});
|
||||
|
||||
context('when sender contract has funds', function () {
|
||||
const funds = ether('1');
|
||||
beforeEach(async function () {
|
||||
await send.ether(other, this.mock.address, funds);
|
||||
});
|
||||
|
||||
it('sends 0 wei', async function () {
|
||||
await this.mock.$sendValue(recipient, 0);
|
||||
expect(await this.recipientTracker.delta()).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('sends non-zero amounts', async function () {
|
||||
await this.mock.$sendValue(recipient, funds.subn(1));
|
||||
expect(await this.recipientTracker.delta()).to.be.bignumber.equal(funds.subn(1));
|
||||
});
|
||||
|
||||
it('sends the whole balance', async function () {
|
||||
await this.mock.$sendValue(recipient, funds);
|
||||
expect(await this.recipientTracker.delta()).to.be.bignumber.equal(funds);
|
||||
expect(await balance.current(this.mock.address)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('reverts when sending more than the balance', async function () {
|
||||
await expectRevert(this.mock.$sendValue(recipient, funds.addn(1)), 'Address: insufficient balance');
|
||||
});
|
||||
|
||||
context('with contract recipient', function () {
|
||||
beforeEach(async function () {
|
||||
this.target = await EtherReceiver.new();
|
||||
});
|
||||
|
||||
it('sends funds', async function () {
|
||||
const tracker = await balance.tracker(this.target.address);
|
||||
|
||||
await this.target.setAcceptEther(true);
|
||||
await this.mock.$sendValue(this.target.address, funds);
|
||||
|
||||
expect(await tracker.delta()).to.be.bignumber.equal(funds);
|
||||
});
|
||||
|
||||
it('reverts on recipient revert', async function () {
|
||||
await this.target.setAcceptEther(false);
|
||||
await expectRevert(
|
||||
this.mock.$sendValue(this.target.address, funds),
|
||||
'Address: unable to send value, recipient may have reverted',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('functionCall', function () {
|
||||
beforeEach(async function () {
|
||||
this.target = await CallReceiverMock.new();
|
||||
});
|
||||
|
||||
context('with valid contract receiver', function () {
|
||||
it('calls the requested function', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
|
||||
|
||||
const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall);
|
||||
|
||||
expectEvent(receipt, 'return$functionCall_address_bytes', {
|
||||
ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']),
|
||||
});
|
||||
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
it('calls the requested empty return function', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunctionEmptyReturn().encodeABI();
|
||||
|
||||
const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall);
|
||||
|
||||
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
it('reverts when the called function reverts with no reason', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsNoReason().encodeABI();
|
||||
|
||||
await expectRevert(
|
||||
this.mock.$functionCall(this.target.address, abiEncodedCall),
|
||||
'Address: low-level call failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when the called function reverts, bubbling up the revert reason', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsReason().encodeABI();
|
||||
|
||||
await expectRevert(this.mock.$functionCall(this.target.address, abiEncodedCall), 'CallReceiverMock: reverting');
|
||||
});
|
||||
|
||||
it('reverts when the called function runs out of gas', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunctionOutOfGas().encodeABI();
|
||||
|
||||
await expectRevert(
|
||||
this.mock.$functionCall(this.target.address, abiEncodedCall, { gas: '120000' }),
|
||||
'Address: low-level call failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when the called function throws', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunctionThrows().encodeABI();
|
||||
|
||||
await expectRevert.unspecified(this.mock.$functionCall(this.target.address, abiEncodedCall));
|
||||
});
|
||||
|
||||
it('bubbles up error message if specified', async function () {
|
||||
const errorMsg = 'Address: expected error';
|
||||
await expectRevert(this.mock.$functionCall(this.target.address, '0x12345678', errorMsg), errorMsg);
|
||||
});
|
||||
|
||||
it('reverts when function does not exist', async function () {
|
||||
const abiEncodedCall = web3.eth.abi.encodeFunctionCall(
|
||||
{
|
||||
name: 'mockFunctionDoesNotExist',
|
||||
type: 'function',
|
||||
inputs: [],
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
await expectRevert(
|
||||
this.mock.$functionCall(this.target.address, abiEncodedCall),
|
||||
'Address: low-level call failed',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with non-contract receiver', function () {
|
||||
it('reverts when address is not a contract', async function () {
|
||||
const [recipient] = accounts;
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
|
||||
|
||||
await expectRevert(this.mock.$functionCall(recipient, abiEncodedCall), 'Address: call to non-contract');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('functionCallWithValue', function () {
|
||||
beforeEach(async function () {
|
||||
this.target = await CallReceiverMock.new();
|
||||
});
|
||||
|
||||
context('with zero value', function () {
|
||||
it('calls the requested function', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
|
||||
|
||||
const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, 0);
|
||||
expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', {
|
||||
ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']),
|
||||
});
|
||||
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
|
||||
});
|
||||
});
|
||||
|
||||
context('with non-zero value', function () {
|
||||
const amount = ether('1.2');
|
||||
|
||||
it('reverts if insufficient sender balance', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
|
||||
|
||||
await expectRevert(
|
||||
this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount),
|
||||
'Address: insufficient balance for call',
|
||||
);
|
||||
});
|
||||
|
||||
it('calls the requested function with existing value', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
|
||||
|
||||
const tracker = await balance.tracker(this.target.address);
|
||||
|
||||
await send.ether(other, this.mock.address, amount);
|
||||
|
||||
const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount);
|
||||
expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', {
|
||||
ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']),
|
||||
});
|
||||
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
|
||||
|
||||
expect(await tracker.delta()).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('calls the requested function with transaction funds', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
|
||||
|
||||
const tracker = await balance.tracker(this.target.address);
|
||||
|
||||
expect(await balance.current(this.mock.address)).to.be.bignumber.equal('0');
|
||||
|
||||
const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount, {
|
||||
from: other,
|
||||
value: amount,
|
||||
});
|
||||
expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', {
|
||||
ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']),
|
||||
});
|
||||
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
|
||||
|
||||
expect(await tracker.delta()).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('reverts when calling non-payable functions', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunctionNonPayable().encodeABI();
|
||||
|
||||
await send.ether(other, this.mock.address, amount);
|
||||
await expectRevert(
|
||||
this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount),
|
||||
'Address: low-level call with value failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('bubbles up error message if specified', async function () {
|
||||
const errorMsg = 'Address: expected error';
|
||||
await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('functionStaticCall', function () {
|
||||
beforeEach(async function () {
|
||||
this.target = await CallReceiverMock.new();
|
||||
});
|
||||
|
||||
it('calls the requested function', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockStaticFunction().encodeABI();
|
||||
|
||||
expect(await this.mock.$functionStaticCall(this.target.address, abiEncodedCall)).to.be.equal(
|
||||
web3.eth.abi.encodeParameters(['string'], ['0x1234']),
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts on a non-static function', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
|
||||
|
||||
await expectRevert(
|
||||
this.mock.$functionStaticCall(this.target.address, abiEncodedCall),
|
||||
'Address: low-level static call failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('bubbles up revert reason', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsReason().encodeABI();
|
||||
|
||||
await expectRevert(
|
||||
this.mock.$functionStaticCall(this.target.address, abiEncodedCall),
|
||||
'CallReceiverMock: reverting',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when address is not a contract', async function () {
|
||||
const [recipient] = accounts;
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
|
||||
|
||||
await expectRevert(this.mock.$functionStaticCall(recipient, abiEncodedCall), 'Address: call to non-contract');
|
||||
});
|
||||
|
||||
it('bubbles up error message if specified', async function () {
|
||||
const errorMsg = 'Address: expected error';
|
||||
await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg);
|
||||
});
|
||||
});
|
||||
|
||||
describe('functionDelegateCall', function () {
|
||||
beforeEach(async function () {
|
||||
this.target = await CallReceiverMock.new();
|
||||
});
|
||||
|
||||
it('delegate calls the requested function', async function () {
|
||||
// pseudorandom values
|
||||
const slot = '0x93e4c53af435ddf777c3de84bb9a953a777788500e229a468ea1036496ab66a0';
|
||||
const value = '0x6a465d1c49869f71fb65562bcbd7e08c8044074927f0297127203f2a9924ff5b';
|
||||
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunctionWritesStorage(slot, value).encodeABI();
|
||||
|
||||
expect(await web3.eth.getStorageAt(this.mock.address, slot)).to.be.equal(constants.ZERO_BYTES32);
|
||||
|
||||
expectEvent(
|
||||
await this.mock.$functionDelegateCall(this.target.address, abiEncodedCall),
|
||||
'return$functionDelegateCall_address_bytes',
|
||||
{ ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']) },
|
||||
);
|
||||
|
||||
expect(await web3.eth.getStorageAt(this.mock.address, slot)).to.be.equal(value);
|
||||
});
|
||||
|
||||
it('bubbles up revert reason', async function () {
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsReason().encodeABI();
|
||||
|
||||
await expectRevert(
|
||||
this.mock.$functionDelegateCall(this.target.address, abiEncodedCall),
|
||||
'CallReceiverMock: reverting',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when address is not a contract', async function () {
|
||||
const [recipient] = accounts;
|
||||
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
|
||||
|
||||
await expectRevert(this.mock.$functionDelegateCall(recipient, abiEncodedCall), 'Address: call to non-contract');
|
||||
});
|
||||
|
||||
it('bubbles up error message if specified', async function () {
|
||||
const errorMsg = 'Address: expected error';
|
||||
await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyCallResult', function () {
|
||||
it('returns returndata on success', async function () {
|
||||
const returndata = '0x123abc';
|
||||
expect(await this.mock.$verifyCallResult(true, returndata, '')).to.equal(returndata);
|
||||
});
|
||||
|
||||
it('reverts with return data and error m', async function () {
|
||||
const errorMsg = 'Address: expected error';
|
||||
await expectRevert(this.mock.$verifyCallResult(false, '0x', errorMsg), errorMsg);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const AddressArraysMock = artifacts.require('AddressArraysMock');
|
||||
const Bytes32ArraysMock = artifacts.require('Bytes32ArraysMock');
|
||||
const Uint256ArraysMock = artifacts.require('Uint256ArraysMock');
|
||||
|
||||
contract('Arrays', function () {
|
||||
describe('findUpperBound', function () {
|
||||
context('Even number of elements', function () {
|
||||
const EVEN_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
|
||||
|
||||
beforeEach(async function () {
|
||||
this.arrays = await Uint256ArraysMock.new(EVEN_ELEMENTS_ARRAY);
|
||||
});
|
||||
|
||||
it('returns correct index for the basic case', async function () {
|
||||
expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5');
|
||||
});
|
||||
|
||||
it('returns 0 for the first element', async function () {
|
||||
expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns index of the last element', async function () {
|
||||
expect(await this.arrays.findUpperBound(20)).to.be.bignumber.equal('9');
|
||||
});
|
||||
|
||||
it('returns first index after last element if searched value is over the upper boundary', async function () {
|
||||
expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('10');
|
||||
});
|
||||
|
||||
it('returns 0 for the element under the lower boundary', async function () {
|
||||
expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('Odd number of elements', function () {
|
||||
const ODD_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
|
||||
|
||||
beforeEach(async function () {
|
||||
this.arrays = await Uint256ArraysMock.new(ODD_ELEMENTS_ARRAY);
|
||||
});
|
||||
|
||||
it('returns correct index for the basic case', async function () {
|
||||
expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5');
|
||||
});
|
||||
|
||||
it('returns 0 for the first element', async function () {
|
||||
expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns index of the last element', async function () {
|
||||
expect(await this.arrays.findUpperBound(21)).to.be.bignumber.equal('10');
|
||||
});
|
||||
|
||||
it('returns first index after last element if searched value is over the upper boundary', async function () {
|
||||
expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('11');
|
||||
});
|
||||
|
||||
it('returns 0 for the element under the lower boundary', async function () {
|
||||
expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('Array with gap', function () {
|
||||
const WITH_GAP_ARRAY = [11, 12, 13, 14, 15, 20, 21, 22, 23, 24];
|
||||
|
||||
beforeEach(async function () {
|
||||
this.arrays = await Uint256ArraysMock.new(WITH_GAP_ARRAY);
|
||||
});
|
||||
|
||||
it('returns index of first element in next filled range', async function () {
|
||||
expect(await this.arrays.findUpperBound(17)).to.be.bignumber.equal('5');
|
||||
});
|
||||
});
|
||||
|
||||
context('Empty array', function () {
|
||||
beforeEach(async function () {
|
||||
this.arrays = await Uint256ArraysMock.new([]);
|
||||
});
|
||||
|
||||
it('always returns 0 for empty array', async function () {
|
||||
expect(await this.arrays.findUpperBound(10)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsafeAccess', function () {
|
||||
for (const { type, artifact, elements } of [
|
||||
{
|
||||
type: 'address',
|
||||
artifact: AddressArraysMock,
|
||||
elements: Array(10)
|
||||
.fill()
|
||||
.map(() => web3.utils.randomHex(20)),
|
||||
},
|
||||
{
|
||||
type: 'bytes32',
|
||||
artifact: Bytes32ArraysMock,
|
||||
elements: Array(10)
|
||||
.fill()
|
||||
.map(() => web3.utils.randomHex(32)),
|
||||
},
|
||||
{
|
||||
type: 'uint256',
|
||||
artifact: Uint256ArraysMock,
|
||||
elements: Array(10)
|
||||
.fill()
|
||||
.map(() => web3.utils.randomHex(32)),
|
||||
},
|
||||
]) {
|
||||
it(type, async function () {
|
||||
const contract = await artifact.new(elements);
|
||||
|
||||
for (const i in elements) {
|
||||
expect(await contract.unsafeAccess(i)).to.be.bignumber.equal(elements[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user