Skip to content

Commit

Permalink
Merge pull request #1412 from banteg/feat/call-override
Browse files Browse the repository at this point in the history
feat: eth_call state override for contract methods
  • Loading branch information
iamdefinitelyahuman authored Jan 29, 2022
2 parents c3dd4a7 + d08b7cd commit b460a76
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Force files to be opened as UTF-8
- Added a new solidity compiler setting `use_latest_patch` in brownie-config.yaml to use the latest patch version of a compiler based on the pragma version of the contract.
- Add cli flag `-r` for raising exceptions to the caller instead of doing a system exit.
- Add `override` argument to contract methods which allows changing the state before the call, including overwriting balance, nonce, code, and storage of any address.

## [1.17.2](https://github.com/eth-brownie/brownie/tree/v1.17.2) - 2021-12-04
### Changed
Expand Down
33 changes: 25 additions & 8 deletions brownie/network/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -1296,11 +1296,15 @@ def __repr__(self) -> str:
def __len__(self) -> int:
return len(self.methods)

def __call__(self, *args: Tuple) -> Any:
def __call__(
self, *args: Tuple, block_identifier: Union[int, str, bytes] = None, override: Dict = None
) -> Any:
fn = self._get_fn_from_args(args)
return fn(*args) # type: ignore
return fn(*args, block_identifier=block_identifier, override=override) # type: ignore

def call(self, *args: Tuple, block_identifier: Union[int, str, bytes] = None) -> Any:
def call(
self, *args: Tuple, block_identifier: Union[int, str, bytes] = None, override: Dict = None
) -> Any:
"""
Call the contract method without broadcasting a transaction.
Expand All @@ -1317,13 +1321,16 @@ def call(self, *args: Tuple, block_identifier: Union[int, str, bytes] = None) ->
A block number or hash that the call is executed at. If not given, the
latest block used. Raises `ValueError` if this value is too far in the
past and you are not using an archival node.
override : dict, optional
A mapping from addresses to balance, nonce, code, state, stateDiff
overrides for the context of the call.
Returns
-------
Contract method return value(s).
"""
fn = self._get_fn_from_args(args)
return fn.call(*args, block_identifier=block_identifier)
return fn.call(*args, block_identifier=block_identifier, override=override)

def transact(self, *args: Tuple) -> TransactionReceiptType:
"""
Expand Down Expand Up @@ -1464,7 +1471,9 @@ def info(self) -> None:
print(f"{self.abi['name']}({_inputs(self.abi)})")
_print_natspec(self.natspec)

def call(self, *args: Tuple, block_identifier: Union[int, str, bytes] = None) -> Any:
def call(
self, *args: Tuple, block_identifier: Union[int, str, bytes] = None, override: Dict = None
) -> Any:
"""
Call the contract method without broadcasting a transaction.
Expand All @@ -1477,6 +1486,9 @@ def call(self, *args: Tuple, block_identifier: Union[int, str, bytes] = None) ->
A block number or hash that the call is executed at. If not given, the
latest block used. Raises `ValueError` if this value is too far in the
past and you are not using an archival node.
override : dict, optional
A mapping from addresses to balance, nonce, code, state, stateDiff
overrides for the context of the call.
Returns
-------
Expand All @@ -1490,7 +1502,7 @@ def call(self, *args: Tuple, block_identifier: Union[int, str, bytes] = None) ->
tx.update({"to": self._address, "data": self.encode_input(*args)})

try:
data = web3.eth.call({k: v for k, v in tx.items() if v}, block_identifier)
data = web3.eth.call({k: v for k, v in tx.items() if v}, block_identifier, override)
except ValueError as e:
raise VirtualMachineError(e) from None

