person playing magic cube

Solidity: Multicall - Aggregate Multiple Contract Calls

There are different implementations of multicall:

In the following section, we will use Multicaller as an example to illustrate the process.

The main idea of Multicaller is to aggregate multiple contract function calls into a single one. It's usually to batch contract reads from off-chain apps. However, it could also be used to batch contract writes.

Multiple Contract Reads

import { defaultAbiCoder } from "ethers/lib/utils"

class Liquidator {
    async fetchIsLiquidatableResults(
        marketId: number,
        positions: Position[],
    ) {
        const price = await this.pythService.fetchPythOraclePrice(marketId)

        const targets = new Array(positions.length).fill(
        const data = =>
  "isLiquidatable", [
        const values = new Array(accountMarkets.length).fill(0)

        return await this.multicaller.callStatic.aggregate(targets, data, values)

    async start() {
        const positions = await this.fetchPositions(marketId)
        const results = await this.fetchIsLiquidatableResults(marketId, positions)

        for (const [i, result] of results.entries()) {
            const isLiquidatable = defaultAbiCoder.decode(["bool"], result)[0]
            const position = positions[i]
            console.log(`${position.account} isLiquidatable: ${isLiquidatable}`)


Multiple Contract Writes

It requires the target contract is compatible with Multicaller if the target contract needs to read msg.sender.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import { LibMulticaller } from "multicaller/LibMulticaller.sol";

contract MulticallerSenderCompatible {
    function _sender() internal view virtual returns (address) {
        return LibMulticaller.sender();

contract Exchange is MulticallerSenderCompatible {
    function openPosition(OpenPositionParams calldata params) external returns (int256, int256) {
        address taker = _sender();
        return _openPositionFor(taker, params);
class Bot {
    async openPosition() {
        const targets = [
        const data = [
            this.oracleAdapter.interface.encodeFunctionData("updatePrice", [priceId, priceData]),
  "openPosition", [params]),
        const values = [

        // update oracle price first, then open position
        const tx = await this.multicaller.connect(taker).aggregateWithSender(targets, data, values)
        await tx.wait()