Repository: crytic/slither

Issue:
[Bug] contract reports ether as locked when ether is sent in Yul
The following contract reports ether as locked despite it being sent in a Yul block
```
contract FPLockedEther {
  receive() payable external {}

  function yulSendEther() external {
    bool success;
    assembly {
      success := call(gas(), caller(), balance(address()), 0,0,0,0)
    }
  }
}
```
```
Contract locking ether found:
	Contract FPLockedEther (locked-ether.sol#1-13) has payable functions:
	 - FPLockedEther.receive() (locked-ether.sol#2-3)
	But does not have a function to withdraw the ether
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#contracts-that-lock-ether
```

It could be that the IR is incorrect here as it should not be a `SOLIDITY_CALL`
```
Contract FPLockedEther
	Function FPLockedEther.receive() (*)
	Function FPLockedEther.yulSendEther() (*)
		Expression: success = call(uint256,uint256,uint256,uint256,uint256,uint256,uint256)(gas()(),caller()(),balance(uint256)(address()()),0,0,0,0)
		IRs:
			TMP_0(uint256) = SOLIDITY_CALL gas()()
			TMP_1(address) := msg.sender(address)
			TMP_2 = CONVERT this to address
			TMP_3(uint256) = SOLIDITY_CALL balance(uint256)(TMP_2)
			TMP_4(uint256) = SOLIDITY_CALL call(uint256,uint256,uint256,uint256,uint256,uint256,uint256)(TMP_0,TMP_1,TMP_3,0,0,0,0)
			success(bool) := TMP_4(uint256)
```


Discussion:
The following code seems to cause this bug:
https://github.com/crytic/slither/blob/635649207d52feff049d92cd75ec5b19edbfc61e/slither/solc_parsing/yul/parse_yul.py#L728-L729

It probably should be `LowLevelCall` instead. The problem is, that `LowLevelCall` requires `destination` in its constructor to be of type `Union[LocalVariable, LocalIRVariable, TemporaryVariableSSA, TemporaryVariable]`, but at the point the code is invoked, none of these variable types is created (we only have `CallExpression`s, at least for the provided `yulSendEther` implementation).

I may try to fix this issue, but I need more information about it:
- should I use `LowLevelCall` here? 
- if I should, then what to provide as `destination` and `result` parameters? 

So far, `LowLevelCall` objects are only created using `convert_to_low_level` invoked on a `HighLevelCall` object, so they aren't created for Yul code.

