Skip to content

TypeError: AbstractContract._abstract_function_call() got multiple values for argument 'signer' #136

Description

@KELs7

While writing test cases for your contract, you can encounter the error message TypeError: AbstractContract._abstract_function_call() got multiple values for argument 'signer' and it might take you several hours to notice that you forgot to use a keyword argument when calling a contract method.

exploit_contract.setlisting(listing_id) # does not work
>> TypeError: AbstractContract._abstract_function_call() got multiple values for argument 'signer'

exploit_contract.setlisting(l=listing_id) # works

The Root Cause: Dynamic Method Creation in AbstractContract

The problem lies in contracting/client.py, specifically within the __init__ method of the AbstractContract class.

Here's the relevant section from contracting/client.py:

class AbstractContract:
    def __init__(self, name, signer, environment, executor: Executor, funcs, return_full_output=False):
        # ... (other initializations) ...

        # set up virtual functions
        for f in funcs:
            # unpack tuple packed in SenecaClient
            func, kwargs = f  # For setlisting, func='setlisting', kwargs=['l']

            # set the kwargs to None. these will fail if they are not provided
            default_kwargs = {}
            for kwarg in kwargs:
                default_kwargs[kwarg] = None # default_kwargs becomes {'l': None}

            # each function is a partial that allows kwarg overloading and overriding
            setattr(self, func, partial(self._abstract_function_call, # <--- THIS IS THE KEY
                                        signer=self.signer,
                                        contract_name=self.name,
                                        executor=self.executor,
                                        func=func,
                                        return_full_output=return_full_output,
                                        # environment=self.environment,
                                        **default_kwargs))

This code does the following:

  1. It loops through all the exported functions of a smart contract.
  2. For each function, it creates a functools.partial object. A partial is like a function template where some arguments are pre-filled.
  3. This partial object is set as an attribute on the AbstractContract instance. So, exploit_contract.setlisting is not a regular function; it's a partial that wraps _abstract_function_call.

The Target Function: _abstract_function_call

All dynamically created methods ultimately call _abstract_function_call. Let's look at its signature:

# contracting/client.py

    def _abstract_function_call(
            self,
            signer, # <--- First positional argument after self
            executor,
            contract_name,
            func,
            environment=None,
            stamps=constants.DEFAULT_STAMPS,
            metering=None,
            now=None,
            return_full_output=False,
            **kwargs # <--- Collects all other keyword arguments
    ):
        # ... function logic ...

Notice that signer is the first positional argument after self. This is crucial.

Tracing the Failing Call: exploit_contract.setlisting(listing_id)

  1. When you call exploit_contract.setlisting(listing_id), you are invoking the partial object created in __init__.
  2. You are providing one positional argument: listing_id.
  3. Python's argument-passing rules state that positional arguments fill the function's parameters from left to right.
  4. The first parameter of _abstract_function_call is self (which is automatically passed).
  5. The second parameter is signer. Therefore, your positional argument listing_id is assigned to the signer parameter.
  6. However, the partial object itself was created with signer=self.signer already "baked in" as a keyword argument.
  7. So, when the call is executed, Python sees two values for the signer argument:
    • One from your positional argument (signer=listing_id).
    • One from the partial's keyword argument (signer=...).

This conflict results in the error: TypeError: AbstractContract._abstract_function_call() got multiple values for argument 'signer'.

Tracing the Working Call: exploit_contract.setlisting(l=listing_id)

  1. When you call exploit_contract.setlisting(l=listing_id), you are providing one keyword argument.
  2. The partial object has its pre-filled keyword arguments (signer, contract_name, etc.). Your new keyword argument l=listing_id is merged with them.
  3. When _abstract_function_call is called, the argument l is not a named parameter in its signature. It is therefore collected by the **kwargs dictionary at the end.
  4. The signer parameter is filled only once by the keyword argument from the partial. There is no conflict.
  5. Inside _abstract_function_call, the kwargs dictionary (which now contains {'l': listing_id}) is correctly passed down to the executor, which executes the smart contract function.

