Repository: ethereum/web3.py

Issue:
eth_call formatter breaks when supplying third argument
### What was wrong?

`eth_call` supports [a third optional argument](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set) which overrides state prior to a call. web3.py makes an incorrect assumption there should only be two arguments.

```python
  File "web3/middleware/formatting.py", line 74, in apply_formatters
    response = make_request(method, formatted_params)
  File "web3/middleware/attrdict.py", line 33, in middleware
    response = make_request(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
  File "web3/middleware/formatting.py", line 73, in apply_formatters
    formatted_params = formatter(params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
  File "eth_utils/decorators.py", line 91, in wrapper
    return ReturnType(result)
  File "eth_utils/applicators.py", line 58, in apply_formatters_to_sequence
    raise IndexError(
IndexError: Too few formatters for sequence: 2 formatters for […]
```

### How can it be fixed?

Make formatter aware of the third option argument for `eth_call`.


Discussion:
Thanks for raising the issue. We'll dig in next opportunity. PRs welcome; this might be a straightforward one for interested contributors.

Resolving patch:
diff --git a/web3/_utils/method_formatters.py b/web3/_utils/method_formatters.py
--- a/web3/_utils/method_formatters.py
+++ b/web3/_utils/method_formatters.py
@@ -87,9 +87,11 @@
 )
 from web3.types import (
     BlockIdentifier,
+    CallOverrideParams,
     RPCEndpoint,
     RPCResponse,
     TReturn,
+    TxParams,
     _Hash32,
 )
 
@@ -315,6 +317,25 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]:
 )
 
 
+call_without_override: Callable[
+    [Tuple[TxParams, BlockIdentifier]],
+    Tuple[Dict[str, Any], int]
+]
+call_without_override = apply_formatters_to_sequence([
+    transaction_param_formatter,
+    to_hex_if_integer,
+])
+call_with_override: Callable[
+    [Tuple[TxParams, BlockIdentifier, CallOverrideParams]],
+    Tuple[Dict[str, Any], int, Dict[str, Any]],
+]
+call_with_override = apply_formatters_to_sequence([
+    transaction_param_formatter,
+    to_hex_if_integer,
+    lambda x: x,
+])
+
+
 estimate_gas_without_block_id: Callable[[Dict[str, Any]], Dict[str, Any]]
 estimate_gas_without_block_id = apply_formatter_at_index(transaction_param_formatter, 0)
 estimate_gas_with_block_id: Callable[
@@ -374,10 +395,10 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]:
     RPC.eth_getUncleByBlockHashAndIndex: apply_formatter_at_index(to_hex_if_integer, 1),
     RPC.eth_newFilter: apply_formatter_at_index(filter_params_formatter, 0),
     RPC.eth_getLogs: apply_formatter_at_index(filter_params_formatter, 0),
