Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

raise ValidationError if ether is sent to non payable functions #1017

Merged
merged 1 commit into from
Sep 19, 2018

Conversation

ankit-maverick
Copy link
Contributor

@ankit-maverick ankit-maverick commented Aug 24, 2018

What was wrong?

Related to Issue #902

How was it fixed?

Raised ValidationError in call, transact, estimateGas, buildTransaction if ether is sent to functions that are not payable.

TODOs:

  • Get the approach and code reviewed by @carver
  • Write tests

Cute Animal Picture

https://i.ytimg.com/vi/g4xLVP_eFec/maxresdefault.jpg

web3/contract.py Outdated
@@ -432,6 +433,15 @@ def estimateGas(self, transaction=None):
"Please ensure that this contract instance has an address."
)

if 'value' in estimate_transaction:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of duplicating this logic in multiple places, I think all of this code can go in the validation middleware.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Looks like this can be a def validate_payable(transaction, abi) to consolidate the logic. Other than that, the approach is fine 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, a helper function would do.
The problem with the middleware approach is that the abi will not be available there.

Copy link
Contributor Author

@ankit-maverick ankit-maverick Aug 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@voith I was wondering the same when I looked at the validation code in middleware since abi is not available there.

@carver Yeah, I will consolidate it.

@ankit-maverick
Copy link
Contributor Author

ankit-maverick commented Aug 25, 2018

@voith @carver I have consolidated the logic as discussed and decided to keep def validate_payable(transaction, abi) in contract.py. Let me know if you think there's a better place to keep it.

I was wondering if tests are needed for this change and if yes, I need some direction on how I should go about them. I tried to look up tests inside tests/core/contracts/ for the various ValueErrors being raised in call, transact, estimateGas, buildTransaction in contract.py but could not find any tests corresponding to them.

@ankit-maverick
Copy link
Contributor Author

@carver @voith ^Need your inputs on the above comment.

@pipermerriam
Copy link
Member

pipermerriam commented Aug 27, 2018

@ankit-maverick yes, we'll want to have tests added for this. These should probably be done with a new contract as to not conflate any of the existing testing contracts. Something simple like

contract PayableTester {
  bool public wasCalled;

  function doPayableCall() public payable {
    wasCalled = true;
  }
  function doNoValueCall() public {
    wasCalled = true;
  }
}

For testing those, you can do something simple like:

  • pre check that wasCalled == False
  • try to transact with/without value
  • verify exception is raised (if one is expected)
  • verify wasCalled is the expected value.

These tests should be done for all of call/transact/estimateGas/buildTransaction and clearly some of the assertions in the list above don't make sense for anything other than transact.

@pipermerriam
Copy link
Member

@ankit-maverick let me know if you need help getting the tests green.

@ankit-maverick
Copy link
Contributor Author

@pipermerriam Sure. I will finish this off this weekend and ask here when I need help.

@ankit-maverick
Copy link
Contributor Author

ankit-maverick commented Sep 6, 2018

@pipermerriam One issue I faced last weekend with these tests is understanding the difference between 2 runs of the same tests [<lambda>-func_first] that got PASSED and [<lambda>-func_last] that got FAILED.

tests/core/contracts/test_contract_buildTransaction.py::test_build_transaction_paying_to_nonpayable_function[<lambda>-func_first] PASSED                                                                 [  5%]
tests/core/contracts/test_contract_buildTransaction.py::test_build_transaction_paying_to_nonpayable_function[<lambda>-func_last] FAILED                                                                  [  7%]
.
.
.
tests/core/contracts/test_contract_buildTransaction.py::test_build_transaction_paying_to_nonpayable_function[address_conversion_func1-func_first] PASSED                                                 [ 55%]
tests/core/contracts/test_contract_buildTransaction.py::test_build_transaction_paying_to_nonpayable_function[address_conversion_func1-func_last] FAILED                                                  [ 57%]

What's difference between both of them?

Edit : Grammar

@carver
Copy link
Collaborator

carver commented Sep 6, 2018

There is an old API style for interacting with contracts that looks like:

