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

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.

6 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.

1 Like

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:

2 Likes

Sorry to revive this, but i was wondering if it's safe to create a new separate contract similar to makerdao's multicall, but using delegateCall instead.

W.R.T the security risk of contract storage being accessed, we can check the msg.sender to be EOA,

As frangio said above, I think they are different things, and is there a special reason that you want to make change like you said.

my bad, i had the impression that delegatecall mimics a call function while preserving msg.sender and msg.value.

but looks like it works more like a cross contract modifier where the function of an external contract is copied to address(this) and executed on address(this). state of calling contract can be changed but state of external contract cannot be changed