Add Multicall module (#2608)
This commit is contained in:
@ -7,6 +7,7 @@
|
|||||||
* `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565))
|
* `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))
|
* `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))
|
* `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)
|
## 4.0.0 (2021-03-23)
|
||||||
|
|
||||||
|
|||||||
19
contracts/mocks/MulticallTest.sol
Normal file
19
contracts/mocks/MulticallTest.sol
Normal file
@ -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
contracts/mocks/MulticallTokenMock.sol
Normal file
10
contracts/mocks/MulticallTokenMock.sol
Normal file
@ -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
contracts/utils/Multicall.sol
Normal file
21
contracts/utils/Multicall.sol
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.
|
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.
|
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:
|
For new data types:
|
||||||
|
|
||||||
@ -94,3 +95,5 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar
|
|||||||
{{Counters}}
|
{{Counters}}
|
||||||
|
|
||||||
{{Strings}}
|
{{Strings}}
|
||||||
|
|
||||||
|
{{Multicall}}
|
||||||
|
|||||||
@ -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].
|
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:
|
||||||
|
[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
test/utils/Multicall.test.js
Normal file
57
test/utils/Multicall.test.js
Normal file
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user