6

Say I want my contract (in assembly) to be more gas efficient and can handle deserialization of a more efficient msg.data myself than the standard ABI..

I am willing to spend time on the client in order for the contract processing to be more gas efficient -- basic question what do I do on the client side to actually make a call to my contract with a totally custom msg.data including the function selector?

foo(bytes memory encodedParams)

Is there a way for me to take over the entire msg.data including the function selector? How do I make that call from the client?

3
  • 1
    You first talk about gas efficiency, then you talk about the client side. There's no gas costs on the client side. Please clarify your question. Nov 23, 2021 at 19:49
  • thanks -- clarified question in the body above -- I want gas efficiency on the contract side but don't know how to make a a call from the client with a totally custom msg.data and bypass the abi Nov 23, 2021 at 19:56
  • 1
    @nxstock-trader You have to use the data field in eth.sendTransaction or eth.call. Unfortunately you won't be able to use the wrapper web3.eth.Contract since it assumes the contract accepts and returns ABI encoded values.
    – Ismael
    Nov 24, 2021 at 5:09

2 Answers 2

12
+100

The question is a little how custom do you want to have it. In general I would start from the contract side.

It is not really required that you use Solidity function selectors. This is primarily a standard that allow easy reusability with different libraries and contracts.

You can use the fallback method to have custom method handling with Solidity:

// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

contract Bytecode {
    fallback(bytes calldata data) external returns (bytes memory) {
        if (bytes4(data) == 0x0000dead) {
            return "Dead";
        }
        return "Alive";
    }
}

This can be extended to also use custom parameter encoding via array slices. Lets assume we want to use packed encoding to optimize on calldata size:

contract Custom {
    fallback(bytes calldata data) external returns (bytes memory) {
        if (bytes4(data) == 0x0000dead) {
            uint8 hasAddress = uint8(bytes1(data[4:5]));
            address someAddress = address(bytes20(data[5:25]));
            if (hasAddress > 0) {
                return abi.encode(someAddress);
            }
            return "Dead";
        }
        return "Alive";
    }
}

This contract can now be called using the Solidity packed encoding.

Example how to call it in Solidity:

(bool success, bytes memory data) = custom.call(abi.encodePacked(bytes4(0x0000dead), true, this));

Example how to call it via Ethers provider:

const data = await provider.call({
  to: "<your_contract_address>",
  data: ethers.utils.solidityPack(["bytes4", "boolean", "address"], ["0x0000dead", true, "0xE5f2A565Ee0Aa9836B4c80a07C8b32aAd7978e22"])
})

These calls return you raw data and to handle the response you still need to decode it.

Example how to decode it in Solidity:

(address response) = abi.decode(data, (address));

Example how to call it via Ethers AbiCoder:

const [response] = ethers.utils.defaultAbiCoder.decode([ "address" ], data);

You can do all kinds of magic with this approach. BUT this is very low level and in many cases will only lower your calldata gas costs. A lot of gas costs is not caused by the calldata encoding, but by checks enforced by Solidity on the calldata and general variables.

Some of these checks are:

  • Bounds checks for dynamic length types such as arrays and bytes
  • Masking of elementary types that don't use the full 32 bytes, such as address or boolean
  • Overflow checks for mathematical operations.

These checks improve the security of your contracts and prevent that unexpected flows can be exploited.

Some of these checks can be disabled with an unchecked block. But if you want to fully optimize it you have to use YUL/assembly.

For an example of custom parameter encoding using YUL you can take a look at the MultiSend contract from the Safe team. There it also uses packed encoding and you can see the example code how to perform the encoding for the data with Ethers in the multisend utility module

1
  • Is it possible though to include a function selector in the call from JS so that you can call a proxied function via the fallaback function?
    – Madbreaks
    Oct 4, 2022 at 19:55
2

First of all, you're going to need to compute the ABI-encoded function call data string as follows:

const data = web3.eth.abi.encodeFunctionCall({
    name: 'insertYourFunctionName',
    type: 'function',
    inputs: [{
        type: 'insertSolidityTypeOfParam1',
        name: 'insertNameOfParam1'
    },{
        type: 'insertSolidityTypeOfParam2',
        name: 'insertNameOfParam2'
    },
    ...
    ]}, [insertValueOfParam1, insertValueOfParam2, ...]);

In your specific case, the computed data would be as follows:

const data = web3.eth.abi.encodeFunctionCall({
    name: 'foo',
    type: 'function',
    inputs: [{
        type: 'bytes',
        name: 'encodedParams'
    }]}, [insertEncodedParamsString]);

Although you don't need to import an ABI file, you do need to know about the structure of the function you intend to call.

Next step is to send the actual transaction with the computed data as follows:

await web3.eth.sendTransaction({
    from: yourWalletAddress,
    to: contractAddress,
    data: data
});

So effectively, the above would be the same as what's happening under the hood when you make a standard web3 function call with an ABI:

const contract = new Contract(abi, contractAddress);

await contract.methods.insertYourFunctionName(
    insertValueOfParam1,
    insertValueOfParam2,
    ...
).send({ from: yourWalletAddress });

Or in your case, it would be:

const contract = new Contract(abi, contractAddress);

await contract.methods.foo(
    insertEncodedParamsString
).send({ from: yourWalletAddress });
1
  • 2
    The question is tagged ethers.js
    – Madbreaks
    Oct 4, 2022 at 19:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.