2

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)
        }
    }

}

1 Answer 1

4

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.

5
  • my man. Good stuff! Is it possible to open a chat? i'd like to make a proposal. Commented Apr 8, 2022 at 16:11
  • Sure, my Linkedin is present on my profile, send me a message on it and we'll get in touch.
    – hroussille
    Commented Apr 8, 2022 at 16:41
  • And in line // Clear the last 16 bytes (level) w := and(w, not(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) I guess you accidentally asumed that amount comes before level ? Or am I missing something? Since clearing the last bits of the slot would mean clearing the second value in the slot. Commented Apr 8, 2022 at 17:50
  • 1
    those last should be first yes. The code is fine, it's just the comment that is misleading I'll edit that.
    – hroussille
    Commented Apr 8, 2022 at 18:41
  • So when clearing first 16 bits you use and(w, not(0x...) and then you write them using or(w, and(newValue, 0x..) . And so, to then get the last 16 bits, using the examples from reading storage, it would then be shr(not(128),w) and then write using or(w, shr(128,newValue)), which doesn't work. And also, could I just write them at the same time to save gas? Commented Apr 9, 2022 at 10:32

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.