Skip to content

Commit

Permalink
docs: guide refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Jul 22, 2024
1 parent 63f7b89 commit dbe605e
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 43 deletions.
19 changes: 3 additions & 16 deletions docs/userguides/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
135 changes: 135 additions & 0 deletions docs/userguides/reverts.md
Original file line number Diff line number Diff line change
@@ -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")
```
29 changes: 7 additions & 22 deletions docs/userguides/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions src/ape/api/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
SignatureError,
TransactionError,
TransactionNotFoundError,
VirtualMachineError,
)
from ape.logging import logger
from ape.types import (
Expand Down Expand Up @@ -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="<ReceiptAPI>")
def __repr__(self) -> str:
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/ape_ethereum/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit dbe605e

Please sign in to comment.