From 15a5e91a9278c42236e650a9dc4b50f6f2802f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Mon, 22 Mar 2021 20:19:12 -0300 Subject: [PATCH 01/16] Kickstart batchcall --- contracts/utils/BatchCall.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 contracts/utils/BatchCall.sol diff --git a/contracts/utils/BatchCall.sol b/contracts/utils/BatchCall.sol new file mode 100644 index 00000000000..b7833885e5c --- /dev/null +++ b/contracts/utils/BatchCall.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./Address.sol"; + +/* + * @dev Provides a way to batch together multiple function calls in a single external call. + */ +abstract contract BatchCall { + function batchcall(bytes[] calldata data) external returns(bytes[] memory results) { + results = new bytes[](data.length); + for (uint i = 0; i < data.length; i++) { + bytes memory result = Address.functionDelegateCall(address(this), data[i]); + results[i] = result; + } + return results; + } +} From d63bc31bc848c27e8322e059b4ff84117f69faab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Wed, 24 Mar 2021 04:35:29 -0300 Subject: [PATCH 02/16] add tests --- contracts/mocks/BatchCallTokenMock.sol | 10 ++++++++ contracts/utils/BatchCall.sol | 5 ++-- test/utils/BatchCall.test.js | 33 ++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 contracts/mocks/BatchCallTokenMock.sol create mode 100644 test/utils/BatchCall.test.js diff --git a/contracts/mocks/BatchCallTokenMock.sol b/contracts/mocks/BatchCallTokenMock.sol new file mode 100644 index 00000000000..4eee7382ed8 --- /dev/null +++ b/contracts/mocks/BatchCallTokenMock.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/BatchCall.sol"; +import "./ERC20Mock.sol"; + +contract BatchCallTokenMock is BatchCall, ERC20Mock { + constructor (uint256 initialBalance) ERC20Mock("BatchCallToken", "BCT", msg.sender, initialBalance) {} +} diff --git a/contracts/utils/BatchCall.sol b/contracts/utils/BatchCall.sol index b7833885e5c..9573263d569 100644 --- a/contracts/utils/BatchCall.sol +++ b/contracts/utils/BatchCall.sol @@ -8,11 +8,10 @@ import "./Address.sol"; * @dev Provides a way to batch together multiple function calls in a single external call. */ abstract contract BatchCall { - function batchcall(bytes[] calldata data) external returns(bytes[] memory results) { + function batchCall(bytes[] calldata data) external returns(bytes[] memory results) { results = new bytes[](data.length); for (uint i = 0; i < data.length; i++) { - bytes memory result = Address.functionDelegateCall(address(this), data[i]); - results[i] = result; + results[i] = Address.functionDelegateCall(address(this), data[i]); } return results; } diff --git a/test/utils/BatchCall.test.js b/test/utils/BatchCall.test.js new file mode 100644 index 00000000000..a238861f5de --- /dev/null +++ b/test/utils/BatchCall.test.js @@ -0,0 +1,33 @@ +const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const BatchCallTokenMock = artifacts.require('BatchCallTokenMock'); + +contract('BatchCallToken', function (accounts) { + const [deployer, alice, bob] = accounts; + const amount = 12000; + + beforeEach(async function () { + this.batchCallToken = await BatchCallTokenMock.new(new BN(amount)); + }); + + it('batches transactions', async function () { + expect(await this.batchCallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); + expect(await this.batchCallToken.balanceOf(bob)).to.be.bignumber.equal(new BN('0')); + + await this.batchCallToken.batchCall([ + this.batchCallToken.contract.methods.transfer(alice, amount / 2).encodeABI(), + this.batchCallToken.contract.methods.transfer(bob, amount / 3).encodeABI(), + ]); + + expect(await this.batchCallToken.balanceOf(alice)).to.be.bignumber.equal(new BN(amount / 2)); + expect(await this.batchCallToken.balanceOf(bob)).to.be.bignumber.equal(new BN(amount / 3)); + }); + + it('bubbles up revert reasons', async function () { + const call = this.batchCallToken.batchCall([ + this.batchCallToken.contract.methods.transfer(alice, amount).encodeABI(), + this.batchCallToken.contract.methods.transfer(bob, amount).encodeABI() + ]); + + await expectRevert(call, "ERC20: transfer amount exceeds balance"); + }); +}); From b3c24d26fd335434d2fe92c4ba6e44cd512d501d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Wed, 24 Mar 2021 19:12:39 -0300 Subject: [PATCH 03/16] fix linting --- test/utils/BatchCall.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/utils/BatchCall.test.js b/test/utils/BatchCall.test.js index a238861f5de..63847d47b26 100644 --- a/test/utils/BatchCall.test.js +++ b/test/utils/BatchCall.test.js @@ -6,7 +6,7 @@ contract('BatchCallToken', function (accounts) { const amount = 12000; beforeEach(async function () { - this.batchCallToken = await BatchCallTokenMock.new(new BN(amount)); + this.batchCallToken = await BatchCallTokenMock.new(new BN(amount), { from: deployer }); }); it('batches transactions', async function () { @@ -25,9 +25,9 @@ contract('BatchCallToken', function (accounts) { it('bubbles up revert reasons', async function () { const call = this.batchCallToken.batchCall([ this.batchCallToken.contract.methods.transfer(alice, amount).encodeABI(), - this.batchCallToken.contract.methods.transfer(bob, amount).encodeABI() + this.batchCallToken.contract.methods.transfer(bob, amount).encodeABI(), ]); - await expectRevert(call, "ERC20: transfer amount exceeds balance"); + await expectRevert(call, 'ERC20: transfer amount exceeds balance'); }); }); From 4e40344dc597b8ad2d51db28c963d0beceb3dbda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Wed, 24 Mar 2021 19:35:43 -0300 Subject: [PATCH 04/16] fix tests --- contracts/mocks/BatchCallTokenMock.sol | 2 +- test/utils/BatchCall.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/mocks/BatchCallTokenMock.sol b/contracts/mocks/BatchCallTokenMock.sol index 4eee7382ed8..cd94012955e 100644 --- a/contracts/mocks/BatchCallTokenMock.sol +++ b/contracts/mocks/BatchCallTokenMock.sol @@ -5,6 +5,6 @@ pragma solidity ^0.8.0; import "../utils/BatchCall.sol"; import "./ERC20Mock.sol"; -contract BatchCallTokenMock is BatchCall, ERC20Mock { +contract BatchCallTokenMock is ERC20Mock, BatchCall { constructor (uint256 initialBalance) ERC20Mock("BatchCallToken", "BCT", msg.sender, initialBalance) {} } diff --git a/test/utils/BatchCall.test.js b/test/utils/BatchCall.test.js index 63847d47b26..e66be1c7293 100644 --- a/test/utils/BatchCall.test.js +++ b/test/utils/BatchCall.test.js @@ -16,7 +16,7 @@ contract('BatchCallToken', function (accounts) { await this.batchCallToken.batchCall([ this.batchCallToken.contract.methods.transfer(alice, amount / 2).encodeABI(), this.batchCallToken.contract.methods.transfer(bob, amount / 3).encodeABI(), - ]); + ], { from: deployer }); expect(await this.batchCallToken.balanceOf(alice)).to.be.bignumber.equal(new BN(amount / 2)); expect(await this.batchCallToken.balanceOf(bob)).to.be.bignumber.equal(new BN(amount / 3)); @@ -26,7 +26,7 @@ contract('BatchCallToken', function (accounts) { const call = this.batchCallToken.batchCall([ this.batchCallToken.contract.methods.transfer(alice, amount).encodeABI(), this.batchCallToken.contract.methods.transfer(bob, amount).encodeABI(), - ]); + ], { from: deployer }); await expectRevert(call, 'ERC20: transfer amount exceeds balance'); }); From 874d7b2c0b06bf404d95a41de7b0deaf2e55a4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Wed, 24 Mar 2021 23:46:44 -0300 Subject: [PATCH 05/16] add documentation --- contracts/utils/BatchCall.sol | 7 +++-- contracts/utils/README.adoc | 3 ++ docs/modules/ROOT/pages/utilities.adoc | 40 ++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/contracts/utils/BatchCall.sol b/contracts/utils/BatchCall.sol index 9573263d569..282dd138d06 100644 --- a/contracts/utils/BatchCall.sol +++ b/contracts/utils/BatchCall.sol @@ -5,10 +5,13 @@ pragma solidity ^0.8.0; import "./Address.sol"; /* - * @dev Provides a way to batch together multiple function calls in a single external call. + * @dev Provides a function to batch together multiple calls in a single external call. */ abstract contract BatchCall { - function batchCall(bytes[] calldata data) external returns(bytes[] memory results) { + /** + * @dev Receives and executes a batch of function calls on this contract. + */ + function batchCall(bytes[] calldata data) external returns(bytes[] memory results) { results = new bytes[](data.length); for (uint i = 0; i < data.length; i++) { results[i] = Address.functionDelegateCall(address(this), data[i]); diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index e5cdd3947cf..c8e15a3e563 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -6,6 +6,7 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ Miscellaneous contracts and libraries containing utility functions you can use to improve security, work with new data types, or safely use low-level primitives. The {Address}, {Arrays} and {Strings} libraries provide more operations related to these native data types, while {SafeCast} adds ways to safely convert between the different signed and unsigned numeric types. +{BatchCall} provides a function to batch together multiple calls in a single external call. For new data types: @@ -92,3 +93,5 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar {{Counters}} {{Strings}} + +{{BatchCall}} diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index ab578ab8225..1ca6cc17426 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -99,3 +99,43 @@ Want to check if an address is a contract? Use xref:api:utils.adoc#Address[`Addr Want to keep track of some numbers that increment by 1 every time you want another one? Check out xref:api:utils.adoc#Counters[`Counters`]. This is useful for lots of things, like creating incremental identifiers, as shown on the xref:erc721.adoc[ERC721 guide]. +=== BatchCall + +The `BatchCall` abstract contract comes with a `batchCall` function that bundles together multiple calls in a single external call. With it, external accounts may perform atomic operations comprising several function calls. + +This is not only useful for EOAs to make multiple calls in a single transaction, it's also a way to revert a previous call if a later one fails. + +Consider this dummy contract: + +[source,solidity] +---- +// contracts/Box.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/BatchCall.sol"; + +contract Box is BatchCall { + function foo() public { + ... + } + + function bar() public { + ... + } +} +---- + +This is how to call the `batchCall` function using Truffle, allowing `foo` and `bar` to be called in a single transaction: +[source,javascript] +---- +// scripts/foobar.js + +const Box = artifacts.require('Box'); +const instance = await Box.new(); + +await instance.batchCall([ + instance.contract.methods.foo().encodeABI(), + instance.contract.methods.bar().encodeABI() +], { from: deployer }); +---- From 17b6e707c6889183c3b0950a93cca6ecf6020e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Wed, 24 Mar 2021 23:50:27 -0300 Subject: [PATCH 06/16] improve docs --- docs/modules/ROOT/pages/utilities.adoc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 1ca6cc17426..665000beab6 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -101,9 +101,7 @@ Want to keep track of some numbers that increment by 1 every time you want anoth === BatchCall -The `BatchCall` abstract contract comes with a `batchCall` function that bundles together multiple calls in a single external call. With it, external accounts may perform atomic operations comprising several function calls. - -This is not only useful for EOAs to make multiple calls in a single transaction, it's also a way to revert a previous call if a later one fails. +The `BatchCall` abstract contract comes with a `batchCall` function that bundles together multiple calls in a single external call. With it, external accounts may perform atomic operations comprising several function calls. This is not only useful for EOAs to make multiple calls in a single transaction, it's also a way to revert a previous call if a later one fails. Consider this dummy contract: @@ -137,5 +135,5 @@ const instance = await Box.new(); await instance.batchCall([ instance.contract.methods.foo().encodeABI(), instance.contract.methods.bar().encodeABI() -], { from: deployer }); +]); ---- From 2f366b97c33844df02a5204f51141aa8334caa99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Thu, 25 Mar 2021 00:13:49 -0300 Subject: [PATCH 07/16] make batchCall payable --- contracts/utils/BatchCall.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/BatchCall.sol b/contracts/utils/BatchCall.sol index 282dd138d06..0e3e6a134aa 100644 --- a/contracts/utils/BatchCall.sol +++ b/contracts/utils/BatchCall.sol @@ -11,7 +11,7 @@ abstract contract BatchCall { /** * @dev Receives and executes a batch of function calls on this contract. */ - function batchCall(bytes[] calldata data) external returns(bytes[] memory results) { + function batchCall(bytes[] calldata data) external payable returns(bytes[] memory results) { results = new bytes[](data.length); for (uint i = 0; i < data.length; i++) { results[i] = Address.functionDelegateCall(address(this), data[i]); From c6b2dc74710e6d6b699542db76235e879aa3dc41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Thu, 25 Mar 2021 00:50:13 -0300 Subject: [PATCH 08/16] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49eb3ebbcfe..b082e544dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * `IERC20Metadata`: add a new extended interface that includes the optional `name()`, `symbol()` and `decimals()` functions. ([#2561](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2561)) * `ERC777`: make reception acquirement optional in `_mint`. ([#2552](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2552)) * `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565)) + * `BatchCall`: add abstract contract with `batchCall(bytes[] calldata data)` function to bundle multiple calls together ([#2608](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2608)) ## 4.0.0 (2021-03-23) From d1518a93bb40c5515618e0ccffa0be6748cd7eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Thu, 25 Mar 2021 00:52:44 -0300 Subject: [PATCH 09/16] rename test case --- test/utils/BatchCall.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/BatchCall.test.js b/test/utils/BatchCall.test.js index e66be1c7293..732719ebb3f 100644 --- a/test/utils/BatchCall.test.js +++ b/test/utils/BatchCall.test.js @@ -9,7 +9,7 @@ contract('BatchCallToken', function (accounts) { this.batchCallToken = await BatchCallTokenMock.new(new BN(amount), { from: deployer }); }); - it('batches transactions', async function () { + it('batches function calls', async function () { expect(await this.batchCallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); expect(await this.batchCallToken.balanceOf(bob)).to.be.bignumber.equal(new BN('0')); From a7397c52f6715814f509e29338df67e7bee0da59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Fri, 26 Mar 2021 20:06:31 -0300 Subject: [PATCH 10/16] go back to pre-existing naming --- contracts/utils/BatchCall.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/utils/BatchCall.sol b/contracts/utils/BatchCall.sol index 0e3e6a134aa..9b22aacaae8 100644 --- a/contracts/utils/BatchCall.sol +++ b/contracts/utils/BatchCall.sol @@ -4,14 +4,14 @@ pragma solidity ^0.8.0; import "./Address.sol"; -/* +/** * @dev Provides a function to batch together multiple calls in a single external call. */ -abstract contract BatchCall { +abstract contract Multicall { /** * @dev Receives and executes a batch of function calls on this contract. */ - function batchCall(bytes[] calldata data) external payable returns(bytes[] memory results) { + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results) { results = new bytes[](data.length); for (uint i = 0; i < data.length; i++) { results[i] = Address.functionDelegateCall(address(this), data[i]); From 01c2a032c2e69c5cb429d1d713b684c46787baa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Fri, 26 Mar 2021 20:11:37 -0300 Subject: [PATCH 11/16] go back to existing naming across files * --- CHANGELOG.md | 2 +- contracts/mocks/BatchCallTokenMock.sol | 6 ++--- .../utils/{BatchCall.sol => Multicall.sol} | 0 contracts/utils/README.adoc | 4 +-- docs/modules/ROOT/pages/utilities.adoc | 12 ++++----- test/utils/BatchCall.test.js | 26 +++++++++---------- 6 files changed, 25 insertions(+), 25 deletions(-) rename contracts/utils/{BatchCall.sol => Multicall.sol} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b082e544dce..d4d29fa170d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ * `IERC20Metadata`: add a new extended interface that includes the optional `name()`, `symbol()` and `decimals()` functions. ([#2561](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2561)) * `ERC777`: make reception acquirement optional in `_mint`. ([#2552](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2552)) * `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565)) - * `BatchCall`: add abstract contract with `batchCall(bytes[] calldata data)` function to bundle multiple calls together ([#2608](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2608)) + * `Multicall`: add abstract contract with `Multicall(bytes[] calldata data)` function to bundle multiple calls together ([#2608](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2608)) ## 4.0.0 (2021-03-23) diff --git a/contracts/mocks/BatchCallTokenMock.sol b/contracts/mocks/BatchCallTokenMock.sol index cd94012955e..46870563663 100644 --- a/contracts/mocks/BatchCallTokenMock.sol +++ b/contracts/mocks/BatchCallTokenMock.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.0; -import "../utils/BatchCall.sol"; +import "../utils/Multicall.sol"; import "./ERC20Mock.sol"; -contract BatchCallTokenMock is ERC20Mock, BatchCall { - constructor (uint256 initialBalance) ERC20Mock("BatchCallToken", "BCT", msg.sender, initialBalance) {} +contract MulticallTokenMock is ERC20Mock, Multicall { + constructor (uint256 initialBalance) ERC20Mock("MulticallToken", "BCT", msg.sender, initialBalance) {} } diff --git a/contracts/utils/BatchCall.sol b/contracts/utils/Multicall.sol similarity index 100% rename from contracts/utils/BatchCall.sol rename to contracts/utils/Multicall.sol diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index c8e15a3e563..1c39dc87e39 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -6,7 +6,7 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ Miscellaneous contracts and libraries containing utility functions you can use to improve security, work with new data types, or safely use low-level primitives. The {Address}, {Arrays} and {Strings} libraries provide more operations related to these native data types, while {SafeCast} adds ways to safely convert between the different signed and unsigned numeric types. -{BatchCall} provides a function to batch together multiple calls in a single external call. +{Multicall} provides a function to batch together multiple calls in a single external call. For new data types: @@ -94,4 +94,4 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar {{Strings}} -{{BatchCall}} +{{Multicall}} diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 665000beab6..cdbb09d9371 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -99,9 +99,9 @@ Want to check if an address is a contract? Use xref:api:utils.adoc#Address[`Addr Want to keep track of some numbers that increment by 1 every time you want another one? Check out xref:api:utils.adoc#Counters[`Counters`]. This is useful for lots of things, like creating incremental identifiers, as shown on the xref:erc721.adoc[ERC721 guide]. -=== BatchCall +=== Multicall -The `BatchCall` abstract contract comes with a `batchCall` function that bundles together multiple calls in a single external call. With it, external accounts may perform atomic operations comprising several function calls. This is not only useful for EOAs to make multiple calls in a single transaction, it's also a way to revert a previous call if a later one fails. +The `Multicall` abstract contract comes with a `multicall` function that bundles together multiple calls in a single external call. With it, external accounts may perform atomic operations comprising several function calls. This is not only useful for EOAs to make multiple calls in a single transaction, it's also a way to revert a previous call if a later one fails. Consider this dummy contract: @@ -111,9 +111,9 @@ Consider this dummy contract: // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "@openzeppelin/contracts/utils/BatchCall.sol"; +import "@openzeppelin/contracts/utils/Multicall.sol"; -contract Box is BatchCall { +contract Box is Multicall { function foo() public { ... } @@ -124,7 +124,7 @@ contract Box is BatchCall { } ---- -This is how to call the `batchCall` function using Truffle, allowing `foo` and `bar` to be called in a single transaction: +This is how to call the `multicall` function using Truffle, allowing `foo` and `bar` to be called in a single transaction: [source,javascript] ---- // scripts/foobar.js @@ -132,7 +132,7 @@ This is how to call the `batchCall` function using Truffle, allowing `foo` and ` const Box = artifacts.require('Box'); const instance = await Box.new(); -await instance.batchCall([ +await instance.multicall([ instance.contract.methods.foo().encodeABI(), instance.contract.methods.bar().encodeABI() ]); diff --git a/test/utils/BatchCall.test.js b/test/utils/BatchCall.test.js index 732719ebb3f..f2808f32526 100644 --- a/test/utils/BatchCall.test.js +++ b/test/utils/BatchCall.test.js @@ -1,31 +1,31 @@ const { BN, expectRevert } = require('@openzeppelin/test-helpers'); -const BatchCallTokenMock = artifacts.require('BatchCallTokenMock'); +const MulticallTokenMock = artifacts.require('MulticallTokenMock'); -contract('BatchCallToken', function (accounts) { +contract('MulticallToken', function (accounts) { const [deployer, alice, bob] = accounts; const amount = 12000; beforeEach(async function () { - this.batchCallToken = await BatchCallTokenMock.new(new BN(amount), { from: deployer }); + this.multicallToken = await MulticallTokenMock.new(new BN(amount), { from: deployer }); }); it('batches function calls', async function () { - expect(await this.batchCallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); - expect(await this.batchCallToken.balanceOf(bob)).to.be.bignumber.equal(new BN('0')); + expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); + expect(await this.multicallToken.balanceOf(bob)).to.be.bignumber.equal(new BN('0')); - await this.batchCallToken.batchCall([ - this.batchCallToken.contract.methods.transfer(alice, amount / 2).encodeABI(), - this.batchCallToken.contract.methods.transfer(bob, amount / 3).encodeABI(), + await this.multicallToken.Multicall([ + this.multicallToken.contract.methods.transfer(alice, amount / 2).encodeABI(), + this.multicallToken.contract.methods.transfer(bob, amount / 3).encodeABI(), ], { from: deployer }); - expect(await this.batchCallToken.balanceOf(alice)).to.be.bignumber.equal(new BN(amount / 2)); - expect(await this.batchCallToken.balanceOf(bob)).to.be.bignumber.equal(new BN(amount / 3)); + expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN(amount / 2)); + expect(await this.multicallToken.balanceOf(bob)).to.be.bignumber.equal(new BN(amount / 3)); }); it('bubbles up revert reasons', async function () { - const call = this.batchCallToken.batchCall([ - this.batchCallToken.contract.methods.transfer(alice, amount).encodeABI(), - this.batchCallToken.contract.methods.transfer(bob, amount).encodeABI(), + const call = this.multicallToken.Multicall([ + this.multicallToken.contract.methods.transfer(alice, amount).encodeABI(), + this.multicallToken.contract.methods.transfer(bob, amount).encodeABI(), ], { from: deployer }); await expectRevert(call, 'ERC20: transfer amount exceeds balance'); From b01bd97c4153b2c31bde6f30da5b9f5534135b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Fri, 26 Mar 2021 20:12:30 -0300 Subject: [PATCH 12/16] fix tests --- test/utils/BatchCall.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/BatchCall.test.js b/test/utils/BatchCall.test.js index f2808f32526..c6c68b47ceb 100644 --- a/test/utils/BatchCall.test.js +++ b/test/utils/BatchCall.test.js @@ -13,7 +13,7 @@ contract('MulticallToken', function (accounts) { expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); expect(await this.multicallToken.balanceOf(bob)).to.be.bignumber.equal(new BN('0')); - await this.multicallToken.Multicall([ + await this.multicallToken.multicall([ this.multicallToken.contract.methods.transfer(alice, amount / 2).encodeABI(), this.multicallToken.contract.methods.transfer(bob, amount / 3).encodeABI(), ], { from: deployer }); @@ -23,7 +23,7 @@ contract('MulticallToken', function (accounts) { }); it('bubbles up revert reasons', async function () { - const call = this.multicallToken.Multicall([ + const call = this.multicallToken.multicall([ this.multicallToken.contract.methods.transfer(alice, amount).encodeABI(), this.multicallToken.contract.methods.transfer(bob, amount).encodeABI(), ], { from: deployer }); From 754ddeba1383a2281ed123087120001bf602e7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Mon, 29 Mar 2021 16:03:36 -0300 Subject: [PATCH 13/16] remove payable from multicall signature --- CHANGELOG.md | 2 +- contracts/utils/Multicall.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d29fa170d..a814b447da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ * `IERC20Metadata`: add a new extended interface that includes the optional `name()`, `symbol()` and `decimals()` functions. ([#2561](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2561)) * `ERC777`: make reception acquirement optional in `_mint`. ([#2552](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2552)) * `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565)) - * `Multicall`: add abstract contract with `Multicall(bytes[] calldata data)` function to bundle multiple calls together ([#2608](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2608)) + * `Multicall`: add abstract contract with `multicall(bytes[] calldata data)` function to bundle multiple calls together ([#2608](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2608)) ## 4.0.0 (2021-03-23) diff --git a/contracts/utils/Multicall.sol b/contracts/utils/Multicall.sol index 9b22aacaae8..2dcb1d7fce7 100644 --- a/contracts/utils/Multicall.sol +++ b/contracts/utils/Multicall.sol @@ -11,7 +11,7 @@ abstract contract Multicall { /** * @dev Receives and executes a batch of function calls on this contract. */ - function multicall(bytes[] calldata data) external payable returns (bytes[] memory results) { + function multicall(bytes[] calldata data) external returns (bytes[] memory results) { results = new bytes[](data.length); for (uint i = 0; i < data.length; i++) { results[i] = Address.functionDelegateCall(address(this), data[i]); From 833f5fcd688ca070a4d8657ae048ac6fa15391e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Mon, 29 Mar 2021 16:04:28 -0300 Subject: [PATCH 14/16] rename batchcall -> multicall test files --- .../mocks/{BatchCallTokenMock.sol => MulticallTokenMock.sol} | 0 test/utils/{BatchCall.test.js => Multicall.test.js} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename contracts/mocks/{BatchCallTokenMock.sol => MulticallTokenMock.sol} (100%) rename test/utils/{BatchCall.test.js => Multicall.test.js} (100%) diff --git a/contracts/mocks/BatchCallTokenMock.sol b/contracts/mocks/MulticallTokenMock.sol similarity index 100% rename from contracts/mocks/BatchCallTokenMock.sol rename to contracts/mocks/MulticallTokenMock.sol diff --git a/test/utils/BatchCall.test.js b/test/utils/Multicall.test.js similarity index 100% rename from test/utils/BatchCall.test.js rename to test/utils/Multicall.test.js From c27b7f2fafd36233bf07d1f0ff91433a47cc6bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Mon, 29 Mar 2021 21:15:00 -0300 Subject: [PATCH 15/16] add 'return values' and 'sequential reverts' tests --- contracts/mocks/MulticallTest.sol | 20 ++++++++++++++++++++ test/utils/Multicall.test.js | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 contracts/mocks/MulticallTest.sol diff --git a/contracts/mocks/MulticallTest.sol b/contracts/mocks/MulticallTest.sol new file mode 100644 index 00000000000..76861d444d8 --- /dev/null +++ b/contracts/mocks/MulticallTest.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./MulticallTokenMock.sol"; +import "hardhat/console.sol"; + +contract MulticallTest { + function testReturnValues(MulticallTokenMock multicallToken, address[] calldata recipients, uint256[] calldata amounts) external { + bytes[] memory calls = new bytes[](recipients.length); + for (uint i = 0; i < recipients.length; i++) { + calls[i] = abi.encodeWithSignature("transfer(address,uint256)", recipients[i], amounts[i]); + } + + bytes[] memory results = multicallToken.multicall(calls); + for (uint i = 0; i < results.length; i++) { + require(abi.decode(results[i], (bool))); + } + } +} diff --git a/test/utils/Multicall.test.js b/test/utils/Multicall.test.js index c6c68b47ceb..c6453bb6122 100644 --- a/test/utils/Multicall.test.js +++ b/test/utils/Multicall.test.js @@ -22,6 +22,30 @@ contract('MulticallToken', function (accounts) { expect(await this.multicallToken.balanceOf(bob)).to.be.bignumber.equal(new BN(amount / 3)); }); + it('returns an array with the result of each call', async function () { + const MulticallTest = artifacts.require('MulticallTest'); + const multicallTest = await MulticallTest.new({ from: deployer }); + await this.multicallToken.transfer(multicallTest.address, amount, { from: deployer }); + expect(await this.multicallToken.balanceOf(multicallTest.address)).to.be.bignumber.equal(new BN(amount)); + + const recipients = [alice, bob]; + const amounts = [amount / 2, amount / 3].map(n => new BN(n)); + + await multicallTest.testReturnValues(this.multicallToken.address, recipients, amounts); + }); + + it('reverts previous calls', async function () { + expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); + + const call = this.multicallToken.multicall([ + this.multicallToken.contract.methods.transfer(alice, amount).encodeABI(), + this.multicallToken.contract.methods.transfer(bob, amount).encodeABI(), + ], { from: deployer }); + + await expectRevert(call, 'ERC20: transfer amount exceeds balance'); + expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); + }); + it('bubbles up revert reasons', async function () { const call = this.multicallToken.multicall([ this.multicallToken.contract.methods.transfer(alice, amount).encodeABI(), From c7e2ebd599e7bbf6c4bf53d2c3f657692cd82087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Mon, 29 Mar 2021 21:16:24 -0300 Subject: [PATCH 16/16] remove hardhat console --- contracts/mocks/MulticallTest.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/mocks/MulticallTest.sol b/contracts/mocks/MulticallTest.sol index 76861d444d8..4a739371745 100644 --- a/contracts/mocks/MulticallTest.sol +++ b/contracts/mocks/MulticallTest.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import "./MulticallTokenMock.sol"; -import "hardhat/console.sol"; contract MulticallTest { function testReturnValues(MulticallTokenMock multicallToken, address[] calldata recipients, uint256[] calldata amounts) external {