Repository: crytic/slither

Issue:
[False-Positive]:`msg.value` in a loop when `msg.value` is not transferred
### Describe the issue:

In the following code snippet, Slither detects the following:

<img width="1309" alt="image" src="https://github.com/crytic/slither/assets/31145285/eacf2441-3e2d-464a-97c3-635498e43120">

This seems invalid as `msg.value` here is just checked as a conditional to ensure no value is sent to this function. In any case, no logic inside this function (including inside the internal `_transfer` function) forwards `msg.value`.

### Code example to reproduce the issue:

```solidity

    modifier noNativeTokens() {
        require(msg.value == 0, "Not aimed to receive native tokens");
        _;
    }
    
   function executeRelayCallBatch(
        bytes[] calldata signatures,
        uint256[] calldata nonces,
        uint256[] calldata validityTimestamps,
        uint256[] calldata values,
        bytes[] calldata payloads
    ) public payable noNativeTokens returns (bytes[] memory) {
        if (
            signatures.length != nonces.length ||
            nonces.length != validityTimestamps.length ||
            validityTimestamps.length != values.length ||
            values.length != payloads.length
        ) {
            revert("Batch ExecuteRelayCall Params Length Mismatch");
        }

        bytes[] memory castedVotes = new bytes[](payloads.length);

        for (uint256 ii; ii < payloads.length; ++ii) {
            require(values[ii] == 0, "Batch entry cannot contain value");

            // cast each votes one by one
            castedVotes[ii] = executeRelayCall(
                signatures[ii],
                nonces[ii],
                validityTimestamps[ii],
                payloads[ii]
            );
        }

        return castedVotes;
    }
```

### Version:

0.10.0

### Relevant log output:

_No response_


Discussion:
Thanks for this! Fwiw there is a dedicated issue template for false positives. We can likely update the detector to check if the use of msg.value is only in a conditional (require, assert, if-revert) and not detect those cases, but we may need to inspect whether it's being compared to zero (cc @smonicas )https://github.com/crytic/slither/blob/e3dcf1ecd3e9de60da046de471c5663ab637993a/slither/core/cfg/node.py#L545
https://github.com/crytic/slither/blob/e3dcf1ecd3e9de60da046de471c5663ab637993a/slither/detectors/statements/msg_value_in_loop.py#L38-L42

Resolving patch:
diff --git a/slither/detectors/statements/msg_value_in_loop.py b/slither/detectors/statements/msg_value_in_loop.py
--- a/slither/detectors/statements/msg_value_in_loop.py
+++ b/slither/detectors/statements/msg_value_in_loop.py
@@ -8,6 +8,9 @@
 from slither.slithir.operations import InternalCall
 from slither.core.declarations import SolidityVariableComposed, Contract
 from slither.utils.output import Output
+from slither.slithir.variables.constant import Constant
+from slither.core.variables import Variable
+from slither.core.expressions.literal import Literal
 
 
 def detect_msg_value_in_loop(contract: Contract) -> List[Node]:
@@ -37,6 +40,21 @@ def msg_value_in_loop(
 
     for ir in node.all_slithir_operations():
         if in_loop_counter > 0 and SolidityVariableComposed("msg.value") in ir.read:
+            # If we find a conditional expression with msg.value and is compared to 0 we don't report it
+            if ir.node.is_conditional() and SolidityVariableComposed("msg.value") in ir.read:
+                compared_to = (
+                    ir.read[1]
+                    if ir.read[0] == SolidityVariableComposed("msg.value")
+                    else ir.read[0]
+                )
+                if (
+                    isinstance(compared_to, Constant)
+                    and compared_to.value == 0
+                    or isinstance(compared_to, Variable)
+                    and isinstance(compared_to.expression, Literal)
+                    and str(compared_to.expression.value) == "0"
+                ):
+                    continue
             results.append(ir.node)
         if isinstance(ir, (InternalCall)):
             msg_value_in_loop(ir.function.entry_point, in_loop_counter, visited, results)


Test patch:
diff --git a/tests/e2e/detectors/test_data/msg-value-loop/0.4.25/msg_value_loop.sol b/tests/e2e/detectors/test_data/msg-value-loop/0.4.25/msg_value_loop.sol
--- a/tests/e2e/detectors/test_data/msg-value-loop/0.4.25/msg_value_loop.sol
+++ b/tests/e2e/detectors/test_data/msg-value-loop/0.4.25/msg_value_loop.sol
@@ -26,4 +26,38 @@ contract C{
         }
     }
 
