Repository: vyperlang/vyper

Issue:
Function selector collision is not detected with the "0" selector
### Version Information

* vyper Version (output of `vyper --version`): 0.1.0-beta.12

### What's your issue about?

Vyper has an edge case in function selector collision detection. If a function with selector `0x00000000` exists, the `__default__` function is overwritten in the resulting contract. Here's an example: 

```
owner: public(address)

@public
def __init__():
    self.owner = msg.sender

@public
@payable
def blockHashAskewLimitary(v: uint256): # function selector: 0x00000000
    send(self.owner, self.balance)

@public
@payable
def __default__():
    assert msg.value != 0
    send(msg.sender, self.balance)
```

Calls made to the above contract with no calldata trigger `blockHashAskewLimitary(uint)`, rather than the default function. It seems that `v` is also successfully decoded, even without calldata.

### How can it be fixed?

Solidity uses `CALLDATASIZE` to differentiate between functions with the "0" selector, and the fallback function. As for the variable unpacking from empty calldata, a check should be in place that each function has some minimum `CALLDATASIZE`.



Discussion:
We should definitely check for collisions between function selectors and reject programs with collisions at compile time!
It seems Vyper does this -- the only edge case I've found is the "0" selector. Note, though, that Solidity does allow a function with a "0" selector along with a fallback function. It differentiates between the two by checking whether `CALLDATASIZE` is less than 4.
> It seems Vyper does this -- the only edge case I've found is the "0" selector. Note, though, that Solidity does allow a function with a "0" selector along with a fallback function. It differentiates between the two by checking whether `CALLDATASIZE` is less than 4.

Thanks - I think we will just reject programs with name that maps to the "0" selector
Actually I see the issue. Consider the IR of the following program:
```python
@public
@payable
def blockHashAskewLimitary(v: uint256): # function selector: 0x00000000
  send(msg.sender, self.balance)

@public
def __default__() -> uint256:
  return 1
```

```python
[seq,
  [return,
    0,
    [lll,
      [seq,
        [mstore, 28, [calldataload, 0]],
        [mstore, 32, 1461501637330902918203684832716283019655932542976],
        [mstore, 64, 170141183460469231731687303715884105727],
        [mstore, 96, -170141183460469231731687303715884105728],
        [mstore, 128, 1701411834604692317316873037158841057270000000000],
        [mstore, 160, -1701411834604692317316873037158841057280000000000],
        # Line 1
        [if,
          [iszero, [mload, 0]],
          [seq,
            # Line 4
            [assert, [call, 0, caller, [balance, address], 0, 0, 0, 0]],
            # Line 1
            stop]],
        # Line 6
        [assert, [iszero, callvalue]],
        # Line 8
        [mstore, 0, 1],
        [seq_unchecked, pass, [return, 0, 32]]],
      0]]]
```

The fallback function doesn't have a selector. Actually the `blockHashAskewLimitary` is getting selected because the result of `(mload 0)` (which in turn contains the result of `(calldataload 0)`) is `0`. This is because of the semantics of `CALLDATALOAD`:
![Screenshot from 2019-09-08 13-19-31](https://user-images.githubusercontent.com/3867501/64493938-60c5f400-d23b-11e9-9cc0-9911b475efe7.png)

If the argument to `CALLDATALOAD` refers to a location larger than (or equal to) `CALLDATASIZE` then `CALLDATALOAD` returns 0 (rather than reverting). So as you suggested, we need to check that `CALLDATASIZE` is at least equal to 4, otherwise jump to the fallback clause.
As an aside, this provides us a clean and gas-efficient way to perform memory zeroing without loops. We can just use `(calldatacopy calldatasize mem_dest len)`. This is a base cost of 3 gas + 3 gas per word copied.

Resolving patch:
diff --git a/vyper/parser/parser.py b/vyper/parser/parser.py
--- a/vyper/parser/parser.py
+++ b/vyper/parser/parser.py
@@ -43,7 +43,13 @@
     raise Exception("Requires python 3.6 or higher for annotation support")
 
 # Header code
-STORE_CALLDATA: List[Any] = ['seq', ['mstore', 28, ['calldataload', 0]]]
+STORE_CALLDATA: List[Any] = \
+        ['seq',
+            # check that calldatasize is at least 4, otherwise
+            # calldataload will load zeros (cf. yellow paper).
+            ['if', ['lt', 'calldatasize', 4],
+                ['goto', 'fallback']],
+            ['mstore', 28, ['calldataload', 0]]]
 # Store limit constants at fixed addresses in memory.
 LIMIT_MEMORY_SET: List[Any] = [
     ['mstore', pos, limit_size]
@@ -57,13 +63,6 @@
 )
 
 
-# Header code
-INITIALIZER_LIST = ['seq', ['mstore', 28, ['calldataload', 0]]]
-# Store limit constants at fixed addresses in memory.
-INITIALIZER_LIST += [['mstore', pos, limit_size] for pos, limit_size in LOADED_LIMIT_MAP.items()]
-INITIALIZER_LLL = LLLnode.from_list(INITIALIZER_LIST, typ=None)
-
-
 def parse_events(sigs, global_ctx):
     for event in global_ctx._events:
         sigs[event.target.id] = EventSignature.from_declaration(event, global_ctx)
@@ -142,9 +141,10 @@ def parse_other_functions(o,
             origcode,
             global_ctx,
         )
-        sub.append(default_func)
+        fallback = default_func
     else:
-        sub.append(LLLnode.from_list(['revert', 0, 0], typ=None, annotation='Default function'))
+        fallback = LLLnode.from_list(['revert', 0, 0], typ=None, annotation='Default function')
+    sub.append(['seq_unchecked', ['label', 'fallback'], fallback])
     if runtime_only:
         return sub
     else:


Test patch:
diff --git a/tests/parser/functions/test_default_function.py b/tests/parser/functions/test_default_function.py
--- a/tests/parser/functions/test_default_function.py
+++ b/tests/parser/functions/test_default_function.py
@@ -94,3 +94,34 @@ def __default__():
     log.Sent(msg.sender)
     """
     assert_compile_failed(lambda: get_contract_with_gas_estimation(code))
+
+
+def test_zero_method_id(w3, get_logs, get_contract_with_gas_estimation):
+    code = """
+Sent: event({sig: uint256})
+
+@public
+@payable
+# function selector: 0x00000000
+def blockHashAskewLimitary(v: uint256) -> uint256:
+    log.Sent(2)
+    return 7
+
+@public
+def __default__():
+    log.Sent(1)
+    """
+
+    c = get_contract_with_gas_estimation(code)
+
+    assert c.blockHashAskewLimitary(0) == 7
+
+    logs = get_logs(
+            w3.eth.sendTransaction({'to': c.address, 'value': 0}), c, 'Sent')
+    assert 1 == logs[0].args.sig
+
+    logs = get_logs(
+            # call blockHashAskewLimitary
+            w3.eth.sendTransaction({'to': c.address, 'value': 0, 'data': '0x00000000'}),
+            c, 'Sent')
+    assert 2 == logs[0].args.sig