Saltar al contenido principal

Implement multicall in router-like contracts

In Solidity, implementing multicall functionality in router-like contracts can significantly reduce gas costs by batching multiple state-modifying calls into a single transaction. This technique is invaluable in contracts similar to those used by platforms like Uniswap and Compound.

Demo Code

Below, we have a sample contract MulticallRouter that demonstrates how to implement multicall functionality with state-modifying operations. This contract includes a multicall function that executes an array of encoded function calls, modifying contract state in an efficient manner.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract MulticallRouter {
uint256 public counter; // State variable to demonstrate state changes
mapping(uint256 => uint256) public data; // Mapping to store arbitrary data

function multicall(bytes[] calldata data) external returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
require(success, "Multicall execution failed");
results[i] = result;
}
}

function incrementCounter(uint256 amount) external {
counter += amount;
}

function updateData(uint256 key, uint256 value) external {
data[key] = value;
}
}

To verify the functionality and efficiency of the MulticallRouter, here is the corresponding testing script:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Test.sol";
import "./MulticallRouter.sol";

contract MulticallTest is Test {
MulticallRouter public router;
bytes[] callData;

function setUp() public {
router = new MulticallRouter();
callData = new bytes[](4);
callData[0] = abi.encodeWithSelector(
router.incrementCounter.selector,
1
);
callData[1] = abi.encodeWithSelector(
router.updateData.selector,
0,
100
);
callData[2] = abi.encodeWithSelector(
router.incrementCounter.selector,
2
);
callData[3] = abi.encodeWithSelector(
router.updateData.selector,
1,
200
);
}

function testIndividualCalls() public {
uint256 gasStart = gasleft();
router.incrementCounter(1);
router.updateData(0, 100);
router.incrementCounter(2);
router.updateData(1, 200);
uint256 gasEnd = gasleft();
uint256 gasUsed = gasStart - gasEnd;
emit log_named_uint("Gas used for individual calls", gasUsed);
}

function testMulticall() public {
uint256 gasStart = gasleft();
router.multicall(callData);
uint256 gasEnd = gasleft();
uint256 gasUsed = gasStart - gasEnd;
emit log_named_uint("Gas used for multicall", gasUsed);
}
}

By running the tests in the Foundry project, we found that testIndividualCalls() consumes 166259 gas, while testMulticall() consumes 139753 gas, indicating that using Multicall can save gas to some extent.

Gas Optimization Analysis

The primary advantage of using multicall is the reduction in gas costs by avoiding multiple transaction overheads. Here’s a comparison of gas usage between individual transactions and a single multicall:

  • Individual Transactions: Each call incurs base transaction costs plus the gas for executing the function.
  • Single Multicall: Incurs only one base transaction cost plus the gas for executing each function within a loop.

By batching calls, multicall can significantly reduce the cumulative gas cost, especially when performing multiple operations.

Recommendations for Gas Optimization

Implementing multicall in router-like contracts can save gas by reducing the number of transactions and leveraging the lower cumulative gas cost of executing multiple operations in a single transaction.