3

My understanding of abi.encode vs. abi.encodePacked is that the latter just uses a more compressed representation of the input parameters. I thought that abi.encode was the default encoding used when calling other contracts so abi.encodePacked should only be used for serializing some parameters and not to call a function.

However, I found this code using abi.encodePacked in a call:

  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }

Does this work only because timezone1Library was compiled to understand abi.encodePacked? Is this default in newer versions of the Solidity compiler?

1 Answer 1

4

abi.encodePacked is a non standard encoding that should preferably not be used due to its ambiguity with dynamic types, if you are not dealing with dynamic types and inputs which size is already multiple of 32 then they can both give the same output.

I suppose you took that piece of code from Ethernaut level 16. Preservation. Where setTimeSignature is defined as such :

bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

This is not a dynamic type, it's a constant fixed size array of bytes8, however the standard encoding specification says :

Note that for any X, len(enc(X)) is a multiple of 32

Leading to 28 bytes of padding after the 4 bytes which is not what the abi expects. You can check for yourself that those produce different outputs :

function encodePacked(uint256 time) public view returns (bytes memory) {
    return abi.encodePacked(setTimeSignature, time);
}

// 0x3beb26c40000000000000000000000000000000000000000000000000000000000000000 (4 + 32 bytes)

function encode(uint256 time) public view returns (bytes memory) {
    return abi.encode(setTimeSignature, time);
}

// 0x3beb26c4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 (32 + 32 bytes)

The first 4 bytes of calldata should be the function selector immediately followed by the encoded parameters (with standard encoding). This is why they are using abi.encodePacked to encode the selector in place, taking only exactly 4 bytes. The encoding of a uint is the same accross abi.encode and abi.encodePacked.

All those are equivalent because uint256 requires no padding:

function encodePacked(uint256 time) public view returns (bytes memory) {
    return abi.encodePacked(setTimeSignature, time);
}

function encodeHybrid(uint256 time) public view returns (bytes memory) {
    return abi.encodePacked(abi.encodePacked(setTimeSignature), abi.encode(time));
}

function encodeHybrid2(uint256 time) public view returns (bytes memory) {
    return abi.encodePacked(abi.encodePacked(setTimeSignature), abi.encodePacked(time));
}

Does this work only because timezone1Library was compiled to understand abi.encodePacked? Is this default in newer versions of the Solidity compiler?

It works only because it is correct in producing what the abi expects as calldata parameters : 4 bytes of function selector followed by the abi.encoded parameters. It does so by relying on a special case though.. This syntax is not always correct (dynamic types or padding required according to the standard encoding will lead to a wrong output).

You can also use a more readable syntax such as :

function encodeWithSignature(uint256 time) public view returns (bytes memory) {
    return abi.encodeWithSignature("setTime(uint256)", time);
}

which does exactly the same as the functions above or your initial example but in a more readable way imo, plus it always produces a correctly encoded output for the parameters.

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.