-    RPC.eth_call: apply_formatters_to_sequence([
-        transaction_param_formatter,
-        to_hex_if_integer,
-    ]),
+    RPC.eth_call: apply_one_of_formatters((
+        (is_length(2), call_without_override),
+        (is_length(3), call_with_override),
+    )),
     RPC.eth_estimateGas: apply_one_of_formatters((
         (is_length(1), estimate_gas_without_block_id),
         (is_length(2), estimate_gas_with_block_id),
diff --git a/web3/eth.py b/web3/eth.py
--- a/web3/eth.py
+++ b/web3/eth.py
@@ -88,6 +88,7 @@
     ENS,
     BlockData,
     BlockIdentifier,
+    CallOverrideParams,
     FilterParams,
     GasPriceStrategy,
     LogReceipt,
@@ -521,8 +522,9 @@ def sign_munger(
     def call_munger(
         self,
         transaction: TxParams,
-        block_identifier: Optional[BlockIdentifier] = None
-    ) -> Tuple[TxParams, BlockIdentifier]:
+        block_identifier: Optional[BlockIdentifier] = None,
+        state_override: Optional[CallOverrideParams] = None,
+    ) -> Union[Tuple[TxParams, BlockIdentifier], Tuple[TxParams, BlockIdentifier, CallOverrideParams]]:  # noqa-E501
         # TODO: move to middleware
         if 'from' not in transaction and is_checksum_address(self.default_account):
             transaction = assoc(transaction, 'from', self.default_account)
@@ -531,7 +533,10 @@ def call_munger(
         if block_identifier is None:
             block_identifier = self.default_block
 
-        return (transaction, block_identifier)
+        if state_override is None:
+            return (transaction, block_identifier)
+        else:
+            return (transaction, block_identifier, state_override)
 
     call: Method[Callable[..., Union[bytes, bytearray]]] = Method(
         RPC.eth_call,
diff --git a/web3/types.py b/web3/types.py
--- a/web3/types.py
+++ b/web3/types.py
@@ -193,6 +193,15 @@ class LogReceipt(TypedDict):
     "value": Wei,
 }, total=False)
 
+
+class CallOverrideParams(TypedDict):
+    balance: Optional[Wei]
+    nonce: Optional[int]
+    code: Optional[Union[bytes, HexStr]]
+    state: Optional[Dict[str, Any]]
+    stateDiff: Optional[Dict[Address, Dict[str, Any]]]
+
+
 GasPriceStrategy = Callable[["Web3", TxParams], Wei]
 
 


Test patch:
diff --git a/tests/integration/test_ethereum_tester.py b/tests/integration/test_ethereum_tester.py
--- a/tests/integration/test_ethereum_tester.py
+++ b/tests/integration/test_ethereum_tester.py
@@ -378,6 +378,10 @@ def test_eth_getTransactionReceipt_mined_deprecated(self, web3, block_with_txn,
                                                                 block_with_txn,
                                                                 mined_txn_hash)
 
+    @pytest.mark.xfail(raises=TypeError, reason="call override param not implemented on eth-tester")
+    def test_eth_call_with_override(self, web3, revert_contract):
+        super().test_eth_call_with_override(web3, revert_contract)
+
     def test_eth_call_revert_with_msg(self, web3, revert_contract, unlocked_account):
         with pytest.raises(TransactionFailed,
                            match='execution reverted: Function has been reverted'):
diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py
--- a/web3/_utils/module_testing/eth_module.py
+++ b/web3/_utils/module_testing/eth_module.py
@@ -974,6 +974,28 @@ def test_eth_call(
         result = web3.codec.decode_single('uint256', call_result)
         assert result == 18
 
+    def test_eth_call_with_override(
+        self, web3: "Web3", revert_contract: "Contract"
+    ) -> None:
+        coinbase = web3.eth.coinbase
+        txn_params = revert_contract._prepare_transaction(
+            fn_name='normalFunction',
+            transaction={'from': coinbase, 'to': revert_contract.address},
+        )
+        call_result = web3.eth.call(txn_params)
+        result = web3.codec.decode_single('bool', call_result)
+        assert result is True
+
+        # override runtime bytecode: `normalFunction` returns `false`
+        override_code = '0x6080604052348015600f57600080fd5b5060043610603c5760003560e01c8063185c38a4146041578063c06a97cb146049578063d67e4b84146051575b600080fd5b60476071565b005b604f60df565b005b605760e4565b604051808215151515815260200191505060405180910390f35b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601b8152602001807f46756e6374696f6e20686173206265656e2072657665727465642e000000000081525060200191505060405180910390fd5b600080fd5b60008090509056fea2646970667358221220bb71e9e9a2e271cd0fbe833524a3ea67df95f25ea13aef5b0a761fa52b538f1064736f6c63430006010033'  # noqa: E501
+        call_result = web3.eth.call(
+            txn_params,
+            'latest',
+            {revert_contract.address: {'code': override_code}}
+        )
+        result = web3.codec.decode_single('bool', call_result)
+        assert result is False
+
     def test_eth_call_with_0_result(
         self, web3: "Web3", math_contract: "Contract"
     ) -> None: