From ef616c55f5874ab4b1364c71acda7739cf129d3a Mon Sep 17 00:00:00 2001 From: Ankit Date: Wed, 19 Sep 2018 10:32:05 +0530 Subject: [PATCH] raise ValidationError on sending ether to non-payable functions --- tests/core/contracts/conftest.py | 94 ++++++++++++++----- .../test_contract_buildTransaction.py | 39 ++++++++ .../contracts/test_contract_call_interface.py | 18 ++++ .../contracts/test_contract_estimateGas.py | 45 +++++++++ .../test_contract_transact_interface.py | 56 +++++++++++ web3/_utils/contracts.py | 19 ++++ web3/contract.py | 1 + 7 files changed, 249 insertions(+), 23 deletions(-) diff --git a/tests/core/contracts/conftest.py b/tests/core/contracts/conftest.py index e407afd910..ca14b2da32 100644 --- a/tests/core/contracts/conftest.py +++ b/tests/core/contracts/conftest.py @@ -387,51 +387,52 @@ def ArraysContract(web3, ARRAYS_CONTRACT): return web3.eth.contract(**ARRAYS_CONTRACT) -CONTRACT_FALLBACK_FUNCTION_SOURCE = """ -contract A { - uint data; - function A() public payable { data = 0; } - function getData() returns (uint r) { return data; } - function() { data = 1; } +CONTRACT_PAYABLE_TESTER_SOURCE = """ +contract PayableTester { + bool public wasCalled; + + function doNoValueCall() public { + wasCalled = true; + } } """ -CONTRACT_FALLBACK_FUNCTION_CODE = "60606040526000808190555060ae806100196000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633bc5de30146053575b3415604957600080fd5b6001600081905550005b3415605d57600080fd5b60636079565b6040518082815260200191505060405180910390f35b600080549050905600a165627a7a72305820045439389e4742569ec078687e6a0c81997709778a0097adbe07ccfd9f7b1a330029" # noqa: E501 +CONTRACT_PAYABLE_TESTER_CODE = "608060405234801561001057600080fd5b5060e88061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063c680362214604e578063e4cb8f5c14607a575b600080fd5b348015605957600080fd5b506060608e565b604051808215151515815260200191505060405180910390f35b348015608557600080fd5b50608c60a0565b005b6000809054906101000a900460ff1681565b60016000806101000a81548160ff0219169083151502179055505600a165627a7a723058205362c7376eda918b0dc3a75d0ffab904a241c9b10b68d5268af6ca405242303e0029" # noqa: E501 -CONTRACT_FALLBACK_FUNCTION_RUNTIME = "606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633bc5de30146053575b3415604957600080fd5b6001600081905550005b3415605d57600080fd5b60636079565b6040518082815260200191505060405180910390f35b600080549050905600a165627a7a72305820045439389e4742569ec078687e6a0c81997709778a0097adbe07ccfd9f7b1a330029" # noqa: E501 +CONTRACT_PAYABLE_TESTER_RUNTIME = "6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063c680362214604e578063e4cb8f5c14607a575b600080fd5b348015605957600080fd5b506060608e565b604051808215151515815260200191505060405180910390f35b348015608557600080fd5b50608c60a0565b005b6000809054906101000a900460ff1681565b60016000806101000a81548160ff0219169083151502179055505600a165627a7a723058205362c7376eda918b0dc3a75d0ffab904a241c9b10b68d5268af6ca405242303e0029" # noqa: E501 -CONTRACT_FALLBACK_FUNCTION_ABI = json.loads('[{"constant": false, "inputs": [], "name": "getData", "outputs": [{"name": "r", "type": "uint256"}], "payable": false, "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "payable": true, "stateMutability": "payable", "type": "constructor"}, {"payable": false, "stateMutability": "nonpayable", "type": "fallback"}]') # noqa: E501 +CONTRACT_PAYABLE_TESTER_ABI = json.loads('[{"constant": true, "inputs": [], "name": "wasCalled", "outputs": [{"name": "", "type": "bool"}], "payable": false, "stateMutability": "view", "type": "function"}, {"constant": false, "inputs": [], "name": "doNoValueCall", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"}]') # noqa: E501 @pytest.fixture() -def FALLBACK_FUNCTION_CODE(): - return CONTRACT_FALLBACK_FUNCTION_CODE +def PAYABLE_TESTER_CODE(): + return CONTRACT_PAYABLE_TESTER_CODE @pytest.fixture() -def FALLBACK_FUNCTION_RUNTIME(): - return CONTRACT_FALLBACK_FUNCTION_RUNTIME +def PAYABLE_TESTER_RUNTIME(): + return CONTRACT_PAYABLE_TESTER_RUNTIME @pytest.fixture() -def FALLBACK_FUNCTION_ABI(): - return CONTRACT_FALLBACK_FUNCTION_ABI +def PAYABLE_TESTER_ABI(): + return CONTRACT_PAYABLE_TESTER_ABI @pytest.fixture() -def FALLBACK_FUNCTION_CONTRACT(FALLBACK_FUNCTION_CODE, - FALLBACK_FUNCTION_RUNTIME, - FALLBACK_FUNCTION_ABI): +def PAYABLE_TESTER_CONTRACT(PAYABLE_TESTER_CODE, + PAYABLE_TESTER_RUNTIME, + PAYABLE_TESTER_ABI): return { - 'bytecode': FALLBACK_FUNCTION_CODE, - 'bytecode_runtime': FALLBACK_FUNCTION_RUNTIME, - 'abi': FALLBACK_FUNCTION_ABI, + 'bytecode': PAYABLE_TESTER_CODE, + 'bytecode_runtime': PAYABLE_TESTER_RUNTIME, + 'abi': PAYABLE_TESTER_ABI, } @pytest.fixture() -def FallballFunctionContract(web3, FALLBACK_FUNCTION_CONTRACT): - return web3.eth.contract(**FALLBACK_FUNCTION_CONTRACT) +def PayableTesterContract(web3, PAYABLE_TESTER_CONTRACT): + return web3.eth.contract(**PAYABLE_TESTER_CONTRACT) # no matter the function selector, this will return back the 32 bytes of data supplied @@ -479,6 +480,53 @@ def FixedReflectionContract(web3): return web3.eth.contract(abi=CONTRACT_FIXED_ABI, bytecode=CONTRACT_REFLECTION_CODE) +CONTRACT_FALLBACK_FUNCTION_SOURCE = """ +contract A { + uint data; + function A() public payable { data = 0; } + function getData() returns (uint r) { return data; } + function() { data = 1; } +} +""" + +CONTRACT_FALLBACK_FUNCTION_CODE = "60606040526000808190555060ae806100196000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633bc5de30146053575b3415604957600080fd5b6001600081905550005b3415605d57600080fd5b60636079565b6040518082815260200191505060405180910390f35b600080549050905600a165627a7a72305820045439389e4742569ec078687e6a0c81997709778a0097adbe07ccfd9f7b1a330029" # noqa: E501 + +CONTRACT_FALLBACK_FUNCTION_RUNTIME = "606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633bc5de30146053575b3415604957600080fd5b6001600081905550005b3415605d57600080fd5b60636079565b6040518082815260200191505060405180910390f35b600080549050905600a165627a7a72305820045439389e4742569ec078687e6a0c81997709778a0097adbe07ccfd9f7b1a330029" # noqa: E501 + +CONTRACT_FALLBACK_FUNCTION_ABI = json.loads('[{"constant": false, "inputs": [], "name": "getData", "outputs": [{"name": "r", "type": "uint256"}], "payable": false, "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "payable": true, "stateMutability": "payable", "type": "constructor"}, {"payable": false, "stateMutability": "nonpayable", "type": "fallback"}]') # noqa: E501 + + +@pytest.fixture() +def FALLBACK_FUNCTION_CODE(): + return CONTRACT_FALLBACK_FUNCTION_CODE + + +@pytest.fixture() +def FALLBACK_FUNCTION_RUNTIME(): + return CONTRACT_FALLBACK_FUNCTION_RUNTIME + + +@pytest.fixture() +def FALLBACK_FUNCTION_ABI(): + return CONTRACT_FALLBACK_FUNCTION_ABI + + +@pytest.fixture() +def FALLBACK_FUNCTION_CONTRACT(FALLBACK_FUNCTION_CODE, + FALLBACK_FUNCTION_RUNTIME, + FALLBACK_FUNCTION_ABI): + return { + 'bytecode': FALLBACK_FUNCTION_CODE, + 'bytecode_runtime': FALLBACK_FUNCTION_RUNTIME, + 'abi': FALLBACK_FUNCTION_ABI, + } + + +@pytest.fixture() +def FallballFunctionContract(web3, FALLBACK_FUNCTION_CONTRACT): + return web3.eth.contract(**FALLBACK_FUNCTION_CONTRACT) + + class LogFunctions: LogAnonymous = 0 LogNoArguments = 1 diff --git a/tests/core/contracts/test_contract_buildTransaction.py b/tests/core/contracts/test_contract_buildTransaction.py index 935f3de537..bc6de9b9d8 100644 --- a/tests/core/contracts/test_contract_buildTransaction.py +++ b/tests/core/contracts/test_contract_buildTransaction.py @@ -5,6 +5,9 @@ from web3._utils.toolz import ( dissoc, ) +from web3.exceptions import ( + ValidationError, +) # Ignore warning in pyethereum 1.6 - will go away with the upgrade pytestmark = pytest.mark.filterwarnings("ignore:implicit cast from 'char *'") @@ -32,6 +35,42 @@ def fallback_function_contract(web3, FallballFunctionContract, address_conversio return _fallback_contract +@pytest.fixture() +def payable_tester_contract(web3, PayableTesterContract, address_conversion_func): + deploy_txn = PayableTesterContract.constructor().transact() + deploy_receipt = web3.eth.waitForTransactionReceipt(deploy_txn) + assert deploy_receipt is not None + payable_tester_address = address_conversion_func(deploy_receipt['contractAddress']) + _payable_tester = PayableTesterContract(address=payable_tester_address) + assert _payable_tester.address == payable_tester_address + return _payable_tester + + +def test_build_transaction_not_paying_to_nonpayable_function( + web3, + payable_tester_contract, + buildTransaction): + txn = buildTransaction(contract=payable_tester_contract, + contract_function='doNoValueCall') + assert dissoc(txn, 'gas') == { + 'to': payable_tester_contract.address, + 'data': '0xe4cb8f5c', + 'value': 0, + 'gasPrice': 1, + 'chainId': None, + } + + +def test_build_transaction_paying_to_nonpayable_function( + web3, + payable_tester_contract, + buildTransaction): + with pytest.raises(ValidationError): + buildTransaction(contract=payable_tester_contract, + contract_function='doNoValueCall', + tx_params={'value': 1}) + + def test_build_transaction_with_contract_no_arguments(web3, math_contract, buildTransaction): txn = buildTransaction(contract=math_contract, contract_function='increment') assert dissoc(txn, 'gas') == { diff --git a/tests/core/contracts/test_contract_call_interface.py b/tests/core/contracts/test_contract_call_interface.py index b5d2005e6d..336c81e990 100644 --- a/tests/core/contracts/test_contract_call_interface.py +++ b/tests/core/contracts/test_contract_call_interface.py @@ -90,6 +90,11 @@ def fixed_reflection_contract(web3, FixedReflectionContract, address_conversion_ return deploy(web3, FixedReflectionContract, address_conversion_func) +@pytest.fixture() +def payable_tester_contract(web3, PayableTesterContract, address_conversion_func): + return deploy(web3, PayableTesterContract, address_conversion_func) + + @pytest.fixture() def call_transaction(): return { @@ -532,6 +537,19 @@ def test_call_abi_no_functions(web3): contract.functions.thisFunctionDoesNotExist().call() +def test_call_not_sending_ether_to_nonpayable_function(payable_tester_contract, call): + result = call(contract=payable_tester_contract, + contract_function='doNoValueCall') + assert result == [] + + +def test_call_sending_ether_to_nonpayable_function(payable_tester_contract, call): + with pytest.raises(ValidationError): + call(contract=payable_tester_contract, + contract_function='doNoValueCall', + tx_params={'value': 1}) + + @pytest.mark.parametrize( 'function, value', ( diff --git a/tests/core/contracts/test_contract_estimateGas.py b/tests/core/contracts/test_contract_estimateGas.py index 841619a0a1..79e1aac3b3 100644 --- a/tests/core/contracts/test_contract_estimateGas.py +++ b/tests/core/contracts/test_contract_estimateGas.py @@ -1,5 +1,9 @@ import pytest +from web3.exceptions import ( + ValidationError, +) + @pytest.fixture(autouse=True) def wait_for_first_block(web3, wait_for_block): @@ -54,6 +58,19 @@ def fallback_function_contract(web3, return _fallback_function_contract +@pytest.fixture() +def payable_tester_contract(web3, PayableTesterContract, address_conversion_func): + deploy_txn = PayableTesterContract.constructor().transact({'from': web3.eth.coinbase}) + deploy_receipt = web3.eth.waitForTransactionReceipt(deploy_txn) + + assert deploy_receipt is not None + payable_tester_address = address_conversion_func(deploy_receipt['contractAddress']) + + _payable_tester = PayableTesterContract(address=payable_tester_address) + assert _payable_tester.address == payable_tester_address + return _payable_tester + + def test_contract_estimateGas(web3, math_contract, estimateGas, transact): gas_estimate = estimateGas(contract=math_contract, contract_function='increment') @@ -92,3 +109,31 @@ def test_contract_estimateGas_with_arguments(web3, math_contract, estimateGas, t gas_used = txn_receipt.get('gasUsed') assert abs(gas_estimate - gas_used) < 21000 + + +def test_estimateGas_not_sending_ether_to_nonpayable_function( + web3, + payable_tester_contract, + estimateGas, + transact): + gas_estimate = estimateGas(contract=payable_tester_contract, + contract_function='doNoValueCall') + + txn_hash = transact( + contract=payable_tester_contract, + contract_function='doNoValueCall') + + txn_receipt = web3.eth.waitForTransactionReceipt(txn_hash) + gas_used = txn_receipt.get('gasUsed') + + assert abs(gas_estimate - gas_used) < 21000 + + +def test_estimateGas_sending_ether_to_nonpayable_function( + web3, + payable_tester_contract, + estimateGas): + with pytest.raises(ValidationError): + estimateGas(contract=payable_tester_contract, + contract_function='doNoValueCall', + tx_params={'value': 1}) diff --git a/tests/core/contracts/test_contract_transact_interface.py b/tests/core/contracts/test_contract_transact_interface.py index 097b391989..e86435c59d 100644 --- a/tests/core/contracts/test_contract_transact_interface.py +++ b/tests/core/contracts/test_contract_transact_interface.py @@ -9,6 +9,9 @@ from web3._utils.empty import ( empty, ) +from web3.exceptions import ( + ValidationError, +) # Ignore warning in pyethereum 1.6 - will go away with the upgrade pytestmark = pytest.mark.filterwarnings("ignore:implicit cast from 'char *'") @@ -63,6 +66,17 @@ def arrays_contract(web3, ArraysContract, address_conversion_func): return _arrays_contract +@pytest.fixture() +def payable_tester_contract(web3, PayableTesterContract, address_conversion_func): + deploy_txn = PayableTesterContract.constructor().transact() + deploy_receipt = web3.eth.waitForTransactionReceipt(deploy_txn) + assert deploy_receipt is not None + address = address_conversion_func(deploy_receipt['contractAddress']) + _payable_tester = PayableTesterContract(address=address) + assert _payable_tester.address == address + return _payable_tester + + def test_transacting_with_contract_no_arguments(web3, math_contract, transact, call): initial_value = call(contract=math_contract, contract_function='counter') @@ -78,6 +92,48 @@ def test_transacting_with_contract_no_arguments(web3, math_contract, transact, c assert final_value - initial_value == 1 +def test_transact_not_sending_ether_to_nonpayable_function( + web3, + payable_tester_contract, + transact, + call): + initial_value = call(contract=payable_tester_contract, + contract_function='wasCalled') + + assert initial_value is False + txn_hash = transact(contract=payable_tester_contract, + contract_function='doNoValueCall') + txn_receipt = web3.eth.waitForTransactionReceipt(txn_hash) + assert txn_receipt is not None + + final_value = call(contract=payable_tester_contract, + contract_function='wasCalled') + + assert final_value is True + + +def test_transact_sending_ether_to_nonpayable_function( + web3, + payable_tester_contract, + transact, + call): + initial_value = call(contract=payable_tester_contract, + contract_function='wasCalled') + + assert initial_value is False + with pytest.raises(ValidationError): + txn_hash = transact(contract=payable_tester_contract, + contract_function='doNoValueCall', + tx_params={'value': 1}) + txn_receipt = web3.eth.waitForTransactionReceipt(txn_hash) + assert txn_receipt is not None + + final_value = call(contract=payable_tester_contract, + contract_function='wasCalled') + + assert final_value is False + + @pytest.mark.parametrize( 'transact_args,transact_kwargs', ( diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 7dc607afe2..997ca6a8fd 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -182,6 +182,11 @@ def prepare_transaction( TODO: make this a public API TODO: add new prepare_deploy_transaction API """ + if fn_abi is None: + fn_abi = find_matching_fn_abi(contract_abi, fn_identifier, fn_args, fn_kwargs) + + validate_payable(transaction, fn_abi) + if transaction is None: prepared_transaction = {} else: @@ -245,3 +250,17 @@ def get_function_info(fn_name, contract_abi=None, fn_abi=None, args=None, kwargs fn_arguments = merge_args_and_kwargs(fn_abi, args, kwargs) return fn_abi, fn_selector, fn_arguments + + +def validate_payable(transaction, abi): + """Raise ValidationError if non-zero ether + is sent to a non payable function. + """ + if 'value' in transaction: + if transaction['value'] != 0: + if "payable" in abi and not abi["payable"]: + raise ValidationError( + "Sending non-zero ether to a contract function " + "with payable=False. Please ensure that " + "transaction's value is 0." + ) diff --git a/web3/contract.py b/web3/contract.py index eb5e68092c..70ac7d7aa6 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -1105,6 +1105,7 @@ def call(self, transaction=None, block_identifier='latest'): raise ValueError( "Please ensure that this contract instance has an address." ) + block_id = parse_block_identifier(self.web3, block_identifier) return call_contract_function(