+    function good1(address[] memory receivers) public payable {
+        require(msg.value == 0);
+        for (uint256 i = 0; i < receivers.length; i++) {
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good2(address[] memory receivers) public payable {
+        uint zero = 0;
+        for (uint256 i = 0; i < receivers.length; i++) {
+            assert(msg.value == zero);
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good3(address[] memory receivers) public payable {
+        for (uint256 i = 0; i < receivers.length; i++) {
+            if (0 != msg.value) {
+                revert();
+            }
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good4(address[] memory receivers) public payable {
+        for (uint256 i = 0; i < receivers.length; i++) {
+            _g();
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function _g() internal {
+        require(msg.value == 0);
+    }
 }
\ No newline at end of file
diff --git a/tests/e2e/detectors/test_data/msg-value-loop/0.4.25/msg_value_loop.sol-0.4.25.zip b/tests/e2e/detectors/test_data/msg-value-loop/0.4.25/msg_value_loop.sol-0.4.25.zip
Binary files a/tests/e2e/detectors/test_data/msg-value-loop/0.4.25/msg_value_loop.sol-0.4.25.zip and b/tests/e2e/detectors/test_data/msg-value-loop/0.4.25/msg_value_loop.sol-0.4.25.zip differ
diff --git a/tests/e2e/detectors/test_data/msg-value-loop/0.5.16/msg_value_loop.sol b/tests/e2e/detectors/test_data/msg-value-loop/0.5.16/msg_value_loop.sol
--- a/tests/e2e/detectors/test_data/msg-value-loop/0.5.16/msg_value_loop.sol
+++ b/tests/e2e/detectors/test_data/msg-value-loop/0.5.16/msg_value_loop.sol
@@ -26,4 +26,38 @@ contract C{
         }
     }
 
+    function good1(address[] memory receivers) public payable {
+        require(msg.value == 0);
+        for (uint256 i = 0; i < receivers.length; i++) {
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good2(address[] memory receivers) public payable {
+        uint zero = 0;
+        for (uint256 i = 0; i < receivers.length; i++) {
+            assert(msg.value == zero);
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good3(address[] memory receivers) public payable {
+        for (uint256 i = 0; i < receivers.length; i++) {
+            if (0 != msg.value) {
+                revert();
+            }
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good4(address[] memory receivers) public payable {
+        for (uint256 i = 0; i < receivers.length; i++) {
+            _g();
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function _g() internal {
+        require(msg.value == 0);
+    }
 }
\ No newline at end of file
diff --git a/tests/e2e/detectors/test_data/msg-value-loop/0.5.16/msg_value_loop.sol-0.5.16.zip b/tests/e2e/detectors/test_data/msg-value-loop/0.5.16/msg_value_loop.sol-0.5.16.zip
Binary files a/tests/e2e/detectors/test_data/msg-value-loop/0.5.16/msg_value_loop.sol-0.5.16.zip and b/tests/e2e/detectors/test_data/msg-value-loop/0.5.16/msg_value_loop.sol-0.5.16.zip differ
diff --git a/tests/e2e/detectors/test_data/msg-value-loop/0.6.11/msg_value_loop.sol b/tests/e2e/detectors/test_data/msg-value-loop/0.6.11/msg_value_loop.sol
--- a/tests/e2e/detectors/test_data/msg-value-loop/0.6.11/msg_value_loop.sol
+++ b/tests/e2e/detectors/test_data/msg-value-loop/0.6.11/msg_value_loop.sol
@@ -26,4 +26,38 @@ contract C{
         }
     }
 
+    function good1(address[] memory receivers) public payable {
+        require(msg.value == 0);
+        for (uint256 i = 0; i < receivers.length; i++) {
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good2(address[] memory receivers) public payable {
+        uint zero = 0;
+        for (uint256 i = 0; i < receivers.length; i++) {
+            assert(msg.value == zero);
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good3(address[] memory receivers) public payable {
+        for (uint256 i = 0; i < receivers.length; i++) {
+            if (0 != msg.value) {
+                revert();
+            }
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good4(address[] memory receivers) public payable {
+        for (uint256 i = 0; i < receivers.length; i++) {
+            _g();
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function _g() internal {
+        require(msg.value == 0);
+    }
 }
\ No newline at end of file
diff --git a/tests/e2e/detectors/test_data/msg-value-loop/0.6.11/msg_value_loop.sol-0.6.11.zip b/tests/e2e/detectors/test_data/msg-value-loop/0.6.11/msg_value_loop.sol-0.6.11.zip
Binary files a/tests/e2e/detectors/test_data/msg-value-loop/0.6.11/msg_value_loop.sol-0.6.11.zip and b/tests/e2e/detectors/test_data/msg-value-loop/0.6.11/msg_value_loop.sol-0.6.11.zip differ
diff --git a/tests/e2e/detectors/test_data/msg-value-loop/0.7.6/msg_value_loop.sol b/tests/e2e/detectors/test_data/msg-value-loop/0.7.6/msg_value_loop.sol
--- a/tests/e2e/detectors/test_data/msg-value-loop/0.7.6/msg_value_loop.sol
+++ b/tests/e2e/detectors/test_data/msg-value-loop/0.7.6/msg_value_loop.sol
@@ -26,4 +26,38 @@ contract C{
         }
     }
 
+    function good1(address[] memory receivers) public payable {
+        require(msg.value == 0);
+        for (uint256 i = 0; i < receivers.length; i++) {
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good2(address[] memory receivers) public payable {
+        uint zero = 0;
+        for (uint256 i = 0; i < receivers.length; i++) {
+            assert(msg.value == zero);
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good3(address[] memory receivers) public payable {
+        for (uint256 i = 0; i < receivers.length; i++) {
+            if (0 != msg.value) {
+                revert();
+            }
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good4(address[] memory receivers) public payable {
+        for (uint256 i = 0; i < receivers.length; i++) {
+            _g();
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function _g() internal {
+        require(msg.value == 0);
+    }
 }
\ No newline at end of file
diff --git a/tests/e2e/detectors/test_data/msg-value-loop/0.7.6/msg_value_loop.sol-0.7.6.zip b/tests/e2e/detectors/test_data/msg-value-loop/0.7.6/msg_value_loop.sol-0.7.6.zip
Binary files a/tests/e2e/detectors/test_data/msg-value-loop/0.7.6/msg_value_loop.sol-0.7.6.zip and b/tests/e2e/detectors/test_data/msg-value-loop/0.7.6/msg_value_loop.sol-0.7.6.zip differ
diff --git a/tests/e2e/detectors/test_data/msg-value-loop/0.8.0/msg_value_loop.sol b/tests/e2e/detectors/test_data/msg-value-loop/0.8.0/msg_value_loop.sol
--- a/tests/e2e/detectors/test_data/msg-value-loop/0.8.0/msg_value_loop.sol
+++ b/tests/e2e/detectors/test_data/msg-value-loop/0.8.0/msg_value_loop.sol
@@ -26,4 +26,38 @@ contract C{
         }
     }
 
+    function good1(address[] memory receivers) public payable {
+        require(msg.value == 0);
+        for (uint256 i = 0; i < receivers.length; i++) {
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good2(address[] memory receivers) public payable {
+        uint zero = 0;
+        for (uint256 i = 0; i < receivers.length; i++) {
+            assert(msg.value == zero);
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good3(address[] memory receivers) public payable {
+        for (uint256 i = 0; i < receivers.length; i++) {
+            if (0 != msg.value) {
+                revert();
+            }
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function good4(address[] memory receivers) public payable {
+        for (uint256 i = 0; i < receivers.length; i++) {
+            _g();
+            balances[receivers[i]] += 1;
+        }
+    }
+
+    function _g() internal {
+        require(msg.value == 0);
+    }
 }
\ No newline at end of file
diff --git a/tests/e2e/detectors/test_data/msg-value-loop/0.8.0/msg_value_loop.sol-0.8.0.zip b/tests/e2e/detectors/test_data/msg-value-loop/0.8.0/msg_value_loop.sol-0.8.0.zip
Binary files a/tests/e2e/detectors/test_data/msg-value-loop/0.8.0/msg_value_loop.sol-0.8.0.zip and b/tests/e2e/detectors/test_data/msg-value-loop/0.8.0/msg_value_loop.sol-0.8.0.zip differ