Repository: vyperlang/vyper

Issue:
Fails: Using empty `bytes[256]` default function parameter on internal constant and private functions
### Version Information

* vyper Version: 0.1.0b10
* OS: osx
* Python Version: 3.7.3

### What's your issue about?

Calling `mint()` on the following contract throws when making use of the default parameters:

```python
@private
@constant
def _checkForERC777TokensInterface_Recipient(
    _to: address,
    _amount: uint256,
    _data: bytes[256]="",
  ):
    assert True


@public
def mint(
    _to: address,
    _amount: uint256,
    _data: bytes[256]="",
  ):
    # NOTE: fails (stack underflow) because of default _data parameter
    self._checkForERC777TokensInterface_Recipient(
      _to,
      _amount,
      _data,
    )
```

#### Using `web3js`:

This throws: `Error: Returned error: VM Exception while processing transaction: stack underflow`
```javascript
await erc777Token.mint(
      wallet.address,
      100,
    );
```

This works:
```javascript
    await erc777Token.mint(
      wallet.address,
      100,
      '0x0000000000000000000000000000000000000001',
    );
```

### How can this be solved?

I posted a solution further down (use `data: bytes[256]="0x0"` in `mint()`). 
Maybe everything is fine and one should just not use default parameters in internal functions...
In that case this issue can be closed.



Discussion:
The above probably fails because vyper creates two methods:
```python
def mint(_to: address,  _amount: uint256)

# and

def mint(_to: address,  _amount: uint256,  _data: bytes[256])
```
And in the first method the `_data` parameter does not exist and therefore can't be passed to the `_checkForERC777TokensInterface_Recipient` call.

Does Vyper create two internal methods for `_checkForERC777TokensInterface_Recipient` as well?


Maybe something like this could be done:

```python
@private
@constant
def _checkForERC777TokensInterface_Recipient(
    _to: address,
    _amount: uint256,
    _data: bytes[256]="",
  ):
    assert True


@public
def mint(
    _to: address,
    _amount: uint256,
    _data: bytes[256]="",
  ):
    self._checkForERC777TokensInterface_Recipient(
      _to,
      _amount,
      _data="", # <-------------------------- CHANGE HERE
    )
```
This fails as well:
```javascript
    await erc777Token.mint(
      wallet.address,
      100,
      ' 0x0000000000000000000000000000000000000000',
    );
```
(same stack underflow error)
This works:

```python
@private
@constant
def _checkForERC777TokensInterface_Recipient(
    _to: address,
    _amount: uint256,
    _data: bytes[256], #  <------------------------------ CHANGE IS HERE
  ):
    assert True


@public
def mint(
    _to: address,
    _amount: uint256,
    _data: bytes[256]="0x0", #  <------------------------------ CHANGE IS HERE
  ):
    self._checkForERC777TokensInterface_Recipient(
      _to,
      _amount,
      _data,
    )
```

### web3js:
```javascript
await erc777Token.mint(
      wallet.address,
      100,
    );
```
@ssteiger am I understanding correctly that the exception only occurs if the default parameter is an empty bytearray, on a private function?


@jacqueswww 
Yes, it seems like the exception only occurs if the default parameter is an empty bytearray
And only if the function is private (functions with `@public` are working)

I exclude all accompanying parameter type changes (with same default value) in  `@private` `_checkForERC777TokensInterface_Recipient`.

### address
```python
# works
def mint(
    _to: address,
    _amount: uint256,
    _data: address=0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24
  ):
```
### string array
```python
# works
def mint(
    _to: address,
    _amount: uint256,
    _data: string[4]="test"
  ):
```
```python
# fails
def mint(
    _to: address,
    _amount: uint256,
    _data: string[4]=""
  ):
```
### boolean
```python
# fails
# Throws: Default parameter values have to be literals.
def mint(
    _to: address,
    _amount: uint256,
    _data: bool=False
  ):
```
### bytes32
```python
# works
def mint(
    _to: address,
    _amount: uint256,
    _data: bytes32=0x0000000000000000000000000000000000000000000000000000000000000000
  ):
```
```python
# fails
# Throws: Base type conversion from or to non-base type: string[0] bytes32
def mint(
    _to: address,
    _amount: uint256,
    _data: bytes32=""
  ):
```
### int128
```python
# works
def mint(
    _to: address,
    _amount: uint256,
    _data: int128=100
  ):
```
### uint256
```python
# works
def mint(
    _to: address,
    _amount: uint256,
    _data: uint256=0
  ):
```
### decimal
```python
# works
def mint(
    _to: address,
    _amount: uint256,
    _data: decimal=1.01
  ):
```
Looks like calling methods of other contracts (**also if they are @public**) produces the same error:
#### This works:

```python
# CONTRACT 1

@public
def _checkForERC777TokensInterface_Recipient(
    _to: address,
    _amount: uint256,
    _data: bytes[256]="",
  ):
    assert True


@public
def mint(
    _to: address,
    _amount: uint256,
    _data: bytes[256]="",
  ):
    self._checkForERC777TokensInterface_Recipient(
      _to,
      _amount,
      _data,
    )
```

