AVM Debugger
The AVM VSCode debugger enables inspection of blockchain logic through Simulate Traces - JSON files containing detailed transaction execution data without on-chain deployment. The extension requires both trace files and source maps that link original code (TEAL or Puya) to compiled instructions.
While the extension works independently, projects created with algokit templates include utilities that automatically generate these debugging artifacts. For full list of available capabilities of debugger extension refer to this documentation.
This tutorial demonstrates the workflow using a Python-based Algorand project. We will walk through identifying and fixing a bug in an Algorand smart contract using the Algorand Virtual Machine (AVM) Debugger. We’ll start with a simple, smart contract containing a deliberate bug, and by using the AVM Debugger, we’ll pinpoint and fix the issue. This guide will walk you through setting up, debugging, and fixing a smart contract using this extension.
Prerequisites
Section titled “Prerequisites”- Visual Studio Code (version 1.80.0 or higher)
- Node.js (version 18.x or higher)
- algokit-cli installed
- Algokit AVM VSCode Debugger extension installed
- Basic understanding of Algorand smart contracts using Python
Step 1: Setup the Debugging Environment
Section titled “Step 1: Setup the Debugging Environment”Install the Algokit AVM VSCode Debugger extension from the VSCode Marketplace by going to extensions in VSCode, then search for Algokit AVM Debugger and click install. You should see the output like the following:

