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:
- It loops through all the exported functions of a smart contract.
- For each function, it creates a
functools.partial object. A partial is like a function template where some arguments are pre-filled.
- 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)
- When you call
exploit_contract.setlisting(listing_id), you are invoking the partial object created in __init__.
- You are providing one positional argument:
listing_id.
- Python's argument-passing rules state that positional arguments fill the function's parameters from left to right.
- The first parameter of
_abstract_function_call is self (which is automatically passed).
- The second parameter is
signer. Therefore, your positional argument listing_id is assigned to the signer parameter.
- However, the
partial object itself was created with signer=self.signer already "baked in" as a keyword argument.
- 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)
- When you call
exploit_contract.setlisting(l=listing_id), you are providing one keyword argument.
- The
partial object has its pre-filled keyword arguments (signer, contract_name, etc.). Your new keyword argument l=listing_id is merged with them.
- 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.
- The
signer parameter is filled only once by the keyword argument from the partial. There is no conflict.
- 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:
- 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.
- 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
-
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.
-
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.
-
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.)
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.The Root Cause: Dynamic Method Creation in
AbstractContractThe problem lies in
contracting/client.py, specifically within the__init__method of theAbstractContractclass.Here's the relevant section from
contracting/client.py:This code does the following:
functools.partialobject. Apartialis like a function template where some arguments are pre-filled.partialobject is set as an attribute on theAbstractContractinstance. So,exploit_contract.setlistingis not a regular function; it's apartialthat wraps_abstract_function_call.The Target Function:
_abstract_function_callAll dynamically created methods ultimately call
_abstract_function_call. Let's look at its signature:Notice that
signeris the first positional argument afterself. This is crucial.Tracing the Failing Call:
exploit_contract.setlisting(listing_id)exploit_contract.setlisting(listing_id), you are invoking thepartialobject created in__init__.listing_id._abstract_function_callisself(which is automatically passed).signer. Therefore, your positional argumentlisting_idis assigned to thesignerparameter.partialobject itself was created withsigner=self.signeralready "baked in" as a keyword argument.signerargument:signer=listing_id).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)exploit_contract.setlisting(l=listing_id), you are providing one keyword argument.partialobject has its pre-filled keyword arguments (signer,contract_name, etc.). Your new keyword argumentl=listing_idis merged with them._abstract_function_callis called, the argumentlis not a named parameter in its signature. It is therefore collected by the**kwargsdictionary at the end.signerparameter is filled only once by the keyword argument from thepartial. There is no conflict._abstract_function_call, thekwargsdictionary (which now contains{'l': listing_id}) is correctly passed down to theexecutor, 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_callmethod incontracting/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 helpfulTypeError.Here is the change in
contracting/client.py:_abstract_function_callsignature: We will add*argsto the function signature. In Python, any parameters defined after*argsor a bare*become keyword-only. This is exactly what we need.argstuple contains anything. If it does, we know the user called the function incorrectly, and we can raise a descriptive error.File:
contracting/client.pyOriginal
_abstract_function_callmethod:New and Improved
_abstract_function_callmethod:How This Fix Works
Capturing Positional Arguments: By adding
*args, any positional argument passed by the user (e.g.,exploit_contract.setlisting(listing_id)) will be collected into theargstuple. Thelisting_idvalue will no longer be incorrectly assigned to thesignerparameter.Enforcing Keyword-Only: The parameters
signer,executor,contract_name, andfuncnow must be provided as keywords. This is not a problem, because thefunctools.partialinAbstractContract.__init__already provides them as keywords. The internal logic remains valid.Providing a Clear Error: The new
if args:block checks if the user provided any positional arguments.self.functionslist that was stored during initialization.TypeErrorwith a message that is vastly more helpful:exploit_contract.setlisting).The New User Experience
With this change, the developer's experience is transformed from hours of confusion to instant feedback.
Old Experience:
New Experience: