Repository: vyperlang/vyper

Issue:
Immutable variables can be read before assignment
### Version Information

* vyper Version (output of `vyper --version`): 0.3.6+commit.4a2124d
* OS: osx
* Python Version: 3.9.13
 
### What's your issue about?

Immutable variables can be read before assignment, their memory location during constructor execution is accessed, but that memory is not initialized yet. Its value is not necessarily `empty(type)` since memory could have been written to ephemerally.

This should not compile:

```python
# @version 0.3.6

a:immutable(uint256)
b:uint256

@payable
@external
def __init__(s:address):
    self.b = a
    a = 12
```

A more interesting example where transient memory written during execution of `create_copy_of()` is written into the storage variable `b`:

```python
# @version 0.3.6

a:immutable(uint256)
b:uint256

@payable
@external
def __init__():
    s:address = 0x9D0464996170c6B9e75eED71c68B99dDEDf279e8  #random contract containing code that is copied to memory before redeploying
    c:address = create_copy_of(s)
    self.b = a
    a = 12
```

### How can it be fixed?

check that immutable variables are assigned before usage


Discussion:

Patch:
diff --git a/Makefile b/Makefile
index daa1c2bf..645b800e 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ freeze: clean init
 	echo Generating binary...
 	export OS="$$(uname -s | tr A-Z a-z)" && \
 	export VERSION="$$(PYTHONPATH=. python vyper/cli/vyper_compile.py --version)" && \
-	pyinstaller --clean --onefile vyper/cli/vyper_compile.py --name "vyper.$${VERSION}.$${OS}" --add-data vyper:vyper
+	pyinstaller --target-architecture=universal2 --clean --onefile vyper/cli/vyper_compile.py --name "vyper.$${VERSION}.$${OS}" --add-data vyper:vyper
 
 clean: clean-build clean-docs clean-pyc clean-test
 
