1

This post highlights how to use CREATE2 to implement smart contract state using contract data. However, the author also says: "if storage doesn’t need to change, you can do away with metamorphic contracts entirely and just deploy static contracts with runtime code that contains the necessary data". How would this be done in Solidity/assembly? (I have a project where I have a lot of immutable data that needs to be accessed on-chain, so logs aren't viable.)

This is how I'm trying to set the data.

    function setData(bytes memory _bytecode) private returns (address) {
        address addr;
        uint256 byteSize; 
        assembly {
            byteSize := mload(_bytecode)
            addr := create(0, add(_bytecode, 0x20), mload(_bytecode))
            if iszero(extcodesize(addr)) {
                revert(0, 0)
            }
        }
        return addr;
    }

Then to retrieve the data, I'm using the block here from the Solidity documentation.

Finally, I'm running this code in the constructor to retrieve the original data:

bytes memory myData = bytes("Hello world!");
address addr = setData(myData);
bytes memory _myData = at(addr);

Alas, setData(myData) returns (Hardhat):

Error: Transaction reverted without a reason

The reversion is happening at revert(0, 0). If I remove this line from the assembly block, I get TransactionExecutionError: Transaction ran out of gas.

7
  • In that case it is better to use the regular CREATE. First create a stub that will deploy a contract and the concatenat the data you want to store.
    – Ismael
    Aug 29 at 1:13
  • Yes, that's what I'm thinking as well. But I'm looking for a block of code. After a dozen hours of trying various approaches, I still can't get it to work. There appears to be some kind of padding at the top of the contract data. If the padding isn't there, the create() call reverts.
    – pandichef
    Aug 29 at 1:58
  • Paste the code in the question if you want help, writing something from scratch takes time.
    – Ismael
    Aug 29 at 2:28
  • That's more than fair. See above.
    – pandichef
    Aug 29 at 2:56
  • You are passing a text as bytecode, and CREATE is expecting the constructor bytecode and when it tries tol execute it it reverts. There you need a construtor bytecode that returns the text. You usually write as assembly opcodes and prefix the data with it.
    – Ismael
    Aug 29 at 4:31
2
+50

This is a wonderful question! The problem you're running into is that in Ethereum the code that you provide to CREATE is executed. We call this code "init code" and it's expected to return the resulting "deployed code". So what you really need is some piece of code that when executed will use the RETURN opcode to return the _bytecode that you provided.

It turns out that you can create a pretty simple bytecode prefix that will accomplish this:

0x600D380380600D6000396000f3

Let's examine how this prefix works:

60 // PUSH1
0D // 13 (push the value 13 to the stack; this prefix has 13 bytes!)
38 // CODESIZE (push the size of this init code to the stack)
03 // SUB (calculate total init code size minus prefix length == _bytecode.length)
80 // DUP (duplicate that number, we need it later)
60 // PUSH1
0D // 13 (push the value 13 to the stack again)
60 // PUSH1
00 // 0 (push the value 0 to the stack)
39 // CODECOPY (copy initcode[13:13+_bytecode.length] into memory at position 0)
60 // PUSH1
00 // 0 (push the value 0 to the stack)
F3 // RETURN (return memory[0:_bytecode.length] == _bytecode!)

Great, so now we understand how this works. How do we use it? You're most of the way there already with your setData function. We just need to add one more line!

function setData(bytes memory _bytecode) private returns (address) {
    address addr;
    uint256 byteSize;
    // Add this line to your code
    _bytecode = abi.encodePacked(bytes13("0x600D380380600D6000396000f3"), _bytecode);
    assembly {
        byteSize := mload(_bytecode)
        addr := create(0, add(_bytecode, 0x20), mload(_bytecode))
        if iszero(extcodesize(addr)) {
            revert(0, 0)
        }
    }
    return addr;
}

You can likely find ways to optimize the prefix I gave you, but I doubt that's really necessary. I hope this helps!

5
  • Amazing answer! Thank you! How would you adapt this code to implement the CREATE2 version discussed in the article? Or would you ever both bother because you can just apply the CREATE version at a new address any time you want to mutate the data?
    – pandichef
    Sep 4 at 4:11
  • Do you have a specific reason why you'd want to use CREATE2?
    – K. Fichter
    Sep 4 at 19:05
  • Not really:) I'm just trying to solve for the most optimal way to store data on layer 1. And what do you think of the at() function in the Solidity docs? It seems to consume a lot more gas than the article suggests. I clocked it at 3,000 gas.
    – pandichef
    Sep 4 at 20:19
  • "Optimal" can mean a lot of different things depending on the exact contract you're trying to deploy. Generally speaking, I would attempt to find ways to avoid storing significant amounts of data first. The CREATE2 thing is very non-standard and will likely give you a huge headache if you aren't confident with Solidity. Write the contracts in a simple way first and optimize later.
    – K. Fichter
    Sep 4 at 21:51
  • 1
    EXTCODECOPY costs 700 gas + 3 gas per word (32 bytes) copied. Your total cost will depend on the amount of data you need to load from the external contract.
    – K. Fichter
    Sep 4 at 21:52

Your Answer

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

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