Resolving patch:
diff --git a/slither/detectors/attributes/locked_ether.py b/slither/detectors/attributes/locked_ether.py
--- a/slither/detectors/attributes/locked_ether.py
+++ b/slither/detectors/attributes/locked_ether.py
@@ -3,7 +3,7 @@
 """
 from typing import List
 
-from slither.core.declarations.contract import Contract
+from slither.core.declarations import Contract, SolidityFunction
 from slither.detectors.abstract_detector import (
     AbstractDetector,
     DetectorClassification,
@@ -17,7 +17,9 @@
     NewContract,
     LibraryCall,
     InternalCall,
+    SolidityCall,
 )
+from slither.slithir.variables import Constant
 from slither.utils.output import Output
 
 
@@ -68,8 +70,28 @@ def do_no_send_ether(contract: Contract) -> bool:
                         ):
                             if ir.call_value and ir.call_value != 0:
                                 return False
-                        if isinstance(ir, (LowLevelCall)):
-                            if ir.function_name in ["delegatecall", "callcode"]:
+                        if isinstance(ir, (LowLevelCall)) and ir.function_name in [
+                            "delegatecall",
+                            "callcode",
+                        ]:
+                            return False
+                        if isinstance(ir, SolidityCall):
+                            call_can_send_ether = ir.function in [
+                                SolidityFunction(
+                                    "delegatecall(uint256,uint256,uint256,uint256,uint256,uint256)"
+                                ),
+                                SolidityFunction(
+                                    "callcode(uint256,uint256,uint256,uint256,uint256,uint256,uint256)"
+                                ),
+                                SolidityFunction(
+                                    "call(uint256,uint256,uint256,uint256,uint256,uint256,uint256)"
+                                ),
+                            ]
+                            nonzero_call_value = call_can_send_ether and (
+                                not isinstance(ir.arguments[2], Constant)
+                                or ir.arguments[2].value != 0
+                            )
+                            if nonzero_call_value:
                                 return False
                         # If a new internal call or librarycall
                         # Add it to the list to explore


Test patch:
diff --git a/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_4_25_locked_ether_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_4_25_locked_ether_sol__0.txt
--- a/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_4_25_locked_ether_sol__0.txt
+++ b/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_4_25_locked_ether_sol__0.txt
@@ -1,5 +1,10 @@
 Contract locking ether found:
-	Contract OnlyLocked (tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol#26) has payable functions:
+	Contract OnlyLocked (tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol#37) has payable functions:
+	 - Locked.receive() (tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol#4-6)
+	But does not have a function to withdraw the ether
+
+Contract locking ether found:
+	Contract UnlockedAssembly (tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol#27-35) has payable functions:
 	 - Locked.receive() (tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol#4-6)
 	But does not have a function to withdraw the ether
 
diff --git a/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_5_16_locked_ether_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_5_16_locked_ether_sol__0.txt
--- a/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_5_16_locked_ether_sol__0.txt
+++ b/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_5_16_locked_ether_sol__0.txt
@@ -1,5 +1,10 @@
 Contract locking ether found:
-	Contract OnlyLocked (tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol#26) has payable functions:
+	Contract OnlyLocked (tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol#37) has payable functions:
+	 - Locked.receive() (tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol#4-6)
+	But does not have a function to withdraw the ether
+
+Contract locking ether found:
+	Contract UnlockedAssembly (tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol#27-35) has payable functions:
 	 - Locked.receive() (tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol#4-6)
 	But does not have a function to withdraw the ether
 
diff --git a/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_6_11_locked_ether_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_6_11_locked_ether_sol__0.txt
--- a/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_6_11_locked_ether_sol__0.txt
+++ b/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_6_11_locked_ether_sol__0.txt
@@ -1,5 +1,5 @@
 Contract locking ether found:
-	Contract OnlyLocked (tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol#26) has payable functions:
+	Contract OnlyLocked (tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol#36) has payable functions:
 	 - Locked.receive_eth() (tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol#4-6)
 	But does not have a function to withdraw the ether
 
diff --git a/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_7_6_locked_ether_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_7_6_locked_ether_sol__0.txt
--- a/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_7_6_locked_ether_sol__0.txt
+++ b/tests/e2e/detectors/snapshots/detectors__detector_LockedEther_0_7_6_locked_ether_sol__0.txt
@@ -1,5 +1,5 @@
 Contract locking ether found:
-	Contract OnlyLocked (tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol#26) has payable functions:
+	Contract OnlyLocked (tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol#36) has payable functions:
 	 - Locked.receive_eth() (tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol#4-6)
 	But does not have a function to withdraw the ether
 
diff --git a/tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol b/tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol
--- a/tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol
+++ b/tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol
@@ -23,4 +23,15 @@ contract Unlocked is Locked, Send{
 
 }
 
+// Still reported because solidity < 0.6.0 doesn't have assembly in the AST
+contract UnlockedAssembly is Locked{
+
+    function withdraw() public {
+        assembly {
+            let success := call(gas(), caller(),100,0,0,0,0)    
+        }
+    }
+
+}
+
 contract OnlyLocked is Locked{ }
diff --git a/tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol-0.4.25.zip b/tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol-0.4.25.zip
Binary files a/tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol-0.4.25.zip and b/tests/e2e/detectors/test_data/locked-ether/0.4.25/locked_ether.sol-0.4.25.zip differ
diff --git a/tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol b/tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol
--- a/tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol
+++ b/tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol
@@ -23,4 +23,15 @@ contract Unlocked is Locked, Send{
 
 }
 
+// Still reported because solidity < 0.6.0 doesn't have assembly in the AST
+contract UnlockedAssembly is Locked{
+
+    function withdraw() public {
+        assembly {
+            let success := call(gas(), caller(),100,0,0,0,0)    
+        }
+    }
+
+}
+
 contract OnlyLocked is Locked{ }
diff --git a/tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol-0.5.16.zip b/tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol-0.5.16.zip
Binary files a/tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol-0.5.16.zip and b/tests/e2e/detectors/test_data/locked-ether/0.5.16/locked_ether.sol-0.5.16.zip differ
diff --git a/tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol b/tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol
--- a/tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol
+++ b/tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol
@@ -23,4 +23,14 @@ contract Unlocked is Locked, Send{
 
 }
 
+contract UnlockedAssembly is Locked{
+
+    function withdraw() public {
+        assembly {
+            let success := call(gas(), caller(),100,0,0,0,0)    
+        }
+    }
+
+}
+
 contract OnlyLocked is Locked{ }
diff --git a/tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol-0.6.11.zip b/tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol-0.6.11.zip
Binary files a/tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol-0.6.11.zip and b/tests/e2e/detectors/test_data/locked-ether/0.6.11/locked_ether.sol-0.6.11.zip differ
diff --git a/tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol b/tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol
--- a/tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol
+++ b/tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol
@@ -23,4 +23,14 @@ contract Unlocked is Locked, Send{
 
 }
 
+contract UnlockedAssembly is Locked{
+
+    function withdraw() public {
+        assembly {
+            let success := call(gas(), caller(),100,0,0,0,0)    
+        }
+    }
+
+}
+
 contract OnlyLocked is Locked{ }
diff --git a/tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol-0.7.6.zip b/tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol-0.7.6.zip
Binary files a/tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol-0.7.6.zip and b/tests/e2e/detectors/test_data/locked-ether/0.7.6/locked_ether.sol-0.7.6.zip differ