Solidity – How to Retrieve Data After mstore in Yul?

assemblysolidityyul

function here:

function encodeAmount(
    uint256 amount,
    bytes memory data
) public pure returns(bytes memory){
    bytes memory amountToEncode = abi.encode(amount);
    assembly {
        mstore(add(add(data, 32), 68), mload(add(amountToEncode, 32)))
    }
    return data;
}

amount: 1000000

data: 0xe5b07cdb0000000000000000000000003416cf6c708da44db2624d63ea0aaef7113527c60000000000000000000000000000000000000000000000000000000000000001

The fn should change the last 32bytes in data, but the function returns the data unchanged.
So how to get the actually changed data?

Best Answer

The fn should change the last 32bytes in data, but the function returns the data unchanged. So how to get the actually changed data ?

There is a depper issue with your code. The provided data seems composed of :

  • 4 bytes function selector : e5b07cdb
  • 32 bytes address : 0000000000000000000000003416cf6c708da44db2624d63ea0aaef7113527c6
  • 32 bytes value : 0000000000000000000000000000000000000000000000000000000000000001

For a total length of 68 bytes.

Now on this line :

mstore(add(add(data, 32), 68), mload(add(amountToEncode, 32)))

You effectively skip the length field with add(data, 32) but then you write the amount after the provided data without changing the length field or updating the free memory pointer. Additionally, this is not necessary :

bytes memory amountToEncode = abi.encode(amount);

Since amount is already a 32 bytes value (uint256) and abi.encoding it just yields a 32 bytes byte array with the very same value. You should also make sure that the data is formatted the way you expect it, that is 68 bytes long. Let me know if you are looking for a more generic approach, able to deal with byte arrays of variable length.

Since it's unclear what exactly you want to do between add data to the existing one or overwrite the last 32 bytes, here's what you need to do in both cases :


Adding data to the existing byte array (use the 2nd version)

This version is flawed, do not use it. I left it only because I believe it can be informative.

function encodeAmount(uint256 amount, bytes memory data) public pure returns(bytes memory) {

        require(data.length == 68, "Expected 68 bytes of data");

        assembly {
            // No need to abi encode amount. It's already a 32 bytes values (uint256).
            // Just write it directly after the existing data
            mstore(add(add(data, 32), 68), amount)

            // Increase the length of the byte array by 32 to account for the added data
            mstore(data, add(mload(data), 32))

            // Set the free memory pointer to data + 32 + data.length to ensure that
            // subsequent allocations do not overwrite what we just added.
            mstore(0x40, add(data, add(mload(data), 32)))
        }
        return data;
    }

Which yields 0xe5b07cdb0000000000000000000000003416cf6c708da44db2624d63ea0aaef7113527c6000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000f4240

  • e5b07cdb (function selector)
  • 0000000000000000000000003416cf6c708da44db2624d63ea0aaef7113527c6 (address)
  • 0000000000000000000000000000000000000000000000000000000000000001 (32 bytes value)
  • 00000000000000000000000000000000000000000000000000000000000f4240 (added 32 bytes value : 1000000)

EDIT: I figured that this version has a flaw if data is not the result of the last memory allocation. mstore(0x40, add(data, add(mload(data), 32))) may take the free memory pointer back and leave the memory in an inconsistent state.

This example that illustrate the flaw :

contract Test {

    struct Wrapper {
        uint256 value;
    }

    function edgeCase(uint256 amount, bytes memory data) public pure returns (bytes memory, Wrapper memory walue) {

        // Assuming data starts at offset 0 :
        // data : [0 : 32 + 68] = 100 bytes
        // Free memory starts at 100

        // wrapper is 32 bytes long allocated at the free memory pointer (100)
        // Free memory now starts at 132
        Wrapper memory wrapper;

        wrapper.value = 0;

        // This will take the free memory pointer back to :
        // data (0) + 32 (length) + data.length (68) + 32 (amount) = 132
        // leaving wrapper in an inconsistent state as it is
        // it overwritten by the byte array returned by encodeAmount.
        // Both wrapper and the encoded byte array use the same memory space.
        // With more than 32 bytes allocated between data and the call
        // the free memory pointer will be decremented.
        return (encodeAmount(amount, data), wrapper);
    }


    function encodeAmount(uint256 amount, bytes memory data) public pure returns(bytes memory) {

        require(data.length == 68, "Expected 68 bytes of data");

        assembly {
            // No need to abi encode amount. It's already a 32 bytes values (uint256).
            // Just write it directly after the existing data
            mstore(add(add(data, 32), 68), amount)

            // Increase the length of the byte array by 32 to account for the added data
            mstore(data, add(mload(data), 32))

            // Set the free memory pointer to data + 32 + data.length to ensure that
            // subsequent allocations do not overwrite what we just added.
            mstore(0x40, add(data, add(mload(data), 32)))
        }
        return data;
    }
}

It's therefore better to use the following version that creates a new byte array and ensure memory consistency for previously allocated data.

I strongly advise using that version and not the previous one due to the flaw I mentioned before.

function encodeAmount(uint256 amount, bytes memory data) public pure returns(bytes memory encoded) {

    require(data.length == 68, "Expected 68 bytes of data");

        assembly {

            // Take a free memory space for the new array
            encoded := mload(0x40)

            // Set the size of the new array
            mstore(encoded, add(mload(data), 32))

            // Set the content of the new array by blocks of 32 bytes
            for {let i:= 0} lt(i, mload(data)) {i := add(i, 32)} {
                mstore(add(add(encoded, 32), i), mload(add(add(data, 32), i)))
            }

            // No need to abi encode amount. It's already a 32 bytes values (uint256).
            // Just write it directly after the existing data
            mstore(add(add(encoded, 32), 68), amount)

            // Update the free memory pointer
            mstore(0x40, add(encoded, add(mload(encoded), 32)))
        }

    return encoded;
}

Overwriting the last 32 bytes of the byte array

function overwriteAmount(uint256 amount, bytes memory data) public pure returns(bytes memory) {

        require(data.length == 68, "Expected 68 bytes of data");

        assembly {
            // No need to abi encode amount. It's already a 32 bytes values (uint256).
            // Just write it directly over the last 32 bytes of data
            mstore(add(add(data, 32), 36), amount)
        }
        return data;
    }

There is no need to update the length or the free memory pointer in that case since we did not change anything to the memory layout, only the memory content of already allocated memory space.

Which yields 0xe5b07cdb0000000000000000000000003416cf6c708da44db2624d63ea0aaef7113527c600000000000000000000000000000000000000000000000000000000000f4240

  • e5b07cdb (function selector)
  • 0000000000000000000000003416cf6c708da44db2624d63ea0aaef7113527c6 (address)
  • 00000000000000000000000000000000000000000000000000000000000f4240 (32 bytes value : 1000000)

I hope that answers your question.

Related Topic