How To Read Mappings and Dynamic Arrays Directly From Storage Using Foundry
by 0xZorz
1. Reading Private Maps
contract MyContract {
mapping(address => uint256) private balances;
function addEntry(address key, uint256 value) public {
balances[key] = value;
}
}
A straightforward contract, with one problem. A private variable that we can’t easily read.
Foundry gives us tools like vm.load(x,y)
that allow us to read arbitrary storage slots. This approach can only be used in tools like Foundry and you can’t read directly from another contract’s storage in a deployed solidity contract (otherwise what’s the point of private variables!).
Here’s an example Foundry script that does this:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import "forge-std/console2.sol";
// forge script script/misc/Maps.s.sol:MyScript -vvvv
contract MyContract {
mapping(address => uint256) private balances;
function addEntry(address key, uint256 value) public {
balances[key] = value;
}
}
contract MyScript is Script {
function getMappingValue(address targetContract, uint256 mapSlot, address key) public view returns (uint256) {
bytes32 slotValue = vm.load(targetContract, keccak256(abi.encode(key, mapSlot)));
return uint256(slotValue);
}
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
MyContract s = new MyContract();
s.addEntry(address(0x3095171469a0db24D9Fb9C789D62dF22BBAfa816), 33);
s.addEntry(address(0x3496306146f9E6fE655443f2fBc9b21d74d10C66), 39);
s.addEntry(address(0x5348cA940E6927A6657874995023e849Fa00e8c1), 45);
uint256 entry0 = getMappingValue(address(s), 0, address(0x3095171469a0db24D9Fb9C789D62dF22BBAfa816));
uint256 entry1 = getMappingValue(address(s), 0, address(0x3496306146f9E6fE655443f2fBc9b21d74d10C66));
console2.log("entry0: ", entry0);
console2.log("entry1: ", entry1);
vm.stopBroadcast();
}
}
2. Reading Private Dynamic Arrays
contract MyContract {
uint256[] private dynamicArray;
function addElement(uint256 element) public {
dynamicArray.push(element);
}
}
Another contract, this time with a dynamic array, but one we also can’t easily
We know that the array dynamicArray
should start from storage slot 0, but how do we get the full contents of the array? The solidity docs explain it here but let’s see how you can do it in Foundry (the full script can be found at the bottom).
function getFullArray(uint256 startSlot, address targetContract) public view returns (uint256[] memory) {
// Load the start slot from storage
bytes32 slotVal = vm.load(targetContract, bytes32(startSlot));
// The first slot contains the size of the array
uint256 arrayLength = uint256(slotVal);
// Initialize an array in memory to hold the values we retrieve from storage
uint256[] memory fullArr = new uint256[](arrayLength);
// Calculate the start of the contiguous section in storage containing the array contents
bytes32 startSlot = keccak256(abi.encodePacked(startSlot));
// Iterate through the slots containing the array contents and store them in memory;
for (uint256 i = 0; i < arrayLength; i++) {
slotVal = vm.load(targetContract, bytes32(uint256(startSlot) + i));
fullArr[i] = uint256(slotVal);
}
return fullArr;
}
We can see from the comments in the function, that the logic to iterate through the slots of a dynamic array is pretty simple if we know the starting slot. In this case, the starting slot is 0, but it could be any other value as well. We could also modify the logic slightly if we wanted to replace a uint256
array with another type that could have multiple values packed into a single slot. If that were the case, this is how it would look:
function getFullArray(uint256 startSlot, address targetContract) public view returns (uint128[] memory) {
// A single 32 byte slot can hold 2 uint128s
uint256 ITEMS_PER_SLOT = 2;
// Load the start slot from storage
bytes32 slotVal = vm.load(targetContract, bytes32(startSlot));
// The first slot contains the size of the array
uint256 arrayLength = uint256(slotVal);
// We can calculate how many slots have been used using ITEMS_PER_SLOT and arrayLength
// This is idiomatic for divison but rounding up: https://stackoverflow.com/a/2422722
uint256 slotsUsed = (arrayLength + (ITEMS_PER_SLOT - 1)) / ITEMS_PER_SLOT;
// Calculate the start of the contiguous section in storage containing the array contents
bytes32 startSlot = keccak256(abi.encodePacked(startSlot));
// Initialize an array in memory to hold the values we retrieve from storage
// We also maintain an index where we've written up to in fullArr
uint128[] memory fullArr = new uint128[](arrayLength);
uint256 fullArrIndex = 0;
// Iterate through the slots containing the array contents and store them in memory;
for (uint256 i = 0; i < slotsUsed; i++) {
slotVal = vm.load(targetContract, bytes32(uint256(startSlot) + i));
// This implementation is hardcoded to expect maximum of two entries per slot
fullArr[fullArrIndex] = uint128(uint256(slotVal));
fullArrIndex++;
if (fullArrIndex >= arrayLength) {
break;
}
// We use bitfshifting to obtain the element we require from a slot of interest
fullArr[fullArrIndex] = uint128(uint256(slotVal) >> 128);
fullArrIndex++;
}
return fullArr;
}
It’s essentially the same as the first script except we have multiple values per slot and we retrieve the value of interest from a slot using bit shifting.
If you found this useful and want to support: zorz.eth
The full script is below:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import "forge-std/console2.sol";
// forge script script/misc/DynamicArray.s.sol:MyScript -vvvv
contract MyContract {
uint256[] private dynamicArray;
function addElement(uint256 element) public {
dynamicArray.push(element);
}
}
contract MyScript is Script {
function getFullArray(uint256 startSlot, address targetContract) public view returns (uint256[] memory) {
// Load the start slot from storage
bytes32 slotVal = vm.load(targetContract, bytes32(startSlot));
// The first slot contains the size of the array
uint256 arrayLength = uint256(slotVal);
// Initialize an array in memory to hold the values we retrieve from storage
uint256[] memory fullArr = new uint256[](arrayLength);
// Calculate the start of the contiguous section in storage containing the array contents
bytes32 startSlot = keccak256(abi.encodePacked(startSlot));
// Iterate through the slots containing the array contents and store them in memory;
for (uint256 i = 0; i < arrayLength; i++) {
slotVal = vm.load(targetContract, bytes32(uint256(startSlot) + i));
fullArr[i] = uint256(slotVal);
}
return fullArr;
}
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
MyContract s = new MyContract();
s.addElement(53);
s.addElement(54);
s.addElement(55);
uint256[] memory a = getFullArray(0, address(s));
for (uint256 i = 0; i < a.length; i++) {
console2.log(a[i]);
}
vm.stopBroadcast();
}
}