Solidity – How to Decode Call Data Effectively

abicalldatadecodingfunction-selectorsolidity

I have data in memory that is used for a function call, so I guess that makes it call data rather than a calldata (data location). This call data, say bytes memory data, consists of a selector and the encoding of the arguments. I would like to decode the data to retrieve its arguments. It does not seem to be possible.

For instance if data was encoded in this way:

string memory helloString = "hello";
bytes memory data = abi.encodeWithSelector(0x12345678, helloString);

how would I be able to retrieve the string "hello"? There is no abi.decodeWithSelector, or is there? For my use-case it would even be sufficient to just shift the entire data by 8 bytes to the left, but then shl(n,data) (assembly) doesn't work for reference types.

Best Answer

If your data was in calldata (data location) you could simply use slices. Unfortunately, slices are not available on memory arrays.

how would I be able to retrieve the string "hello"? There is no abi.decodeWithSelector, or is there? For my use-case it would even be sufficient to just shift the entire data by 8 bytes to the left, but then shl(n,data) (assembly) doesn't work for reference types.

shl doesn't care about reference or value types, it just takes a value and shifts it. Your issue is that you didn't gave it the right value : data is a memory pointer, while you want to shift the actual data contained there.

Now you will need to do a bit more than just shifting if your data is more than 32 bytes long (which is always true with dynamic types such as string due to the way abi encoding works).

To do so, you could use the following function, which works regardless of length, type or number of arguments :

function extractCalldata(bytes memory calldataWithSelector) internal pure returns (bytes memory) {
        bytes memory calldataWithoutSelector;

        require(calldataWithSelector.length >= 4);

        assembly {
            let totalLength := mload(calldataWithSelector)
            let targetLength := sub(totalLength, 4)
            calldataWithoutSelector := mload(0x40)
            
            // Set the length of callDataWithoutSelector (initial length - 4)
            mstore(calldataWithoutSelector, targetLength)

            // Mark the memory space taken for callDataWithoutSelector as allocated
            mstore(0x40, add(calldataWithoutSelector, add(0x20, targetLength)))


            // Process first 32 bytes (we only take the last 28 bytes)
            mstore(add(calldataWithoutSelector, 0x20), shl(0x20, mload(add(calldataWithSelector, 0x20))))

            // Process all other data by chunks of 32 bytes
            for { let i := 0x1C } lt(i, targetLength) { i := add(i, 0x20) } {
                mstore(add(add(calldataWithoutSelector, 0x20), i), mload(add(add(calldataWithSelector, 0x20), add(i, 0x04))))
            }
        }

        return calldataWithoutSelector;
    }

And call it to extract the part of interest :

function example() public pure returns (string memory) {
        string memory helloString = "hello";
        bytes memory dataWithSelector = abi.encodeWithSelector(0x12345678, helloString);
        return abi.decode(extractCalldata(dataWithSelector), (string));
    }

Which outputs hello as expected.

I hope this answers your question.