diff --git a/src/utils/Multicaller.sol b/src/utils/Multicaller.sol index ec878b84..b471d3a2 100644 --- a/src/utils/Multicaller.sol +++ b/src/utils/Multicaller.sol @@ -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 // ============================================================= @@ -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)) } } -} +} \ No newline at end of file