Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Multicall module #2608

Merged
merged 18 commits into from
Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565))
* `ERC20FlashMint`: add an implementation of the ERC3156 extension for flash-minting ERC20 tokens. ([#2543](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2543))
* `SignatureChecker`: add a signature verification library that supports both EOA and ERC1271 compliant contracts as signers. ([#2532](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2532))
* `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)

Expand Down
19 changes: 19 additions & 0 deletions contracts/mocks/MulticallTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./MulticallTokenMock.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)));
}
}
}
10 changes: 10 additions & 0 deletions contracts/mocks/MulticallTokenMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../utils/Multicall.sol";
import "./ERC20Mock.sol";

contract MulticallTokenMock is ERC20Mock, Multicall {
constructor (uint256 initialBalance) ERC20Mock("MulticallToken", "BCT", msg.sender, initialBalance) {}
}
21 changes: 21 additions & 0 deletions contracts/utils/Multicall.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./Address.sol";

/**
* @dev Provides a function to batch together multiple calls in a single external call.
*/
abstract contract Multicall {
/**
* @dev Receives and executes a batch of function calls on this contract.
*/
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]);
}
return results;
}
}
3 changes: 3 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
{Multicall} provides a function to batch together multiple calls in a single external call.

For new data types:

Expand Down Expand Up @@ -94,3 +95,5 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar
{{Counters}}

{{Strings}}

{{Multicall}}
38 changes: 38 additions & 0 deletions docs/modules/ROOT/pages/utilities.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,41 @@ 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].

=== Multicall

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:

[source,solidity]
----
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/Multicall.sol";

contract Box is Multicall {
function foo() public {
...
}

function bar() public {
...
}
}
----

This is how to call the `multicall` function using Truffle, allowing `foo` and `bar` to be called in a single transaction:
martriay marked this conversation as resolved.
Show resolved Hide resolved
[source,javascript]
----
// scripts/foobar.js

const Box = artifacts.require('Box');
const instance = await Box.new();

await instance.multicall([
instance.contract.methods.foo().encodeABI(),
instance.contract.methods.bar().encodeABI()
]);
----
57 changes: 57 additions & 0 deletions test/utils/Multicall.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { BN, expectRevert } = require('@openzeppelin/test-helpers');
const MulticallTokenMock = artifacts.require('MulticallTokenMock');

contract('MulticallToken', function (accounts) {
const [deployer, alice, bob] = accounts;
const amount = 12000;

beforeEach(async function () {
this.multicallToken = await MulticallTokenMock.new(new BN(amount), { from: deployer });
});

it('batches function calls', async function () {
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([
this.multicallToken.contract.methods.transfer(alice, amount / 2).encodeABI(),
this.multicallToken.contract.methods.transfer(bob, amount / 3).encodeABI(),
], { from: deployer });

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('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(),
this.multicallToken.contract.methods.transfer(bob, amount).encodeABI(),
], { from: deployer });

await expectRevert(call, 'ERC20: transfer amount exceeds balance');
});
});