The Proposed Change

The goal of a good framework is to guide the developer and provide clear, actionable error messages. The current situation fails that test.

We can fix this by making a small but powerful change to the _abstract_function_call method in contracting/client.py. The solution is to force the internal arguments (signer, executor, etc.) to be keyword-only arguments. This allows us to cleanly capture any mistaken positional arguments from the user and raise a much more helpful TypeError.

Here is the change in contracting/client.py:

  1. Modify the _abstract_function_call signature: We will add *args to the function signature. In Python, any parameters defined after *args or a bare * become keyword-only. This is exactly what we need.
  2. Add a check for positional arguments: Inside the function, we'll check if the args tuple contains anything. If it does, we know the user called the function incorrectly, and we can raise a descriptive error.

File: contracting/client.py

Original _abstract_function_call method:

    def _abstract_function_call(
            self,
            signer,
            executor,
            contract_name,
            func,
            environment=None,
            stamps=constants.DEFAULT_STAMPS,
            metering=None,
            now=None,
            return_full_output=False,
            **kwargs
    ):
        # ... existing implementation ...

New and Improved _abstract_function_call method:

    def _abstract_function_call(
            self,
            *args,  # Captures any positional arguments like `listing_id`
            signer, # Now a keyword-only argument
            executor, # Keyword-only
            contract_name, # Keyword-only
            func, # Keyword-only
            environment=None,
            stamps=constants.DEFAULT_STAMPS,
            metering=None,
            now=None,
            return_full_output=False,
            **kwargs
    ):
        # --- START: New Error-Checking Logic ---
        if args:
            # Find the expected keyword arguments for the function that was called.
            expected_kwargs = []
            for f_name, kw_list in self.functions:
                if f_name == func:
                    expected_kwargs = kw_list
                    break
            
            example_str = ""
            if expected_kwargs:
                example_str = f" Example: contract.{func}({expected_kwargs[0]}='some_value')"

            raise TypeError(
                f"Error calling '{contract_name}.{func}': "
                f"Positional arguments are not allowed. Please use keyword arguments."
                f"\nExpected keywords: {expected_kwargs}.{example_str}"
            )
        # --- END: New Error-Checking Logic ---

        # The rest of the function remains the same.
        environment = environment or self.environment

How This Fix Works

  1. Capturing Positional Arguments: By adding *args, any positional argument passed by the user (e.g., exploit_contract.setlisting(listing_id)) will be collected into the args tuple. The listing_id value will no longer be incorrectly assigned to the signer parameter.

  2. Enforcing Keyword-Only: The parameters signer, executor, contract_name, and func now must be provided as keywords. This is not a problem, because the functools.partial in AbstractContract.__init__ already provides them as keywords. The internal logic remains valid.

  3. Providing a Clear Error: The new if args: block checks if the user provided any positional arguments.

    • If they did, it looks up the function's expected argument names from the self.functions list that was stored during initialization.
    • It then raises a TypeError with a message that is vastly more helpful:
      • It identifies the function that was called incorrectly (exploit_contract.setlisting).
      • It explicitly states the problem: "Positional arguments are not allowed."
      • It tells the developer exactly how to fix it: "Please use keyword arguments."
      • It even provides the list of expected keywords and a usage example.

The New User Experience

With this change, the developer's experience is transformed from hours of confusion to instant feedback.

Old Experience:

exploit_contract.setlisting(listing_id)
>> TypeError: AbstractContract._abstract_function_call() got multiple values for argument 'signer'
# (Developer is confused: "Why is it talking about 'signer'?")

New Experience:

exploit_contract.setlisting(listing_id)
>> TypeError: Error calling 'exploit_contract.setlisting': Positional arguments are not allowed. Please use keyword arguments.
   Expected keywords: ['l']. Example: contract.setlisting(l='some_value')
# (Developer immediately understands the mistake and how to correct it.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions