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.
The issue is that
0x40
is the address of the free memory pointer, and you are responsible for it when you create anassembly
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 : add0x20
(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 :
Assuming that you don't deal with variable sizes, this operation is constant and could (should) be replaced by:
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.