Step 2: Set Up the Example Smart Contract
Section titled “Step 2: Set Up the Example Smart Contract”We aim to debug smart contract code in a project generated via algokit init. Refer to set up Algokit. Here’s the Algorand Python code for an tictactoe smart contract. The bug is in the move method, where games_played is updated by 2 for guest and 1 for host (which should be updated by 1 for both guest and host).
Remove hello_world folder
Create a new tic tac toe smart contract starter via algokit generate smart-contract -a contract_name "TicTacToe"
Replace the content of contract.py with the code below.
# pyright: reportMissingModuleSource=falsefrom typing import Literal, Tuple, TypeAlias
from algopy import ( ARC4Contract, BoxMap, Global, LocalState, OnCompleteAction, Txn, UInt64, arc4, gtxn, itxn, op, subroutine, urange,)
Board: TypeAlias = arc4.StaticArray[arc4.Byte, Literal[9]]HOST_MARK = 1GUEST_MARK = 2
class GameState(arc4.Struct, kw_only=True): board: Board host: arc4.Address guest: arc4.Address is_over: arc4.Bool turns: arc4.UInt8
class TicTacToe(ARC4Contract): def __init__(self) -> None: self.id_counter = UInt64(0)
self.games_played = LocalState(UInt64) self.games_won = LocalState(UInt64)
self.games = BoxMap(UInt64, GameState)
@subroutine def opt_in(self) -> None: self.games_played[Txn.sender] = UInt64(0) self.games_won[Txn.sender] = UInt64(0)
@arc4.abimethod(allow_actions=[OnCompleteAction.NoOp, OnCompleteAction.OptIn]) def new_game(self, mbr: gtxn.PaymentTransaction) -> UInt64: if Txn.on_completion == OnCompleteAction.OptIn: self.opt_in()
self.id_counter += 1
assert mbr.receiver == Global.current_application_address pre_new_game_box, exists = op.AcctParamsGet.acct_min_balance( Global.current_application_address ) assert exists self.games[self.id_counter] = GameState( board=arc4.StaticArray[arc4.Byte, Literal[9]].from_bytes(op.bzero(9)), host=arc4.Address(Txn.sender), guest=arc4.Address(), is_over=arc4.Bool(False), # noqa: FBT003 turns=arc4.UInt8(), ) post_new_game_box, exists = op.AcctParamsGet.acct_min_balance( Global.current_application_address ) assert exists assert mbr.amount == (post_new_game_box - pre_new_game_box)
return self.id_counter
@arc4.abimethod def delete_game(self, game_id: UInt64) -> None: game = self.games[game_id].copy()
assert game.guest == arc4.Address() or game.is_over.native assert Txn.sender == self.games[game_id].host.native
pre_del_box, exists = op.AcctParamsGet.acct_min_balance( Global.current_application_address ) assert exists del self.games[game_id] post_del_box, exists = op.AcctParamsGet.acct_min_balance( Global.current_application_address ) assert exists
itxn.Payment( receiver=game.host.native, amount=pre_del_box - post_del_box ).submit()
@arc4.abimethod(allow_actions=[OnCompleteAction.NoOp, OnCompleteAction.OptIn]) def join(self, game_id: UInt64) -> None: if Txn.on_completion == OnCompleteAction.OptIn: self.opt_in()
assert self.games[game_id].host.native != Txn.sender assert self.games[game_id].guest == arc4.Address()
self.games[game_id].guest = arc4.Address(Txn.sender)
@arc4.abimethod def move(self, game_id: UInt64, x: UInt64, y: UInt64) -> None: game = self.games[game_id].copy()
assert not game.is_over.native
assert game.board[self.coord_to_matrix_index(x, y)] == arc4.Byte()
assert Txn.sender == game.host.native or Txn.sender == game.guest.native is_host = Txn.sender == game.host.native
if is_host: assert game.turns.native % 2 == 0 self.games[game_id].board[self.coord_to_matrix_index(x, y)] = arc4.Byte( HOST_MARK ) else: assert game.turns.native % 2 == 1 self.games[game_id].board[self.coord_to_matrix_index(x, y)] = arc4.Byte( GUEST_MARK )
self.games[game_id].turns = arc4.UInt8( self.games[game_id].turns.native + UInt64(1) )
is_over, is_draw = self.is_game_over(self.games[game_id].board.copy()) if is_over: self.games[game_id].is_over = arc4.Bool(True) self.games_played[game.host.native] += UInt64(1) self.games_played[game.guest.native] += UInt64(2) # incorrect code here
if not is_draw: winner = game.host if is_host else game.guest self.games_won[winner.native] += UInt64(1)
@arc4.baremethod(allow_actions=[OnCompleteAction.CloseOut]) def close_out(self) -> None: pass
@subroutine def coord_to_matrix_index(self, x: UInt64, y: UInt64) -> UInt64: return 3 * y + x
@subroutine def is_game_over(self, board: Board) -> Tuple[bool, bool]: for i in urange(3): # Row check if board[3 * i] == board[3 * i + 1] == board[3 * i + 2] != arc4.Byte(): return True, False
# Column check if board[i] == board[i + 3] == board[i + 6] != arc4.Byte(): return True, False
# Diagonal check if board[0] == board[4] == board[8] != arc4.Byte(): return True, False if board[2] == board[4] == board[6] != arc4.Byte(): return True, False
# Draw check if ( board[0] == board[1] == board[2] == board[3] == board[4] == board[5] == board[6] == board[7] == board[8] != arc4.Byte() ): return True, True
return False, FalseAdd the below deployment code in deploy.config file:
import logging
import algokit_utilsfrom algosdk.v2client.algod import AlgodClientfrom algosdk.v2client.indexer import IndexerClientfrom algokit_utils import ( EnsureBalanceParameters, TransactionParameters, ensure_funded,)from algokit_utils.beta.algorand_client import AlgorandClientimport base64
import algosdk.abifrom algokit_utils import ( EnsureBalanceParameters, TransactionParameters, ensure_funded,)from algokit_utils.beta.algorand_client import AlgorandClientfrom algokit_utils.beta.client_manager import AlgoSdkClientsfrom algokit_utils.beta.composer import PayParamsfrom algosdk.atomic_transaction_composer import TransactionWithSignerfrom algosdk.util import algos_to_microalgosfrom algosdk.v2client.algod import AlgodClientfrom algosdk.v2client.indexer import IndexerClient
logger = logging.getLogger(__name__)
# define deployment behaviour based on supplied app specdef deploy( algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: algokit_utils.ApplicationSpecification, deployer: algokit_utils.Account,) -> None: from smart_contracts.artifacts.tictactoe.tic_tac_toe_client import ( TicTacToeClient, )
app_client = TicTacToeClient( algod_client, creator=deployer, indexer_client=indexer_client, )
app_client.deploy( on_schema_break=algokit_utils.OnSchemaBreak.AppendApp, on_update=algokit_utils.OnUpdate.AppendApp, )
last_game_id = app_client.get_global_state().id_counter algorand = AlgorandClient.from_clients(AlgoSdkClients(algod_client, indexer_client)) algorand.set_suggested_params_timeout(0)
host = algorand.account.random() ensure_funded( algorand.client.algod, EnsureBalanceParameters( account_to_fund=host.address, min_spending_balance_micro_algos=algos_to_microalgos(200_000), ), )
print(f"balance of host address: ",algod_client.account_info(host.address)["amount"]); print(f"host address: ",host.address);
ensure_funded( algorand.client.algod, EnsureBalanceParameters( account_to_fund=app_client.app_address, min_spending_balance_micro_algos=algos_to_microalgos(10_000), ), ) print(f"app_client address: ",app_client.app_address);
game_id = app_client.opt_in_new_game( mbr=TransactionWithSigner( txn=algorand.transactions.payment( PayParams( sender=host.address, receiver=app_client.app_address, amount=2_500 + 400 * (5 + 8 + 75), ) ), signer=host.signer, ), transaction_parameters=TransactionParameters( signer=host.signer, sender=host.address, boxes=[(0, b"games" + (last_game_id + 1).to_bytes(8, "big"))], ), )
guest = algorand.account.random() ensure_funded( algorand.client.algod, EnsureBalanceParameters( account_to_fund=guest.address, min_spending_balance_micro_algos=algos_to_microalgos(10), ), )
app_client.opt_in_join( game_id=game_id.return_value, transaction_parameters=TransactionParameters( signer=guest.signer, sender=guest.address, boxes=[(0, b"games" + game_id.return_value.to_bytes(8, "big"))], ), )
moves = [ ((0, 0), (2, 2)), ((1, 1), (2, 1)), ((0, 2), (2, 0)), ]
for host_move, guest_move in moves: app_client.move( game_id=game_id.return_value, x=host_move[0], y=host_move[1], transaction_parameters=TransactionParameters( signer=host.signer, sender=host.address, boxes=[(0, b"games" + game_id.return_value.to_bytes(8, "big"))], accounts=[guest.address], ), )
# app_client.join(game_id=game_id.return_value)
app_client.move( game_id=game_id.return_value, x=guest_move[0], y=guest_move[1], transaction_parameters=TransactionParameters( signer=guest.signer, sender=guest.address, boxes=[(0, b"games" + game_id.return_value.to_bytes(8, "big"))], accounts=[host.address], ), )
game_state = algosdk.abi.TupleType( [ algosdk.abi.ArrayStaticType(algosdk.abi.ByteType(), 9), algosdk.abi.AddressType(), algosdk.abi.AddressType(), algosdk.abi.BoolType(), algosdk.abi.UintType(8), ] ).decode( base64.b64decode( algorand.client.algod.application_box_by_name( app_client.app_id, box_name=b"games" + game_id.return_value.to_bytes(8, "big") )["value"] ) ) assert game_state[3]Step 3: Compile & Deploy the Smart Contract
Section titled “Step 3: Compile & Deploy the Smart Contract”To enable debugging mode and full tracing for each step in the execution, go to main.py file and add:
from algokit_utils.config import configconfig.configure(debug=True, trace_all=True)For more details, refer to Debugger:
Next compile the smart contract using AlgoKit:
algokit project run buildThis will generate the following files in artifacts: approval.teal, clear.teal, clear.puya.map, approval.puya.map and arc32.json files.
The .puya.map files are result of the execution of puyapy compiler (which project run build command orchestrated and invokes automatically). The compiler has an option called --output-source-maps which is enabled by default.
Deploy the smart contract on localnet:
algokit project deploy localnetThis will automatically generate *.appln.trace.avm.json files in debug_traces folder, .teal and .teal.map files in sources.
The .teal.map files are source maps for TEAL and those are automatically generated every time an app is deployed via algokit-utils. Even if the developer is only interested in debugging puya source maps, the teal source maps would also always be available as a backup in case there is a need to fall back to more lower level source map.
Expected Behavior
Section titled “Expected Behavior”The expected behavior is that
games_played should be updated by 1 for both guest and host
When move method is called, games_played will get updated incorrectly for guest player.
Step 4: Start the debugger
Section titled “Step 4: Start the debugger”In the VSCode, go to run and debug on left side. This will load the compiled smart contract into the debugger. In the run and debug, select debug TEAL via Algokit AVM Debugger. It will ask to select the appropriate debug_traces file.

Figure: Load Debugger in VSCode
Next it will ask you to select the source map file. Select the approval.puya.map file. Which would indicate to the debug extension that you would like to debug the given trace file using Puya sourcemaps, allowing you to step through high level python code. If you need to change the debugger to use TEAL or puya sourcemaps for other frontends such as Typescript, remove the individual record from .algokit/sources/sources.avm.json file or run the debugger commands via VSCode command palette

Step 5: Debugging the smart contract
Section titled “Step 5: Debugging the smart contract”Let’s now debug the issue:

Enter into the app_id of the transaction_group.json file. This opens the contract. Set a breakpoint in the move method. You can also add additional breakpoints.

On left side, you can see Program State which includes program counter, opcode, stack, scratch space. In On-chain State you will be able to see global, local and box storages for the application id deployed on localnet.
:::note: We have used localnet but the contracts can be deployed on any other network. A trace file is in a sense agnostic of the network in which the trace file was generated in. As long as its a complete simulate trace that contains state, stack and scratch states in the execution trace - debugger will work just fine with those as well. :::
Once you start step operations of debugging, it will get populated according to the contract. Now you can step-into the code.
Step 6: Analyze the Output
Section titled “Step 6: Analyze the Output”Observe the games_played variable for guest is increased by 2 (incorrectly) whereas for host is increased correctly.

Step 7: Fix the Bug
Section titled “Step 7: Fix the Bug”Now that we’ve identified the bug, let’s fix it in our original smart contract in move method:
@arc4.abimethoddef move(self, game_id: UInt64, x: UInt64, y: UInt64) -> None: game = self.games[game_id].copy()
assert not game.is_over.native
assert game.board[self.coord_to_matrix_index(x, y)] == arc4.Byte()
assert Txn.sender == game.host.native or Txn.sender == game.guest.native is_host = Txn.sender == game.host.native
if is_host: assert game.turns.native % 2 == 0 self.games[game_id].board[self.coord_to_matrix_index(x, y)] = arc4.Byte( HOST_MARK ) else: assert game.turns.native % 2 == 1 self.games[game_id].board[self.coord_to_matrix_index(x, y)] = arc4.Byte( GUEST_MARK )
self.games[game_id].turns = arc4.UInt8(self.games[game_id].turns.native + UInt64(1))
is_over, is_draw = self.is_game_over(self.games[game_id].board.copy()) if is_over: self.games[game_id].is_over = arc4.Bool(True) self.games_played[game.host.native] += UInt64(1) self.games_played[game.guest.native] += UInt64(1) # changed here
if not is_draw: winner = game.host if is_host else game.guest self.games_won[winner.native] += UInt64(1)Step 8: Re-deploy
Section titled “Step 8: Re-deploy”Re-compile and re-deploy the contract using the step 3.
Step 9: Verify again using Debugger
Section titled “Step 9: Verify again using Debugger”Reset the sources.avm.json file, then restart the debugger selecting approval.puya.source.map file. Run through steps 4 to 6 to verify that the games_played now updates as expected, confirming the bug has been fixed as seen below.

Summary
Section titled “Summary”In this tutorial, we walked through the process of using the AVM debugger from AlgoKit Python utils to debug an Algorand Smart Contract. We set up a debugging environment, loaded a smart contract with a planted bug, stepped through the execution, and identified the issue. This process can be invaluable when developing and testing smart contracts on the Algorand blockchain. It’s highly recommended to thoroughly test your smart contracts to ensure they function as expected and prevent costly errors in production before deploying them to the main network.
Next steps
Section titled “Next steps”To learn more, refer to documentation of the debugger extension to learn more about Debugging session.