3

I have a simple contract that just sets a string private state variable in contract's slot 1.

Below, I write a test that is supposed to set that private string variable to a new value and then I go in and read it.

function test_non_owner_can_view_password() public {
    // owner sets a new password
    string memory newPassword = "Nicenew#pwd123";
    vm.prank(address(owner));
    passwordStore.setPassword(newPassword);

    // non-owner can see what the new password is
    vm.startPrank(address(1));
    bytes32 value = vm.load(address(passwordStore), bytes32(uint256(1)));
    string memory pwd = string(abi.encodePacked(value));
    console.log(pwd); // we can see the password here
    assertEq(abi.encodePacked(newPassword), abi.encodePacked(pwd));
}

if I run the test with -vvvv, I can clearily see why its failing. The byte arrays are not same.

├─ emit log_named_bytes(key:       Left, val: 0x4e6963656e657723707764313233)
├─ emit log_named_bytes(key:      Right, val: 0x4e6963656e65772370776431323300000000000000000000000000000000001c)

If I convert to string, it still fails, even though the results appear to be the same.

Assert with:

assertEq(
    string(abi.encodePacked(newPassword)),
    string(abi.encodePacked(pwd))
);

Results in

├─ emit log(: Error: a == b not satisfied [string])
├─ emit log_named_string(key:       Left, val: Nicenew#pwd123)
├─ emit log_named_string(key:      Right, val: Nicenew#pwd123)

The extra characters that are returned from vm.load() (i.e. 1c) correspond to the number of hex digits in the length of the string but because they are there, how can i get these values to be equal? For example, if I bytes32(abi.encodePacked("Nicenew#pwd123")), I will get 0x4e6963656e657723707764313233000000000000000000000000000000000000 which still is not equal to 0x4e6963656e65772370776431323300000000000000000000000000000000001c.

How can I get around this?

2 Answers 2

1

When you use vm.load to view the 1st storage slot, you are getting the entire slot returned, for strings 31 bytes and less, its stored alongside its length in the storage slot where the variable is declared.

If you just want to remove the extra zeros and the hex digits that store the length of the password string, you can modify your test function like this:

function test_non_owner_can_view_password() public {
    // owner sets a new password
    string memory newPassword = "Nicenew#pwd123";
    vm.prank(address(owner));
    passwordStore.setPassword(newPassword);

    // non-owner can see what the new password is
    vm.startPrank(address(1));
    bytes32 value = vm.load(address(passwordStore), bytes32(uint256(1)));
    string memory pwd = string(abi.encodePacked(value));
    console.log(pwd); // we can see the password here
    // convert `pwd` string into bytes and remove unnecessary hex digits
    bytes memory inputBytes = bytes(pwd);
    bytes memory result = new bytes(inputBytes.length - 18);
    for (uint256 i = 0; i < inputBytes.length - 18; i++) {
            result[i] = inputBytes[i];
        }
    // removed: 00000000000000000000000000000000001c ~ 18 bytes

    assertEq(abi.encodePacked("Nicenew#pwd123"), abi.encodePacked(result));
    }

To put this into perspective, if you set the password to a string longer than 31 bytes, you'll have to take the keccak256 hash of the initial storage slot to view the string's actual data:

function test_non_owner_can_view_password() public {
    // owner sets a new password
    string memory newPassword = "LongPasswordLongPasswordLongPasswordLongPasswordLongPasswordLongPasswordLongPasswordLongPasswordLongPasswordLongPassword";
    vm.prank(address(owner));
    passwordStore.setPassword(newPassword);

    // non-owner can see what the new password is
    vm.startPrank(address(1));

console.logBytes(abi.encodePacked("LongPasswordLongPasswordLongPasswordLongPasswordLongPasswordLongPasswordLongPasswordLongPasswordLongPassword"));

    bytes32 value = vm.load(address(passwordStore), bytes32(uint256(1)));
    console.log("value:");
    bytes32 valuesValue = vm.load(address(passwordStore), bytes32(uint256(keccak256(abi.encodePacked(uint(1))))));
    bytes32 valuesValue2 = vm.load(address(passwordStore), bytes32(uint256(keccak256(abi.encodePacked(uint(1)))) + 1));
    bytes32 valuesValue3 = vm.load(address(passwordStore), bytes32(uint256(keccak256(abi.encodePacked(uint(1)))) + 2));
    bytes32 valuesValue4 = vm.load(address(passwordStore), bytes32(uint256(keccak256(abi.encodePacked(uint(1)))) + 3));
    
    console.logBytes32(value);
    console.log("valuesValue:");
    console.logBytes32(valuesValue);
    console.logBytes32(valuesValue2);
    console.logBytes32(valuesValue3);
    console.logBytes32(valuesValue4);
    }

This password was so long, it took up 4 seperate storage slots for its data alone.

enter image description here

4
  • 1
    Thank you! The way I ended up fixing this shortly after posting the question was to cast the bytes32 into bytes31 effectively cutting off the last byte. Then I did abi.encodePacked() with that new bytes31 byte array and a new bytes(1). Thank you for the long password implementation too.
    – SKA
    Commented Oct 22, 2023 at 15:26
  • 1
    @SKA That's smart, I'll add your solution to my answer body as an additional fix for anyone who encounters this issue in the future!
    – Rohan Nero
    Commented Oct 22, 2023 at 16:31
  • @SKA Can you share your lines of code that takes off the extra zeros with new bytes(1)? I'm a little confused about what you did after casting the initial value to bytes31.
    – Rohan Nero
    Commented Oct 22, 2023 at 19:16
  • 1
    Like this: bytes31 cutData = bytes31(abi.encodePacked(data)); and then this bytes32 dataWithoutLength = bytes32(abi.encodePacked(cutData, new bytes(1)));
    – SKA
    Commented Oct 22, 2023 at 19:57
1

The packing of the string length in the same storage slot as the string data is intentional per the design of Solidity. From the Solidity docs:

... for short values (shorter than 32 bytes) the array elements are stored together with the length in the same slot.

In particular: if the data is at most 31 bytes long, the elements are stored in the higher-order bytes (left aligned) and the lowest-order byte stores the value length * 2.

Instead of using vm.load to get the raw storage value for assertEq, you could implement a getter function to have Solidity return the proper string value. For example, implement a passwordStore.getPassword function which returns the variable, OR set the variable within passwordStore to be public.

If you must use storage slots, you can follow the rules described in the docs to distinguish short arrays and long arrays:

For byte arrays that store data which is 32 or more bytes long, the main slot p stores length * 2 + 1 and the data is stored as usual in keccak256(p). This means that you can distinguish a short array from a long array by checking if the lowest bit is set: short (not set) and long (set).

Then implement additional logic to properly parse the data from there.

1
  • Thank you! This was just an exercise in which I had to use a private string AND the getter function has access control on it.
    – SKA
    Commented Oct 22, 2023 at 15:27

Your Answer

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

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