Repository: vyperlang/vyper

Issue:
Insufficient zero-padding bug for functions returning byte arrays of size < 16
### Version Information

* vyper Version (output of `vyper --version`): 0.1.0b11+commit.c775cda (the latest master)
  * The buggy behavior happens for both recent releases, [v0.1.0-beta.10](https://github.com/ethereum/vyper/releases/tag/v0.1.0-beta.10) and [v0.1.0-beta.11](https://github.com/ethereum/vyper/releases/tag/v0.1.0-beta.11), as well.

### What's your issue about?

#### Summary:

A function whose return type is `bytes[n]` where `n < 16`, returns a value that does *not* conform to [the ABI encoding](https://solidity.readthedocs.io/en/v0.5.10/abi-spec.html), having incorrect zero-padding.

#### Details:

For example, [the `get_deposit_count` function of the deposit contract](https://github.com/ethereum/eth2.0-specs/blob/v0.8.0/deposit_contract/contracts/validator_registration.v.py#L60), whose return type is `bytes[8]`, returns the following 96 bytes (in hexadecimal notation):
```
0x0000000000000000000000000000000000000000000000000000000000000020
  0000000000000000000000000000000000000000000000000000000000000008
  deadbeefdeadbeef000000000000000000000000000000000000000000000020
                                                                ^^  <-- problem!
```
where the first 32 bytes (the first line) denotes the header (the offset 32 = 0x20), the second 32 bytes (the second line) denotes the length of the byte array (8), and the "`deadbeefdeadbeef`" denotes the content of the byte array (`bytes[8]`). *Here, the problem is that the last byte is 32 (= 0x20) while it should be 0 (= 0x00) according to the ABI specification.* This means that any contract (written in either Solidity or Vyper) that calls to this type of functions expecting the correct zero-padding may misbehave.

What happens is that the compiled bytecode of `get_deposit_count` fails to sufficiently pad zeros when it prepares for the return value. In the following return statement,
```
return self.to_little_endian_64(self.deposit_count)
```
it first copies the returned value (of 8 bytes) to some specific region of the memory, and puts only 8 bytes of zero-padding after that, instead of 24 bytes of zeros, which results in including some garbage values in the last 16 bytes. Indeed, in this particular case, the last 16 bytes (`000000000000000000000000020`) came from the side-effect of the sub-function call to `self.to_little_endian_64()`.

To be more specific, in the following zero-padding LLL code of `get_deposit_count` (generated by [the `zero_pad` function of `vyper/parser/stmt.py`](https://github.com/ethereum/vyper/blob/c775cda6a34b62612b4160d9017ad701f4f52db8/vyper/parser/stmt.py#L780-L799)):
```
            /* Zero pad */
            [with,
              _ceil32_end,
              [ceil32, [mload, 640]],
              [repeat,
                736,
                [mload, 640],
    ??? -->     8,
                [seq,
                  [if, [gt, [mload, 736], _ceil32_end], break],
                  [mstore8, [add, 672, [mload, 736]], 0]]]],
            [mstore, 608, 32],
            [return, 608, [ceil32, [add, [mload, 640], 64]]],
            # Line 58
            stop]],
```
the third argument of the `repeat` loop is 8, where it should be at least 24.  In a quick examination of the Vyper compiler code, it seems that the number `8` comes from the `maxlen` of the type `bytes[8]`, but I couldn't have a chance to further investigate the root cause and a quick fix.


Indeed, the same problem happens in [the `to_little_endian_64` function](https://github.com/ethereum/eth2.0-specs/blob/v0.8.0/deposit_contract/contracts/validator_registration.v.py#L30) as well,  which has less effect because it is a private function, though.


_[This bug was privately reported on July 22, 2019, confirmed as not a security vulnerability, and made public here for the transparency.]_


Discussion:

Resolving patch:
diff --git a/vyper/parser/events.py b/vyper/parser/events.py
--- a/vyper/parser/events.py
+++ b/vyper/parser/events.py
@@ -16,6 +16,7 @@
     make_byte_array_copier,
     make_setter,
     unwrap_location,
+    zero_pad,
 )
 from vyper.types.types import (
     BaseType,
@@ -108,24 +109,9 @@ def pack_args_by_32(holder, maxlen, arg, typ, context, placeholder,
         copier = make_byte_array_copier(dest_placeholder, source_lll, pos=pos)
         holder.append(copier)
         # Add zero padding.
-        new_maxlen = ceil32(source_lll.typ.maxlen)
-
-        holder.append([
-            'with', '_ceil32_end', ['ceil32', ['mload', dest_placeholder]], [
-                'seq', ['with', '_bytearray_loc', dest_placeholder, [
-                    'seq', ['repeat', zero_pad_i, ['mload', '_bytearray_loc'], new_maxlen, [
-                        'seq',
-                        # stay within allocated bounds
-                        ['if', ['ge', ['mload', zero_pad_i], '_ceil32_end'], 'break'],
-                        [
-                            'mstore8',
-                            ['add', ['add', '_bytearray_loc', 32], ['mload', zero_pad_i]],
-                            0,
-                        ],
-                    ]],
-                ]],
-            ]
-        ])
+        holder.append(
+            zero_pad(dest_placeholder, maxlen, zero_pad_i=zero_pad_i)
+        )
 
         # Increment offset counter.
         increment_counter = LLLnode.from_list([
diff --git a/vyper/parser/parser_utils.py b/vyper/parser/parser_utils.py
--- a/vyper/parser/parser_utils.py
+++ b/vyper/parser/parser_utils.py
@@ -887,16 +887,17 @@ def annotate_ast(
     RewriteUnarySubVisitor().visit(parsed_ast)
 
 
-def zero_pad(bytez_placeholder, maxlen, context):
+def zero_pad(bytez_placeholder, maxlen, context=None, zero_pad_i=None):
     zero_padder = LLLnode.from_list(['pass'])
     if maxlen > 0:
         # Iterator used to zero pad memory.
-        zero_pad_i = context.new_placeholder(BaseType('uint256'))
+        if zero_pad_i is None:
+            zero_pad_i = context.new_placeholder(BaseType('uint256'))
         zero_padder = LLLnode.from_list([
-            'repeat', zero_pad_i, ['mload', bytez_placeholder], maxlen, [
+            'repeat', zero_pad_i, ['mload', bytez_placeholder], ceil32(maxlen), [
                 'seq',
                 # stay within allocated bounds
-                ['if', ['gt', ['mload', zero_pad_i], maxlen], 'break'],
+                ['if', ['gt', ['mload', zero_pad_i], ceil32(maxlen)], 'break'],
                 ['mstore8', ['add', ['add', 32, bytez_placeholder], ['mload', zero_pad_i]], 0]
             ]],
             annotation="Zero pad",
@@ -961,7 +962,7 @@ def gen_tuple_return(stmt, context, sub):
     # Is from a call expression.
     if sub.args and len(sub.args[0].args) > 0 and sub.args[0].args[0].value == 'call':
         # self-call to public.
-        mem_pos = sub.args[0].args[-1]
+        mem_pos = sub
         mem_size = get_size_of_type(sub.typ) * 32
         return LLLnode.from_list(['return', mem_pos, mem_size], typ=sub.typ)
 
diff --git a/vyper/parser/stmt.py b/vyper/parser/stmt.py
--- a/vyper/parser/stmt.py
+++ b/vyper/parser/stmt.py
@@ -37,6 +37,7 @@
     make_return_stmt,
     make_setter,
     unwrap_location,
+    zero_pad,
 )
 from vyper.types import (
     BaseType,
@@ -777,27 +778,6 @@ def parse_return(self):
         if not self.stmt.value:
             raise TypeMismatchException("Expecting to return a value", self.stmt)
 
-        def zero_pad(bytez_placeholder, maxlen):
-            zero_padder = LLLnode.from_list(['pass'])
-            if maxlen > 0:
-                # Iterator used to zero pad memory.
-                zero_pad_i = self.context.new_placeholder(BaseType('uint256'))
-                zero_padder = LLLnode.from_list([
-                    'with', '_ceil32_end', ['ceil32', ['mload', bytez_placeholder]], [
-                        'repeat', zero_pad_i, ['mload', bytez_placeholder], maxlen, [
-                            'seq',
-                            # stay within allocated bounds
-                            ['if', ['gt', ['mload', zero_pad_i], '_ceil32_end'], 'break'],
-                            [
-                                'mstore8',
-                                ['add', ['add', 32, bytez_placeholder], ['mload', zero_pad_i]],
-                                0
-                            ],
-                        ],
-                    ],
-                ], annotation="Zero pad")
-            return zero_padder
-
         sub = Expr(self.stmt.value, self.context).lll_node
 
         # Returning a value (most common case)
@@ -882,7 +862,7 @@ def zero_pad(bytez_placeholder, maxlen):
                         sub,
                         pos=getpos(self.stmt)
                     ),
-                    zero_pad(bytez_placeholder, sub.typ.maxlen),
+                    zero_pad(bytez_placeholder, sub.typ.maxlen, self.context),
                     ['mstore', len_placeholder, 32],
                     make_return_stmt(
                         self.stmt,


Test patch:
diff --git a/tests/parser/types/test_bytes.py b/tests/parser/types/test_bytes.py
--- a/tests/parser/types/test_bytes.py
+++ b/tests/parser/types/test_bytes.py
@@ -232,3 +232,39 @@ def test() -> bool:
     c = get_contract(code)
 
     assert c.test() is True
+
+
+def test_zero_padding_with_private(get_contract):
+    code = """
+counter: uint256
+
+@private
+@constant
+def to_little_endian_64(value: uint256) -> bytes[8]:
+    y: uint256 = 0
+    x: uint256 = value
+    for _ in range(8):
+        y = shift(y, 8)
+        y = y + bitwise_and(x, 255)
+        x = shift(x, -8)
+    return slice(convert(y, bytes32), start=24, len=8)
+
+@public
+def set_count(i: uint256):
+    self.counter = i
+
+@public
+@constant
+def get_count() -> bytes[24]:
+    return self.to_little_endian_64(self.counter)
+    """
+
+    c = get_contract(code)
+
+    assert c.get_count() == b'\x00\x00\x00\x00\x00\x00\x00\x00'
+    c.set_count(1, transact={})
+    assert c.get_count() == b'\x01\x00\x00\x00\x00\x00\x00\x00'
+    c.set_count(0xf0f0f0, transact={})
+    assert c.get_count() == b'\xf0\xf0\xf0\x00\x00\x00\x00\x00'
+    c.set_count(0x0101010101010101, transact={})
+    assert c.get_count() == b'\x01\x01\x01\x01\x01\x01\x01\x01'
diff --git a/tests/parser/types/test_bytes_zero_padding.py b/tests/parser/types/test_bytes_zero_padding.py
new file mode 100644
--- /dev/null
+++ b/tests/parser/types/test_bytes_zero_padding.py
@@ -0,0 +1,40 @@
+import hypothesis
+import pytest
+
+
+@pytest.fixture(scope='module')
+def little_endian_contract(get_contract_module):
+    code = """
+@private
+@constant
+def to_little_endian_64(value: uint256) -> bytes[8]:
+    y: uint256 = 0
+    x: uint256 = value
+    for _ in range(8):
+        y = shift(y, 8)
+        y = y + bitwise_and(x, 255)
+        x = shift(x, -8)
+    return slice(convert(y, bytes32), start=24, len=8)
+
+@public
+@constant
+def get_count(counter: uint256) -> bytes[24]:
+    return self.to_little_endian_64(counter)
+    """
+    c = get_contract_module(code)
+    return c
+
+
+@hypothesis.given(
+    value=hypothesis.strategies.integers(
+        min_value=0,
+        max_value=2**64,
+    )
+)
+@hypothesis.settings(
+    deadline=400,
+)
+def test_zero_pad_range(little_endian_contract, value):
+    actual_bytes = value.to_bytes(8, byteorder="little")
+    contract_bytes = little_endian_contract.get_count(value)
+    assert contract_bytes == actual_bytes