const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS, ZERO_BYTES32 } = constants; const { expect } = require('chai'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); const TimelockController = artifacts.require('TimelockController'); const CallReceiverMock = artifacts.require('CallReceiverMock'); const Implementation2 = artifacts.require('Implementation2'); const ERC721 = artifacts.require('$ERC721'); const ERC1155 = artifacts.require('$ERC1155'); const MINDELAY = time.duration.days(1); const salt = '0x025e7b0be353a74631ad648c667493c0e1cd31caa4cc2d3520fdc171ea0cc726'; // a random value function genOperation(target, value, data, predecessor, salt) { const id = web3.utils.keccak256( web3.eth.abi.encodeParameters( ['address', 'uint256', 'bytes', 'uint256', 'bytes32'], [target, value, data, predecessor, salt], ), ); return { id, target, value, data, predecessor, salt }; } function genOperationBatch(targets, values, payloads, predecessor, salt) { const id = web3.utils.keccak256( web3.eth.abi.encodeParameters( ['address[]', 'uint256[]', 'bytes[]', 'uint256', 'bytes32'], [targets, values, payloads, predecessor, salt], ), ); return { id, targets, values, payloads, predecessor, salt }; } contract('TimelockController', function (accounts) { const [, admin, proposer, canceller, executor, 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'); beforeEach(async function () { // Deploy new timelock this.mock = await TimelockController.new(MINDELAY, [proposer], [executor], admin); expect(await this.mock.hasRole(CANCELLER_ROLE, proposer)).to.be.equal(true); await this.mock.revokeRole(CANCELLER_ROLE, proposer, { from: admin }); await this.mock.grantRole(CANCELLER_ROLE, canceller, { from: admin }); // Mocks this.callreceivermock = await CallReceiverMock.new({ from: admin }); this.implementation2 = await Implementation2.new({ from: admin }); }); shouldSupportInterfaces(['ERC1155Receiver']); it('initial state', async function () { expect(await this.mock.getMinDelay()).to.be.bignumber.equal(MINDELAY); expect(await this.mock.TIMELOCK_ADMIN_ROLE()).to.be.equal(TIMELOCK_ADMIN_ROLE); expect(await this.mock.PROPOSER_ROLE()).to.be.equal(PROPOSER_ROLE); expect(await this.mock.EXECUTOR_ROLE()).to.be.equal(EXECUTOR_ROLE); expect(await this.mock.CANCELLER_ROLE()).to.be.equal(CANCELLER_ROLE); expect( await Promise.all([PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, proposer))), ).to.be.deep.equal([true, false, false]); expect( await Promise.all([PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, canceller))), ).to.be.deep.equal([false, true, false]); expect( await Promise.all([PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, executor))), ).to.be.deep.equal([false, false, true]); }); it('optional admin', async function () { const mock = await TimelockController.new(MINDELAY, [proposer], [executor], ZERO_ADDRESS, { from: other }); expect(await mock.hasRole(TIMELOCK_ADMIN_ROLE, admin)).to.be.equal(false); expect(await mock.hasRole(TIMELOCK_ADMIN_ROLE, other)).to.be.equal(false); }); 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.be.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.be.equal(this.operation.id); }); }); describe('simple', function () { describe('schedule', function () { beforeEach(async function () { this.operation = genOperation( '0x31754f590B97fD975Eb86938f18Cc304E264D2F2', 0, '0x3bf92ccc', ZERO_BYTES32, salt, ); }); it('proposer can schedule', async function () { const receipt = await this.mock.schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, ); expectEvent(receipt, 'CallScheduled', { id: this.operation.id, index: web3.utils.toBN(0), target: this.operation.target, value: web3.utils.toBN(this.operation.value), data: this.operation.data, predecessor: this.operation.predecessor, delay: MINDELAY, }); const block = await web3.eth.getBlock(receipt.receipt.blockHash); expect(await this.mock.getTimestamp(this.operation.id)).to.be.bignumber.equal( web3.utils.toBN(block.timestamp).add(MINDELAY), ); }); it('prevent overwriting active operation', async function () { await this.mock.schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, ); await expectRevert( this.mock.schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, ), 'TimelockController: operation already scheduled', ); }); it('prevent non-proposer from committing', async function () { await expectRevert( this.mock.schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, MINDELAY, { from: other }, ), `AccessControl: account ${other.toLowerCase()} is missing role ${PROPOSER_ROLE}`, ); }); it('enforce minimum delay', async function () { await expectRevert( this.mock.schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, MINDELAY - 1, { from: proposer }, ), 'TimelockController: insufficient delay', ); }); }); describe('execute', function () { beforeEach(async function () { this.operation = genOperation( '0xAe22104DCD970750610E6FE15E623468A98b15f7', 0, '0x13e414de', ZERO_BYTES32, '0xc1059ed2dc130227aa1d1d539ac94c641306905c020436c636e19e3fab56fc7f', ); }); it('revert if operation is not scheduled', async function () { await expectRevert( this.mock.execute( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, { from: executor }, ), 'TimelockController: operation is not ready', ); }); describe('with scheduled operation', function () { beforeEach(async function () { ({ receipt: this.receipt, logs: this.logs } = await this.mock.schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, )); }); it('revert if execution comes too early 1/2', async function () { await expectRevert( this.mock.execute( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, { from: executor }, ), 'TimelockController: operation is not ready', ); }); it('revert if execution comes too early 2/2', async function () { const timestamp = await this.mock.getTimestamp(this.operation.id); await time.increaseTo(timestamp - 5); // -1 is too tight, test sometime fails await expectRevert( this.mock.execute( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, { from: executor }, ), 'TimelockController: operation is not ready', ); }); describe('on time', function () { beforeEach(async function () { const timestamp = await this.mock.getTimestamp(this.operation.id); await time.increaseTo(timestamp); }); it('executor can reveal', async function () { const receipt = await this.mock.execute( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, { from: executor }, ); expectEvent(receipt, 'CallExecuted', { id: this.operation.id, index: web3.utils.toBN(0), target: this.operation.target, value: web3.utils.toBN(this.operation.value), data: this.operation.data, }); }); it('prevent non-executor from revealing', async function () { await expectRevert( this.mock.execute( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, { from: other }, ), `AccessControl: account ${other.toLowerCase()} is missing role ${EXECUTOR_ROLE}`, ); }); }); }); }); }); describe('batch', function () { describe('schedule', function () { beforeEach(async function () { this.operation = genOperationBatch( Array(8).fill('0xEd912250835c812D4516BBD80BdaEA1bB63a293C'), Array(8).fill(0), Array(8).fill('0x2fcb7a88'), ZERO_BYTES32, '0x6cf9d042ade5de78bed9ffd075eb4b2a4f6b1736932c2dc8af517d6e066f51f5', ); }); it('proposer can schedule', async function () { const receipt = await this.mock.scheduleBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, ); for (const i in this.operation.targets) { expectEvent(receipt, 'CallScheduled', { id: this.operation.id, index: web3.utils.toBN(i), target: this.operation.targets[i], value: web3.utils.toBN(this.operation.values[i]), data: this.operation.payloads[i], predecessor: this.operation.predecessor, delay: MINDELAY, }); } const block = await web3.eth.getBlock(receipt.receipt.blockHash); expect(await this.mock.getTimestamp(this.operation.id)).to.be.bignumber.equal( web3.utils.toBN(block.timestamp).add(MINDELAY), ); }); it('prevent overwriting active operation', async function () { await this.mock.scheduleBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, ); await expectRevert( this.mock.scheduleBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, ), 'TimelockController: operation already scheduled', ); }); it('length of batch parameter must match #1', async function () { await expectRevert( this.mock.scheduleBatch( this.operation.targets, [], this.operation.payloads, this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, ), 'TimelockController: length mismatch', ); }); it('length of batch parameter must match #1', async function () { await expectRevert( this.mock.scheduleBatch( this.operation.targets, this.operation.values, [], this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, ), 'TimelockController: length mismatch', ); }); it('prevent non-proposer from committing', async function () { await expectRevert( this.mock.scheduleBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, MINDELAY, { from: other }, ), `AccessControl: account ${other.toLowerCase()} is missing role ${PROPOSER_ROLE}`, ); }); it('enforce minimum delay', async function () { await expectRevert( this.mock.scheduleBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, MINDELAY - 1, { from: proposer }, ), 'TimelockController: insufficient delay', ); }); }); describe('execute', function () { beforeEach(async function () { this.operation = genOperationBatch( Array(8).fill('0x76E53CcEb05131Ef5248553bEBDb8F70536830b1'), Array(8).fill(0), Array(8).fill('0x58a60f63'), ZERO_BYTES32, '0x9545eeabc7a7586689191f78a5532443698538e54211b5bd4d7dc0fc0102b5c7', ); }); it('revert if operation is not scheduled', async function () { await expectRevert( this.mock.executeBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, { from: executor }, ), 'TimelockController: operation is not ready', ); }); describe('with scheduled operation', function () { beforeEach(async function () { ({ receipt: this.receipt, logs: this.logs } = await this.mock.scheduleBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, )); }); it('revert if execution comes too early 1/2', async function () { await expectRevert( this.mock.executeBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, { from: executor }, ), 'TimelockController: operation is not ready', ); }); it('revert if execution comes too early 2/2', async function () { const timestamp = await this.mock.getTimestamp(this.operation.id); await time.increaseTo(timestamp - 5); // -1 is to tight, test sometime fails await expectRevert( this.mock.executeBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, { from: executor }, ), 'TimelockController: operation is not ready', ); }); describe('on time', function () { beforeEach(async function () { const timestamp = await this.mock.getTimestamp(this.operation.id); await time.increaseTo(timestamp); }); it('executor can reveal', async function () { const receipt = await this.mock.executeBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, { from: executor }, ); for (const i in this.operation.targets) { expectEvent(receipt, 'CallExecuted', { id: this.operation.id, index: web3.utils.toBN(i), target: this.operation.targets[i], value: web3.utils.toBN(this.operation.values[i]), data: this.operation.payloads[i], }); } }); it('prevent non-executor from revealing', async function () { await expectRevert( this.mock.executeBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, { from: other }, ), `AccessControl: account ${other.toLowerCase()} is missing role ${EXECUTOR_ROLE}`, ); }); it('length mismatch #1', async function () { await expectRevert( this.mock.executeBatch( [], this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, { from: executor }, ), 'TimelockController: length mismatch', ); }); it('length mismatch #2', async function () { await expectRevert( this.mock.executeBatch( this.operation.targets, [], this.operation.payloads, this.operation.predecessor, this.operation.salt, { from: executor }, ), 'TimelockController: length mismatch', ); }); it('length mismatch #3', async function () { await expectRevert( this.mock.executeBatch( this.operation.targets, this.operation.values, [], this.operation.predecessor, this.operation.salt, { from: executor }, ), 'TimelockController: length mismatch', ); }); }); }); it('partial execution', async function () { const operation = genOperationBatch( [this.callreceivermock.address, this.callreceivermock.address, this.callreceivermock.address], [0, 0, 0], [ this.callreceivermock.contract.methods.mockFunction().encodeABI(), this.callreceivermock.contract.methods.mockFunctionThrows().encodeABI(), this.callreceivermock.contract.methods.mockFunction().encodeABI(), ], ZERO_BYTES32, '0x8ac04aa0d6d66b8812fb41d39638d37af0a9ab11da507afd65c509f8ed079d3e', ); await this.mock.scheduleBatch( operation.targets, operation.values, operation.payloads, operation.predecessor, operation.salt, MINDELAY, { from: proposer }, ); await time.increase(MINDELAY); await expectRevert( this.mock.executeBatch( operation.targets, operation.values, operation.payloads, operation.predecessor, operation.salt, { from: executor }, ), 'TimelockController: underlying transaction reverted', ); }); }); }); describe('cancel', function () { beforeEach(async function () { this.operation = genOperation( '0xC6837c44AA376dbe1d2709F13879E040CAb653ca', 0, '0x296e58dd', ZERO_BYTES32, '0xa2485763600634800df9fc9646fb2c112cf98649c55f63dd1d9c7d13a64399d9', ); ({ receipt: this.receipt, logs: this.logs } = await this.mock.schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, MINDELAY, { from: proposer }, )); }); it('canceller can cancel', async function () { const receipt = await this.mock.cancel(this.operation.id, { from: canceller }); expectEvent(receipt, 'Cancelled', { id: this.operation.id }); }); it('cannot cancel invalid operation', async function () { await expectRevert( this.mock.cancel(constants.ZERO_BYTES32, { from: canceller }), 'TimelockController: operation cannot be cancelled', ); }); it('prevent non-canceller from canceling', async function () { await expectRevert( this.mock.cancel(this.operation.id, { from: other }), `AccessControl: account ${other.toLowerCase()} is missing role ${CANCELLER_ROLE}`, ); }); }); }); describe('maintenance', function () { it('prevent unauthorized maintenance', async function () { await expectRevert(this.mock.updateDelay(0, { from: other }), 'TimelockController: caller must be timelock'); }); it('timelock scheduled maintenance', async function () { const newDelay = time.duration.hours(6); const operation = genOperation( this.mock.address, 0, this.mock.contract.methods.updateDelay(newDelay.toString()).encodeABI(), ZERO_BYTES32, '0xf8e775b2c5f4d66fb5c7fa800f35ef518c262b6014b3c0aee6ea21bff157f108', ); await this.mock.schedule( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY, { from: proposer }, ); await time.increase(MINDELAY); const receipt = await this.mock.execute( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor }, ); expectEvent(receipt, 'MinDelayChange', { newDuration: newDelay.toString(), oldDuration: MINDELAY }); expect(await this.mock.getMinDelay()).to.be.bignumber.equal(newDelay); }); }); describe('dependency', function () { beforeEach(async function () { this.operation1 = genOperation( '0xdE66bD4c97304200A95aE0AadA32d6d01A867E39', 0, '0x01dc731a', ZERO_BYTES32, '0x64e932133c7677402ead2926f86205e2ca4686aebecf5a8077627092b9bb2feb', ); this.operation2 = genOperation( '0x3c7944a3F1ee7fc8c5A5134ba7c79D11c3A1FCa3', 0, '0x8f531849', this.operation1.id, '0x036e1311cac523f9548e6461e29fb1f8f9196b91910a41711ea22f5de48df07d', ); await this.mock.schedule( this.operation1.target, this.operation1.value, this.operation1.data, this.operation1.predecessor, this.operation1.salt, MINDELAY, { from: proposer }, ); await this.mock.schedule( this.operation2.target, this.operation2.value, this.operation2.data, this.operation2.predecessor, this.operation2.salt, MINDELAY, { from: proposer }, ); await time.increase(MINDELAY); }); it('cannot execute before dependency', async function () { await expectRevert( this.mock.execute( this.operation2.target, this.operation2.value, this.operation2.data, this.operation2.predecessor, this.operation2.salt, { from: executor }, ), 'TimelockController: missing dependency', ); }); it('can execute after dependency', async function () { await this.mock.execute( this.operation1.target, this.operation1.value, this.operation1.data, this.operation1.predecessor, this.operation1.salt, { from: executor }, ); await this.mock.execute( this.operation2.target, this.operation2.value, this.operation2.data, this.operation2.predecessor, this.operation2.salt, { from: executor }, ); }); }); describe('usage scenario', function () { this.timeout(10000); it('call', async function () { const operation = genOperation( this.implementation2.address, 0, this.implementation2.contract.methods.setValue(42).encodeABI(), ZERO_BYTES32, '0x8043596363daefc89977b25f9d9b4d06c3910959ef0c4d213557a903e1b555e2', ); await this.mock.schedule( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY, { from: proposer }, ); await time.increase(MINDELAY); await this.mock.execute( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor }, ); expect(await this.implementation2.getValue()).to.be.bignumber.equal(web3.utils.toBN(42)); }); it('call reverting', async function () { const operation = genOperation( this.callreceivermock.address, 0, this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), ZERO_BYTES32, '0xb1b1b276fdf1a28d1e00537ea73b04d56639128b08063c1a2f70a52e38cba693', ); await this.mock.schedule( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY, { from: proposer }, ); await time.increase(MINDELAY); await expectRevert( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), 'TimelockController: underlying transaction reverted', ); }); it('call throw', async function () { const operation = genOperation( this.callreceivermock.address, 0, this.callreceivermock.contract.methods.mockFunctionThrows().encodeABI(), ZERO_BYTES32, '0xe5ca79f295fc8327ee8a765fe19afb58f4a0cbc5053642bfdd7e73bc68e0fc67', ); await this.mock.schedule( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY, { from: proposer }, ); await time.increase(MINDELAY); await expectRevert( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), 'TimelockController: underlying transaction reverted', ); }); it('call out of gas', async function () { const operation = genOperation( this.callreceivermock.address, 0, this.callreceivermock.contract.methods.mockFunctionOutOfGas().encodeABI(), ZERO_BYTES32, '0xf3274ce7c394c5b629d5215723563a744b817e1730cca5587c567099a14578fd', ); await this.mock.schedule( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY, { from: proposer }, ); await time.increase(MINDELAY); await expectRevert( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, gas: '70000', }), 'TimelockController: underlying transaction reverted', ); }); it('call payable with eth', async function () { const operation = genOperation( this.callreceivermock.address, 1, this.callreceivermock.contract.methods.mockFunction().encodeABI(), ZERO_BYTES32, '0x5ab73cd33477dcd36c1e05e28362719d0ed59a7b9ff14939de63a43073dc1f44', ); await this.mock.schedule( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY, { from: proposer }, ); await time.increase(MINDELAY); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); await this.mock.execute( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, value: 1 }, ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(1)); }); it('call nonpayable with eth', async function () { const operation = genOperation( this.callreceivermock.address, 1, this.callreceivermock.contract.methods.mockFunctionNonPayable().encodeABI(), ZERO_BYTES32, '0xb78edbd920c7867f187e5aa6294ae5a656cfbf0dea1ccdca3751b740d0f2bdf8', ); await this.mock.schedule( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY, { from: proposer }, ); await time.increase(MINDELAY); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); await expectRevert( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), 'TimelockController: underlying transaction reverted', ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); }); it('call reverting with eth', async function () { const operation = genOperation( this.callreceivermock.address, 1, this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), ZERO_BYTES32, '0xdedb4563ef0095db01d81d3f2decf57cf83e4a72aa792af14c43a792b56f4de6', ); await this.mock.schedule( operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY, { from: proposer }, ); await time.increase(MINDELAY); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); await expectRevert( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), 'TimelockController: underlying transaction reverted', ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); }); }); describe('safe receive', function () { describe('ERC721', function () { const name = 'Non Fungible Token'; const symbol = 'NFT'; const tokenId = new BN(1); beforeEach(async function () { this.token = await ERC721.new(name, symbol); await this.token.$_mint(other, tokenId); }); it('can receive an ERC721 safeTransfer', async function () { await this.token.safeTransferFrom(other, this.mock.address, tokenId, { from: other }); }); }); describe('ERC1155', function () { const uri = 'https://token-cdn-domain/{id}.json'; const tokenIds = { 1: new BN(1000), 2: new BN(2000), 3: new BN(3000), }; beforeEach(async function () { this.token = await ERC1155.new(uri); await this.token.$_mintBatch(other, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); it('can receive ERC1155 safeTransfer', async function () { await this.token.safeTransferFrom( other, this.mock.address, ...Object.entries(tokenIds)[0], // id + amount '0x', { from: other }, ); }); it('can receive ERC1155 safeBatchTransfer', async function () { await this.token.safeBatchTransferFrom( other, this.mock.address, Object.keys(tokenIds), Object.values(tokenIds), '0x', { from: other }, ); }); }); }); });