1

What's the proper way to copy a string (or bytes) from memory to storage? When copied directly into a state variable lvalue (or into a mapping), there's no problem, but when assigning a string memory rvalue into a string storage lvalue, there's a compiler error.

The Solidity documentation suggests that it ought to be possible to assign a memory reference to a storage reference and that the behavior is that the pointed-to value in memory is copied into the pointed-to location in storage. Why is it different when the lvalue is a reference instead of a state variable or mapping?

I suppose I could code this myself in Yul, but I'd prefer to avoid re-implementing the rules regarding the handling of short string/bytes in storage.

pragma solidity ^0.8.15;

contract Works {
  string public thing;

  function setThing(string memory newValue) public {
    thing = newValue;
  }
}

contract AlsoWorks {
  mapping(bytes32 => string) public things;

  function setThing(bytes32 key, string memory newValue) public {
    things[key] = newValue;
  }
}

contract Fails {
  string public thing;

  function setThing(string memory newValue) public {
    string storage thingLocal = thing;
    thingLocal = newValue;
  }
}

What I'm actually trying to do here is to store a string at a slot that is impossible to accidentally clobber so that it is preserved across contract upgrades. A more concrete example:

pragma solidity ^0.8.15;

abstract contract MagicSlot {
  uint256 internal constant _MAGIC_SLOT = 0xcafef00dcafef00dcafef00dcafef00dcafef00dcafef00dcafef00dcafef00d;

  function _magic() internal pure returns (string storage result) {
    assembly ("memory-safe") {
      result.slot := _MAGIC_SLOT
    }
  }

  function magic() external view returns (string memory) {
    return _magic();
  }
}

contract Fails is MagicSlot {
  function setMagic(string memory newValue) public {
    // TypeError: Expression has to be an lvalue.
    // TypeError: Type string memory is not implicitly convertible to expected type string storage pointer.
    _magic() = newValue;
  }
}

contract AlsoFails is MagicSlot {
  function setMagic(string memory newValue) public {
    string storage magicRef = _magic();
    // TypeError: Type string memory is not implicitly convertible to expected type string storage pointer.
    magicRef = newValue;
  }
}
5
  • Not exactly sure as to why there is that behavior... but if you need, I already implemented that logic in yul handling short and long strings here : ethereum.stackexchange.com/a/126284/84305
    – hroussille
    Jun 20 at 16:08
  • Thanks. I see 2 quick improvements you could use in your code. For one, you have a bug: 0x19 + 1 != 0x20. The constant 0x19 should be 0x1f. And for the other, you can use shl instead of mul to save 2 gas on each operation. I'll take a closer look later when I'm in front of my main machine Jun 20 at 16:43
  • nice catch for the 0x19 you are right, I edited my answer. However, for shl / shr vs mul it's the other way around, mul is 5 gas while shr / shl are 3 each.
    – hroussille
    Jun 20 at 17:18
  • Yes, mul is 5 gas and shl/shr are 3. Therefore, instead of mul(x, 2) you should write shl(1, x). Likewise, instead of div(x, 2), you should write shr(1, x). This saves 2 gas per operation. Jun 20 at 19:17
  • Ah okay I understood it the other way around. I'll leave it as it is for readability, feel free to change if you end up using the code.
    – hroussille
    Jun 20 at 19:44

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.