How to Debug Contract Reverts in GenLayer: A Complete Developer Guide

Use direct_vm.expect_revert("<message>") in isolated tests to reproduce and assert contract reverts, combined with temporary print() statements to inspect state before the raise clause.

GenLayer contracts are standard Python classes inheriting from gl.Contract. When a contract method raises an exception, the GenVM rolls back the transaction and returns a revert containing the exception message. This guide covers the exact workflow for debugging these reverts in the genlayerlabs/genlayer-project-boilerplate repository.

Understanding How GenLayer Reverts Work

Every GenLayer contract revert originates from a Python raise statement. The GenVM catches this exception, discards all state changes, and surfaces the message to the caller.

In contracts/football_bets.py, three distinct revert paths exist:

  • create_bet raises Bet already created when duplicate bets are attempted
  • resolve_bet raises Bet already resolved for double-resolution attempts
  • resolve_bet raises Game not finished when match data shows incomplete status

These reverts protect contract integrity by enforcing business rules at the code level.

Locating Revert Sources in Contract Code

The first debugging step is matching the revert message to its source. Search the contract for the exact string returned in the transaction receipt.

Example: Finding the "Bet Already Created" Revert

In contracts/football_bets.py, lines 73-75 implement this check:

if bet_id in self.bets[gl.message.sender_address]:
    raise Exception("Bet already created")

Example: Finding the "Bet Already Resolved" Revert

Lines 91-93 in the same file handle this case:

if bet.has_resolved:
    raise Exception("Bet already resolved")

Line 97-99 adds another guard:

if not is_finished:
    raise Exception("Game not finished")

Use grep "Your revert message" or your IDE's search to jump directly to the triggering condition.

Reproducing Reverts with Direct-Mode Tests

Direct tests run contracts in-memory without blockchain dependencies. This isolation lets you mock external calls—web fetches, LLM prompts—and verify exact revert behavior.

The expect_revert Context Manager

The test harness provides direct_vm.expect_revert("<message>") as a context manager. Any revert with a matching message passes the test; mismatches or missing reverts fail.

In tests/direct/test_resolve_bet.py, lines 74-85 demonstrate testing double-resolution:

def test_resolve_already_resolved_fails(direct_vm, direct_deploy, direct_alice):
    contract = direct_deploy("contracts/football_bets.py")
    direct_vm.sender = direct_alice

    # Create and resolve the bet first

    contract.create_bet("2024-06-20", "Spain", "Italy", "1")
    _setup_match_mocks(direct_vm, "1:0", 1)
    contract.resolve_bet("2024-06-20_spain_italy")

    # Second resolve must revert

    with direct_vm.expect_revert("Bet already resolved"):
        contract.resolve_bet("2024-06-20_spain_italy")

Setting Up Mocks for Isolated Testing

External dependencies are mocked with vm.mock_web() and vm.mock_llm(). The _setup_match_mocks helper in tests/direct/test_resolve_bet.py configures these:

def _setup_match_mocks(vm, score: str, is_finished: int):
    vm.mock_web("https://www.bbc.com/...", '<html>match data</html>')
    vm.mock_llm("extract score and status", f'{{"score": "{score}", "finished": {is_finished}}}')

This pattern removes network variability and lets you test edge cases deterministically.

Adding Diagnostic Output for Deep Debugging

While print() statements are suppressed in production deployments, they appear in direct test output. Add temporary logging before raise statements to expose internal state.

Example: Debugging Unexpected Reverts

Modify resolve_bet temporarily:

def resolve_bet(self, bet_id: str) -> None:
    bet = self.bets[gl.message.sender_address][bet_id]
    print(f"DEBUG: bet_id={bet_id}, has_resolved={bet.has_resolved}, match_time={bet.match_time}")
    
    if bet.has_resolved:
        raise Exception("Bet already resolved")
    # ... remaining logic

Run the test with pytest tests/direct/test_resolve_bet.py -v -s to see debug output. Remove or comment these lines before committing.

Step-by-Step Debugging Workflow

Follow this systematic approach to resolve any GenLayer contract revert:

  1. Capture the revert message from the transaction receipt or test failure

  2. Locate the source with grep -r "revert message" contracts/

  3. Analyze the condition triggering the raise—check state variables, parameters, and external data

  4. Create a minimal direct test that reproduces the exact state:

    • Set direct_vm.sender appropriately
    • Deploy with direct_deploy("contracts/<file>.py")
    • Mock external calls with vm.mock_web() and vm.mock_llm()
  5. Assert the revert using with direct_vm.expect_revert("<message>"):

  6. Add debug prints if the revert path is unclear, re-run, then remove them

  7. Fix the logic and run pytest tests/direct/ -v to verify no regressions

Key Files for Revert Debugging

File Purpose
contracts/football_bets.py Core contract with raise statements at lines 73-75, 91-93, 97-99
tests/direct/test_resolve_bet.py Demonstrates expect_revert usage and mock setup
tests/direct/test_create_bet.py Shows "Bet already created" revert testing

Summary

  • GenLayer reverts are Python exceptions caught by the GenVM, rolling back state and returning the message
  • Locate reverts by searching contract files for the exact error string
  • Test reverts in isolation using direct_vm.expect_revert("<message>") with mocked dependencies
  • Debug state with temporary print() statements visible only in direct test output
  • Verify fixes by running the full direct test suite to prevent regressions

Frequently Asked Questions

What makes a GenLayer contract revert?

A GenLayer contract reverts when any method raises a Python Exception. The GenVM intercepts this, reverts all state changes, and returns the exception message to the caller. This differs from Ethereum's require() or revert() opcodes—it's standard Python exception handling wrapped in transaction semantics.

How do I test that my contract reverts correctly?

Use with direct_vm.expect_revert("exact message"): in direct-mode tests. This context manager asserts that the enclosed code raises the specified revert. Combine it with vm.mock_web() and vm.mock_llm() to control external dependencies and test edge cases deterministically, as shown in tests/direct/test_resolve_bet.py.

Can I see print output from my contract when testing?

Yes—print() and logger output appears in direct test results but is suppressed in production. Add diagnostic prints before raise statements, run with pytest -s, then remove them before deploying. This is the recommended method for inspecting state that triggers unexpected reverts.

Why does my test fail even though the contract reverts?

expect_revert requires an exact message match. Check for typos, whitespace differences, or dynamic message construction. Also ensure you're using the context manager correctly—it must wrap the call that triggers the revert, not subsequent assertions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →