Repository: crytic/slither

Issue:
[Bug]: Assigning to array literal (len==1) to state variable results in invalid IR operation
### Describe the issue:

When assigning an array literal to a state variable works fine when the array literal has at least two elements. 
For example:
`arr = [1,2]` results in the IR operation `arr(uint256[]) =  ['1', '2']`.

However, when the array literal only has one element, the `rvalue` of the IR operation becomes a single element instead of an array:
`arr = [1]` results in `arr(uint256[]) := 1(uint256)`
This is buggy since the type of `lvalue` and `rvalue` mismatch. 
`arr(uint256[]) = ['1']` is expected in this case.

It seems like when the array literal's length is 1, Slither will consider this as an `Assignment` operation instead of `InitArray` operation, while the latter one is expected. 

### Code example to reproduce the issue:

```solidity
contract A {
    uint[] arr;
    function foo(uint i) public returns (uint) {
        arr = [1];
        return arr[i];
    }
}
```

### Version:

0.9.2

### Relevant log output:

```shell
Contract A
        Function A.foo(uint256) (*)
                Expression: arr = (1)
                IRs:
                        arr(uint256[]) := 1(uint256) # Buggy IR Operation
                Expression: arr[i]
                IRs:
                        REF_0(uint256) -> arr[i]
                        RETURN REF_0
```



Discussion:
After a quick inspection into the code, I found the root cause may be the handling of `TupleExpressions` [here](https://github.com/crytic/slither/blob/64b2e0c063cf93fb479481abc432371404629f32/slither/visitors/slithir/expression_to_slithir.py#L535) when visiting expressions in AST to build slithIR.
```
def _post_tuple_expression(self, expression: TupleExpression) -> None:
    expressions = [get(e) if e else None for e in expression.expressions]
    if len(expressions) == 1:
        val = expressions[0]
    else:
        val = expressions
    set_val(expression, val)
```
When the array literal has only one elements, it goes into the `if len(expressions) == 1` True branch and unpacks the tuple mistakenly. 

The if branch here seems to handle the case where there are unnecessary brackets, e.g., `uint x = (1);`. But it is indistinguishable whether the tuple expression should be unpacked or not when it is an array initialization. 


Resolving patch:
diff --git a/slither/visitors/slithir/expression_to_slithir.py b/slither/visitors/slithir/expression_to_slithir.py
--- a/slither/visitors/slithir/expression_to_slithir.py
+++ b/slither/visitors/slithir/expression_to_slithir.py
@@ -220,6 +220,12 @@ def _post_assignement_operation(self, expression: AssignmentOperation) -> None:
                 operation.set_expression(expression)
                 self._result.append(operation)
                 set_val(expression, left)
+            elif isinstance(left.type, ArrayType):
+                # Special case for init of array, when the right has only one element
+                operation = InitArray([right], left)
+                operation.set_expression(expression)
+                self._result.append(operation)
+                set_val(expression, left)
             else:
                 operation = convert_assignment(
                     left, right, expression.type, expression.expression_return_type


Test patch:
diff --git a/tests/test_ssa_generation.py b/tests/test_ssa_generation.py
--- a/tests/test_ssa_generation.py
+++ b/tests/test_ssa_generation.py
@@ -27,6 +27,7 @@
     BinaryType,
     InternalCall,
     Index,
+    InitArray,
 )
 from slither.slithir.utils.ssa import is_used_later
 from slither.slithir.variables import (
@@ -796,20 +797,20 @@ def test_memory_array():
             uint val1;
             uint val2;
         }
-        
+
         function test_array() internal {
             A[] memory a= new A[](4);
             // Create REF_0 -> a_1[2]
             accept_array_entry(a[2]);
-            
+
             // Create REF_1 -> a_1[3]
             accept_array_entry(a[3]);
-            
+
             A memory alocal;
             accept_array_entry(alocal);
-            
+
         }
-        
+
         // val_1 = ϕ(val_0, REF_0, REF_1, alocal_1)
         // val_0 is an unknown external value
         function accept_array_entry(A memory val) public returns (uint) {
@@ -818,9 +819,9 @@ def test_memory_array():
             // Create REF_2 -> val_1.val1
             return b(val.val1);
         }
-        
+
         function b(uint arg) public returns (uint){
-            // arg_1 = ϕ(arg_0, zero_1, REF_2) 
+            // arg_1 = ϕ(arg_0, zero_1, REF_2)
             return arg + 1;
         }
     }"""
@@ -862,12 +863,12 @@ def test_storage_array():
             uint val1;
             uint val2;
         }
-        
+
         // NOTE(hbrodin): a is never written, should only become a_0. Same for astorage (astorage_0). Phi-nodes at entry
-        // should only add new versions of a state variable if it is actually written. 
+        // should only add new versions of a state variable if it is actually written.
         A[] a;
         A astorage;
-        
+
         function test_array() internal {
             accept_array_entry(a[2]);
             accept_array_entry(a[3]);
@@ -877,7 +878,7 @@ def test_storage_array():
         function accept_array_entry(A storage val) internal returns (uint) {
             // val is either a[2], a[3] or astorage_0. Ideally this could be identified.
             uint five = 5;
-            
+
             // NOTE(hbrodin): If the following line is enabled, there would ideally be a phi-node representing writes
             // to either a or astorage.
             //val.val2 = 4;
@@ -1059,3 +1060,20 @@ def test_issue_473():
         # return is for second phi
         assert len(return_value.values) == 1
         assert second_phi.lvalue in return_value.values
+
+
+def test_issue_1748():
+    source = """
+    contract Contract {
+        uint[] arr;
+        function foo(uint i) public {
+            arr = [1];
+        }
+    }
+    """
+    with slither_from_source(source) as slither:
+        c = slither.get_contract_from_name("Contract")[0]
+        f = c.functions[0]
+        operations = f.slithir_operations
+        assign_op = operations[0]
+        assert isinstance(assign_op, InitArray)