Skip to content

Instantly share code, notes, and snippets.

@zemse
Last active 3 months ago
Embed
What would you like to do?
Calldata Optimisation

Calldata Optimisation

Reducing gas costs for L2 users by optimising calldata.

Motivation

Transaction fees on L2s like Optimism and Arbitrum involve paying the calldata gas costs for the batch submission on L1. And it accounts for good amount of gas fees.

For example, breakup of $4.19 Uniswap Trade on Arbitrum (explorer):

  • L1 Fixed Cost: $1.77
  • L1 Calldata Cost: $2.30
  • L2 Computation: $0.12

You can see that, the calldata cost is over 50%.

Abstract

On L1, calldata is not compact because decoding costs would be more. But on L2, where calldata is costly while computation is cheaper, calldata can be encoded in a compact way, without much worring about decoding costs. Now there are a lot of ways to do that, which might cause security as well as compatibility issues. This document proposes a way to do that.

Specification

  • Function compact selectors are 1 byte.
  • Parameters are closedly packed.
  • Normal solidity functions (according to ABI spec) should still be exposted to allow other contracts to CALL.
  • It is only intended an EOA to use such route to interact with contract using a compact calldata.

TODO: think about compact dynamic length stuff

Security Considerations

It is possible that a compact calldata might collide with the normal calldata. It should be ensured that the compact selector should not collide with any function selector's first byte.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import {console} from 'hardhat/console.sol';
library Calldata {
function getUint8(uint offset) internal returns (uint8 val) {
assembly {
val := calldataload(offset)
}
}
function getUint96(uint offset) internal returns (uint96 val) {
assembly {
val := calldataload(offset)
}
}
function getAddress(uint offset) internal returns (address val) {
assembly {
val := calldataload(offset)
}
}
}
uint8 constant TRANSFER_SELECTOR = 1;
uint8 constant BURN_SELECTOR = 160;
contract MyContract {
fallback(bytes calldata data) external returns (bytes memory) {
uint8 selector = Calldata.getUint8(0);
// NOTE: need to have selectors such that it does not collide
// with other functions.
if(selector == TRANSFER_SELECTOR) {
// 0x TRANSFER_SELECTOR + address + uint96
transfer(
Calldata.getAddress(1),
Calldata.getUint96(21)
);
} else if(selector == BURN_SELECTOR) {
// 0x BURN_SELECTOR + uint96
burn(
Calldata.getUint96(21)
);
}
Calldata.getAddress(12);
}
function transfer(address dest, uint96 amt) public {
// exec code
}
function burn(uint96 amt) public {
// exec code
}
}
error Collsion(uint8 compressed, bytes4 actual);
contract MyContractCollisionTest {
bytes4[] actualSelectors = [
MyContract.transfer.selector,
MyContract.burn.selector
];
uint8[] compressedSelectors = [
TRANSFER_SELECTOR,
BURN_SELECTOR
];
constructor() {
selectorCollisionTest();
}
function selectorCollisionTest() public {
for(uint i; i < actualSelectors.length; i++) {
for(uint j; j < compressedSelectors.length; j++) {
if(uint32(actualSelectors[i]) >> 24 == compressedSelectors[j]) revert Collsion(compressedSelectors[j], actualSelectors[i]);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment