1

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?

1 Answer 1

1

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.

1
  • Thanks! I encountered this issue during a work in progress when I hadn't updated the free memory pointer yet, and I just realized this it is likely that Solidity uses it when returning... Nov 30, 2021 at 17:21

Your Answer

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

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