Skip to content

Commit ae9c89b

Browse files
authored
✨ 1.0 (#3)
1 parent 363d59a commit ae9c89b

7 files changed

Lines changed: 185 additions & 113 deletions

File tree

‎.gas-snapshot‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
MulticallerTest:testMulticallerForwardsMessageValue() (gas: 108197)
2-
MulticallerTest:testMulticallerGetNames() (gas: 135741)
3-
MulticallerTest:testMulticallerReentrancyGuard() (gas: 20292)
4-
MulticallerTest:testMulticallerReturnDataIsProperlyEncoded() (gas: 111358)
5-
MulticallerTest:testMulticallerReturnDataIsProperlyEncoded(string,string,uint256) (runs: 256, μ: 57052, ~: 31199)
6-
MulticallerTest:testMulticallerReturnDataIsProperlyEncoded(uint256,uint256,uint256,uint256) (runs: 256, μ: 111460, ~: 111460)
7-
MulticallerTest:testMulticallerRevertWithCustomError() (gas: 29124)
8-
MulticallerTest:testMulticallerRevertWithMessage() (gas: 31849)
9-
MulticallerTest:testMulticallerRevertWithMessage(string) (runs: 256, μ: 32699, ~: 32782)
10-
MulticallerTest:testMulticallerRevertWithNothing() (gas: 29010)
11-
MulticallerTest:testMulticallerTargetGetMulticallerSender() (gas: 30236)
12-
MulticallerTest:testMulticallerWithNoData() (gas: 13571)
13-
MulticallerTest:test__codesize() (gas: 19436)
1+
MulticallerTest:testMulticallerForwardsMessageValue() (gas: 191692)
2+
MulticallerTest:testMulticallerGetNames() (gas: 135832)
3+
MulticallerTest:testMulticallerReentrancyGuard() (gas: 19731)
4+
MulticallerTest:testMulticallerReturnDataIsProperlyEncoded() (gas: 111449)
5+
MulticallerTest:testMulticallerReturnDataIsProperlyEncoded(string,string,uint256) (runs: 256, μ: 58140, ~: 31137)
6+
MulticallerTest:testMulticallerReturnDataIsProperlyEncoded(uint256,uint256,uint256,uint256) (runs: 256, μ: 111551, ~: 111551)
7+
MulticallerTest:testMulticallerRevertWithCustomError() (gas: 28637)
8+
MulticallerTest:testMulticallerRevertWithMessage() (gas: 31362)
9+
MulticallerTest:testMulticallerRevertWithMessage(string) (runs: 256, μ: 32199, ~: 32295)
10+
MulticallerTest:testMulticallerRevertWithNothing() (gas: 28523)
11+
MulticallerTest:testMulticallerTargetGetMulticallerSender() (gas: 30027)
12+
MulticallerTest:testMulticallerWithNoData() (gas: 13513)
13+
MulticallerTest:test__codesize() (gas: 20104)
1414
TestPlus:test__codesize() (gas: 1102)

‎README.md‎

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44
[![CI][ci-shield]][ci-url]
55
[![MIT License][license-shield]][license-url]
66

7-
> **Warning**
8-
> This repository is still under heavy construction and review. Please do not use yet.
7+
Efficiently call multiple contracts in a single transaction.
8+
9+
Allows for optional "forwarding" of `msg.sender` to the contracts called.
910

1011
## Deployments
1112

1213
| Chain | Multicaller |
1314
|---|---|
14-
| Ethereum | [`0x000000000000a9797e1Bb9daD17E838ba5860A82`](https://etherscan.io/address/0x000000000000a9797e1Bb9daD17E838ba5860A82) |
15-
| Goerli | [`0x000000000000a9797e1Bb9daD17E838ba5860A82`](https://goerli.etherscan.io/address/0x000000000000a9797e1Bb9daD17E838ba5860A82) |
16-
| Polygon | [`0x000000000000a9797e1Bb9daD17E838ba5860A82`](https://polygonscan.com/address/0x000000000000a9797e1Bb9daD17E838ba5860A82) |
17-
| Mumbai | [`0x000000000000a9797e1Bb9daD17E838ba5860A82`](https://mumbai.polygonscan.com/address/0x000000000000a9797e1Bb9daD17E838ba5860A82) |
18-
15+
| Ethereum | [`0x00000000000066F8295B13Fb252b7f873CBBA71d`](https://etherscan.io/address/0x00000000000066F8295B13Fb252b7f873CBBA71d) |
16+
| Goerli | [`0x00000000000066F8295B13Fb252b7f873CBBA71d`](https://goerli.etherscan.io/address/0x00000000000066F8295B13Fb252b7f873CBBA71d) |
17+
| Polygon | [`0x00000000000066F8295B13Fb252b7f873CBBA71d`](https://polygonscan.com/address/0x00000000000066F8295B13Fb252b7f873CBBA71d) |
18+
| Mumbai | [`0x00000000000066F8295B13Fb252b7f873CBBA71d`](https://mumbai.polygonscan.com/address/0x00000000000066F8295B13Fb252b7f873CBBA71d) |
19+
| Optimism | [`0x00000000000066F8295B13Fb252b7f873CBBA71d`](https://optimistic.etherscan.io/address/0x00000000000066F8295B13Fb252b7f873CBBA71d) |
20+
| Arbitrum | [`0x00000000000066F8295B13Fb252b7f873CBBA71d`](https://arbiscan.io/address/address/0x00000000000066F8295B13Fb252b7f873CBBA71d) |
1921

2022
Please open an issue if you need help to deploy to an EVM chain of your choice.
2123

@@ -24,7 +26,7 @@ Please open an issue if you need help to deploy to an EVM chain of your choice.
2426
```ml
2527
src
2628
├─ Multicaller.sol — "The multicaller contract"
27-
└─ MulticallerReader.sol — "Library to read the sender of the multicaller contract"
29+
└─ MulticallerReader.sol — "Library to read the `msg.sender` of the multicaller contract"
2830
```
2931

3032
## Installation
@@ -45,7 +47,9 @@ npm install multicaller
4547

4648
## API
4749

48-
### `aggregate`
50+
### Multicaller
51+
52+
#### `aggregate`
4953
```solidity
5054
function aggregate(address[] calldata targets, bytes[] calldata data)
5155
external
@@ -56,7 +60,7 @@ Aggregates multiple calls in a single transaction.
5660

5761
The `msg.value` will be forwarded to the starting call.
5862

59-
### `aggregateWithSender`
63+
#### `aggregateWithSender`
6064
```solidity
6165
function aggregateWithSender(address[] calldata targets, bytes[] calldata data)
6266
external
@@ -67,18 +71,36 @@ Aggregates multiple calls in a single transaction.
6771

6872
The `msg.value` will be forwarded to the starting call.
6973

70-
This method will set `sender` to the `msg.sender` temporarily for the span of its execution.
74+
This method will set the multicaller sender to the `msg.sender` temporarily for the span of its execution.
7175

7276
This method does not support reentrancy.
7377

74-
### `sender`
78+
#### `fallback`
7579
```solidity
76-
function sender() external view returns (address)
80+
fallback() external payable
7781
```
7882
Returns the address that called `aggregateWithSender` on the contract.
7983

8084
The value is always the zero address outside a transaction.
8185

86+
### MulticallerReader
87+
88+
Library to read the sender of the multicaller contract.
89+
90+
#### `multicallerSender`
91+
```solidity
92+
function multicallerSender() internal view returns (address)
93+
```
94+
Returns the address that called `aggregateWithSender` on the multicaller.
95+
96+
#### `sender`
97+
```solidity
98+
function sender() internal view returns (address result)
99+
```
100+
Returns the address that called `aggregateWithSender` on the multicaller, if `msg.sender` is the multicaller.
101+
102+
Otherwise, returns `msg.sender`.
103+
82104
## Design
83105

84106
The contracts are designed with a priority on efficiency and minimalism.
@@ -93,11 +115,14 @@ We **do not give any warranties** and **will not be liable for any loss** incurr
93115

94116
## Acknowledgments
95117

96-
This repository is inspired by and directly modified from:
118+
Multicaller is inspired by and directly modified from:
97119

98120
- [Solady](https://github.com/vectorized/solady)
99121
- [MakerDao's Multicall](https://github.com/makerdao/multicall)
100122

123+
This project is a public good initiative of [sound.xyz](https://sound.xyz) and Solady.
124+
125+
We would like to thank our [reviewers and contributors](credits.txt) for their invaluable help.
101126

102127
[npm-shield]: https://img.shields.io/npm/v/multicaller.svg
103128
[npm-url]: https://www.npmjs.com/package/multicaller

‎credits.txt‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Many thanks to the following for their reviews and inputs on Multicaller:
2+
3+
- atarpara
4+
- moodlezoup
5+
- ncitron
6+
- a16z
7+
- soundxyz
8+
- 0age
9+
- philogy
10+
- pcaversaccio
11+
- 0xdineshkumarsm
12+
- z0r0z

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "multicaller",
33
"license": "MIT",
4-
"version": "0.0.1",
4+
"version": "1.0.0",
55
"description": "Efficient multicaller contract",
66
"files": [
77
"src/**/*.sol"

‎src/Multicaller.sol‎

Lines changed: 52 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,17 @@ contract Multicaller {
2222
*/
2323
error Reentrancy();
2424

25-
// =============================================================
26-
// STORAGE
27-
// =============================================================
28-
29-
/**
30-
* @dev The storage slot for the sender and reentrancy guard flag.
31-
*/
32-
bytes32 private _sender;
33-
3425
// =============================================================
3526
// CONSTRUCTOR
3627
// =============================================================
3728

3829
constructor() payable {
3930
assembly {
40-
sstore(_sender.slot, shl(160, 1))
31+
// Throughout this code, we will abuse returndatasize
32+
// in place of zero anywhere before a call to save a bit of gas.
33+
// We will use storage slot zero to store the caller at
34+
// bits [0..159] and reentrancy guard flag at bit 160.
35+
sstore(returndatasize(), shl(160, 1))
4136
}
4237
}
4338

@@ -48,24 +43,23 @@ contract Multicaller {
4843
/**
4944
* @dev Returns the address that called `aggregateWithSender` on this contract.
5045
* The value is always the zero address outside a transaction.
51-
* @return The caller address.
5246
*/
53-
function sender() external view returns (address) {
47+
fallback() external payable {
5448
assembly {
55-
mstore(0x00, and(sub(shl(160, 1), 1), sload(_sender.slot)))
56-
return(0x00, 0x20)
49+
mstore(returndatasize(), and(sub(shl(160, 1), 1), sload(returndatasize())))
50+
return(returndatasize(), 0x20)
5751
}
5852
}
5953

6054
/**
6155
* @dev Aggregates multiple calls in a single transaction.
62-
* The `msg.value` will be forwarded to the starting call.
56+
* The `msg.value` will be forwarded to the last call.
6357
* This method will set `sender` to the `msg.sender` temporarily
6458
* for the span of its execution.
6559
* This method does not support reentrancy.
6660
* @param targets An array of addresses to call.
6761
* @param data An array of calldata to forward to the targets.
68-
* @return An array of the returndata from each of the call.
62+
* @return An array of the returndata from each call.
6963
*/
7064
function aggregateWithSender(address[] calldata targets, bytes[] calldata data)
7165
external
@@ -75,36 +69,38 @@ contract Multicaller {
7569
assembly {
7670
if iszero(eq(targets.length, data.length)) {
7771
// Store the function selector of `ArrayLengthsMismatch()`.
78-
mstore(0x00, 0x3b800a46)
72+
mstore(returndatasize(), 0x3b800a46)
7973
// Revert with (offset, size).
8074
revert(0x1c, 0x04)
8175
}
8276

83-
if iszero(and(sload(_sender.slot), shl(160, 1))) {
77+
if iszero(and(sload(returndatasize()), shl(160, 1))) {
8478
// Store the function selector of `Reentrancy()`.
85-
mstore(0x00, 0xab143c06)
79+
mstore(returndatasize(), 0xab143c06)
8680
// Revert with (offset, size).
8781
revert(0x1c, 0x04)
8882
}
89-
// Set the `_sender` slot temporarily for the span of this transaction.
90-
sstore(_sender.slot, caller())
83+
// Set the sender slot temporarily for the span of this transaction.
84+
sstore(returndatasize(), caller())
9185

92-
mstore(0x00, 0x20) // Store the memory offset of the `results`.
86+
mstore(returndatasize(), 0x20) // Store the memory offset of the `results`.
9387
mstore(0x20, data.length) // Store `data.length` into `results`.
9488
// Early return if no data.
95-
if iszero(data.length) { return(0x00, 0x40) }
89+
if iszero(data.length) { return(returndatasize(), 0x40) }
9690

9791
let results := 0x40
98-
// `shl` 5 is equivalent to multiplying by 0x20.
99-
let end := shl(5, data.length)
92+
// Left shift by 5 is equivalent to multiplying by 0x20.
93+
data.length := shl(5, data.length)
10094
// Copy the offsets from calldata into memory.
101-
calldatacopy(0x40, data.offset, end)
102-
// Pointer to the top of the memory (i.e. start of the free memory).
103-
let resultsOffset := end
104-
// The callvalue to forward to the starting call.
105-
let v := callvalue()
106-
107-
for { end := add(results, end) } 1 {} {
95+
calldatacopy(results, data.offset, data.length)
96+
// Offset into `results`.
97+
let resultsOffset := data.length
98+
// Pointer to the last result.
99+
let lastResults := add(0x20, data.length)
100+
// Pointer to the end of `results`.
101+
let end := add(results, data.length)
102+
103+
for {} 1 {} {
108104
// The offset of the current bytes in the calldata.
109105
let o := add(data.offset, mload(results))
110106
let memPtr := add(resultsOffset, 0x40)
@@ -118,7 +114,7 @@ contract Multicaller {
118114
call(
119115
gas(), // Remaining gas.
120116
calldataload(targets.offset), // Address to call.
121-
v, // Amount of ETH to send.
117+
mul(callvalue(), eq(results, lastResults)), // ETH to send.
122118
memPtr, // Start of input calldata in memory.
123119
calldataload(o), // Size of input calldata.
124120
0x00, // We will use returndatacopy instead.
@@ -129,34 +125,32 @@ contract Multicaller {
129125
returndatacopy(0x00, 0x00, returndatasize())
130126
revert(0x00, returndatasize())
131127
}
132-
// We only forward the callvalue for the starting call.
133-
v := 0
134128
// Advance the `targets.offset`.
135129
targets.offset := add(targets.offset, 0x20)
136130
// Append the current `resultsOffset` into `results`.
137131
mstore(results, resultsOffset)
138132
results := add(results, 0x20)
139-
// Append the returndatasize, and the return data.
133+
// Append the returndatasize, and the returndata.
140134
mstore(memPtr, returndatasize())
141135
returndatacopy(add(memPtr, 0x20), 0x00, returndatasize())
142136
// Advance the `resultsOffset` by `returndatasize() + 0x20`,
143-
// rounded up to the next multiple of 32.
137+
// rounded up to the next multiple of 0x20.
144138
resultsOffset := and(add(add(resultsOffset, returndatasize()), 0x3f), not(0x1f))
145139
if iszero(lt(results, end)) { break }
146140
}
147-
// Restore the `_sender` slot.
148-
sstore(_sender.slot, shl(160, 1))
141+
// Restore the `sender` slot.
142+
sstore(0, shl(160, 1))
149143
// Direct return.
150144
return(0x00, add(resultsOffset, 0x40))
151145
}
152146
}
153147

154148
/**
155149
* @dev Aggregates multiple calls in a single transaction.
156-
* The `msg.value` will be forwarded to the starting call.
150+
* The `msg.value` will be forwarded to the last call.
157151
* @param targets An array of addresses to call.
158152
* @param data An array of calldata to forward to the targets.
159-
* @return An array of the returndata from each of the call.
153+
* @return An array of the returndata from each call.
160154
*/
161155
function aggregate(address[] calldata targets, bytes[] calldata data)
162156
external
@@ -166,27 +160,29 @@ contract Multicaller {
166160
assembly {
167161
if iszero(eq(targets.length, data.length)) {
168162
// Store the function selector of `ArrayLengthsMismatch()`.
169-
mstore(0x00, 0x3b800a46)
163+
mstore(returndatasize(), 0x3b800a46)
170164
// Revert with (offset, size).
171165
revert(0x1c, 0x04)
172166
}
173167

174-
mstore(0x00, 0x20) // Store the memory offset of the `results`.
168+
mstore(returndatasize(), 0x20) // Store the memory offset of the `results`.
175169
mstore(0x20, data.length) // Store `data.length` into `results`.
176170
// Early return if no data.
177-
if iszero(data.length) { return(0x00, 0x40) }
171+
if iszero(data.length) { return(returndatasize(), 0x40) }
178172

179173
let results := 0x40
180-
// `shl` 5 is equivalent to multiplying by 0x20.
181-
let end := shl(5, data.length)
174+
// Left shift by 5 is equivalent to multiplying by 0x20.
175+
data.length := shl(5, data.length)
182176
// Copy the offsets from calldata into memory.
183-
calldatacopy(0x40, data.offset, end)
184-
// Pointer to the top of the memory (i.e. start of the free memory).
185-
let resultsOffset := end
186-
// The callvalue to forward to the starting call.
187-
let v := callvalue()
188-
189-
for { end := add(results, end) } 1 {} {
177+
calldatacopy(results, data.offset, data.length)
178+
// Offset into `results`.
179+
let resultsOffset := data.length
180+
// Pointer to the last result.
181+
let lastResults := add(0x20, data.length)
182+
// Pointer to the end of `results`.
183+
let end := add(results, data.length)
184+
185+
for {} 1 {} {
190186
// The offset of the current bytes in the calldata.
191187
let o := add(data.offset, mload(results))
192188
let memPtr := add(resultsOffset, 0x40)
@@ -200,7 +196,7 @@ contract Multicaller {
200196
call(
201197
gas(), // Remaining gas.
202198
calldataload(targets.offset), // Address to call.
203-
v, // Amount of ETH to send.
199+
mul(callvalue(), eq(results, lastResults)), // ETH to send.
204200
memPtr, // Start of input calldata in memory.
205201
calldataload(o), // Size of input calldata.
206202
0x00, // We will use returndatacopy instead.
@@ -211,18 +207,16 @@ contract Multicaller {
211207
returndatacopy(0x00, 0x00, returndatasize())
212208
revert(0x00, returndatasize())
213209
}
214-
// We only forward the callvalue for the starting call.
215-
v := 0
216210
// Advance the `targets.offset`.
217211
targets.offset := add(targets.offset, 0x20)
218212
// Append the current `resultsOffset` into `results`.
219213
mstore(results, resultsOffset)
220214
results := add(results, 0x20)
221-
// Append the returndatasize, and the return data.
215+
// Append the returndatasize, and the returndata.
222216
mstore(memPtr, returndatasize())
223217
returndatacopy(add(memPtr, 0x20), 0x00, returndatasize())
224218
// Advance the `resultsOffset` by `returndatasize() + 0x20`,
225-
// rounded up to the next multiple of 32.
219+
// rounded up to the next multiple of 0x20.
226220
resultsOffset := and(add(add(resultsOffset, returndatasize()), 0x3f), not(0x1f))
227221
if iszero(lt(results, end)) { break }
228222
}

0 commit comments

Comments
 (0)