contract.buildTransaction(...transaction_params...).doNoValueCall()

(ie~ function last)

versus the new style of:

contract.functions.doNoValueCall().buildTransaction(...transaction_params...)

(ie~ function first)

The test failures imply that the changes work with the new style, but not the old style, API.


If you're curious, here is how the tests are set up:

if api_style == 'func_first':
function = contract.functions[contract_function]
result = getattr(function(*func_args, **func_kwargs), api_call_desig)(tx_params)
elif api_style == 'func_last':
api_call_cls = getattr(contract, api_call_desig)
with pytest.deprecated_call():
result = getattr(api_call_cls(tx_params), contract_function)(*func_args, **func_kwargs)

@ankit-maverick
Copy link
Contributor Author

ankit-maverick commented Sep 9, 2018

@carver @pipermerriam I have finished tests for buildTransaction, call, estimateGas, transact but am finding it tough to understand the error logs to see why the old API style (func_last) for interacting with contracts is failing when I pass tx_params={'value': 1} to any of these function calls. Need your help on this.

Edit : It doesn't fail in the way it is currently failing when I pass any other parameter like gasPrice or don't pass any parameter at all. The error stacktrace flew through evm and eth-tester but I couldn't make much sense of it and find the root cause of the issue.


function doPayableCall() public payable {
wasCalled = true;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we're never really testing doPayableCall here. That's maybe fine (since it should be covered by other tests), but let's remove it from the contract and ABI, then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I will remove it.

payable_tester_contract,
estimateGas):
with pytest.raises(ValidationError):
estimateGas(contract=payable_tester_contract,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would focus on why this is failing. Many of the other functions will fail if this fails, because calls like buildTransaction will call estimateGas under the hood, if no gas limit is supplied (which is common).

Can you reproduce outside of testing, with payable_tester_contract.estimateGas(dict(value=1)).doNoValueCall()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could reproduce it. Will dig deeper into it.

>>> contract_instance.estimateGas(dict(value=1)).doNoValueCall()
API Exception: {'type': 'TransactionFailed', 'args': (), 'message': ''}
Traceback (most recent call last):
  File "/Users/ankit/web3.py/venv/lib/python3.7/site-packages/jsonrpc/manager.py", line 112, in _get_responses
  File "/Users/ankit/web3.py/venv/lib/python3.7/site-packages/testrpc/server.py", line 34, in inner
  File "/Users/ankit/web3.py/venv/lib/python3.7/site-packages/testrpc/rpc.py", line 138, in eth_estimateGas
  File "/Users/ankit/web3.py/venv/lib/python3.7/site-packages/testrpc/client/client.py", line 253, in estimate_gas
  File "/Users/ankit/web3.py/venv/lib/python3.7/site-packages/testrpc/client/client.py", line 263, in send_transaction
  File "/Users/ankit/web3.py/venv/lib/python3.7/site-packages/testrpc/client/client.py", line 58, in inner
  File "/Users/ankit/web3.py/venv/lib/python3.7/site-packages/testrpc/client/utils.py", line 104, in inner
  File "/Users/ankit/web3.py/venv/lib/python3.7/site-packages/testrpc/client/client.py", line 225, in _send_transaction
  File "/Users/ankit/web3.py/venv/lib/python3.7/site-packages/ethereum/tester.py", line 338, in send
  File "/Users/ankit/web3.py/venv/lib/python3.7/site-packages/ethereum/tester.py", line 296, in _send
ethereum.tester.TransactionFailed
127.0.0.1 - - [13/Sep/2018 08:08:42] "POST / HTTP/1.1" 200 147
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/ankit/web3.py/web3/contract.py", line 1506, in estimate_gas_for_function
    gas_estimate = web3.eth.estimateGas(estimate_transaction)
  File "/Users/ankit/web3.py/web3/eth.py", line 304, in estimateGas
    [transaction],
  File "/Users/ankit/web3.py/web3/manager.py", line 112, in request_blocking
    raise ValueError(response["error"])
ValueError: {'code': -32000, 'message': 'Server error', 'data': {'type': 'TransactionFailed', 'args': [], 'message': ''}}

@voith
Copy link
Contributor

voith commented Sep 13, 2018

@ankit-maverick So here's what's happening:
The old API looked something like this:

contract.transact({'value': 1}).doNoValueCall()

Notice here that the function doNoValueCall() is called after transact().
Your validate_payable expects to get abi of the specific function that is being called.

Now for the old API you've added validation in thetransact method. However, since the function is called after calling transact, self.abi contains the entire contract abi (list) and hence your check
if "payable" in abi and not abi["payable"] fails.

You should probably add the check inside Transactor where you will get the function name, from which you can extract the correct abi:

def __getattr__(self, function_name):

Do the same for other methods like estimateGas, call...

def FALLBACK_FUNCTION_CONTRACT(FALLBACK_FUNCTION_CODE,
FALLBACK_FUNCTION_RUNTIME,
FALLBACK_FUNCTION_ABI):
def PAYABLE_TESTER_CONTRACT(PAYABLE_TESTER_CODE,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @njgheorghita I was looking at this stuff and it might be a good test-bed for seeing how py-ethpm and maybe pytest-ethereum could clean up these test fixtures.

@ankit-maverick
Copy link
Contributor Author

@voith Thanks a ton. I have moved validate_payable inside Transactor, Caller for old API methods in the latest commit and the old API tests are passing.

@ankit-maverick
Copy link
Contributor Author

@carver @pipermerriam @voith Need your final review on this PR.

web3/contract.py Outdated

def get_function_abi_from_contract_abi(contract_abi, function_name):
for abi in contract_abi:
if abi['name'] == function_name:
Copy link
Contributor

@voith voith Sep 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A contract can have multiple functions with the same name but different signatures.
eg.

contract FunctionOverloadingContract {
  function someMethod(int i) public {
     // do something
  }
  function someMethod(string s) public {
     // do something
  }
}

get_function_abi_from_contract_abi might return the wrong abi in such a case.

Have a look find_matching_fn_abi to understand more.
This code makes me think that the validation should probably be moved to methods like call_contract_function, transact_with_contract_function, build_transaction_for_function, estimate_gas_for_function . I have not done a complete analysis, but just throwing an idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@voith This idea makes more sense than the approach I have taken. I could get rid of get_function_abi_from_contract_abi and better use find_matching_fn_abi & check validate_payable inside lower level base functions call_contract_function, transact_with_contract_function, build_transaction_for_function, estimate_gas_for_function which are being called from both new and old style APIs.

@ankit-maverick
Copy link
Contributor Author

@carver @pipermerriam @voith Please review. If all looks good, I will squash the commits before we merge this.

web3/contract.py Outdated
if fn_abi is None:
fn_abi = find_matching_fn_abi(contract_abi, function_name, args, kwargs)

validate_payable(transaction, fn_abi)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like all four of these copy/pasted chunks come just before a prepare_transaction(). Can the lines move inside prepare_transaction(), so the repeats can be deleted?

Copy link
Contributor Author

@ankit-maverick ankit-maverick Sep 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, don't know how I missed it. Pushing the change now.

@carver
Copy link
Collaborator

carver commented Sep 18, 2018

Great, I think this is ready for your squash and we can get it merged @ankit-maverick !

@ankit-maverick
Copy link
Contributor Author

@carver @pipermerriam Ready to go in.

@ankit-maverick
Copy link
Contributor Author

@carver @voith @pipermerriam Thanks a lot. This was my first contribution to anything crypto and I learned a lot of concepts while working through this PR(although I assumed it to be a very straightforward PR in the beginning).

@carver
Copy link
Collaborator

carver commented Sep 20, 2018

Thanks for the contribution @ankit-maverick !

@ccolorado
Copy link

Will this make it to the next release (4.7.2)?
I don't want to think on how less miserable my life would have been, if this was included on 4.7.1

@carver
Copy link
Collaborator

carver commented Oct 15, 2018

Ah, this went into the master branch which is on v5, it would need to be backported and merged into v4 to go into 4.7.2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants