Solidity Yul: Debugging Weird Behavior of uint32 Memory Arrays

arraysmemorysolidityyul

There's something I can't understand in the way Solidity handles dynamic-size memory arrays. For instance, let us consider the following contract:

pragma solidity ^0.8.0;                                                                                 
                                                                                                       
contract Test {                                                                                    
    function test() public pure returns (uint len, uint32[] memory) {                                   
        uint32[] memory res = new uint32[](10);                                                         
        assembly {                                                                                      
            len := mload(res)                                                                           
        }                                                                                               
        return (len, res);                                                                              
    }                                                                                                   
                                                                                                    
    function secondTest() public pure returns (uint len, uint32[] memory) {                             
        uint32[] memory res;                                                                         
        assembly {                                                                                      
            res := mload(0x40)                                                                          
            mstore(res, 10)                                                                             
            len := mload(res)                                                                           
        }                                                                                               
        return (len, res);                                                                              
    }                                                                                                
}

I would expect these two functions to be equivalent: declaring a dynamic-size memory array of size 10 and then retrieving its length. However, calling them with web3py using Ganache, here's the results I got:

>>> contract.functions.test().call()
[10, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
>>> contract.functions.secondTest().call()
[10, [64, 10, 64, 10, 64, 10, 64, 10, 64, 10]]

It seems that the array is weirdly initialized in the second case, even though it was allocated from the free-memory pointer without anything having touched said pointer beforehand.

Investigating further, I tried to run the following functions:

function thirdTest() public pure returns (uint len, uint32[] memory) {                              
    uint32[] memory res;                                                                            
    assembly {                                                                                      
        res := mload(0x40)                                                                          
        len := mload(res)                                                                           
    }                                                                                               
    return (len, res);                                                                           
}                                                                                                   
                                                                                                    
function fourthTest() public pure returns (uint32[] memory, uint32 len) {                           
    uint32[] memory res;                                                                            
    assembly {                                                                                      
        res := mload(0x40)                                                                          
        len := mload(res)                                                                           
    }                                                                                               
    return (res, len);                                                                              
}

This yielded the following results:

>>> network.contract.functions.thirdTest().call()
[0, []]
>>> network.contract.functions.fourthTest().call()
[[0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64], 0]

Note that the only difference between thirdTest and fourthTest is the order in which they return, and that the only difference between secondTest and thirdTest is that the former performs an mstore(res, 10) before computing len.

All in all, the only valid implementation is the first one (and the third one for an empty array, or so it seems). Why is that? What is wrong with the other ones, and where do these behaviors come from?

Best Answer

You are not updating the free memory pointer, the following code should show you the steps in a clear way.

function secondTest() public pure returns (uint len, uint32[] memory) {                             
    uint32[] memory res;                                                                         
    assembly {                                                                                      
        res := mload(0x40)

        // Update the free memory pointer
        // Add 0x20 (one word) to account for the length of the array
        // Add one word per element as each of them requires 32bytes (1 word)
        mstore(0x40, add(add(res, 0x20), mul(10, 0x20)))
        // Store the length                                                                          
        mstore(res, 10)
        len := mload(res)                                                                           
    }                                                                                               
    return (len, res);                                                                              
}    

The issue is that 0x40 is the address of the free memory pointer, and you are responsible for it when you create an assembly block and allocate / free memory.

So, res is declared at the free memory pointer : res := mload(0x40) but the first word is reserved for the length of the array (10) so you must increment the memory pointer to account for that : add 0x20 (32) to the free memory pointer.

Then you know that you have 10 elements each taking 32 bytes or 1 word, so you add 10 * 0x20 to the memory pointer.

those 2 steps are combined in :

mstore(0x40, add(add(res, 0x20), mul(10, 0x20)))

Assuming that you don't deal with variable sizes, this operation is constant and could (should) be replaced by:

mstore(0x40, add(res, 0x160))

That way the generated code outside of your assembly block will not overwrite your data in any way as the free memory pointer points to a memory arrea after your array.