Procházet zdrojové kódy

Add ERC721 and ERC1155 receiver support in Governor, Timelock (#3230)

* add ERC721 and ERC1155 receiver support in Governor, Timelock and MinimalForwarder (#3174)

* revert the nft receiver hooks from MinimalForwarder and linting updates

* add ERC165 support & simplify test

* add changelog entry

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Ashwin Yardi před 3 roky
rodič
revize
76fca3aec8

+ 2 - 0
CHANGELOG.md

@@ -13,6 +13,8 @@
  * `TimelockController`: Add a separate canceller role for the ability to cancel. ([#3165](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3165))
  * `draft-ERC20Permit`: replace `immutable` with `constant` for `_PERMIT_TYPEHASH` since the `keccak256` of string literals is treated specially and the hash is evaluated at compile time. ([#3196](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3196))
  * `ERC20Wrapper`: the `decimals()` function now tries to fetch the value from the underlying token instance. If that calls revert, then the default value is used. ([#3259](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3259))
+ * `Governor`: Implement `IERC721Receiver` and `IERC1155Receiver` to improve token custody by governors. ([#3230](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3230))
+ * `TimelockController`: Implement `IERC721Receiver` and `IERC1155Receiver` to improve token custody by timelocks. ([#3230](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3230))
 
 ### Breaking changes
 

+ 42 - 1
contracts/governance/Governor.sol

@@ -3,6 +3,8 @@
 
 pragma solidity ^0.8.0;
 
+import "../token/ERC721/IERC721Receiver.sol";
+import "../token/ERC1155/IERC1155Receiver.sol";
 import "../utils/cryptography/ECDSA.sol";
 import "../utils/cryptography/draft-EIP712.sol";
 import "../utils/introspection/ERC165.sol";
@@ -24,7 +26,7 @@ import "./IGovernor.sol";
  *
  * _Available since v4.3._
  */
-abstract contract Governor is Context, ERC165, EIP712, IGovernor {
+abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receiver, IERC1155Receiver {
     using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
     using SafeCast for uint256;
     using Timers for Timers.BlockNumber;
@@ -97,6 +99,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
                 this.castVoteWithReasonAndParamsBySig.selector ^
                 this.getVotesWithParams.selector) ||
             interfaceId == type(IGovernor).interfaceId ||
+            interfaceId == type(IERC1155Receiver).interfaceId ||
             super.supportsInterface(interfaceId);
     }
 
@@ -552,4 +555,42 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
     function _executor() internal view virtual returns (address) {
         return address(this);
     }
+
+    /**
+     * @dev See {IERC721Receiver-onERC721Received}.
+     */
+    function onERC721Received(
+        address,
+        address,
+        uint256,
+        bytes memory
+    ) public virtual override returns (bytes4) {
+        return this.onERC721Received.selector;
+    }
+
+    /**
+     * @dev See {IERC1155Receiver-onERC1155Received}.
+     */
+    function onERC1155Received(
+        address,
+        address,
+        uint256,
+        uint256,
+        bytes memory
+    ) public virtual override returns (bytes4) {
+        return this.onERC1155Received.selector;
+    }
+
+    /**
+     * @dev See {IERC1155Receiver-onERC1155BatchReceived}.
+     */
+    function onERC1155BatchReceived(
+        address,
+        address,
+        uint256[] memory,
+        uint256[] memory,
+        bytes memory
+    ) public virtual override returns (bytes4) {
+        return this.onERC1155BatchReceived.selector;
+    }
 }

+ 48 - 1
contracts/governance/TimelockController.sol

@@ -4,6 +4,8 @@
 pragma solidity ^0.8.0;
 
 import "../access/AccessControl.sol";
+import "../token/ERC721/IERC721Receiver.sol";
+import "../token/ERC1155/IERC1155Receiver.sol";
 
 /**
  * @dev Contract module which acts as a timelocked controller. When set as the
@@ -20,7 +22,7 @@ import "../access/AccessControl.sol";
  *
  * _Available since v3.3._
  */
-contract TimelockController is AccessControl {
+contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver {
     bytes32 public constant TIMELOCK_ADMIN_ROLE = keccak256("TIMELOCK_ADMIN_ROLE");
     bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
     bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
@@ -117,6 +119,13 @@ contract TimelockController is AccessControl {
      */
     receive() external payable {}
 
+    /**
+     * @dev See {IERC165-supportsInterface}.
+     */
+    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, AccessControl) returns (bool) {
+        return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId);
+    }
+
     /**
      * @dev Returns whether an id correspond to a registered operation. This
      * includes both Pending, Ready and Done operations.
@@ -365,4 +374,42 @@ contract TimelockController is AccessControl {
         emit MinDelayChange(_minDelay, newDelay);
         _minDelay = newDelay;
     }
+
+    /**
+     * @dev See {IERC721Receiver-onERC721Received}.
+     */
+    function onERC721Received(
+        address,
+        address,
+        uint256,
+        bytes memory
+    ) public virtual override returns (bytes4) {
+        return this.onERC721Received.selector;
+    }
+
+    /**
+     * @dev See {IERC1155Receiver-onERC1155Received}.
+     */
+    function onERC1155Received(
+        address,
+        address,
+        uint256,
+        uint256,
+        bytes memory
+    ) public virtual override returns (bytes4) {
+        return this.onERC1155Received.selector;
+    }
+
+    /**
+     * @dev See {IERC1155Receiver-onERC1155BatchReceived}.
+     */
+    function onERC1155BatchReceived(
+        address,
+        address,
+        uint256[] memory,
+        uint256[] memory,
+        bytes memory
+    ) public virtual override returns (bytes4) {
+        return this.onERC1155BatchReceived.selector;
+    }
 }

+ 55 - 0
test/governance/Governor.test.js

@@ -14,6 +14,8 @@ const {
 const Token = artifacts.require('ERC20VotesMock');
 const Governor = artifacts.require('GovernorMock');
 const CallReceiver = artifacts.require('CallReceiverMock');
+const ERC721Mock = artifacts.require('ERC721Mock');
+const ERC1155Mock = artifacts.require('ERC1155Mock');
 
 contract('Governor', function (accounts) {
   const [ owner, proposer, voter1, voter2, voter3, voter4 ] = accounts;
@@ -55,6 +57,7 @@ contract('Governor', function (accounts) {
 
   shouldSupportInterfaces([
     'ERC165',
+    'ERC1155Receiver',
     'Governor',
     'GovernorWithParams',
   ]);
@@ -574,4 +577,56 @@ contract('Governor', function (accounts) {
       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 = new BN(1);
+
+      beforeEach(async function () {
+        this.token = await ERC721Mock.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: new BN(1000),
+        2: new BN(2000),
+        3: new BN(3000),
+      };
+
+      beforeEach(async function () {
+        this.token = await ERC1155Mock.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 },
+        );
+      });
+    });
+  });
 });

+ 148 - 85
test/governance/TimelockController.test.js

@@ -1,11 +1,18 @@
-const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
+const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
 const { 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 ERC721Mock = artifacts.require('ERC721Mock');
+const ERC1155Mock = artifacts.require('ERC1155Mock');
+
 const MINDELAY = time.duration.days(1);
 
 function genOperation (target, value, data, predecessor, salt) {
@@ -52,40 +59,44 @@ contract('TimelockController', function (accounts) {
 
   beforeEach(async function () {
     // Deploy new timelock
-    this.timelock = await TimelockController.new(
+    this.mock = await TimelockController.new(
       MINDELAY,
       [ proposer ],
       [ executor ],
       { from: admin },
     );
 
-    expect(await this.timelock.hasRole(CANCELLER_ROLE, proposer)).to.be.equal(true);
-    await this.timelock.revokeRole(CANCELLER_ROLE, proposer, { from: admin });
-    await this.timelock.grantRole(CANCELLER_ROLE, canceller, { from: 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.timelock.getMinDelay()).to.be.bignumber.equal(MINDELAY);
+    expect(await this.mock.getMinDelay()).to.be.bignumber.equal(MINDELAY);
 
-    expect(await this.timelock.TIMELOCK_ADMIN_ROLE()).to.be.equal(TIMELOCK_ADMIN_ROLE);
-    expect(await this.timelock.PROPOSER_ROLE()).to.be.equal(PROPOSER_ROLE);
-    expect(await this.timelock.EXECUTOR_ROLE()).to.be.equal(EXECUTOR_ROLE);
-    expect(await this.timelock.CANCELLER_ROLE()).to.be.equal(CANCELLER_ROLE);
+    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.timelock.hasRole(role, proposer),
+      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.timelock.hasRole(role, canceller),
+      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.timelock.hasRole(role, executor),
+      this.mock.hasRole(role, executor),
     ))).to.be.deep.equal([ false, false, true ]);
   });
 
@@ -99,7 +110,7 @@ contract('TimelockController', function (accounts) {
           '0xba41db3be0a9929145cfe480bd0f1f003689104d275ae912099f925df424ef94',
           '0x60d9109846ab510ed75c15f979ae366a8a2ace11d34ba9788c13ac296db50e6e',
         );
-        expect(await this.timelock.hashOperation(
+        expect(await this.mock.hashOperation(
           this.operation.target,
           this.operation.value,
           this.operation.data,
@@ -116,7 +127,7 @@ contract('TimelockController', function (accounts) {
           '0xce8f45069cc71d25f71ba05062de1a3974f9849b004de64a70998bca9d29c2e7',
           '0x8952d74c110f72bfe5accdf828c74d53a7dfb71235dfa8a1e8c75d8576b372ff',
         );
-        expect(await this.timelock.hashOperationBatch(
+        expect(await this.mock.hashOperationBatch(
           this.operation.targets,
           this.operation.values,
           this.operation.payloads,
@@ -138,7 +149,7 @@ contract('TimelockController', function (accounts) {
         });
 
         it('proposer can schedule', async function () {
-          const receipt = await this.timelock.schedule(
+          const receipt = await this.mock.schedule(
             this.operation.target,
             this.operation.value,
             this.operation.data,
@@ -159,12 +170,12 @@ contract('TimelockController', function (accounts) {
 
           const block = await web3.eth.getBlock(receipt.receipt.blockHash);
 
-          expect(await this.timelock.getTimestamp(this.operation.id))
+          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.timelock.schedule(
+          await this.mock.schedule(
             this.operation.target,
             this.operation.value,
             this.operation.data,
@@ -175,7 +186,7 @@ contract('TimelockController', function (accounts) {
           );
 
           await expectRevert(
-            this.timelock.schedule(
+            this.mock.schedule(
               this.operation.target,
               this.operation.value,
               this.operation.data,
@@ -190,7 +201,7 @@ contract('TimelockController', function (accounts) {
 
         it('prevent non-proposer from commiting', async function () {
           await expectRevert(
-            this.timelock.schedule(
+            this.mock.schedule(
               this.operation.target,
               this.operation.value,
               this.operation.data,
@@ -205,7 +216,7 @@ contract('TimelockController', function (accounts) {
 
         it('enforce minimum delay', async function () {
           await expectRevert(
-            this.timelock.schedule(
+            this.mock.schedule(
               this.operation.target,
               this.operation.value,
               this.operation.data,
@@ -232,7 +243,7 @@ contract('TimelockController', function (accounts) {
 
         it('revert if operation is not scheduled', async function () {
           await expectRevert(
-            this.timelock.execute(
+            this.mock.execute(
               this.operation.target,
               this.operation.value,
               this.operation.data,
@@ -246,7 +257,7 @@ contract('TimelockController', function (accounts) {
 
         describe('with scheduled operation', function () {
           beforeEach(async function () {
-            ({ receipt: this.receipt, logs: this.logs } = await this.timelock.schedule(
+            ({ receipt: this.receipt, logs: this.logs } = await this.mock.schedule(
               this.operation.target,
               this.operation.value,
               this.operation.data,
@@ -259,7 +270,7 @@ contract('TimelockController', function (accounts) {
 
           it('revert if execution comes too early 1/2', async function () {
             await expectRevert(
-              this.timelock.execute(
+              this.mock.execute(
                 this.operation.target,
                 this.operation.value,
                 this.operation.data,
@@ -272,11 +283,11 @@ contract('TimelockController', function (accounts) {
           });
 
           it('revert if execution comes too early 2/2', async function () {
-            const timestamp = await this.timelock.getTimestamp(this.operation.id);
+            const timestamp = await this.mock.getTimestamp(this.operation.id);
             await time.increaseTo(timestamp - 5); // -1 is too tight, test sometime fails
 
             await expectRevert(
-              this.timelock.execute(
+              this.mock.execute(
                 this.operation.target,
                 this.operation.value,
                 this.operation.data,
@@ -290,12 +301,12 @@ contract('TimelockController', function (accounts) {
 
           describe('on time', function () {
             beforeEach(async function () {
-              const timestamp = await this.timelock.getTimestamp(this.operation.id);
+              const timestamp = await this.mock.getTimestamp(this.operation.id);
               await time.increaseTo(timestamp);
             });
 
             it('executor can reveal', async function () {
-              const receipt = await this.timelock.execute(
+              const receipt = await this.mock.execute(
                 this.operation.target,
                 this.operation.value,
                 this.operation.data,
@@ -314,7 +325,7 @@ contract('TimelockController', function (accounts) {
 
             it('prevent non-executor from revealing', async function () {
               await expectRevert(
-                this.timelock.execute(
+                this.mock.execute(
                   this.operation.target,
                   this.operation.value,
                   this.operation.data,
@@ -343,7 +354,7 @@ contract('TimelockController', function (accounts) {
         });
 
         it('proposer can schedule', async function () {
-          const receipt = await this.timelock.scheduleBatch(
+          const receipt = await this.mock.scheduleBatch(
             this.operation.targets,
             this.operation.values,
             this.operation.payloads,
@@ -366,12 +377,12 @@ contract('TimelockController', function (accounts) {
 
           const block = await web3.eth.getBlock(receipt.receipt.blockHash);
 
-          expect(await this.timelock.getTimestamp(this.operation.id))
+          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.timelock.scheduleBatch(
+          await this.mock.scheduleBatch(
             this.operation.targets,
             this.operation.values,
             this.operation.payloads,
@@ -382,7 +393,7 @@ contract('TimelockController', function (accounts) {
           );
 
           await expectRevert(
-            this.timelock.scheduleBatch(
+            this.mock.scheduleBatch(
               this.operation.targets,
               this.operation.values,
               this.operation.payloads,
@@ -397,7 +408,7 @@ contract('TimelockController', function (accounts) {
 
         it('length of batch parameter must match #1', async function () {
           await expectRevert(
-            this.timelock.scheduleBatch(
+            this.mock.scheduleBatch(
               this.operation.targets,
               [],
               this.operation.payloads,
@@ -412,7 +423,7 @@ contract('TimelockController', function (accounts) {
 
         it('length of batch parameter must match #1', async function () {
           await expectRevert(
-            this.timelock.scheduleBatch(
+            this.mock.scheduleBatch(
               this.operation.targets,
               this.operation.values,
               [],
@@ -427,7 +438,7 @@ contract('TimelockController', function (accounts) {
 
         it('prevent non-proposer from commiting', async function () {
           await expectRevert(
-            this.timelock.scheduleBatch(
+            this.mock.scheduleBatch(
               this.operation.targets,
               this.operation.values,
               this.operation.payloads,
@@ -442,7 +453,7 @@ contract('TimelockController', function (accounts) {
 
         it('enforce minimum delay', async function () {
           await expectRevert(
-            this.timelock.scheduleBatch(
+            this.mock.scheduleBatch(
               this.operation.targets,
               this.operation.values,
               this.operation.payloads,
@@ -469,7 +480,7 @@ contract('TimelockController', function (accounts) {
 
         it('revert if operation is not scheduled', async function () {
           await expectRevert(
-            this.timelock.executeBatch(
+            this.mock.executeBatch(
               this.operation.targets,
               this.operation.values,
               this.operation.payloads,
@@ -483,7 +494,7 @@ contract('TimelockController', function (accounts) {
 
         describe('with scheduled operation', function () {
           beforeEach(async function () {
-            ({ receipt: this.receipt, logs: this.logs } = await this.timelock.scheduleBatch(
+            ({ receipt: this.receipt, logs: this.logs } = await this.mock.scheduleBatch(
               this.operation.targets,
               this.operation.values,
               this.operation.payloads,
@@ -496,7 +507,7 @@ contract('TimelockController', function (accounts) {
 
           it('revert if execution comes too early 1/2', async function () {
             await expectRevert(
-              this.timelock.executeBatch(
+              this.mock.executeBatch(
                 this.operation.targets,
                 this.operation.values,
                 this.operation.payloads,
@@ -509,11 +520,11 @@ contract('TimelockController', function (accounts) {
           });
 
           it('revert if execution comes too early 2/2', async function () {
-            const timestamp = await this.timelock.getTimestamp(this.operation.id);
+            const timestamp = await this.mock.getTimestamp(this.operation.id);
             await time.increaseTo(timestamp - 5); // -1 is to tight, test sometime fails
 
             await expectRevert(
-              this.timelock.executeBatch(
+              this.mock.executeBatch(
                 this.operation.targets,
                 this.operation.values,
                 this.operation.payloads,
@@ -527,12 +538,12 @@ contract('TimelockController', function (accounts) {
 
           describe('on time', function () {
             beforeEach(async function () {
-              const timestamp = await this.timelock.getTimestamp(this.operation.id);
+              const timestamp = await this.mock.getTimestamp(this.operation.id);
               await time.increaseTo(timestamp);
             });
 
             it('executor can reveal', async function () {
-              const receipt = await this.timelock.executeBatch(
+              const receipt = await this.mock.executeBatch(
                 this.operation.targets,
                 this.operation.values,
                 this.operation.payloads,
@@ -553,7 +564,7 @@ contract('TimelockController', function (accounts) {
 
             it('prevent non-executor from revealing', async function () {
               await expectRevert(
-                this.timelock.executeBatch(
+                this.mock.executeBatch(
                   this.operation.targets,
                   this.operation.values,
                   this.operation.payloads,
@@ -567,7 +578,7 @@ contract('TimelockController', function (accounts) {
 
             it('length mismatch #1', async function () {
               await expectRevert(
-                this.timelock.executeBatch(
+                this.mock.executeBatch(
                   [],
                   this.operation.values,
                   this.operation.payloads,
@@ -581,7 +592,7 @@ contract('TimelockController', function (accounts) {
 
             it('length mismatch #2', async function () {
               await expectRevert(
-                this.timelock.executeBatch(
+                this.mock.executeBatch(
                   this.operation.targets,
                   [],
                   this.operation.payloads,
@@ -595,7 +606,7 @@ contract('TimelockController', function (accounts) {
 
             it('length mismatch #3', async function () {
               await expectRevert(
-                this.timelock.executeBatch(
+                this.mock.executeBatch(
                   this.operation.targets,
                   this.operation.values,
                   [],
@@ -630,7 +641,7 @@ contract('TimelockController', function (accounts) {
             '0x8ac04aa0d6d66b8812fb41d39638d37af0a9ab11da507afd65c509f8ed079d3e',
           );
 
-          await this.timelock.scheduleBatch(
+          await this.mock.scheduleBatch(
             operation.targets,
             operation.values,
             operation.payloads,
@@ -641,7 +652,7 @@ contract('TimelockController', function (accounts) {
           );
           await time.increase(MINDELAY);
           await expectRevert(
-            this.timelock.executeBatch(
+            this.mock.executeBatch(
               operation.targets,
               operation.values,
               operation.payloads,
@@ -664,7 +675,7 @@ contract('TimelockController', function (accounts) {
           ZERO_BYTES32,
           '0xa2485763600634800df9fc9646fb2c112cf98649c55f63dd1d9c7d13a64399d9',
         );
-        ({ receipt: this.receipt, logs: this.logs } = await this.timelock.schedule(
+        ({ receipt: this.receipt, logs: this.logs } = await this.mock.schedule(
           this.operation.target,
           this.operation.value,
           this.operation.data,
@@ -676,20 +687,20 @@ contract('TimelockController', function (accounts) {
       });
 
       it('canceller can cancel', async function () {
-        const receipt = await this.timelock.cancel(this.operation.id, { from: canceller });
+        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.timelock.cancel(constants.ZERO_BYTES32, { from: canceller }),
+          this.mock.cancel(constants.ZERO_BYTES32, { from: canceller }),
           'TimelockController: operation cannot be cancelled',
         );
       });
 
       it('prevent non-canceller from canceling', async function () {
         await expectRevert(
-          this.timelock.cancel(this.operation.id, { from: other }),
+          this.mock.cancel(this.operation.id, { from: other }),
           `AccessControl: account ${other.toLowerCase()} is missing role ${CANCELLER_ROLE}`,
         );
       });
@@ -699,7 +710,7 @@ contract('TimelockController', function (accounts) {
   describe('maintenance', function () {
     it('prevent unauthorized maintenance', async function () {
       await expectRevert(
-        this.timelock.updateDelay(0, { from: other }),
+        this.mock.updateDelay(0, { from: other }),
         'TimelockController: caller must be timelock',
       );
     });
@@ -707,14 +718,14 @@ contract('TimelockController', function (accounts) {
     it('timelock scheduled maintenance', async function () {
       const newDelay = time.duration.hours(6);
       const operation = genOperation(
-        this.timelock.address,
+        this.mock.address,
         0,
-        this.timelock.contract.methods.updateDelay(newDelay.toString()).encodeABI(),
+        this.mock.contract.methods.updateDelay(newDelay.toString()).encodeABI(),
         ZERO_BYTES32,
         '0xf8e775b2c5f4d66fb5c7fa800f35ef518c262b6014b3c0aee6ea21bff157f108',
       );
 
-      await this.timelock.schedule(
+      await this.mock.schedule(
         operation.target,
         operation.value,
         operation.data,
@@ -724,7 +735,7 @@ contract('TimelockController', function (accounts) {
         { from: proposer },
       );
       await time.increase(MINDELAY);
-      const receipt = await this.timelock.execute(
+      const receipt = await this.mock.execute(
         operation.target,
         operation.value,
         operation.data,
@@ -734,7 +745,7 @@ contract('TimelockController', function (accounts) {
       );
       expectEvent(receipt, 'MinDelayChange', { newDuration: newDelay.toString(), oldDuration: MINDELAY });
 
-      expect(await this.timelock.getMinDelay()).to.be.bignumber.equal(newDelay);
+      expect(await this.mock.getMinDelay()).to.be.bignumber.equal(newDelay);
     });
   });
 
@@ -754,7 +765,7 @@ contract('TimelockController', function (accounts) {
         this.operation1.id,
         '0x036e1311cac523f9548e6461e29fb1f8f9196b91910a41711ea22f5de48df07d',
       );
-      await this.timelock.schedule(
+      await this.mock.schedule(
         this.operation1.target,
         this.operation1.value,
         this.operation1.data,
@@ -763,7 +774,7 @@ contract('TimelockController', function (accounts) {
         MINDELAY,
         { from: proposer },
       );
-      await this.timelock.schedule(
+      await this.mock.schedule(
         this.operation2.target,
         this.operation2.value,
         this.operation2.data,
@@ -777,7 +788,7 @@ contract('TimelockController', function (accounts) {
 
     it('cannot execute before dependency', async function () {
       await expectRevert(
-        this.timelock.execute(
+        this.mock.execute(
           this.operation2.target,
           this.operation2.value,
           this.operation2.data,
@@ -790,7 +801,7 @@ contract('TimelockController', function (accounts) {
     });
 
     it('can execute after dependency', async function () {
-      await this.timelock.execute(
+      await this.mock.execute(
         this.operation1.target,
         this.operation1.value,
         this.operation1.data,
@@ -798,7 +809,7 @@ contract('TimelockController', function (accounts) {
         this.operation1.salt,
         { from: executor },
       );
-      await this.timelock.execute(
+      await this.mock.execute(
         this.operation2.target,
         this.operation2.value,
         this.operation2.data,
@@ -821,7 +832,7 @@ contract('TimelockController', function (accounts) {
         '0x8043596363daefc89977b25f9d9b4d06c3910959ef0c4d213557a903e1b555e2',
       );
 
-      await this.timelock.schedule(
+      await this.mock.schedule(
         operation.target,
         operation.value,
         operation.data,
@@ -831,7 +842,7 @@ contract('TimelockController', function (accounts) {
         { from: proposer },
       );
       await time.increase(MINDELAY);
-      await this.timelock.execute(
+      await this.mock.execute(
         operation.target,
         operation.value,
         operation.data,
@@ -852,7 +863,7 @@ contract('TimelockController', function (accounts) {
         '0xb1b1b276fdf1a28d1e00537ea73b04d56639128b08063c1a2f70a52e38cba693',
       );
 
-      await this.timelock.schedule(
+      await this.mock.schedule(
         operation.target,
         operation.value,
         operation.data,
@@ -863,7 +874,7 @@ contract('TimelockController', function (accounts) {
       );
       await time.increase(MINDELAY);
       await expectRevert(
-        this.timelock.execute(
+        this.mock.execute(
           operation.target,
           operation.value,
           operation.data,
@@ -884,7 +895,7 @@ contract('TimelockController', function (accounts) {
         '0xe5ca79f295fc8327ee8a765fe19afb58f4a0cbc5053642bfdd7e73bc68e0fc67',
       );
 
-      await this.timelock.schedule(
+      await this.mock.schedule(
         operation.target,
         operation.value,
         operation.data,
@@ -895,7 +906,7 @@ contract('TimelockController', function (accounts) {
       );
       await time.increase(MINDELAY);
       await expectRevert(
-        this.timelock.execute(
+        this.mock.execute(
           operation.target,
           operation.value,
           operation.data,
@@ -916,7 +927,7 @@ contract('TimelockController', function (accounts) {
         '0xf3274ce7c394c5b629d5215723563a744b817e1730cca5587c567099a14578fd',
       );
 
-      await this.timelock.schedule(
+      await this.mock.schedule(
         operation.target,
         operation.value,
         operation.data,
@@ -927,7 +938,7 @@ contract('TimelockController', function (accounts) {
       );
       await time.increase(MINDELAY);
       await expectRevert(
-        this.timelock.execute(
+        this.mock.execute(
           operation.target,
           operation.value,
           operation.data,
@@ -948,7 +959,7 @@ contract('TimelockController', function (accounts) {
         '0x5ab73cd33477dcd36c1e05e28362719d0ed59a7b9ff14939de63a43073dc1f44',
       );
 
-      await this.timelock.schedule(
+      await this.mock.schedule(
         operation.target,
         operation.value,
         operation.data,
@@ -959,10 +970,10 @@ contract('TimelockController', function (accounts) {
       );
       await time.increase(MINDELAY);
 
-      expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(web3.utils.toBN(0));
+      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.timelock.execute(
+      await this.mock.execute(
         operation.target,
         operation.value,
         operation.data,
@@ -971,7 +982,7 @@ contract('TimelockController', function (accounts) {
         { from: executor, value: 1 },
       );
 
-      expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(web3.utils.toBN(0));
+      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));
     });
 
@@ -984,7 +995,7 @@ contract('TimelockController', function (accounts) {
         '0xb78edbd920c7867f187e5aa6294ae5a656cfbf0dea1ccdca3751b740d0f2bdf8',
       );
 
-      await this.timelock.schedule(
+      await this.mock.schedule(
         operation.target,
         operation.value,
         operation.data,
@@ -995,11 +1006,11 @@ contract('TimelockController', function (accounts) {
       );
       await time.increase(MINDELAY);
 
-      expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(web3.utils.toBN(0));
+      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.timelock.execute(
+        this.mock.execute(
           operation.target,
           operation.value,
           operation.data,
@@ -1010,7 +1021,7 @@ contract('TimelockController', function (accounts) {
         'TimelockController: underlying transaction reverted',
       );
 
-      expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(web3.utils.toBN(0));
+      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));
     });
 
@@ -1023,7 +1034,7 @@ contract('TimelockController', function (accounts) {
         '0xdedb4563ef0095db01d81d3f2decf57cf83e4a72aa792af14c43a792b56f4de6',
       );
 
-      await this.timelock.schedule(
+      await this.mock.schedule(
         operation.target,
         operation.value,
         operation.data,
@@ -1034,11 +1045,11 @@ contract('TimelockController', function (accounts) {
       );
       await time.increase(MINDELAY);
 
-      expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(web3.utils.toBN(0));
+      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.timelock.execute(
+        this.mock.execute(
           operation.target,
           operation.value,
           operation.data,
@@ -1049,8 +1060,60 @@ contract('TimelockController', function (accounts) {
         'TimelockController: underlying transaction reverted',
       );
 
-      expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(web3.utils.toBN(0));
+      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 ERC721Mock.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 ERC1155Mock.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 },
+        );
+      });
+    });
+  });
 });