1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279 |
- const { ethers } = require('hardhat');
- const { expect } = require('chai');
- const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
- const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
- const { GovernorHelper } = require('../helpers/governance');
- const { OperationState } = require('../helpers/enums');
- const time = require('../helpers/time');
- const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior');
- const salt = '0x025e7b0be353a74631ad648c667493c0e1cd31caa4cc2d3520fdc171ea0cc726'; // a random value
- const MINDELAY = time.duration.days(1);
- const DEFAULT_ADMIN_ROLE = ethers.ZeroHash;
- const PROPOSER_ROLE = ethers.id('PROPOSER_ROLE');
- const EXECUTOR_ROLE = ethers.id('EXECUTOR_ROLE');
- const CANCELLER_ROLE = ethers.id('CANCELLER_ROLE');
- const getAddress = obj => obj.address ?? obj.target ?? obj;
- function genOperation(target, value, data, predecessor, salt) {
- const id = ethers.keccak256(
- ethers.AbiCoder.defaultAbiCoder().encode(
- ['address', 'uint256', 'bytes', 'uint256', 'bytes32'],
- [getAddress(target), value, data, predecessor, salt],
- ),
- );
- return { id, target, value, data, predecessor, salt };
- }
- function genOperationBatch(targets, values, payloads, predecessor, salt) {
- const id = ethers.keccak256(
- ethers.AbiCoder.defaultAbiCoder().encode(
- ['address[]', 'uint256[]', 'bytes[]', 'uint256', 'bytes32'],
- [targets.map(getAddress), values, payloads, predecessor, salt],
- ),
- );
- return { id, targets, values, payloads, predecessor, salt };
- }
- async function fixture() {
- const [admin, proposer, canceller, executor, other] = await ethers.getSigners();
- const mock = await ethers.deployContract('TimelockController', [MINDELAY, [proposer], [executor], admin]);
- const callreceivermock = await ethers.deployContract('CallReceiverMock');
- const implementation2 = await ethers.deployContract('Implementation2');
- expect(await mock.hasRole(CANCELLER_ROLE, proposer)).to.be.true;
- await mock.connect(admin).revokeRole(CANCELLER_ROLE, proposer);
- await mock.connect(admin).grantRole(CANCELLER_ROLE, canceller);
- return {
- admin,
- proposer,
- canceller,
- executor,
- other,
- mock,
- callreceivermock,
- implementation2,
- };
- }
- describe('TimelockController', function () {
- beforeEach(async function () {
- Object.assign(this, await loadFixture(fixture));
- });
- shouldSupportInterfaces(['ERC1155Receiver']);
- it('initial state', async function () {
- expect(await this.mock.getMinDelay()).to.equal(MINDELAY);
- expect(await this.mock.DEFAULT_ADMIN_ROLE()).to.equal(DEFAULT_ADMIN_ROLE);
- expect(await this.mock.PROPOSER_ROLE()).to.equal(PROPOSER_ROLE);
- expect(await this.mock.EXECUTOR_ROLE()).to.equal(EXECUTOR_ROLE);
- expect(await this.mock.CANCELLER_ROLE()).to.equal(CANCELLER_ROLE);
- expect(
- await Promise.all(
- [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.proposer)),
- ),
- ).to.deep.equal([true, false, false]);
- expect(
- await Promise.all(
- [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.canceller)),
- ),
- ).to.deep.equal([false, true, false]);
- expect(
- await Promise.all(
- [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.executor)),
- ),
- ).to.deep.equal([false, false, true]);
- });
- it('optional admin', async function () {
- const mock = await ethers.deployContract('TimelockController', [
- MINDELAY,
- [this.proposer],
- [this.executor],
- ethers.ZeroAddress,
- ]);
- expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, this.admin)).to.be.false;
- expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, mock.target)).to.be.true;
- });
- describe('methods', function () {
- describe('operation hashing', function () {
- it('hashOperation', async function () {
- this.operation = genOperation(
- '0x29cebefe301c6ce1bb36b58654fea275e1cacc83',
- '0xf94fdd6e21da21d2',
- '0xa3bc5104',
- '0xba41db3be0a9929145cfe480bd0f1f003689104d275ae912099f925df424ef94',
- '0x60d9109846ab510ed75c15f979ae366a8a2ace11d34ba9788c13ac296db50e6e',
- );
- expect(
- await this.mock.hashOperation(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- ),
- ).to.equal(this.operation.id);
- });
- it('hashOperationBatch', async function () {
- this.operation = genOperationBatch(
- Array(8).fill('0x2d5f21620e56531c1d59c2df9b8e95d129571f71'),
- Array(8).fill('0x2b993cfce932ccee'),
- Array(8).fill('0xcf51966b'),
- '0xce8f45069cc71d25f71ba05062de1a3974f9849b004de64a70998bca9d29c2e7',
- '0x8952d74c110f72bfe5accdf828c74d53a7dfb71235dfa8a1e8c75d8576b372ff',
- );
- expect(
- await this.mock.hashOperationBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- ),
- ).to.equal(this.operation.id);
- });
- });
- describe('simple', function () {
- describe('schedule', function () {
- beforeEach(async function () {
- this.operation = genOperation(
- '0x31754f590B97fD975Eb86938f18Cc304E264D2F2',
- 0n,
- '0x3bf92ccc',
- ethers.ZeroHash,
- salt,
- );
- });
- it('proposer can schedule', async function () {
- const tx = await this.mock
- .connect(this.proposer)
- .schedule(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- );
- await expect(tx)
- .to.emit(this.mock, 'CallScheduled')
- .withArgs(
- this.operation.id,
- 0n,
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- MINDELAY,
- )
- .to.emit(this.mock, 'CallSalt')
- .withArgs(this.operation.id, this.operation.salt);
- expect(await this.mock.getTimestamp(this.operation.id)).to.equal(
- (await time.clockFromReceipt.timestamp(tx)) + MINDELAY,
- );
- });
- it('prevent overwriting active operation', async function () {
- await this.mock
- .connect(this.proposer)
- .schedule(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- );
- await expect(
- this.mock
- .connect(this.proposer)
- .schedule(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Unset));
- });
- it('prevent non-proposer from committing', async function () {
- await expect(
- this.mock
- .connect(this.other)
- .schedule(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount')
- .withArgs(this.other, PROPOSER_ROLE);
- });
- it('enforce minimum delay', async function () {
- await expect(
- this.mock
- .connect(this.proposer)
- .schedule(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY - 1n,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockInsufficientDelay')
- .withArgs(MINDELAY - 1n, MINDELAY);
- });
- it('schedule operation with salt zero', async function () {
- await expect(
- this.mock
- .connect(this.proposer)
- .schedule(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- ethers.ZeroHash,
- MINDELAY,
- ),
- ).to.not.emit(this.mock, 'CallSalt');
- });
- });
- describe('execute', function () {
- beforeEach(async function () {
- this.operation = genOperation(
- '0xAe22104DCD970750610E6FE15E623468A98b15f7',
- 0n,
- '0x13e414de',
- ethers.ZeroHash,
- '0xc1059ed2dc130227aa1d1d539ac94c641306905c020436c636e19e3fab56fc7f',
- );
- });
- it('revert if operation is not scheduled', async function () {
- await expect(
- this.mock
- .connect(this.executor)
- .execute(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
- });
- describe('with scheduled operation', function () {
- beforeEach(async function () {
- await this.mock
- .connect(this.proposer)
- .schedule(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- );
- });
- it('revert if execution comes too early 1/2', async function () {
- await expect(
- this.mock
- .connect(this.executor)
- .execute(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
- });
- it('revert if execution comes too early 2/2', async function () {
- // -1 is too tight, test sometime fails
- await this.mock.getTimestamp(this.operation.id).then(clock => time.increaseTo.timestamp(clock - 5n));
- await expect(
- this.mock
- .connect(this.executor)
- .execute(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
- });
- describe('on time', function () {
- beforeEach(async function () {
- await this.mock.getTimestamp(this.operation.id).then(time.increaseTo.timestamp);
- });
- it('executor can reveal', async function () {
- await expect(
- this.mock
- .connect(this.executor)
- .execute(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.emit(this.mock, 'CallExecuted')
- .withArgs(this.operation.id, 0n, this.operation.target, this.operation.value, this.operation.data);
- });
- it('prevent non-executor from revealing', async function () {
- await expect(
- this.mock
- .connect(this.other)
- .execute(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount')
- .withArgs(this.other, EXECUTOR_ROLE);
- });
- it('prevents reentrancy execution', async function () {
- // Create operation
- const reentrant = await ethers.deployContract('$TimelockReentrant');
- const reentrantOperation = genOperation(
- reentrant,
- 0n,
- reentrant.interface.encodeFunctionData('reenter'),
- ethers.ZeroHash,
- salt,
- );
- // Schedule so it can be executed
- await this.mock
- .connect(this.proposer)
- .schedule(
- reentrantOperation.target,
- reentrantOperation.value,
- reentrantOperation.data,
- reentrantOperation.predecessor,
- reentrantOperation.salt,
- MINDELAY,
- );
- // Advance on time to make the operation executable
- await this.mock.getTimestamp(reentrantOperation.id).then(time.increaseTo.timestamp);
- // Grant executor role to the reentrant contract
- await this.mock.connect(this.admin).grantRole(EXECUTOR_ROLE, reentrant);
- // Prepare reenter
- const data = this.mock.interface.encodeFunctionData('execute', [
- getAddress(reentrantOperation.target),
- reentrantOperation.value,
- reentrantOperation.data,
- reentrantOperation.predecessor,
- reentrantOperation.salt,
- ]);
- await reentrant.enableRentrancy(this.mock, data);
- // Expect to fail
- await expect(
- this.mock
- .connect(this.executor)
- .execute(
- reentrantOperation.target,
- reentrantOperation.value,
- reentrantOperation.data,
- reentrantOperation.predecessor,
- reentrantOperation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(reentrantOperation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
- // Disable reentrancy
- await reentrant.disableReentrancy();
- const nonReentrantOperation = reentrantOperation; // Not anymore
- // Try again successfully
- await expect(
- this.mock
- .connect(this.executor)
- .execute(
- nonReentrantOperation.target,
- nonReentrantOperation.value,
- nonReentrantOperation.data,
- nonReentrantOperation.predecessor,
- nonReentrantOperation.salt,
- ),
- )
- .to.emit(this.mock, 'CallExecuted')
- .withArgs(
- nonReentrantOperation.id,
- 0n,
- getAddress(nonReentrantOperation),
- nonReentrantOperation.value,
- nonReentrantOperation.data,
- );
- });
- });
- });
- });
- });
- describe('batch', function () {
- describe('schedule', function () {
- beforeEach(async function () {
- this.operation = genOperationBatch(
- Array(8).fill('0xEd912250835c812D4516BBD80BdaEA1bB63a293C'),
- Array(8).fill(0n),
- Array(8).fill('0x2fcb7a88'),
- ethers.ZeroHash,
- '0x6cf9d042ade5de78bed9ffd075eb4b2a4f6b1736932c2dc8af517d6e066f51f5',
- );
- });
- it('proposer can schedule', async function () {
- const tx = this.mock
- .connect(this.proposer)
- .scheduleBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- );
- for (const i in this.operation.targets) {
- await expect(tx)
- .to.emit(this.mock, 'CallScheduled')
- .withArgs(
- this.operation.id,
- i,
- getAddress(this.operation.targets[i]),
- this.operation.values[i],
- this.operation.payloads[i],
- this.operation.predecessor,
- MINDELAY,
- )
- .to.emit(this.mock, 'CallSalt')
- .withArgs(this.operation.id, this.operation.salt);
- }
- expect(await this.mock.getTimestamp(this.operation.id)).to.equal(
- (await time.clockFromReceipt.timestamp(tx)) + MINDELAY,
- );
- });
- it('prevent overwriting active operation', async function () {
- await this.mock
- .connect(this.proposer)
- .scheduleBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- );
- await expect(
- this.mock
- .connect(this.proposer)
- .scheduleBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Unset));
- });
- it('length of batch parameter must match #1', async function () {
- await expect(
- this.mock
- .connect(this.proposer)
- .scheduleBatch(
- this.operation.targets,
- [],
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength')
- .withArgs(this.operation.targets.length, this.operation.payloads.length, 0n);
- });
- it('length of batch parameter must match #1', async function () {
- await expect(
- this.mock
- .connect(this.proposer)
- .scheduleBatch(
- this.operation.targets,
- this.operation.values,
- [],
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength')
- .withArgs(this.operation.targets.length, 0n, this.operation.payloads.length);
- });
- it('prevent non-proposer from committing', async function () {
- await expect(
- this.mock
- .connect(this.other)
- .scheduleBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount')
- .withArgs(this.other, PROPOSER_ROLE);
- });
- it('enforce minimum delay', async function () {
- await expect(
- this.mock
- .connect(this.proposer)
- .scheduleBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY - 1n,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockInsufficientDelay')
- .withArgs(MINDELAY - 1n, MINDELAY);
- });
- });
- describe('execute', function () {
- beforeEach(async function () {
- this.operation = genOperationBatch(
- Array(8).fill('0x76E53CcEb05131Ef5248553bEBDb8F70536830b1'),
- Array(8).fill(0n),
- Array(8).fill('0x58a60f63'),
- ethers.ZeroHash,
- '0x9545eeabc7a7586689191f78a5532443698538e54211b5bd4d7dc0fc0102b5c7',
- );
- });
- it('revert if operation is not scheduled', async function () {
- await expect(
- this.mock
- .connect(this.executor)
- .executeBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
- });
- describe('with scheduled operation', function () {
- beforeEach(async function () {
- await this.mock
- .connect(this.proposer)
- .scheduleBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- );
- });
- it('revert if execution comes too early 1/2', async function () {
- await expect(
- this.mock
- .connect(this.executor)
- .executeBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
- });
- it('revert if execution comes too early 2/2', async function () {
- // -1 is to tight, test sometime fails
- await this.mock.getTimestamp(this.operation.id).then(clock => time.increaseTo.timestamp(clock - 5n));
- await expect(
- this.mock
- .connect(this.executor)
- .executeBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
- });
- describe('on time', function () {
- beforeEach(async function () {
- await this.mock.getTimestamp(this.operation.id).then(time.increaseTo.timestamp);
- });
- it('executor can reveal', async function () {
- const tx = this.mock
- .connect(this.executor)
- .executeBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- );
- for (const i in this.operation.targets) {
- await expect(tx)
- .to.emit(this.mock, 'CallExecuted')
- .withArgs(
- this.operation.id,
- i,
- this.operation.targets[i],
- this.operation.values[i],
- this.operation.payloads[i],
- );
- }
- });
- it('prevent non-executor from revealing', async function () {
- await expect(
- this.mock
- .connect(this.other)
- .executeBatch(
- this.operation.targets,
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount')
- .withArgs(this.other, EXECUTOR_ROLE);
- });
- it('length mismatch #1', async function () {
- await expect(
- this.mock
- .connect(this.executor)
- .executeBatch(
- [],
- this.operation.values,
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength')
- .withArgs(0, this.operation.payloads.length, this.operation.values.length);
- });
- it('length mismatch #2', async function () {
- await expect(
- this.mock
- .connect(this.executor)
- .executeBatch(
- this.operation.targets,
- [],
- this.operation.payloads,
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength')
- .withArgs(this.operation.targets.length, this.operation.payloads.length, 0n);
- });
- it('length mismatch #3', async function () {
- await expect(
- this.mock
- .connect(this.executor)
- .executeBatch(
- this.operation.targets,
- this.operation.values,
- [],
- this.operation.predecessor,
- this.operation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength')
- .withArgs(this.operation.targets.length, 0n, this.operation.values.length);
- });
- it('prevents reentrancy execution', async function () {
- // Create operation
- const reentrant = await ethers.deployContract('$TimelockReentrant');
- const reentrantBatchOperation = genOperationBatch(
- [reentrant],
- [0n],
- [reentrant.interface.encodeFunctionData('reenter')],
- ethers.ZeroHash,
- salt,
- );
- // Schedule so it can be executed
- await this.mock
- .connect(this.proposer)
- .scheduleBatch(
- reentrantBatchOperation.targets,
- reentrantBatchOperation.values,
- reentrantBatchOperation.payloads,
- reentrantBatchOperation.predecessor,
- reentrantBatchOperation.salt,
- MINDELAY,
- );
- // Advance on time to make the operation executable
- await this.mock.getTimestamp(reentrantBatchOperation.id).then(time.increaseTo.timestamp);
- // Grant executor role to the reentrant contract
- await this.mock.connect(this.admin).grantRole(EXECUTOR_ROLE, reentrant);
- // Prepare reenter
- const data = this.mock.interface.encodeFunctionData('executeBatch', [
- reentrantBatchOperation.targets.map(getAddress),
- reentrantBatchOperation.values,
- reentrantBatchOperation.payloads,
- reentrantBatchOperation.predecessor,
- reentrantBatchOperation.salt,
- ]);
- await reentrant.enableRentrancy(this.mock, data);
- // Expect to fail
- await expect(
- this.mock
- .connect(this.executor)
- .executeBatch(
- reentrantBatchOperation.targets,
- reentrantBatchOperation.values,
- reentrantBatchOperation.payloads,
- reentrantBatchOperation.predecessor,
- reentrantBatchOperation.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(reentrantBatchOperation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
- // Disable reentrancy
- await reentrant.disableReentrancy();
- const nonReentrantBatchOperation = reentrantBatchOperation; // Not anymore
- // Try again successfully
- const tx = this.mock
- .connect(this.executor)
- .executeBatch(
- nonReentrantBatchOperation.targets,
- nonReentrantBatchOperation.values,
- nonReentrantBatchOperation.payloads,
- nonReentrantBatchOperation.predecessor,
- nonReentrantBatchOperation.salt,
- );
- for (const i in nonReentrantBatchOperation.targets) {
- await expect(tx)
- .to.emit(this.mock, 'CallExecuted')
- .withArgs(
- nonReentrantBatchOperation.id,
- i,
- nonReentrantBatchOperation.targets[i],
- nonReentrantBatchOperation.values[i],
- nonReentrantBatchOperation.payloads[i],
- );
- }
- });
- });
- });
- it('partial execution', async function () {
- const operation = genOperationBatch(
- [this.callreceivermock, this.callreceivermock, this.callreceivermock],
- [0n, 0n, 0n],
- [
- this.callreceivermock.interface.encodeFunctionData('mockFunction'),
- this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'),
- this.callreceivermock.interface.encodeFunctionData('mockFunction'),
- ],
- ethers.ZeroHash,
- '0x8ac04aa0d6d66b8812fb41d39638d37af0a9ab11da507afd65c509f8ed079d3e',
- );
- await this.mock
- .connect(this.proposer)
- .scheduleBatch(
- operation.targets,
- operation.values,
- operation.payloads,
- operation.predecessor,
- operation.salt,
- MINDELAY,
- );
- await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
- await expect(
- this.mock
- .connect(this.executor)
- .executeBatch(
- operation.targets,
- operation.values,
- operation.payloads,
- operation.predecessor,
- operation.salt,
- ),
- ).to.be.revertedWithCustomError(this.mock, 'FailedCall');
- });
- });
- });
- describe('cancel', function () {
- beforeEach(async function () {
- this.operation = genOperation(
- '0xC6837c44AA376dbe1d2709F13879E040CAb653ca',
- 0n,
- '0x296e58dd',
- ethers.ZeroHash,
- '0xa2485763600634800df9fc9646fb2c112cf98649c55f63dd1d9c7d13a64399d9',
- );
- await this.mock
- .connect(this.proposer)
- .schedule(
- this.operation.target,
- this.operation.value,
- this.operation.data,
- this.operation.predecessor,
- this.operation.salt,
- MINDELAY,
- );
- });
- it('canceller can cancel', async function () {
- await expect(this.mock.connect(this.canceller).cancel(this.operation.id))
- .to.emit(this.mock, 'Cancelled')
- .withArgs(this.operation.id);
- });
- it('cannot cancel invalid operation', async function () {
- await expect(this.mock.connect(this.canceller).cancel(ethers.ZeroHash))
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
- .withArgs(
- ethers.ZeroHash,
- GovernorHelper.proposalStatesToBitMap([OperationState.Waiting, OperationState.Ready]),
- );
- });
- it('prevent non-canceller from canceling', async function () {
- await expect(this.mock.connect(this.other).cancel(this.operation.id))
- .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount')
- .withArgs(this.other, CANCELLER_ROLE);
- });
- });
- });
- describe('maintenance', function () {
- it('prevent unauthorized maintenance', async function () {
- await expect(this.mock.connect(this.other).updateDelay(0n))
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnauthorizedCaller')
- .withArgs(this.other);
- });
- it('timelock scheduled maintenance', async function () {
- const newDelay = time.duration.hours(6);
- const operation = genOperation(
- this.mock,
- 0n,
- this.mock.interface.encodeFunctionData('updateDelay', [newDelay]),
- ethers.ZeroHash,
- '0xf8e775b2c5f4d66fb5c7fa800f35ef518c262b6014b3c0aee6ea21bff157f108',
- );
- await this.mock
- .connect(this.proposer)
- .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
- await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
- await expect(
- this.mock
- .connect(this.executor)
- .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
- )
- .to.emit(this.mock, 'MinDelayChange')
- .withArgs(MINDELAY, newDelay);
- expect(await this.mock.getMinDelay()).to.equal(newDelay);
- });
- });
- describe('dependency', function () {
- beforeEach(async function () {
- this.operation1 = genOperation(
- '0xdE66bD4c97304200A95aE0AadA32d6d01A867E39',
- 0n,
- '0x01dc731a',
- ethers.ZeroHash,
- '0x64e932133c7677402ead2926f86205e2ca4686aebecf5a8077627092b9bb2feb',
- );
- this.operation2 = genOperation(
- '0x3c7944a3F1ee7fc8c5A5134ba7c79D11c3A1FCa3',
- 0n,
- '0x8f531849',
- this.operation1.id,
- '0x036e1311cac523f9548e6461e29fb1f8f9196b91910a41711ea22f5de48df07d',
- );
- await this.mock
- .connect(this.proposer)
- .schedule(
- this.operation1.target,
- this.operation1.value,
- this.operation1.data,
- this.operation1.predecessor,
- this.operation1.salt,
- MINDELAY,
- );
- await this.mock
- .connect(this.proposer)
- .schedule(
- this.operation2.target,
- this.operation2.value,
- this.operation2.data,
- this.operation2.predecessor,
- this.operation2.salt,
- MINDELAY,
- );
- await this.mock.getTimestamp(this.operation2.id).then(time.increaseTo.timestamp);
- });
- it('cannot execute before dependency', async function () {
- await expect(
- this.mock
- .connect(this.executor)
- .execute(
- this.operation2.target,
- this.operation2.value,
- this.operation2.data,
- this.operation2.predecessor,
- this.operation2.salt,
- ),
- )
- .to.be.revertedWithCustomError(this.mock, 'TimelockUnexecutedPredecessor')
- .withArgs(this.operation1.id);
- });
- it('can execute after dependency', async function () {
- await this.mock
- .connect(this.executor)
- .execute(
- this.operation1.target,
- this.operation1.value,
- this.operation1.data,
- this.operation1.predecessor,
- this.operation1.salt,
- );
- await this.mock
- .connect(this.executor)
- .execute(
- this.operation2.target,
- this.operation2.value,
- this.operation2.data,
- this.operation2.predecessor,
- this.operation2.salt,
- );
- });
- });
- describe('usage scenario', function () {
- this.timeout(10000);
- it('call', async function () {
- const operation = genOperation(
- this.implementation2,
- 0n,
- this.implementation2.interface.encodeFunctionData('setValue', [42n]),
- ethers.ZeroHash,
- '0x8043596363daefc89977b25f9d9b4d06c3910959ef0c4d213557a903e1b555e2',
- );
- await this.mock
- .connect(this.proposer)
- .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
- await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
- await this.mock
- .connect(this.executor)
- .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt);
- expect(await this.implementation2.getValue()).to.equal(42n);
- });
- it('call reverting', async function () {
- const operation = genOperation(
- this.callreceivermock,
- 0n,
- this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'),
- ethers.ZeroHash,
- '0xb1b1b276fdf1a28d1e00537ea73b04d56639128b08063c1a2f70a52e38cba693',
- );
- await this.mock
- .connect(this.proposer)
- .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
- await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
- await expect(
- this.mock
- .connect(this.executor)
- .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
- ).to.be.revertedWithCustomError(this.mock, 'FailedCall');
- });
- it('call throw', async function () {
- const operation = genOperation(
- this.callreceivermock,
- 0n,
- this.callreceivermock.interface.encodeFunctionData('mockFunctionThrows'),
- ethers.ZeroHash,
- '0xe5ca79f295fc8327ee8a765fe19afb58f4a0cbc5053642bfdd7e73bc68e0fc67',
- );
- await this.mock
- .connect(this.proposer)
- .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
- await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
- // Targeted function reverts with a panic code (0x1) + the timelock bubble the panic code
- await expect(
- this.mock
- .connect(this.executor)
- .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
- ).to.be.revertedWithPanic(PANIC_CODES.ASSERTION_ERROR);
- });
- it('call out of gas', async function () {
- const operation = genOperation(
- this.callreceivermock,
- 0n,
- this.callreceivermock.interface.encodeFunctionData('mockFunctionOutOfGas'),
- ethers.ZeroHash,
- '0xf3274ce7c394c5b629d5215723563a744b817e1730cca5587c567099a14578fd',
- );
- await this.mock
- .connect(this.proposer)
- .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
- await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
- await expect(
- this.mock
- .connect(this.executor)
- .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, {
- gasLimit: '100000',
- }),
- ).to.be.revertedWithCustomError(this.mock, 'FailedCall');
- });
- it('call payable with eth', async function () {
- const operation = genOperation(
- this.callreceivermock,
- 1,
- this.callreceivermock.interface.encodeFunctionData('mockFunction'),
- ethers.ZeroHash,
- '0x5ab73cd33477dcd36c1e05e28362719d0ed59a7b9ff14939de63a43073dc1f44',
- );
- await this.mock
- .connect(this.proposer)
- .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
- await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
- expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
- expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
- await this.mock
- .connect(this.executor)
- .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, {
- value: 1,
- });
- expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
- expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(1n);
- });
- it('call nonpayable with eth', async function () {
- const operation = genOperation(
- this.callreceivermock,
- 1,
- this.callreceivermock.interface.encodeFunctionData('mockFunctionNonPayable'),
- ethers.ZeroHash,
- '0xb78edbd920c7867f187e5aa6294ae5a656cfbf0dea1ccdca3751b740d0f2bdf8',
- );
- await this.mock
- .connect(this.proposer)
- .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
- await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
- expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
- expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
- await expect(
- this.mock
- .connect(this.executor)
- .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
- ).to.be.revertedWithCustomError(this.mock, 'FailedCall');
- expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
- expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
- });
- it('call reverting with eth', async function () {
- const operation = genOperation(
- this.callreceivermock,
- 1,
- this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'),
- ethers.ZeroHash,
- '0xdedb4563ef0095db01d81d3f2decf57cf83e4a72aa792af14c43a792b56f4de6',
- );
- await this.mock
- .connect(this.proposer)
- .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
- await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
- expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
- expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
- await expect(
- this.mock
- .connect(this.executor)
- .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
- ).to.be.revertedWithCustomError(this.mock, 'FailedCall');
- expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
- expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
- });
- });
- describe('safe receive', function () {
- describe('ERC721', function () {
- const tokenId = 1n;
- beforeEach(async function () {
- this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']);
- await this.token.$_mint(this.other, tokenId);
- });
- it('can receive an ERC721 safeTransfer', async function () {
- await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, tokenId);
- });
- });
- describe('ERC1155', function () {
- const tokenIds = {
- 1: 1000n,
- 2: 2000n,
- 3: 3000n,
- };
- beforeEach(async function () {
- this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']);
- await this.token.$_mintBatch(this.other, Object.keys(tokenIds), Object.values(tokenIds), '0x');
- });
- it('can receive ERC1155 safeTransfer', async function () {
- await this.token.connect(this.other).safeTransferFrom(
- this.other,
- this.mock,
- ...Object.entries(tokenIds)[0n], // id + amount
- '0x',
- );
- });
- it('can receive ERC1155 safeBatchTransfer', async function () {
- await this.token
- .connect(this.other)
- .safeBatchTransferFrom(this.other, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x');
- });
- });
- });
- });
|