Expand Down Expand Up @@ -1676,7 +1688,9 @@ class ContractCall(_ContractMethod):
Bytes4 method signature.
"""

def __call__(self, *args: Tuple, block_identifier: Union[int, str, bytes] = None) -> Any:
def __call__(
self, *args: Tuple, block_identifier: Union[int, str, bytes] = None, override: Dict = None
) -> Any:
"""
Call the contract method without broadcasting a transaction.
Expand All @@ -1689,14 +1703,17 @@ def __call__(self, *args: Tuple, block_identifier: Union[int, str, bytes] = None
A block number or hash that the call is executed at. If not given, the
latest block used. Raises `ValueError` if this value is too far in the
past and you are not using an archival node.
override : dict, optional
A mapping from addresses to balance, nonce, code, state, stateDiff
overrides for the context of the call.
Returns
-------
Contract method return value(s).
"""

if not CONFIG.argv["always_transact"] or block_identifier is not None:
return self.call(*args, block_identifier=block_identifier)
return self.call(*args, block_identifier=block_identifier, override=override)

args, tx = _get_tx(self._owner, args)
tx.update({"gas_price": 0, "from": self._owner or accounts[0]})
Expand Down
33 changes: 31 additions & 2 deletions docs/api-network.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1065,12 +1065,13 @@ Contract Internal Attributes
ContractCall
------------

.. py:class:: brownie.network.contract.ContractCall(*args, block_identifier=None)
.. py:class:: brownie.network.contract.ContractCall(*args, block_identifier=None, override=None)
Calls a non state-changing contract method without broadcasting a transaction, and returns the result. ``args`` must match the required inputs for the method.

* ``args``: Input arguments for the call. The expected inputs are shown in the method's ``__repr__`` value.
* ``block_identifier``: A block number or hash that the call is executed at. If ``None``, the latest block is used. Raises `ValueError` if this value is too far in the past and you are not using an archival node.
* ``override``: A mapping from addresses to balance, nonce, code, state, stateDiff overrides for the context of the call.

Inputs and return values are formatted via methods in the :ref:`convert<api-convert>` module. Multiple values are returned inside a :func:`ReturnValue <brownie.convert.datatypes.ReturnValue>`.

Expand All @@ -1081,6 +1082,8 @@ ContractCall
>>> Token[0].allowance(accounts[0], accounts[2])
0
For override see :ref:`ContractTx.call<override>` docs.

ContractCall Attributes
***********************

Expand Down Expand Up @@ -1197,12 +1200,14 @@ ContractTx Attributes
ContractTx Methods
******************

.. py:classmethod:: ContractTx.call(*args, block_identifier=None)

.. py:classmethod:: ContractTx.call(*args, block_identifier=None, override=None)
Calls the contract method without broadcasting a transaction, and returns the result.

* ``args``: Input arguments for the call. The expected inputs are shown in the method's ``__repr__`` value.
* ``block_identifier``: A block number or hash that the call is executed at. If ``None``, the latest block is used. Raises `ValueError` if this value is too far in the past and you are not using an archival node.
* ``override``: A mapping from addresses to balance, nonce, code, state, stateDiff overrides for the context of the call.

Inputs and return values are formatted via methods in the :ref:`convert<api-convert>` module. Multiple values are returned inside a :func:`ReturnValue <brownie.convert.datatypes.ReturnValue>`.

Expand All @@ -1211,6 +1216,30 @@ ContractTx Methods
>>> Token[0].transfer.call(accounts[2], 10000, {'from': accounts[0]})
True
.. _override:

The override argument allows replacing balance, nonce and code associated with an address, as well as overwriting individual storage slot value.
See `Geth docs <https://geth.ethereum.org/docs/rpc/ns-eth>`_ for more details.

For example, you can query an exchange rate of an imbalanced Curve pool if it had a different A parameter:

.. code-block:: python
>>> for A in [300, 1000, 2000]:
override = {
"0x5a6A4D54456819380173272A5E8E9B9904BdF41B": {
"stateDiff": {
"0x0000000000000000000000000000000000000000000000000000000000000009": hex(A * 100),
}
}
}
result = pool.get_dy_underlying(0, 1, 1e18, override=override)
print(A, result.to("ether"))
300 0.884657790783695579
1000 0.961374099348799411
2000 0.979998831913646748
.. py:classmethod:: ContractTx.decode_input(hexstr)
Decodes hexstring input data for this method.
Expand Down

0 comments on commit b460a76

Please sign in to comment.