38

When we call a function on an external contract, the EVM computes a bytes buffer which contains the function signature and arguments.

There are two ways to serialize arguments:

  1. abi.encode()
  2. abi.encodePacked()

The differences between the two functions are well documented and I have understood them.

What I want to understand is why there are two functions in the first place. It is not clear when we should use the first and when - the second.

How can I see which function is used internally when I execute the following calls?

Call 1:

(success, ) = address(c).call(abi.encodeWithSignature("myfunction(uint256,uint256)", 400,500));

Call 2:

(success, ) = address(c).call(abi.encodeWithSelector(bytes4(keccak256("myfunction(uint256,uint256)")), 400,500)  )

Call 3:

(success, ) = address(c).call(  bytes4(keccak256("myfunction(uint256,uint256)")),    400, 500);

Call 4:

contract_instance.myfunction(400,500);

Thanks a lot!

4 Answers 4

41

I'd recommend to use 4 at all times if possible. It is easy to remember and write. The behavior is that it will revert the whole transaction if the call fails, which is the most reasonable alternative in general.

  • abi.encode encodes its parameters using the ABI specs. The ABI was designed to make calls to contracts. Parameters are padded to 32 bytes. If you are making calls to a contract you likely have to use abi.encode

  • abi.encodePacked encodes its parameters using the minimal space required by the type. Encoding an uint8 it will use 1 byte. It is used when you want to save some space - not call a contract.

  • abi.encodeWithSignature - same as encode but with the function signature as the first parameter. Use when the signature is known and you don't want to calculate the selector.

  • abi.encodeWithSelector - same as encode but selector is the first parameter. It almost equal to encodeWithSignature - use whatever fits best.

Do not use case 3, as it has been deprecated. A call with abi.encode should do the same job.

4
  • Thanks a lot. So you mean abi.encodePacked is not used in my 4 calls example ? abi.encodePacked is used manually when i want to serialize values for another usage than function calls ?
    – Bob5421
    Jan 3, 2021 at 19:30
  • @Bob5421 When calling a function directly in solidity the compiler generate code similar to abi.encode. The function abi.encodePacked is mostly used to serialize things, like in signatures messages for example. It can be used for functions calls but you should ensure the data layout generated matches the expected by the function.
    – Ismael
    Jan 3, 2021 at 20:52
  • abi.encodeWithSignature is deprecated ? I am still using it in 0.8.0+. It works fine while doing call() /deletgatecall() etc
    – 0xAnon
    Mar 4, 2022 at 17:11
  • 2
    @Coderboi No, abi.encodeWithSignature is not deprecated. What was deprecated is address.call(..) with multiple parameters, now it accepts a single bytes parameter.
    – Ismael
    Mar 4, 2022 at 18:47
9

Cas 4 is more expensive and safer than other cases:

Due to the fact that the EVM considers a call to a non-existing contract to always succeed, Solidity includes an extra check using the "extcodesize" opcode when performing external calls. This ensures that the contract that is about to be called either actually exists (it contains code) or an exception is raised.

The low-level calls which operate on addresses rather than contract instances (i.e. .call(), .delegatecall(), .staticcall(), .send() and .transfer()) do not include this check, which makes them cheaper in terms of gas but also less safe.

0

I just want to add on other individual answers by sharing practical code that allows you to exactly see the differences using the concept of concatenation. The outcome of each function that uses both of the methods gives different results.

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract concatenation {
    string public a;
    string public b;
    function ExampleFunction () external {
        a = string(abi.encode("hello", " world"));
        b = string(abi.encodePacked("hello", " world"));
    }

    // a "Failed to decode output"
    // b string: hello world

   function ExampleFunction2 () external {
        bool y=true;
        a = string(abi.encode(y, " world"));
        b = string(abi.encodePacked(y, " world"));
    }

    // a string: []@[] word
    // b string: [] world

    function ExampleFunction3 () external {
        bool y=true;
        uint256 nonce=123;
        a = string(abi.encode(y, nonce));
        b = string(abi.encodePacked(y, nonce));
    }

    // a string: []{
    // b string: []{
}

This helps me see the differences more clearly and thought of sharing it here.

The main idea is padding. abi.encode uses padding while abi.encodePacked does not. Therefore, this could be seen from above as the first function "ExampleFunction" was unable to stringify an abi encoded string. As you may see here.

Unless there is a compelling reason, abi.encode should be preferred.

2
  • pastebin.com/pn8hG4U4
    – srghma
    Mar 15, 2022 at 18:05
  • Are you sure? When I ran the first a = string(abi.encode("hello", " world")); in chisel REPL, it ran fine for me. I got @�hello world
    – sayandcode
    Oct 1, 2023 at 12:48
0

abi.encodeWithSignature(string memory signature, ...) returns (bytes memory) is equivalent to abi.encodeWithSelector(bytes4(keccak256(bytes(signature)), ...)

Your Answer

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

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