diff --git a/setup.py b/setup.py
index 0966a8e3..05cb5225 100644
--- a/setup.py
+++ b/setup.py
@@ -14,8 +14,8 @@ extras_require = {
         "pytest-xdist>=2.5,<3.0",
         "pytest-split>=0.7.0,<1.0",
         "pytest-rerunfailures>=10.2,<11",
-        "eth-tester[py-evm]>=0.8.0b3,<0.9",
-        "py-evm>=0.6.1a2,<0.7",
+        "eth-tester[py-evm]>=0.9.0b1,<0.10",
+        "py-evm>=0.7.0a1,<0.8",
         "web3==6.0.0",
         "tox>=3.15,<4.0",
         "lark==1.1.2",
diff --git a/vyper/codegen/module.py b/vyper/codegen/module.py
index 320cf43b..5d05c27e 100644
--- a/vyper/codegen/module.py
+++ b/vyper/codegen/module.py
@@ -153,12 +153,31 @@ def generate_ir_for_module(global_ctx: GlobalContext) -> tuple[IRnode, IRnode]:
         init_func_ir = generate_ir_for_function(
             init_function, global_ctx, skip_nonpayable_check=False, is_ctor_context=True
         )
-        deploy_code.append(init_func_ir)
 
         # pass the amount of memory allocated for the init function
         # so that deployment does not clobber while preparing immutables
         # note: (deploy mem_ofst, code, extra_padding)
         init_mem_used = init_function._metadata["type"]._ir_info.frame_info.mem_used
+
+        # force msize to be initialized past the end of immutables section
+        # so that builtins which use `msize` for "dynamic" memory
+        # allocation do not clobber uninitialized immutables.
+        # cf. GH issue 3101.
+        # note mload/iload X touches bytes from X to X+32, and msize rounds up
+        # to the nearest 32, so `iload`ing `immutables_len - 32` guarantees
+        # that `msize` will refer to a memory location of at least
+        # `<immutables_start> + immutables_len` (where <immutables_start> ==
+        # `_mem_deploy_end` as defined in the assembler).
+        # note:
+        #   mload 32 => msize == 64
+        #   mload 33 => msize == 96
+        # assumption in general: (mload X) => msize == ceil32(X + 32)
+        # see py-evm extend_memory: after_size = ceil32(start_position + size)
+        if immutables_len > 0:
+            deploy_code.append(["iload", max(0, immutables_len - 32)])
+
+        deploy_code.append(init_func_ir)
+
         deploy_code.append(["deploy", init_mem_used, runtime, immutables_len])
 
         # internal functions come after everything else
diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py
index e30f021c..f061bd8e 100644
--- a/vyper/compiler/output.py
+++ b/vyper/compiler/output.py
@@ -208,7 +208,7 @@ def _build_asm(asm_list):
         else:
             output_string += str(node) + " "
 
-            if isinstance(node, str) and node.startswith("PUSH"):
+            if isinstance(node, str) and node.startswith("PUSH") and node != "PUSH0":
                 assert in_push == 0
                 in_push = int(node[4:])
                 output_string += "0x"
@@ -303,7 +303,7 @@ def _build_opcodes(bytecode: bytes) -> str:
     while bytecode_sequence:
         op = bytecode_sequence.popleft()
         opcode_output.append(opcode_map[op])
-        if "PUSH" in opcode_output[-1]:
+        if "PUSH" in opcode_output[-1] and opcode_output[-1] != "PUSH0":
             push_len = int(opcode_map[op][4:])
             push_values = [hex(bytecode_sequence.popleft())[2:] for i in range(push_len)]
             opcode_output.append(f"0x{''.join(push_values).upper()}")
diff --git a/vyper/evm/opcodes.py b/vyper/evm/opcodes.py
index b9f1e77c..76529da1 100644
--- a/vyper/evm/opcodes.py
+++ b/vyper/evm/opcodes.py
@@ -25,11 +25,12 @@ EVM_VERSIONS: Dict[str, int] = {
     "istanbul": 2,
     "berlin": 3,
     "paris": 4,
+    "shanghai": 5,
     # ETC Forks
     "atlantis": 0,
     "agharta": 1,
 }
-DEFAULT_EVM_VERSION: str = "paris"
+DEFAULT_EVM_VERSION: str = "shanghai"
 
 
 # opcode as hex value
@@ -102,6 +103,7 @@ OPCODES: OpcodeMap = {
     "MSIZE": (0x59, 0, 1, 2),
     "GAS": (0x5A, 0, 1, 2),
     "JUMPDEST": (0x5B, 0, 0, 1),
+    "PUSH0": (0x5F, 0, 1, 2),
     "PUSH1": (0x60, 0, 1, 3),
     "PUSH2": (0x61, 0, 1, 3),
     "PUSH3": (0x62, 0, 1, 3),
diff --git a/vyper/ir/compile_ir.py b/vyper/ir/compile_ir.py
index c24b3a67..57ea4ca7 100644
--- a/vyper/ir/compile_ir.py
+++ b/vyper/ir/compile_ir.py
@@ -3,7 +3,7 @@ import functools
 import math
 
 from vyper.codegen.ir_node import IRnode
-from vyper.evm.opcodes import get_opcodes
+from vyper.evm.opcodes import get_opcodes, version_check
 from vyper.exceptions import CodegenPanic, CompilerPanic
 from vyper.utils import MemoryPositions
 from vyper.version import version_tuple
@@ -23,7 +23,8 @@ def num_to_bytearray(x):
 
 def PUSH(x):
     bs = num_to_bytearray(x)
-    if len(bs) == 0:
+    # starting in shanghai, can do push0 directly with no immediates
+    if len(bs) == 0 and not version_check(begin="shanghai"):
         bs = [0]
     return [f"PUSH{len(bs)}"] + bs
 
@@ -149,7 +150,7 @@ def _add_postambles(asm_ops):
 
     global _revert_label
 
-    _revert_string = [_revert_label, "JUMPDEST", "PUSH1", 0, "DUP1", "REVERT"]
+    _revert_string = [_revert_label, "JUMPDEST", *PUSH(0), "DUP1", "REVERT"]
 
     if _revert_label in asm_ops:
         # shared failure block
@@ -555,13 +556,10 @@ def _compile_to_assembly(code, withargs=None, existing_labels=None, break_dest=N
         o = _compile_to_assembly(code.args[0], withargs, existing_labels, break_dest, height)
         o.extend(
             [
-                "PUSH1",
-                MemoryPositions.FREE_VAR_SPACE,
+                *PUSH(MemoryPositions.FREE_VAR_SPACE),
                 "MSTORE",
-                "PUSH1",
-                32,
-                "PUSH1",
-                MemoryPositions.FREE_VAR_SPACE,
+                *PUSH(32),
+                *PUSH(MemoryPositions.FREE_VAR_SPACE),
                 "SHA3",
             ]
         )
@@ -572,16 +570,12 @@ def _compile_to_assembly(code, withargs=None, existing_labels=None, break_dest=N
         o.extend(_compile_to_assembly(code.args[1], withargs, existing_labels, break_dest, height))
         o.extend(
             [
-                "PUSH1",
-                MemoryPositions.FREE_VAR_SPACE2,
+                *PUSH(MemoryPositions.FREE_VAR_SPACE2),
                 "MSTORE",
-                "PUSH1",
-                MemoryPositions.FREE_VAR_SPACE,
+                *PUSH(MemoryPositions.FREE_VAR_SPACE),
                 "MSTORE",
-                "PUSH1",
-                64,
-                "PUSH1",
-                MemoryPositions.FREE_VAR_SPACE,
+                *PUSH(64),
+                *PUSH(MemoryPositions.FREE_VAR_SPACE),
                 "SHA3",
             ]
         )


Test patch:
diff --git a/tests/compiler/ir/test_compile_ir.py b/tests/compiler/ir/test_compile_ir.py
index 91007da3..706c31e0 100644
--- a/tests/compiler/ir/test_compile_ir.py
+++ b/tests/compiler/ir/test_compile_ir.py
@@ -68,4 +68,4 @@ def test_pc_debugger():
     debugger_ir = ["seq", ["mstore", 0, 32], ["pc_debugger"]]
     ir_nodes = IRnode.from_list(debugger_ir)
     _, line_number_map = compile_ir.assembly_to_evm(compile_ir.compile_to_assembly(ir_nodes))
-    assert line_number_map["pc_breakpoints"][0] == 5
+    assert line_number_map["pc_breakpoints"][0] == 4
diff --git a/tests/compiler/test_opcodes.py b/tests/compiler/test_opcodes.py
index 67ea10c3..f36fcfac 100644
--- a/tests/compiler/test_opcodes.py
+++ b/tests/compiler/test_opcodes.py
@@ -8,9 +8,11 @@ from vyper.exceptions import CompilerPanic
 @pytest.fixture(params=list(opcodes.EVM_VERSIONS))
 def evm_version(request):
     default = opcodes.active_evm_version
-    opcodes.active_evm_version = opcodes.EVM_VERSIONS[request.param]
-    yield request.param
-    opcodes.active_evm_version = default
+    try:
+        opcodes.active_evm_version = opcodes.EVM_VERSIONS[request.param]
+        yield request.param
+    finally:
+        opcodes.active_evm_version = default
 
 
 def test_opcodes():
@@ -42,17 +44,20 @@ def test_version_check(evm_version):
 
 
 def test_get_opcodes(evm_version):
-    op = opcodes.get_opcodes()
-    if evm_version in ("paris", "berlin"):
-        assert "CHAINID" in op
-        assert op["SLOAD"][-1] == 2100
+    ops = opcodes.get_opcodes()
+    if evm_version in ("paris", "berlin", "shanghai"):
+        assert "CHAINID" in ops
+        assert ops["SLOAD"][-1] == 2100
+        if evm_version in ("shanghai",):
+            assert "PUSH0" in ops
     elif evm_version == "istanbul":
-        assert "CHAINID" in op
-        assert op["SLOAD"][-1] == 800
+        assert "CHAINID" in ops
+        assert ops["SLOAD"][-1] == 800
     else:
-        assert "CHAINID" not in op
-        assert op["SLOAD"][-1] == 200
+        assert "CHAINID" not in ops
+        assert ops["SLOAD"][-1] == 200
+
     if evm_version in ("byzantium", "atlantis"):
-        assert "CREATE2" not in op
+        assert "CREATE2" not in ops
     else:
-        assert op["CREATE2"][-1] == 32000
+        assert ops["CREATE2"][-1] == 32000
diff --git a/tests/compiler/test_sha3_32.py b/tests/compiler/test_sha3_32.py
index 9fbdf6f0..e1cbf9c8 100644
--- a/tests/compiler/test_sha3_32.py
+++ b/tests/compiler/test_sha3_32.py
@@ -1,9 +1,12 @@
 from vyper.codegen.ir_node import IRnode
+from vyper.evm.opcodes import version_check
 from vyper.ir import compile_ir, optimizer
 
 
 def test_sha3_32():
     ir = ["sha3_32", 0]
     evm = ["PUSH1", 0, "PUSH1", 0, "MSTORE", "PUSH1", 32, "PUSH1", 0, "SHA3"]
+    if version_check(begin="shanghai"):
+        evm = ["PUSH0", "PUSH0", "MSTORE", "PUSH1", 32, "PUSH0", "SHA3"]
     assert compile_ir.compile_to_assembly(IRnode.from_list(ir)) == evm
     assert compile_ir.compile_to_assembly(optimizer.optimize(IRnode.from_list(ir))) == evm
diff --git a/tests/parser/features/test_immutable.py b/tests/parser/features/test_immutable.py
index 488943f7..7300d0f2 100644
--- a/tests/parser/features/test_immutable.py
+++ b/tests/parser/features/test_immutable.py
@@ -241,6 +241,56 @@ def get_immutable() -> uint256:
     assert c.get_immutable() == n + 2
 
 
+# GH issue 3101
+def test_immutables_initialized(get_contract):
+    dummy_code = """
+@external
+def foo() -> uint256:
+    return 1
+    """
+    dummy_contract = get_contract(dummy_code)
+
+    code = """
+a: public(immutable(uint256))
+b: public(uint256)
+
+@payable
+@external
+def __init__(to_copy: address):
+    c: address = create_copy_of(to_copy)
+    self.b = a
+    a = 12
+    """
+    c = get_contract(code, dummy_contract.address)
+
+    assert c.b() == 0
+
+
+# GH issue 3101, take 2
+def test_immutables_initialized2(get_contract, get_contract_from_ir):
+    dummy_contract = get_contract_from_ir(
+        ["deploy", 0, ["seq"] + ["invalid"] * 600, 0], no_optimize=True
+    )
+
+    # rekt because immutables section extends past allocated memory
+    code = """
+a0: immutable(uint256[10])
+a: public(immutable(uint256))
+b: public(uint256)
+
+@payable
+@external
+def __init__(to_copy: address):
+    c: address = create_copy_of(to_copy)
+    self.b = a
+    a = 12
+    a0 = empty(uint256[10])
+    """
+    c = get_contract(code, dummy_contract.address)
+
+    assert c.b() == 0
+
+
 # GH issue 3292
 def test_internal_functions_called_by_ctor_location(get_contract):
     code = """