Storage Assembly Yul – How to Use sstore Struct within Mapping in Inline Assembly

assemblystorageyul

I'm trying to read from and write to a mapping of structs using inline assembly.

getValues() is an example of how I'm able to read something from storage. I can read the first two values using and() and shr(). But how do I read the next values within struct?

The code in writeTo() is able to write to a mapping(uint => uint). But I cant figure out how to write to a slot in a struct within a mapping(uint => Struct).

What values should be hashed for sstore() to write to correct location? (whatever location that might be, but I assume getting the answer to how to jump to next slot within the struct using shr()(?) might answer that question)

// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;


contract Assemble2{
    
    mapping(uint => Info) infos;

    struct Info{
        uint128 level;
        uint128 amount;

        uint128 num;
        uint128 time;

        address sender;
    }


    constructor(){
    
        infos[0] = Info(
            2,3, 4,5, msg.sender
        );
    }

    function getValues(uint _id) public view returns(uint a, uint b, uint c, uint d, address e){
        Info storage info = infos[_id]; 
        assembly {
            let w := sload(info.slot) 
           
            a := and(w, 0xfff) // gets first value in first slot
            b := shr(128, w) // gets second value in first slot

            //c := shr(?, w) // how to jump to next slot and get first value?
            //d := shr(?, w) // how to jump to next slot and get second value?

            //e := shr(?, w) // how to jump to third slot and get first value?
        }
    }


    function writeTo(uint _id) public {
        Info storage info = infos[_id];

        uint newLevel = 10;
        assembly {
            mstore(0, newLevel)
            mstore(32, info.slot)
            // how to include position within struct?

            // do I somehow hash the sload with the pointer to the position?
            // let w := sload(info.slot)
            // b := shr(128, w)

            let hash := keccak256(0,64)

            sstore(hash,2)
        }
    }

}

Best Answer

getValues() is an example of how I'm able to read something from storage. I can read the first two values using and() and shr(). But how do I read the next values within struct?

The storage layout of structs is quite simple, if the stuct data doesn't fit in one slot then it will simply use the next one(s) too.

So assuming the struct storage slot is 0, the layout will be the following :

  • slot 0 : [amount (0x10) - level (0x0)]
  • slot 1 : [time (0x10) - num (0x0)]
  • slot 2 : [sender (0x0)]

Now keep in mind that uint128 take 16 bytes of memory, and address takes 20.

With :

a := and(w, 0xfff) // gets first value in first slot

You are only getting the first 12 bits (1.5 byte) of the level member but level is a 128 bit (16 bytes) value.

So accounting for both the storage layout of structs and the real size of the members you are trying to read, a better getValues implementation could be :

function getValues(uint _id) public view returns(uint a, uint b, uint c, uint d, address e){
    Info storage info = infos[_id]; 
    assembly {
        // Load info.slot 
        let w := sload(info.slot) 
       
        // Get the first 16 bytes of sload(info.slot) : level
        a := and(w, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)

        // Shift sload(info.slot) 16 bytes right : amount
        // Values in assembly are unsigned int by default, shifting right pads with 0s
        // So, no need to apply the mask here but you could if you wanted to
        b := shr(128, w)

        // Load info.slot + 1
        w:= sload(add(info.slot, 1))

        // Get the first 16 bytes of sload(info.slot + 1) : num
        c := and(w, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)

        // Shift sload(info.slot + 1) 16 bytes right : time
        d := shr(128, w)

        // Load info.slot + 2 and take the first 20 bytes : sender
        e := and(sload(add(info.slot, 2)), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
    }
}

The code in writeTo() is able to write to a mapping(uint => uint). But I cant figure out how to write to a slot in a struct within a mapping(uint => Struct).

We'll just use the same ideas for that one by being carefull not to change anything else than the level field like so :

function writeTo(uint _id) public {
        Info storage info = infos[_id];

        uint newLevel = 10;
        assembly {

            // Load info.slot : [amount, level]
            let w := sload(info.slot)

            // Clear the first 16 bytes (level)
            w := and(w, not(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))

            // Write the first 16 bytes with the first 16 bytes of newLevel
            w := or(w, and(newLevel, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))

            // Update the storage value
            sstore(info.slot, w)
        }
    }

If some bitwise operations are a bit confusing, I always end up on this great answer describing them.

What values should be hashed for sstore() to write to correct location? (whatever location that might be, but I assume getting the answer to how to jump to next slot within the struct using shr()(?) might answer that question)

Since you were already getting the storage slot from info.slot there is no need to hash anything, the hash operation is the way to compute the target slot but in your case you don't need to since it's already available...

But the storage slot of a mapping element is described here, keccak256(h(k) . p) where h(k) is just the key in your case that is _id and p is the storage slot of the mapping itself : infos.slot (infoS not info, we are targeting the mapping here).

Those 2 functions are therefore perfectly equivalent :

function getStorageSlotAssembly(uint _id) public view returns (bytes32 slot) {
       assembly {
           mstore(0x00, _id)
           mstore(0x20, infos.slot)
           slot := keccak256(0, 0x40)
       }
   }

function getStorageSlotSolidity(uint _id) public view returns (bytes32 slot) {
         Info storage info = infos[_id];

         assembly {
             slot := info.slot
         }
   }

Just make sure to read the doc thoroughly as h(k) might actually do something if you were to use different a key type.

I hope that answers your question.