diff --git a/docs/userguides/contracts.md b/docs/userguides/contracts.md index 4e720c5ec9..dd16cde0d8 100644 --- a/docs/userguides/contracts.md +++ b/docs/userguides/contracts.md @@ -257,22 +257,9 @@ assert receipt.return_value == 123 ``` Transactions may also fail. -When a transaction fails, Ape (by default) raises the virtual machine error which may crash your script. -You will see exception trace data like this: - -``` - File "$HOME/ApeProjects/ape-playground/scripts/fail.py", line 7, in main - contract.setNumber(5, sender=owner) - -ERROR: (ContractLogicError) Transaction failed. -``` - -If you know a transaction is going to revert and you wish to allow it, use the `raise_on_revert=False` flag: - -```python -receipt = contract.setNumber(5, sender=owner, raise_on_revert=False) -assert receipt.failed -``` +This is called a `revert`! +When a transaction reverts, Ape (by default) raises the virtual machine error which may crash your script. +To learn more reverts, see the [reverts guide](../reverts.html). For more general information on transactions in the Ape framework, see [this guide](./transactions.html). diff --git a/docs/userguides/reverts.md b/docs/userguides/reverts.md index e69de29bb2..a3f4a67bf7 100644 --- a/docs/userguides/reverts.md +++ b/docs/userguides/reverts.md @@ -0,0 +1,135 @@ +# Reverts + +Reverts occur when a transaction or call fails for any reason. +In the case of EVM networks, reverts result in funds being returned to the sender (besides network-fees) and contract-state changes being undone. +Typically, in smart-contracts, user-defined reverts occur from `assert` statements in Vyper and `require` statements in Solidity. + +Here is a Vyper example of an `assert` statement that would cause a revert: + +```python +assert msg.sender == self.owner, "!authorized" +``` + +The string `"!authorized"` after the assertion is the revert-message to forward to the user. + +In solidity, it might look like this: + +```solidity +require(msg.sender == owner, "!authorized"); +``` + +In Ape, reverts automatically become Python exceptions. +When [interacting with a contract](../contracts.html#contract-interaction) and encountering a revert, your program will crash and you will see a stacktrace showing you where the revert occurred. +For example, assume you have contract instance variable `contract` with a Vyper method called `setNumber()`, and it reverts when the user is not the owner of the contract. +Calling it may look like: + +```python +receipt = contract.setNumber(123, sender=not_owner) +``` + +And when it fails, you may see a stacktrace like this: + +```shell + File "$HOME/ApeProjects/ape-project/scripts/fail.py", line 8, in main + receipt = contract.setNumber(5, sender=not_owner) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "$HOME/ApeProjects/ape-project/contracts/VyperContract.vy", line 98, in +setNumber + assert msg.sender == self.owner, "!authorized" + ^^^^^^^^^^^^^^^^^^^^^^^ + +ERROR: (ContractLogicError) !authorized +``` + +One way to allow exceptions is to simply use `try:` / `except:` blocks: + +```python +from ape.exceptions import ContractLogicError + +try: + receipt = contract.setNumber(123, sender=not_owner) +except ContractLogicError as err: + receipt = None + print(f"The transaction failed: {err}") +# continue on! +``` + +If you wish to allow reverts without having Ape raise exceptions, use the `raise_on_revert=False` flag: + +```shell +receipt = contract.setNumber(123, sender=not_owner, raise_on_revert=False) +print(receipt.failed) +# Also, access the `error` on the receipt: +print(receipt.error) +``` + +## Dev Messsages + +Dev messages allow smart-contract authors to save gas by avoiding revert-messages. +If you are using a provider that supports tracing features and a compiler that can detect `dev` messages, and you encounter a revert without a revert-message but it has a dev-message, Ape will show the dev-message: + +```python +assert msg.sender == self.owner # dev: !authorized" +``` + +And you will see a similar stacktrace as if you had used a revert-message. + +In Solidity, it might look like this: + +```solidity +require(msg.sender == owner); // @dev !authorized +``` + +## Custom Errors + +As of Solidity 0.8.4, custom errors have been introduced to the ABI. +To make assertions on custom errors, you can use the types defined on your contracts. + +For example, if you have a contract like: + +```solidity +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.4; + +error Unauthorized(address unauth_address); + +contract MyContract { + address payable owner = payable(msg.sender); + function withdraw() public { + if (msg.sender != owner) + revert Unauthorized(msg.sender); + owner.transfer(address(this).balance); + } +} +``` + +, and you have an instance of this contract on variable `contract`, you can reference the custom exception by doing: + +```python +contract.Unauthorized +``` + +When invoking `withdraw()` with an unauthorized account using Ape, you will get an exception similar to those from `require()` statements, a subclass of `ContractLogicError`: + +```python +contract.withdraw(sender=hacker) # assuming 'hacker' refers to the account without authorization. +``` + +## Built-in Errors + +Besides user-defined `ContractLogicError`s, there are also builtin-errors from compilers, such as bounds-checking of arrays or paying a non-payable method, etc. +These are also `ContractLogicError` sub-classes. +Sometimes, compiler plugins such as `ape-vyper` or `ape-solidity` exports these error classes for you to use. + +```python +from ape import accounts, Contract +from ape_vyper.exceptions import FallbackNotDefinedError + +my_contract = Contract("0x...") +account = accounts.load("test-account") + +try: + my_contract(sender=account) +except FallbackNotDefinedError: + print("fallback not defined") +``` diff --git a/docs/userguides/testing.md b/docs/userguides/testing.md index 0359d35987..7c3e62a2c9 100644 --- a/docs/userguides/testing.md +++ b/docs/userguides/testing.md @@ -306,9 +306,10 @@ def test_account_balance(project, owner, receiver, nft): assert actual == expect ``` -## Testing Transaction Failures +## Testing Transaction Reverts Similar to `pytest.raises()`, you can use `ape.reverts()` to assert that contract transactions fail and revert. +To learn more about reverts in Ape, see the [reverts guide](../reverts.html). From our earlier example we can see this in action: @@ -429,28 +430,12 @@ def foo(): ### Custom Errors -As of Solidity 0.8.4, custom errors have been introduced to the ABI. -To make assertions on custom errors, you can use the types defined on your contracts. +In your tests, you can make assertions about custom errors raised. +(For more information on custom errors, [see reverts guide on custom errors](../reverts.html#custom-errors).) -For example, if I have a contract called `MyContract.sol`: - -```solidity -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.4; - -error Unauthorized(address unauth_address); - -contract MyContract { - address payable owner = payable(msg.sender); - function withdraw() public { - if (msg.sender != owner) - revert Unauthorized(msg.sender); - owner.transfer(address(this).balance); - } -} -``` - -I can ensure unauthorized withdraws are disallowed by writing the following test: +For example, assume a custom exception in a Solidity contract (variable `contract`) is called `Unauthorized`. +It can be accessed via `contract.Unauthorized`. +We can ensure unauthorized withdraws are disallowed by writing the following test: ```python import ape diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index f09d9148c6..54982ae36c 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -18,7 +18,6 @@ SignatureError, TransactionError, TransactionNotFoundError, - VirtualMachineError, ) from ape.logging import logger from ape.types import ( @@ -289,7 +288,7 @@ class ReceiptAPI(ExtraAttributesMixin, BaseInterfaceModel): status: int txn_hash: str transaction: TransactionAPI - _error: Optional[VirtualMachineError] = None + _error: Optional[TransactionError] = None @log_instead_of_fail(default="") def __repr__(self) -> str: @@ -336,11 +335,11 @@ def debug_logs_lines(self) -> list[str]: return [" ".join(map(str, ln)) for ln in self.debug_logs_typed] @property - def error(self) -> Optional[VirtualMachineError]: + def error(self) -> Optional[TransactionError]: return self._error @error.setter - def error(self, value: VirtualMachineError): + def error(self, value: TransactionError): self._error = value def show_debug_logs(self): diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index 8b4293e648..026ec46feb 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -255,7 +255,7 @@ def source_traceback(self) -> SourceTraceback: return SourceTraceback.model_validate([]) def raise_for_status(self): - err = None + err: Optional[TransactionError] = None if self.gas_limit is not None and self.ran_out_of_gas: err = OutOfGasError(txn=self)