Multicall by OZ and MakerDao has a difference

Hi there, as the title implies, OZ is using a delegatecall in a multicall and MakerDao is using a normal call. I’m wondering what the logics behind this difference are.

From OZ; https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Multicall.sol

    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;
    }

From MakerDao, https://github.com/makerdao/multicall/blob/master/src/Multicall.sol

    struct Call {
        address target;
        bytes callData;
    }
    function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) {
        blockNumber = block.number;
        returnData = new bytes[](calls.length);
        for(uint256 i = 0; i < calls.length; i++) {
            (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData);
            require(success);
            returnData[i] = ret;
        }
    }
1 Like

These two contracts are completely different. The shared contract name is confusing but fortunately the function names are different.

Notice that MakerDao’s aggregate will call each function on a different target contract. Since it uses call, msg.sender will be the Multicall contract itself.

OpenZeppelin’s multicall, on the other hand, performs delegatecall to this. Since it uses delegatecall, msg.sender remains the same account that invoked multicall in the first place.

OpenZeppelin’s is a module that a developer can include in their own contract to allow users to batch multiple function calls to it in one transaction. Since msg.sender is preserved, it’s equivalent to sending multiple transactions from an EOA (non-contract account).

MakerDao’s is a helper contract that a developer can deploy in order to use as a sort of forwarding proxy that will aggregate the result of multiple calls. Since msg.sender is not preserved, its use is more limited, but many circumstances may still find it useful.

3 Likes

Yes, @frangio, I see your points. By design, these two have two distinct purposes.

If multicall can only be used for making delegate calls, its scope of applicability is limited, right?

The way aggregate is designed is more generic although not using delegatecall can be an improving point.

Would OZ consider developing a more generic multicall function?

It’s not that aggregate is more generic. They do different things, they don’t compare.

At the moment we are not considering offering a module like aggregate.

Right. By saying more generic, I was referring to the fact that the array calls can include different tareget addresses which can of course be all the same. That is then reducing the function to a multicall to a single address with multiple calldata.

But it is alright. This is enough to see the differences clearly. Thanks, @frangio .

Ah I see why you’d say it’s more generic. But the thing is that delegatecall and call do very very different things, so it’s not possible to say that one is more generic than the other.

Just making a toy example, and if the function signature is something like this, it would be somewhat more generic.

function multicall(address[] memory targets, bytes[] calldata data, bool delegate) external returns (bytes[] memory results);

Okay I see what you mean about making a more generic multicall with delegatecall. But this is not a good idea. delegatecall gives the target contract arbitrary access to storage. It’s safe to delegate to this, but generally unsafe to delegate to an arbitrary contract.

Try searching online to better understand how it works and why it’s unsafe. Quick one I found:

Yes and no. Since multicall sounds like a generic function with a broad scope of applicability, it should be able to suit different needs, otherwise, it should be named as multiDelegatecall.

I was not focusing on whether delegate call should be sent to multiple targets, but rather on the interface design. My background in Python is heavy, where a function can have a long list of parameters such that it can be used in a variety of relevant scenarios.

I take that design principle here, maybe I should not, but the ability to call different targets with different calldata not in a delegate call fashion does sound like belonging to a function named as multicall.