Skip to content

Commit

Permalink
👷 vectorized multicaller with sender
Browse files Browse the repository at this point in the history
  • Loading branch information
Flocqst committed Oct 18, 2024
1 parent 3c175ef commit 7310d14
Showing 1 changed file with 108 additions and 155 deletions.
263 changes: 108 additions & 155 deletions src/utils/Multicaller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
pragma solidity ^0.8.4;

/**
* @title Multicaller
* @title MulticallerWithSender
* @author vectorized.eth
* @notice Contract that allows for efficient aggregation
* of multiple calls in a single transaction.
* @notice Contract that allows for efficient aggregation of multiple calls
* in a single transaction, while "forwarding" the `msg.sender`.
*/
contract Multicaller {
contract MulticallerWithSender {
// =============================================================
// ERRORS
// =============================================================
Expand All @@ -17,180 +17,133 @@ contract Multicaller {
*/
error ArrayLengthsMismatch();

/**
* @dev This function does not support reentrancy.
*/
error Reentrancy();

// =============================================================
// CONSTRUCTOR
// =============================================================

constructor() payable {
assembly {
// Throughout this code, we will abuse returndatasize
// in place of zero anywhere before a call to save a bit of gas.
// We will use storage slot zero to store the caller at
// bits [0..159] and reentrancy guard flag at bit 160.
sstore(returndatasize(), shl(160, 1))
}
}

// =============================================================
// AGGREGATION OPERATIONS
// =============================================================

/**
* @dev Returns the address that called `aggregateWithSender` on this contract.
* The value is always the zero address outside a transaction.
*/
receive() external payable {
assembly {
mstore(returndatasize(), and(sub(shl(160, 1), 1), sload(returndatasize())))
return(returndatasize(), 0x20)
}
}

/**
* @dev Aggregates multiple calls in a single transaction.
* @param data An array of calldata to forward to this contract.
* @param values How much ETH to forward with each call.
* @param refundTo The address to transfer any remaining ETH in the contract after the calls.
* If `address(0)`, remaining ETH will NOT be refunded.
* If `address(1)`, remaining ETH will be refunded to `msg.sender`.
* If anything else, remaining ETH will be refunded to `refundTo`.
* This method will set `sender` to the `msg.sender` temporarily
* for the span of its execution.
* This method does not support reentrancy.
* @param targets An array of addresses to call.
* @param data An array of calldata to forward to the targets.
* @param values How much ETH to forward to each target.
* @return An array of the returndata from each call.
*/
function aggregate(
function aggregateWithSender(
address[] calldata targets,
bytes[] calldata data,
uint256[] calldata values,
address refundTo
uint256[] calldata values
) external payable returns (bytes[] memory) {
assembly {
if iszero(eq(data.length, values.length)) {
if iszero(and(eq(targets.length, data.length), eq(data.length, values.length))) {
// Store the function selector of `ArrayLengthsMismatch()`.
mstore(returndatasize(), 0x3b800a46)
// Revert with (offset, size).
revert(0x1c, 0x04)
}

let resultsSize := 0x40

if data.length {
let results := 0x40
// Left shift by 5 is equivalent to multiplying by 0x20.
data.length := shl(5, data.length)
// Copy the offsets from calldata into memory.
calldatacopy(results, data.offset, data.length)
// Offset into `results`.
let resultsOffset := data.length
// Pointer to the end of `results`.
let end := add(results, data.length)
// For deriving the calldata offsets from the `results` pointer.
let valuesOffsetDiff := sub(values.offset, results)

for {} 1 {} {
// The offset of the current bytes in the calldata.
let o := add(data.offset, mload(results))
let memPtr := add(resultsOffset, 0x40)
// Copy the current bytes from calldata to the memory.
calldatacopy(
memPtr,
add(o, 0x20), // The offset of the current bytes' bytes.
calldataload(o) // The length of the current bytes.
)
if iszero(
call(
gas(), // Remaining gas.
address(), // Address to call (this contract).
calldataload(add(valuesOffsetDiff, results)), // ETH to send.
memPtr, // Start of input calldata in memory.
calldataload(o), // Size of input calldata.
0x00, // We will use returndatacopy instead.
0x00 // We will use returndatacopy instead.
)
) {
// Bubble up the revert if the call reverts.
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
// Append the current `resultsOffset` into `results`.
mstore(results, resultsOffset)
// Append the returndatasize, and the returndata.
mstore(memPtr, returndatasize())
returndatacopy(add(memPtr, 0x20), 0x00, returndatasize())
// Advance the `resultsOffset` by `returndatasize() + 0x20`,
// rounded up to the next multiple of 0x20.
resultsOffset :=
and(
add(add(resultsOffset, returndatasize()), 0x3f),
not(0x1f)
)
// Advance the `results` pointer.
results := add(results, 0x20)
if eq(results, end) { break }
}
resultsSize := add(resultsOffset, 0x40)
if iszero(and(sload(returndatasize()), shl(160, 1))) {
// Store the function selector of `Reentrancy()`.
mstore(returndatasize(), 0xab143c06)
// Revert with (offset, size).
revert(0x1c, 0x04)
}

if refundTo {
// Force transfers all the remaining ETH in the contract to `refundTo`,
// with a gas stipend of 100000, which should be enough for most use cases.
// If sending via a regular call fails, force sends the ETH by
// creating a temporary contract which uses `SELFDESTRUCT` to force send the ETH.
if selfbalance() {
// If `refundTo` is `address(1)`, replace it with the `msg.sender`.
refundTo :=
xor(refundTo, mul(eq(refundTo, 1), xor(refundTo, caller())))
// Transfer the ETH and check if it succeeded or not.
if iszero(
call(
100000,
refundTo,
selfbalance(),
codesize(),
0x00,
codesize(),
0x00
)
) {
mstore(0x00, refundTo) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
// We can directly use `SELFDESTRUCT` in the contract creation.
// Compatible with `SENDALL`: https://eips.ethereum.org/EIPS/eip-4758
if iszero(create(selfbalance(), 0x0b, 0x16)) {
// Coerce gas estimation to provide enough gas for the `create` above.
revert(codesize(), codesize())
}
}
}
}
mstore(returndatasize(), 0x20) // Store the memory offset of the `results`.
mstore(0x20, data.length) // Store `data.length` into `results`.
// Early return if no data.
if iszero(data.length) { return(returndatasize(), 0x40) }

mstore(0x00, 0x20) // Store the memory offset of the `results`.
mstore(0x20, shr(5, data.length)) // Store `data.length` into `results`.
// Direct return.
return(0x00, resultsSize)
}
}
// Set the sender slot temporarily for the span of this transaction.
sstore(returndatasize(), caller())

/**
* @dev For receiving ETH.
* Does nothing and returns nothing.
* Called instead of `fallback()` if the calldatasize is zero.
*/
receive() external payable {}
let results := 0x40
// Left shift by 5 is equivalent to multiplying by 0x20.
data.length := shl(5, data.length)
// Copy the offsets from calldata into memory.
calldatacopy(results, data.offset, data.length)
// Offset into `results`.
let resultsOffset := data.length
// Pointer to the end of `results`.
// Recycle `data.length` to avoid stack too deep.
data.length := add(results, data.length)

/**
* @dev Decompresses the calldata and performs a delegatecall
* with the decompressed calldata to itself.
*
* Accompanying JavaScript library to compress the calldata:
* https://github.com/vectorized/solady/blob/main/js/solady.js
* (See: `LibZip.cdCompress`)
*/
fallback() external payable {
assembly {
// If the calldata starts with the bitwise negation of
// `bytes4(keccak256("aggregate(bytes[],uint256[],address)"))`.
let s := calldataload(returndatasize())
if eq(shr(224, s), 0x84522fae) {
mstore(returndatasize(), not(s))
let o := 4
for { let i := o } lt(i, calldatasize()) {} {
let c := byte(returndatasize(), calldataload(i))
i := add(i, 1)
if iszero(c) {
let d := byte(returndatasize(), calldataload(i))
i := add(i, 1)
// Fill with either 0xff or 0x00.
mstore(o, not(returndatasize()))
if iszero(gt(d, 0x7f)) {
codecopy(o, codesize(), add(d, 1))
}
o := add(o, add(and(d, 0x7f), 1))
continue
}
mstore8(o, c)
o := add(o, 1)
for {} 1 {} {
// The offset of the current bytes in the calldata.
let o := add(data.offset, mload(results))
let memPtr := add(resultsOffset, 0x40)
// Copy the current bytes from calldata to the memory.
calldatacopy(
memPtr,
add(o, 0x20), // The offset of the current bytes' bytes.
calldataload(o) // The length of the current bytes.
)
if iszero(
call(
gas(), // Remaining gas.
calldataload(targets.offset), // Address to call.
calldataload(values.offset), // ETH to send.
memPtr, // Start of input calldata in memory.
calldataload(o), // Size of input calldata.
0x00, // We will use returndatacopy instead.
0x00 // We will use returndatacopy instead.
)
) {
// Bubble up the revert if the call reverts.
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
let success :=
delegatecall(gas(), address(), 0x00, o, 0x00, 0x00)
returndatacopy(0x00, 0x00, returndatasize())
if iszero(success) { revert(0x00, returndatasize()) }
return(0x00, returndatasize())
// Advance the `targets.offset`.
targets.offset := add(targets.offset, 0x20)
// Advance the `values.offset`.
values.offset := add(values.offset, 0x20)
// Append the current `resultsOffset` into `results`.
mstore(results, resultsOffset)
results := add(results, 0x20)
// Append the returndatasize, and the returndata.
mstore(memPtr, returndatasize())
returndatacopy(add(memPtr, 0x20), 0x00, returndatasize())
// Advance the `resultsOffset` by `returndatasize() + 0x20`,
// rounded up to the next multiple of 0x20.
resultsOffset := and(add(add(resultsOffset, returndatasize()), 0x3f), not(0x1f))
if iszero(lt(results, data.length)) { break }
}
revert(returndatasize(), returndatasize())
// Restore the `sender` slot.
sstore(0, shl(160, 1))
// Direct return.
return(0x00, add(resultsOffset, 0x40))
}
}
}
}

0 comments on commit 7310d14

Please sign in to comment.