Repository: vyperlang/vyper

Issue:
_abi_encode with empty string
### Version Information

* vyper Version (output of `vyper --version`): 0.3.2
* OS: linux
* Python Version (output of `python --version`): 3.10.2

### What's your issue about?

```
@external
def f1() -> Bytes[100]:
    return _abi_encode("")
```

Expected: `0x20, 0x40, 0x20, 0`
Found: `0, 0x40, 0x20, 0`

It is another Solidity test and the similar `_abi_encode` problem.
The first value (I guess it is the offset) is always 0x20 if the string is not empty.


Discussion:
looks like a bug. Will investigate 

Resolving patch:
diff --git a/vyper/codegen/core.py b/vyper/codegen/core.py
--- a/vyper/codegen/core.py
+++ b/vyper/codegen/core.py
@@ -45,17 +45,12 @@ def _codecopy_gas_bound(num_bytes):
 
 
 # Copy byte array word-for-word (including layout)
+# TODO make this a private function
 def make_byte_array_copier(dst, src):
     assert isinstance(src.typ, ByteArrayLike)
     assert isinstance(dst.typ, ByteArrayLike)
 
-    if src.typ.maxlen > dst.typ.maxlen:
-        raise TypeMismatch(f"Cannot cast from {src.typ} to {dst.typ}")
-    # stricter check for zeroing a byte array.
-    if src.value == "~empty" and src.typ.maxlen != dst.typ.maxlen:
-        raise TypeMismatch(
-            f"Bad type for clearing bytes: expected {dst.typ} but got {src.typ}"
-        )  # pragma: notest
+    _check_assign_bytes(dst, src)
 
     if src.value == "~empty":
         # set length word to 0.
@@ -175,7 +170,7 @@ def _dynarray_make_setter(dst, src):
 # copy an entire (32-byte) word, depending on the copy routine chosen.
 # TODO maybe always pad to ceil32, to reduce dirty bytes bugs
 def copy_bytes(dst, src, length, length_bound):
-    annotation = f"copy_bytes from {src} to {dst}"
+    annotation = f"copy up to {length_bound} bytes from {src} to {dst}"
 
     src = IRnode.from_list(src)
     dst = IRnode.from_list(dst)
@@ -185,6 +180,13 @@ def copy_bytes(dst, src, length, length_bound):
         "copy_bytes_count"
     ) as (b2, length), dst.cache_when_complex("dst") as (b3, dst):
 
+        assert length_bound >= 0
+
+        if length_bound == 0:
+            return IRnode.from_list(["seq"], annotation=annotation)
+
+        assert src.is_pointer and dst.is_pointer
+
         # fast code for common case where num bytes is small
         # TODO expand this for more cases where num words is less than ~8
         if length_bound <= 32:
@@ -590,11 +592,10 @@ def dummy_node_for_type(typ):
 def _check_assign_bytes(left, right):
     if right.typ.maxlen > left.typ.maxlen:
         raise TypeMismatch(f"Cannot cast from {right.typ} to {left.typ}")  # pragma: notest
+
     # stricter check for zeroing a byte array.
     if right.value == "~empty" and right.typ.maxlen != left.typ.maxlen:
-        raise TypeMismatch(
-            f"Bad type for clearing bytes: expected {left.typ} but got {right.typ}"
-        )  # pragma: notest
+        raise TypeMismatch(f"Cannot cast from empty({right.typ}) to {left.typ}")  # pragma: notest
 
 
 def _check_assign_list(left, right):


Test patch:
diff --git a/tests/functional/codegen/test_abi_encode.py b/tests/functional/codegen/test_abi_encode.py
--- a/tests/functional/codegen/test_abi_encode.py
+++ b/tests/functional/codegen/test_abi_encode.py
@@ -382,3 +382,24 @@ def foo(bs: DynArray[DynArray[DynArray[uint256, 3], 3], 3]) -> (uint256, Bytes[1
         [[19, 20, 21], [22, 23, 24], [25, 26, 27]],
     ]
     assert c.foo(bs) == [2 ** 256 - 1, abi_encode("(uint256[][][])", (bs,))]
+
+
+@pytest.mark.parametrize("empty_literal", ('b""', '""', "empty(Bytes[1])", "empty(String[1])"))
+def test_abi_encode_empty_string(get_contract, abi_encode, empty_literal):
+    code = f"""
+@external
+def foo(ensure_tuple: bool) -> Bytes[96]:
+    if ensure_tuple:
+        return _abi_encode({empty_literal}) # default ensure_tuple=True
+    else:
+        return _abi_encode({empty_literal}, ensure_tuple=False)
+    """
+
+    c = get_contract(code)
+
+    # eth-abi does not encode zero-length string correctly -
+    # see https://github.com/ethereum/eth-abi/issues/157
+    expected_output = b"\x00" * 32
+    assert c.foo(False) == expected_output
+    expected_output = b"\x00" * 31 + b"\x20" + b"\x00" * 32
+    assert c.foo(True) == expected_output