#### This fails:
```python
# CONTRACT 1

@public
def _checkForERC777TokensInterface_Recipient(
    _to: address,
    _amount: uint256,
    _data: bytes[256]="",
  ):
    ERC777TokensSender(_from).tokensToSend(_operator, _from, _to, _amount, _data, _operatorData)


@public
def mint(
    _to: address,
    _amount: uint256,
    _data: bytes[256]="",
  ):
    self._checkForERC777TokensInterface_Recipient(
      _to,
      _amount,
      _data,
    )



# CONTRACT 2 (ERC777TokensSender)

ERC777Received: event({
    _operator: indexed(address),
    _from: indexed(address),
    _to: indexed(address),
    _amount: uint256,
    _data: bytes[256],
    _operatorData: bytes[256]
})

# This fails (revert)
@public
def tokensReceived(
    _operator: address,
    _from: address,
    _to: address,
    _amount: uint256,
    _data: bytes[256]="",
    _operatorData: bytes[256]=""
  ):
    log.ERC777Received(_operator, _from, _to, _amount, _data, _operatorData)
```
### This works
```python
# CONTRACT 2 (ERC777TokensSender)

@public
def tokensReceived(
    _operator: address,
    _from: address,
    _to: address,
    _amount: uint256,
    _data: bytes[256]="",
    _operatorData: bytes[256]=""
  ):
    assert True
```
@ssteiger do you have the same issue with v0.1.0b9?
@charles-cooper I've installed v0.1.0b10 on my system using `make`. 
Do you know of a way to easily downgrade to v0.1.0b9?
If you are installing from a local repository, you can `git checkout` the `v0.1.0-beta.9` tag and reinstall using `make`.
Just tested it. Same `stack underflow` error with  `vyper-0.1.0b8` and `vyper-0.1.0b9`.
Also only when the methods are `@private`. Setting them to `@public` makes the error disappear.

this is the most minimal reproducible example i've found:
```python
# stack underflow
@private
def bar(x: bytes[32]):
    pass
@public
def foo(x: bytes[32] = b'\x00'):
    self.bar(x)
```
vs
```python
# runs without exception
@private
def bar(x: bytes[32]):
    pass
@public
def foo(x: bytes[32] = b'\x01'):
    self.bar(x)
```

(these differ in one instruction - `mstore 480 0` vs `mstore 480 0x100000000000000000000000000000000000000000000000000000000000000`)
As far as I have traced this, it is related to the dynamic packing/unpacking of the empty byte array into the argument section for the private function.
https://github.com/ethereum/vyper/blob/master/vyper/parser/self_call.py#L113
After returning back from the function the unpacker unpacks too may stack items.

Resolving patch:
diff --git a/vyper/parser/self_call.py b/vyper/parser/self_call.py
--- a/vyper/parser/self_call.py
+++ b/vyper/parser/self_call.py
@@ -18,9 +18,9 @@
     ByteArrayLike,
     ListType,
     TupleLike,
-    ceil32,
     get_size_of_type,
     get_static_size_of_type,
+    has_dynamic_data,
 )
 
 
@@ -108,22 +108,42 @@ def call_self_private(stmt_expr, context, sig):
                 [get_static_size_of_type(arg.typ)
                     for arg in expr_args])
         static_pos = arg_pos + static_arg_size
-        total_arg_size = ceil32(inargsize - 4)
+        needs_dyn_section = any(
+                [has_dynamic_data(arg.typ)
+                    for arg in expr_args])
 
-        if static_arg_size != total_arg_size:  # requires dynamic section.
+        if needs_dyn_section:
             ident = 'push_args_%d_%d_%d' % (sig.method_id, stmt_expr.lineno, stmt_expr.col_offset)
             start_label = ident + '_start'
             end_label = ident + '_end'
             i_placeholder = context.new_placeholder(BaseType('uint256'))
+
+            # Calculate copy start position.
+            # Given | static | dynamic | section in memory,
+            # copy backwards so the values are in order on the stack.
+            # We calculate i, the end of the whole encoded part
+            # (i.e. the starting index for copy)
+            # by taking ceil32(len<arg>) + offset<arg> + arg_pos
+            # for the last dynamic argument and arg_pos is the start
+            # the whole argument section.
+            for idx, arg in enumerate(expr_args):
+                if isinstance(arg.typ, ByteArrayLike):
+                    last_idx = idx
             push_args += [
-                ['mstore', i_placeholder, arg_pos + total_arg_size],
+                ['with', 'offset', ['mload', arg_pos + last_idx * 32],
+                    ['with', 'len_pos', ['add', arg_pos, 'offset'],
+                        ['with', 'len_value', ['mload', 'len_pos'],
+                            ['mstore', i_placeholder,
+                                ['add', 'len_pos', ['ceil32', 'len_value']]]]]]
+            ]
+            # loop from end of dynamic section to start of dynamic section,
+            # pushing each element onto the stack.
+            push_args += [
+
                 ['label', start_label],
-                ['if', ['lt', ['mload', i_placeholder], static_pos], ['goto', end_label]],
-                [
-                    'if_unchecked',
-                    ['ne', ['mload', ['mload', i_placeholder]], 0],
-                    ['mload', ['mload', i_placeholder]],
-                ],
+                ['if', ['lt', ['mload', i_placeholder], static_pos],
+                    ['goto', end_label]],
+                ['mload', ['mload', i_placeholder]],
                 ['mstore', i_placeholder, ['sub', ['mload', i_placeholder], 32]],  # decrease i
                 ['goto', start_label],
                 ['label', end_label]


Test patch:
diff --git a/tests/parser/features/decorators/test_private.py b/tests/parser/features/decorators/test_private.py
--- a/tests/parser/features/decorators/test_private.py
+++ b/tests/parser/features/decorators/test_private.py
@@ -546,3 +546,18 @@ def call_arr() -> int128:
 
     c = get_contract(code)
     assert c.call_arr() == 42
+
+
+def test_private_zero_bytearray(get_contract):
+    private_test_code = """
+@private
+def inner(xs: bytes[256]):
+    pass
+@public
+def outer(xs: bytes[256] = "") -> bool:
+    self.inner(xs)
+    return True
+    """
+
+    c = get_contract(private_test_code)
+    assert c.outer()