├── tests ├── __init__.py ├── detectors │ ├── __init__.py │ ├── optimizations │ │ ├── constant_gtxn.py │ │ ├── self_access.py │ │ └── sender_access.py │ ├── subroutine_patterns.py │ └── router_with_assembled_constants.py ├── pyteal_parsing │ ├── __init__.py │ ├── arc4_application.py │ ├── normal_application.py │ └── control_flow_constructs.py ├── group_transactions │ ├── __init__.py │ ├── basic │ │ ├── __init__.py │ │ ├── expected_output_close_to.yaml │ │ ├── expected_output_asset_close_to.yaml │ │ ├── expected_output.yaml │ │ └── expected_output_update_delete.yaml │ └── utils.py ├── transaction_context │ ├── __init__.py │ ├── test_group_indices.py │ └── test_addr_fields.py ├── regex │ ├── regex.txt │ ├── vote_approval.teal │ └── atomic_swap.py ├── parsing │ ├── teal5-itxna.teal │ ├── teal6-acct_params_get.teal │ ├── teal3-gload.teal │ ├── teal5-ecdsa.teal │ ├── asset_holding_get.teal │ ├── subroutine_jump_back.teal │ ├── comments.teal │ ├── teal4-test0.teal │ ├── teal4-test2.teal │ ├── teal4-test1.teal │ ├── multiple_retsub.teal │ ├── global_fields.teal │ ├── teal1-instructions.teal │ ├── teal4-test3.teal │ ├── teal4-test5.teal │ ├── teal4-test4.teal │ ├── asset_params_get.teal │ ├── teal5-extract.teal │ ├── teal8-instructions.teal │ ├── teal5-itxn.teal │ ├── teal5-mixed.teal │ ├── transaction_fields.teal │ ├── teal5-app_params_get.teal │ ├── teal4-random-opcodes.teal │ ├── teal7-instructions.teal │ ├── teal6-instructions.teal │ └── instructions.teal ├── test_mode_detector.py ├── test_regex.py ├── test_parsing_using_pyteal.py ├── test_string_representation.py ├── test_versions.py ├── test_detectors_using_pyteal.py ├── test_subroutine_identification.py └── utils.py ├── tealer ├── analyses │ ├── __init__.py │ ├── utils │ │ └── __init__.py │ └── dataflow │ │ ├── __init__.py │ │ └── transaction_context │ │ ├── utils │ │ └── __init__.py │ │ └── all_constraints.py ├── teal │ ├── context │ │ └── __init__.py │ ├── __init__.py │ ├── instructions │ │ ├── asset_holding_field.py │ │ ├── __init__.py │ │ ├── parse_asset_holding_field.py │ │ ├── app_params_field.py │ │ ├── parse_app_params_field.py │ │ ├── parse_asset_params_field.py │ │ ├── parse_global_field.py │ │ ├── parse_acct_params_field.py │ │ ├── asset_params_field.py │ │ └── acct_params_field.py │ ├── functions.py │ ├── global_field.py │ └── subroutine.py ├── utils │ ├── regex │ │ └── __init__.py │ ├── command_line │ │ └── __init__.py │ ├── arc4_abi.py │ ├── __init__.py │ ├── algorand_constants.py │ ├── comparable_enum.py │ └── code_complexity.py ├── execution_context │ ├── __init__.py │ └── transactions.py ├── exceptions.py ├── detectors │ ├── __init__.py │ ├── optimizations │ │ ├── __init__.py │ │ ├── sender_access.py │ │ ├── self_access.py │ │ └── constant_gtxn.py │ ├── all_detectors.py │ ├── is_deletable.py │ ├── anyone_can_delete.py │ ├── anyone_can_update.py │ └── is_updatable.py ├── __init__.py └── printers │ ├── all_printers.py │ ├── __init__.py │ ├── full_cfg.py │ ├── function_cfg.py │ ├── abstract_printer.py │ ├── transaction_context.py │ └── call_graph.py ├── CODEOWNERS ├── setup.py ├── plugin_example ├── template │ ├── tealer_plugin │ │ ├── detectors │ │ │ ├── __init__.py │ │ │ └── example.py │ │ └── __init__.py │ └── setup.py ├── rekey_plugin │ ├── tealer_rekey_plugin │ │ ├── detectors │ │ │ ├── __init__.py │ │ │ └── rekeyto_stateless.py │ │ └── __init__.py │ └── setup.py └── README.md ├── examples ├── vote_opt_out.png └── printers │ ├── call-graph.dot.png │ ├── cfg_full_cfg.dot.png │ ├── transaction-context.dot.png │ ├── subroutine-cfg_foobar_cfg.dot.png │ ├── subroutine-cfg_contract_shortened_cfg.dot.png │ ├── transaction-context.teal │ ├── subroutine-cfg.teal │ ├── call-graph.dot │ ├── cfg.teal │ ├── cfg.py │ ├── subroutine-cfg.py │ ├── human-summary.py │ ├── call-graph.py │ ├── call-graph.teal │ ├── human-summary.teal │ ├── subroutine-cfg_contract_shortened_cfg.dot │ ├── subroutine-cfg_foobar_cfg.dot │ ├── transaction-context.py │ └── transaction-context.dot ├── Makefile ├── .gitignore ├── scripts ├── ci_darglint.sh └── test_algorand_contracts.sh ├── mypy.ini ├── .github └── workflows │ ├── darglint.yml │ ├── pip-audit.yml │ ├── tests.yml │ ├── publish.yml │ ├── pytest310.yml │ ├── pytest.yml │ └── linter.yml ├── pyproject.toml └── CONTRIBUTING.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tealer/analyses/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/detectors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tealer/analyses/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tealer/teal/context/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tealer/utils/regex/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyteal_parsing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tealer/analyses/dataflow/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tealer/execution_context/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tealer/utils/command_line/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/group_transactions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/transaction_context/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @montyly 2 | * @S3v3ru5 3 | -------------------------------------------------------------------------------- /tests/group_transactions/basic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup() 4 | -------------------------------------------------------------------------------- /tests/regex/regex.txt: -------------------------------------------------------------------------------- 1 | main_l7 => 2 | int 0 3 | return -------------------------------------------------------------------------------- /plugin_example/template/tealer_plugin/detectors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tealer/analyses/dataflow/transaction_context/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin_example/rekey_plugin/tealer_rekey_plugin/detectors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vote_opt_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crytic/tealer/HEAD/examples/vote_opt_out.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dev: 2 | python3 -m venv env && source ./env/bin/activate 3 | python3 -m pip install -e .[dev] 4 | -------------------------------------------------------------------------------- /tests/parsing/teal5-itxna.teal: -------------------------------------------------------------------------------- 1 | #pragma version 5 2 | 3 | itxna Accounts 1 4 | itxna Accounts 1 5 | == 6 | 7 | -------------------------------------------------------------------------------- /examples/printers/call-graph.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crytic/tealer/HEAD/examples/printers/call-graph.dot.png -------------------------------------------------------------------------------- /examples/printers/cfg_full_cfg.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crytic/tealer/HEAD/examples/printers/cfg_full_cfg.dot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build products 2 | # -------------- 3 | 4 | build/ 5 | dist/ 6 | tealer.egg-info/ 7 | __pycache__ 8 | 9 | .venv/ 10 | -------------------------------------------------------------------------------- /examples/printers/transaction-context.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crytic/tealer/HEAD/examples/printers/transaction-context.dot.png -------------------------------------------------------------------------------- /examples/printers/subroutine-cfg_foobar_cfg.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crytic/tealer/HEAD/examples/printers/subroutine-cfg_foobar_cfg.dot.png -------------------------------------------------------------------------------- /tests/parsing/teal6-acct_params_get.teal: -------------------------------------------------------------------------------- 1 | #pragma version 6 2 | int 0 3 | acct_params_get AcctBalance 4 | ! 5 | // a comment 6 | assert 7 | int 0 8 | == -------------------------------------------------------------------------------- /examples/printers/subroutine-cfg_contract_shortened_cfg.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crytic/tealer/HEAD/examples/printers/subroutine-cfg_contract_shortened_cfg.dot.png -------------------------------------------------------------------------------- /tests/parsing/teal3-gload.teal: -------------------------------------------------------------------------------- 1 | #pragma version 4 2 | 3 | gload 0 0 4 | int 2 5 | == 6 | 7 | int 0 8 | gloads 0 9 | byte "txn 1" 10 | == 11 | 12 | gaid 0 13 | gaids -------------------------------------------------------------------------------- /tests/parsing/teal5-ecdsa.teal: -------------------------------------------------------------------------------- 1 | #pragma version 5 2 | byte "testdata" 3 | sha512_256 4 | byte "dummy1" 5 | byte "dummy2" 6 | byte "dummy3" 7 | ecdsa_pk_decompress Secp256k1 8 | ecdsa_verify Secp256k1 9 | -------------------------------------------------------------------------------- /tests/parsing/asset_holding_get.teal: -------------------------------------------------------------------------------- 1 | #pragma version 2 2 | 3 | // Check balance 4 | asset_holding_get AssetBalance 5 | pop 6 | 7 | // Check Frozenness 8 | asset_holding_get AssetFrozen 9 | pop 10 | -------------------------------------------------------------------------------- /scripts/ci_darglint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pip install darglint 4 | 5 | darglint . 6 | 7 | if [ $? -ne 0 ] 8 | then 9 | echo "Darglint failed. Please fix the above issues" 10 | exit 255 11 | fi 12 | -------------------------------------------------------------------------------- /tests/parsing/subroutine_jump_back.teal: -------------------------------------------------------------------------------- 1 | #pragma version 5 2 | b main 3 | getmod: 4 | % 5 | retsub 6 | is_odd: 7 | int 2 8 | b getmod 9 | main: 10 | int 5 11 | callsub is_odd 12 | return 13 | -------------------------------------------------------------------------------- /tests/parsing/comments.teal: -------------------------------------------------------------------------------- 1 | #pragma version 1 2 | 3 | // some comment 4 | 5 | sha512_256 // another comment 6 | 7 | == // one more comment 8 | 9 | /////////////////// 10 | // final comment // 11 | /////////////////// 12 | -------------------------------------------------------------------------------- /tealer/utils/arc4_abi.py: -------------------------------------------------------------------------------- 1 | from Cryptodome.Hash import SHA512 2 | 3 | 4 | def get_method_selector(method_signature: str) -> str: 5 | hash_obj = SHA512.new(truncate="256") 6 | hash_obj.update(method_signature.encode("utf-8")) 7 | return f"0x{hash_obj.hexdigest()[:8]}" 8 | -------------------------------------------------------------------------------- /tests/detectors/optimizations/constant_gtxn.py: -------------------------------------------------------------------------------- 1 | from tealer.detectors.optimizations.constant_gtxn import ConstantGtxn 2 | 3 | GTXN = """ 4 | #pragma version 3 5 | int 1 6 | gtxns CloseRemainderTo 7 | int 1 8 | """ 9 | 10 | constant_gtxn = [(GTXN, ConstantGtxn, [[2, 3]])] 11 | -------------------------------------------------------------------------------- /tests/detectors/optimizations/self_access.py: -------------------------------------------------------------------------------- 1 | from tealer.detectors.optimizations.self_access import SelfAccess 2 | 3 | GTXN = """ 4 | #pragma version 3 5 | txn GroupIndex 6 | gtxns CloseRemainderTo 7 | int 1 8 | """ 9 | 10 | self_access = [(GTXN, SelfAccess, [[2, 3]])] 11 | -------------------------------------------------------------------------------- /tealer/exceptions.py: -------------------------------------------------------------------------------- 1 | """Defines exceptions classes for representing exceptions in tealer. 2 | 3 | * TealerException: Exception class for common exceptions in tealer. 4 | """ 5 | 6 | 7 | class TealerException(Exception): 8 | """Exception class to represent common exceptions in tealer""" 9 | -------------------------------------------------------------------------------- /tests/detectors/optimizations/sender_access.py: -------------------------------------------------------------------------------- 1 | from tealer.detectors.optimizations.sender_access import SenderAccess 2 | 3 | SENDER_ACCESS = """ 4 | #pragma version 3 5 | int 1 6 | txna Accounts 0 7 | int 2 8 | """ 9 | 10 | sender_access = [(SENDER_ACCESS, SenderAccess, [[3]])] 11 | -------------------------------------------------------------------------------- /tests/parsing/teal4-test0.teal: -------------------------------------------------------------------------------- 1 | #pragma version 4 2 | 3 | b main 4 | 5 | success: 6 | int 1 7 | retsub 8 | 9 | failure: 10 | int 0 11 | retsub 12 | 13 | main: 14 | byte "A test program" 15 | callsub success 16 | int 200 17 | pop 18 | return 19 | 20 | -------------------------------------------------------------------------------- /tests/parsing/teal4-test2.teal: -------------------------------------------------------------------------------- 1 | #pragma version 4 2 | // jump to main loop 3 | b main 4 | 5 | // subroutine 6 | my_subroutine: 7 | int 100 8 | retsub 9 | 10 | 11 | main: 12 | int 1 13 | callsub my_subroutine 14 | int 2 15 | callsub my_subroutine 16 | //int 3 17 | //callsub my_subroutine 18 | return 19 | 20 | -------------------------------------------------------------------------------- /tests/parsing/teal4-test1.teal: -------------------------------------------------------------------------------- 1 | #pragma version 4 2 | // jump to main loop 3 | b main 4 | 5 | int 4 // unreachable 6 | 7 | // subroutine 8 | my_subroutine: 9 | // implement subroutine code 10 | // with the two args 11 | retsub 12 | 13 | 14 | main: 15 | int 1 16 | int 5 17 | callsub my_subroutine 18 | return 19 | 20 | -------------------------------------------------------------------------------- /tests/parsing/multiple_retsub.teal: -------------------------------------------------------------------------------- 1 | #pragma version 5 2 | b main 3 | push_zero: 4 | int 0 5 | retsub 6 | is_even: 7 | int 2 8 | % 9 | bz return_1 10 | callsub push_zero 11 | retsub 12 | return_1: 13 | int 1 14 | retsub 15 | main: 16 | int 4 17 | callsub is_even 18 | return 19 | -------------------------------------------------------------------------------- /tests/parsing/global_fields.teal: -------------------------------------------------------------------------------- 1 | #pragma version 2 2 | 3 | // https://developer.algorand.org/docs/reference/teal/opcodes/#global 4 | global MinTxnFee 5 | global MinBalance 6 | global MaxTxnLife 7 | global ZeroAddress 8 | global GroupSize 9 | global LogicSigVersion 10 | global Round 11 | global LatestTimestamp 12 | global CurrentApplicationID 13 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | warn_incomplete_stub = true 3 | ignore_missing_imports = true 4 | disallow_untyped_calls = true 5 | disallow_untyped_defs = true 6 | disallow_incomplete_defs = true 7 | check_untyped_defs = true 8 | disallow_untyped_decorators = true 9 | warn_redundant_casts = true 10 | warn_no_return = true 11 | warn_unreachable = true 12 | 13 | -------------------------------------------------------------------------------- /tests/parsing/teal1-instructions.teal: -------------------------------------------------------------------------------- 1 | #pragma version 1 2 | sha256 3 | sha512_256 4 | keccak256 5 | err 6 | ed25519verify 7 | + 8 | - 9 | / 10 | * 11 | < 12 | > 13 | <= 14 | >= 15 | && 16 | || 17 | == 18 | != 19 | ! 20 | len 21 | itob 22 | btoi 23 | % 24 | | 25 | & 26 | ^ 27 | ~ 28 | mulw 29 | intcblock 1 30 | intc 1 31 | intc_0 32 | intc_1 33 | intc_2 34 | intc_3 -------------------------------------------------------------------------------- /tests/parsing/teal4-test3.teal: -------------------------------------------------------------------------------- 1 | #pragma version 4 2 | // jump to main loop 3 | b main 4 | 5 | int 10 // unreachable 6 | 7 | // subroutine 8 | my_subroutine: 9 | int 100 10 | retsub 11 | 12 | int 20 // unreachable 13 | 14 | main: 15 | int 1 16 | callsub my_subroutine 17 | int 2 18 | callsub my_subroutine 19 | int 3 20 | callsub my_subroutine 21 | return 22 | 23 | -------------------------------------------------------------------------------- /tealer/analyses/dataflow/transaction_context/all_constraints.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unused-import 2 | from tealer.analyses.dataflow.transaction_context.int_fields import GroupIndices 3 | from tealer.analyses.dataflow.transaction_context.txn_types import TxnType 4 | from tealer.analyses.dataflow.transaction_context.addr_fields import AddrFields 5 | from tealer.analyses.dataflow.transaction_context.fee_field import FeeField 6 | -------------------------------------------------------------------------------- /examples/printers/transaction-context.teal: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | global GroupSize 3 | int 4 4 | <= 5 | bnz main_l2 6 | global GroupSize 7 | int 5 8 | > 9 | global GroupSize 10 | int 11 11 | < 12 | && 13 | txn GroupIndex 14 | int 2 15 | == 16 | && 17 | assert 18 | b main_l3 19 | main_l2: 20 | global GroupSize 21 | int 3 22 | == 23 | assert 24 | txn GroupIndex 25 | int 1 26 | == 27 | assert 28 | main_l3: 29 | int 1 30 | return 31 | -------------------------------------------------------------------------------- /tests/parsing/teal4-test5.teal: -------------------------------------------------------------------------------- 1 | #pragma version 4 2 | 3 | // i = 0 4 | int 0 5 | store 10 6 | 7 | loop: 8 | // assert gtxn[i].RekeyTo == ZeroAddress 9 | load 10 10 | gtxns RekeyTo 11 | global ZeroAddress 12 | == 13 | assert 14 | 15 | // i += 1 16 | load 10 17 | int 1 18 | + 19 | store 10 20 | 21 | // loop if i < GroupSize 22 | load 10 23 | global GroupSize 24 | < 25 | bnz loop 26 | -------------------------------------------------------------------------------- /plugin_example/template/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="tealer-plugin", 5 | description="plugin example for tealer", 6 | author="Trail of Bits", 7 | version="0.0", 8 | packages=find_packages(), 9 | python_requires=">=3.6", 10 | install_requires=["tealer"], 11 | entry_points={ 12 | "teal_analyzer.plugin": "tealer_plugin=tealer_plugin:make_plugin", 13 | }, 14 | ) 15 | -------------------------------------------------------------------------------- /tests/parsing/teal4-test4.teal: -------------------------------------------------------------------------------- 1 | #pragma version 4 2 | 3 | // i = 0 4 | int 0 5 | store 10 6 | 7 | loop: 8 | // assert gtxn[i].RekeyTo == ZeroAddress 9 | load 10 10 | gtxns RekeyTo 11 | global ZeroAddress 12 | == 13 | assert 14 | 15 | // i += 1 16 | load 10 17 | int 1 18 | + 19 | store 10 20 | 21 | // loop if i < GroupSize 22 | load 10 23 | global GroupSize 24 | < 25 | bnz loop 26 | 27 | return 28 | -------------------------------------------------------------------------------- /tests/group_transactions/basic/expected_output_close_to.yaml: -------------------------------------------------------------------------------- 1 | name: BasicRekeyTo 2 | operations: 3 | - operation: case_13 4 | vulnerable_transactions: 5 | - txn_id: T2 6 | contracts: 7 | - contract: LogicSig_2 8 | function: lsig2_case_13 9 | - operation: case_26 10 | vulnerable_transactions: 11 | - txn_id: T2 12 | contracts: 13 | - contract: LogicSig_2 14 | function: lsig2_case_26 15 | -------------------------------------------------------------------------------- /tests/group_transactions/basic/expected_output_asset_close_to.yaml: -------------------------------------------------------------------------------- 1 | name: BasicRekeyTo 2 | operations: 3 | - operation: case_14 4 | vulnerable_transactions: 5 | - txn_id: T2 6 | contracts: 7 | - contract: LogicSig_2 8 | function: lsig2_case_14 9 | - operation: case_25 10 | vulnerable_transactions: 11 | - txn_id: T2 12 | contracts: 13 | - contract: LogicSig_2 14 | function: lsig2_case_25 15 | -------------------------------------------------------------------------------- /tealer/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Common util functions used by other packages in tealer. 2 | 3 | Modules: 4 | code_complexity: Contains util functions to compute code complexity 5 | of the contracts. 6 | 7 | command_line: Defines output util functions used by command line 8 | argument handlers. 9 | 10 | comparable_enum: Defines a comparable enum class. 11 | 12 | output: Defines util functions to output dot files and util 13 | classes to store, represent detector results. 14 | """ 15 | -------------------------------------------------------------------------------- /plugin_example/rekey_plugin/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="tealer-rekeyTo-plugin", 5 | description="tealer plugin for detecting paths with missing rekeyTo in stateless contracts.", 6 | author="Trail of Bits", 7 | version="0.0.1", 8 | packages=find_packages(), 9 | python_requires=">=3.6", 10 | install_requires=["tealer"], 11 | entry_points={ 12 | "teal_analyzer.plugin": "rekey_to_plugin=tealer_rekey_plugin:make_plugin", 13 | }, 14 | ) 15 | -------------------------------------------------------------------------------- /scripts/test_algorand_contracts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | git clone https://github.com/algorand/smart-contracts.git 4 | 5 | cd smart-contracts || exit 255 6 | 7 | while IFS= read -r -d '' target 8 | do 9 | if ! tealer print human-summary --contracts "$target" > /dev/null; then 10 | echo "tests failed" 11 | exit 1 12 | fi 13 | if ! tealer detect --contracts "$target" > /dev/null; then 14 | echo "tests failed" 15 | exit 1 16 | fi 17 | done < <(find . -name "*.teal" -print0) 18 | exit 0 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/parsing/asset_params_get.teal: -------------------------------------------------------------------------------- 1 | #pragma version 2 2 | 3 | // Check total against arg 4 4 | asset_params_get AssetTotal 5 | pop 6 | txna ApplicationArgs 4 7 | btoi 8 | == 9 | 10 | asset_params_get AssetDecimals 11 | asset_params_get AssetUnitName 12 | asset_params_get AssetDefaultFrozen 13 | asset_params_get AssetName 14 | asset_params_get AssetURL 15 | asset_params_get AssetMetadataHash 16 | asset_params_get AssetManager 17 | asset_params_get AssetReserve 18 | asset_params_get AssetFreeze 19 | asset_params_get AssetClawback 20 | asset_params_get AssetCreator 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/darglint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Darglint 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - dev 9 | pull_request: 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | tests: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python 3.9 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: 3.9 24 | - name: Run Tests 25 | run: | 26 | bash scripts/ci_darglint.sh 27 | -------------------------------------------------------------------------------- /tealer/detectors/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for tealer detectors. 2 | 3 | Detectors are analyzers which go through the parsed code and look for 4 | certain patterns in the program which are of interest to anyone invested 5 | in the security of the contract. 6 | 7 | This module contains Abstract base class for printers and implementation of 8 | few detectors that detect common issues in algorand smart contracts. 9 | 10 | Modules: 11 | abstract_detector: Defines abstract base class for detectors. 12 | 13 | all_detectors: Collects and exports detectors defined in tealer. 14 | """ 15 | -------------------------------------------------------------------------------- /tests/parsing/teal5-extract.teal: -------------------------------------------------------------------------------- 1 | #pragma version 5 2 | 3 | byte 0x123456789abcaa 4 | extract 0 6 5 | byte 0x123456789abcaa 6 | != 7 | 8 | byte 0x123456789abc 9 | int 5 10 | int 1 11 | extract3 12 | byte 0xbc 13 | == 14 | 15 | byte 0x123456789abcdef0 16 | int 1 17 | extract_uint16 18 | int 0x3456 19 | == 20 | byte 0x123456789abcdef0 21 | int 1 22 | extract_uint32 23 | int 0x3456789a 24 | == 25 | byte 0x123456789abcdef0 26 | int 0 27 | extract_uint64 28 | int 0x123456789abcdef0 29 | == 30 | byte 0x123456789abcdef0 31 | int 0 32 | extract_uint64 33 | int 0x123456789abcdef 34 | != 35 | 36 | -------------------------------------------------------------------------------- /examples/printers/subroutine-cfg.teal: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | txna ApplicationArgs 0 3 | method "method_foo_bar()void" 4 | == 5 | bnz main_l2 6 | err 7 | main_l2: 8 | txn OnCompletion 9 | int NoOp 10 | == 11 | assert 12 | callsub methodfoobar_1 13 | int 1 14 | return 15 | 16 | // foo_bar 17 | foobar_0: 18 | store 0 19 | load 0 20 | int 2 21 | % 22 | int 0 23 | == 24 | bnz foobar_0_l2 25 | byte "Bar" 26 | log 27 | b foobar_0_l3 28 | foobar_0_l2: 29 | byte "Foo" 30 | log 31 | foobar_0_l3: 32 | retsub 33 | 34 | // method_foo_bar 35 | methodfoobar_1: 36 | int 0 37 | callsub foobar_0 38 | retsub 39 | -------------------------------------------------------------------------------- /tealer/detectors/optimizations/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for tealer detectors. 2 | 3 | Detectors are analyzers which go through the parsed code and look for 4 | certain patterns in the program which are of interest to anyone invested 5 | in the security of the contract. 6 | 7 | This module contains Abstract base class for printers and implementation of 8 | few detectors that detect common issues in algorand smart contracts. 9 | 10 | Modules: 11 | abstract_detector: Defines abstract base class for detectors. 12 | 13 | all_detectors: Collects and exports detectors defined in tealer. 14 | """ 15 | -------------------------------------------------------------------------------- /examples/printers/call-graph.dot: -------------------------------------------------------------------------------- 1 | digraph g{ 2 | methodc_5[label=methodc_5]; 3 | methodb_4[label=methodb_4]; 4 | methoda_3[label=methoda_3]; 5 | recursivesubroutine_1[label=recursivesubroutine_1]; 6 | mysubroutine_0[label=mysubroutine_0]; 7 | mainsubroutine_2[label=mainsubroutine_2]; 8 | __entry__[label=__entry__]; 9 | methodb_4 -> mainsubroutine_2; 10 | methoda_3 -> mysubroutine_0; 11 | recursivesubroutine_1 -> recursivesubroutine_1; 12 | mainsubroutine_2 -> mysubroutine_0; 13 | mainsubroutine_2 -> recursivesubroutine_1; 14 | __entry__ -> methoda_3; 15 | __entry__ -> methodb_4; 16 | __entry__ -> methodc_5; 17 | } 18 | -------------------------------------------------------------------------------- /examples/printers/cfg.teal: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | txna ApplicationArgs 0 3 | method "create()void" 4 | == 5 | bnz main_l4 6 | txna ApplicationArgs 0 7 | method "opt_in()void" 8 | == 9 | bnz main_l3 10 | err 11 | main_l3: 12 | txn OnCompletion 13 | int OptIn 14 | == 15 | txn ApplicationID 16 | int 0 17 | != 18 | && 19 | assert 20 | callsub optin_1 21 | int 1 22 | return 23 | main_l4: 24 | txn OnCompletion 25 | int NoOp 26 | == 27 | txn ApplicationID 28 | int 0 29 | == 30 | && 31 | assert 32 | callsub create_0 33 | int 1 34 | return 35 | 36 | // create 37 | create_0: 38 | retsub 39 | 40 | // opt_in 41 | optin_1: 42 | retsub 43 | -------------------------------------------------------------------------------- /plugin_example/rekey_plugin/tealer_rekey_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Type, List, TYPE_CHECKING 2 | 3 | from tealer_rekey_plugin.detectors.rekeyto_stateless import CanRekey 4 | 5 | if TYPE_CHECKING: 6 | from tealer.detectors.abstract_detector import AbstractDetector 7 | from tealer.printers.abstract_printer import AbstractPrinter 8 | 9 | 10 | def make_plugin() -> Tuple[List[Type["AbstractDetector"]], List[Type["AbstractPrinter"]]]: 11 | plugin_detectors: List[Type["AbstractDetector"]] = [CanRekey] 12 | plugin_printers: List[Type["AbstractPrinter"]] = [] 13 | 14 | return plugin_detectors, plugin_printers 15 | -------------------------------------------------------------------------------- /tealer/utils/algorand_constants.py: -------------------------------------------------------------------------------- 1 | MAX_GROUP_SIZE = 16 2 | MIN_ALGORAND_FEE = 1000 # in micro algos 3 | ZERO_ADDRESS = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEVAL4QAJS7JHB4" 4 | MAX_UINT64 = (1 << 64) - 1 # 2**64 - 1 5 | # Maximum number of inner transactions a group transaction can have. 6 | MAX_NUM_INNER_TXN = 256 7 | 8 | # Over estimation of maximum cost a transaction can consume. 9 | # There can be a maximum of 256 inner transactions in a group. 10 | # There can be maximum of 16 transactions in a group. 11 | # Each inner txn and txn costs MIN_ALGORAND Fee 12 | MAX_TRANSACTION_COST = (MAX_GROUP_SIZE + MAX_NUM_INNER_TXN) * MIN_ALGORAND_FEE 13 | -------------------------------------------------------------------------------- /tests/parsing/teal8-instructions.teal: -------------------------------------------------------------------------------- 1 | #pragma version 8 // B0 2 | int 1 3 | int 2 4 | int 3 5 | bury 1 6 | popn 1 7 | dupn 1 8 | pushbytess b32(AA) base64 AA 0x00 "00" 9 | pushints 1 2 3 4 0x5 10 | callsub label1 11 | 12 | proto 1 2 // B1 13 | frame_dig 1 14 | frame_bury 1 15 | switch label2 label3 16 | 17 | match label2 label3 // B2 18 | 19 | box_create // B3 20 | box_extract 21 | box_replace 22 | box_del 23 | box_len 24 | box_get 25 | box_put 26 | int 1 27 | return 28 | 29 | label1: // B4 30 | int 1 31 | retsub 32 | 33 | label2: // B5 34 | int 2 35 | 36 | label3: // B6 37 | int 3 -------------------------------------------------------------------------------- /plugin_example/template/tealer_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Type, List, TYPE_CHECKING 2 | 3 | from tealer_plugin.detectors.example import Example 4 | 5 | if TYPE_CHECKING: 6 | from tealer.detectors.abstract_detector import AbstractDetector 7 | from tealer.printers.abstract_printer import AbstractPrinter 8 | 9 | 10 | def make_plugin() -> Tuple[List[Type["AbstractDetector"]], List[Type["AbstractPrinter"]]]: 11 | # import and add detectors, printers defined to the below lists 12 | plugin_detectors: List[Type["AbstractDetector"]] = [Example] 13 | plugin_printers: List[Type["AbstractPrinter"]] = [] 14 | 15 | return plugin_detectors, plugin_printers 16 | -------------------------------------------------------------------------------- /examples/printers/cfg.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=undefined-variable 2 | # type: ignore[name-defined] 3 | from pyteal import * # pylint: disable=wildcard-import,unused-wildcard-import 4 | 5 | router = Router( 6 | name="CFGExample", 7 | bare_calls=BareCallActions(), 8 | ) 9 | 10 | 11 | @router.method(no_op=CallConfig.CREATE) 12 | def create() -> Expr: 13 | return Return() 14 | 15 | 16 | @router.method(opt_in=CallConfig.CALL) 17 | def opt_in() -> Expr: 18 | return Return() 19 | 20 | 21 | pragma(compiler_version="0.22.0") 22 | application_approval_program, _, _ = router.compile_program(version=7) 23 | 24 | if __name__ == "__main__": 25 | print(application_approval_program) 26 | -------------------------------------------------------------------------------- /tealer/__init__.py: -------------------------------------------------------------------------------- 1 | """Static analyzer for Algorand Smart Contracts. 2 | 3 | Tealer comes with the utilities to parse and represent 4 | smart contracts written in TEAL. Tealer also contains 5 | detectors that analyze the TEAL contracts and printers 6 | that print interesting information extracted from the 7 | contract. 8 | 9 | Modules: 10 | detectors: Module for tealer detectors. 11 | 12 | printers: Module for tealer printers 13 | 14 | teal: Contains functions, classes for parsing and representing teal contracts. 15 | 16 | utils: Common util functions used by other packages in Tealer. 17 | 18 | exceptions: Defines exceptions classes for representing exceptions in tealer. 19 | """ 20 | -------------------------------------------------------------------------------- /.github/workflows/pip-audit.yml: -------------------------------------------------------------------------------- 1 | name: pip-audit 2 | 3 | on: 4 | push: 5 | branches: [ dev, main ] 6 | pull_request: 7 | branches: [ dev, main ] 8 | schedule: [ cron: "0 7 * * 2" ] 9 | 10 | jobs: 11 | audit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | - name: Set up Python 3.10 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: "3.10" 20 | - name: Install pip-audit 21 | run: | 22 | python -m pip install --upgrade pip setuptools wheel 23 | python -m pip install pip-audit 24 | - name: Run pip-audit 25 | run: | 26 | python -m pip install . 27 | pip-audit --desc 28 | -------------------------------------------------------------------------------- /tealer/printers/all_printers.py: -------------------------------------------------------------------------------- 1 | """Collects and exports printers defined in tealer. 2 | 3 | Exported printers are: 4 | 5 | * ``human-summary``: PrinterHumanSummary prints summary of the contract. 6 | * ``call-graph``: PrinterCallGraph exports call-graph of the contract. 7 | * ``function-cfg``: PrinterFunctionCFG exports contract subroutines in dot format. 8 | * ``cfg``: PrinterCFG exports CFG of the entire contract. 9 | """ 10 | 11 | # pylint: disable=unused-import 12 | from tealer.printers.human_summary import PrinterHumanSummary 13 | from tealer.printers.call_graph import PrinterCallGraph 14 | from tealer.printers.function_cfg import PrinterFunctionCFG 15 | from tealer.printers.full_cfg import PrinterCFG 16 | from tealer.printers.transaction_context import PrinterTransactionContext 17 | -------------------------------------------------------------------------------- /examples/printers/subroutine-cfg.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=undefined-variable 2 | # type: ignore[name-defined] 3 | from pyteal import * # pylint: disable=wildcard-import, unused-wildcard-import 4 | 5 | 6 | @Subroutine(TealType.none) # type: ignore[misc] 7 | def foo_bar(n: Expr) -> Expr: 8 | return If((n % Int(2)) == Int(0), Log(Bytes("Foo")), Log(Bytes("Bar"))) 9 | 10 | 11 | router = Router( 12 | name="SubroutineCFGExample", 13 | bare_calls=BareCallActions(), 14 | ) 15 | 16 | 17 | @router.method(no_op=CallConfig.ALL) 18 | def method_foo_bar() -> Expr: 19 | return foo_bar(Int(0)) 20 | 21 | 22 | pragma(compiler_version="0.22.0") 23 | application_approval_program, _, _ = router.compile_program(version=7) 24 | 25 | if __name__ == "__main__": 26 | print(application_approval_program) 27 | -------------------------------------------------------------------------------- /tests/test_mode_detector.py: -------------------------------------------------------------------------------- 1 | import pytest # pylint: disable=unused-import 2 | 3 | from tealer.teal.parse_teal import parse_teal 4 | from tealer.utils.teal_enums import ExecutionMode 5 | 6 | STATEFULL = """ 7 | #pragma version 5 8 | itxn_begin 9 | int pay 10 | itxn_field TypeEnum 11 | itxn_submit 12 | """ 13 | 14 | STATELESS = """ 15 | bytes "secret" 16 | arg_0 17 | == 18 | """ 19 | 20 | ANY = """ 21 | addr 5V2KYGC366NJNMIFLQOER2RUTZGYJSDXH7CLJUCDB7DZAMCDRM3YUHF4OM 22 | txn Sender 23 | == 24 | """ 25 | 26 | 27 | def test_mode() -> None: 28 | teal = parse_teal(STATEFULL) 29 | assert teal.version == 5 30 | assert teal.mode == ExecutionMode.STATEFUL 31 | 32 | teal = parse_teal(STATELESS) 33 | assert teal.version == 1 34 | assert teal.mode == ExecutionMode.STATELESS 35 | 36 | teal = parse_teal(ANY) 37 | assert teal.version == 1 38 | assert teal.mode == ExecutionMode.ANY 39 | -------------------------------------------------------------------------------- /tealer/teal/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains functions, classes for parsing and representing teal contracts. 2 | 3 | Module contains implementations for representing and parsing teal contracts. 4 | 5 | Additional to instructions, this modules contains implementation of Teal, BasicBlock 6 | classes. BasicBlock class is used to represent a basic block in the teal contract. 7 | Teal class is used to represent the teal contract. Teal class also comes with 8 | api methods to run detectors, printers on the contract. 9 | 10 | Modules: 11 | instructions: Contains functions, classes for parsing and representing teal 12 | instructions. 13 | 14 | basic_blocks: Defines class to represent basic blocks of teal contracts. 15 | 16 | global_field: Defines classes to represent global fields. 17 | 18 | parse_teal: Defines parsing functions for teal contracts. 19 | 20 | teal: Defines class to represent Teal contracts. 21 | """ 22 | -------------------------------------------------------------------------------- /tests/parsing/teal5-itxn.teal: -------------------------------------------------------------------------------- 1 | #pragma version 5 2 | 3 | itxn_begin 4 | int axfer 5 | itxn_field TypeEnum 6 | 7 | int 0 8 | itxn_field AssetAmount 9 | 10 | txna Assets 0 11 | itxn_field XferAsset 12 | 13 | global CurrentApplicationAddress 14 | itxn_field AssetReceiver 15 | itxn_submit 16 | 17 | itxn_begin 18 | int acfg 19 | itxn_field TypeEnum 20 | txn ApplicationArgs 1 21 | btoi 22 | itxn_field ConfigAssetTotal 23 | int 0 24 | itxn_field ConfigAssetDecimals 25 | byte "x" 26 | itxn_field ConfigAssetUnitName 27 | byte "X" 28 | itxn_field ConfigAssetName 29 | global CurrentApplicationAddress 30 | itxn_field ConfigAssetFreeze 31 | itxn_submit 32 | 33 | itxn_begin 34 | int afrz 35 | itxn_field TypeEnum 36 | 37 | txna Assets 0 38 | itxn_field FreezeAsset 39 | 40 | txn ApplicationArgs 1 41 | btoi 42 | itxn_field FreezeAssetFrozen 43 | 44 | txn Sender 45 | itxn_field FreezeAssetAccount 46 | 47 | itxn_submit 48 | 49 | int 1 50 | return 51 | 52 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | defaults: 4 | run: 5 | # To load bashrc 6 | shell: bash -ieo pipefail {0} 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - dev 13 | pull_request: 14 | schedule: 15 | # run CI every day even if no PRs/merges occur 16 | - cron: '0 12 * * *' 17 | 18 | jobs: 19 | tests: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | python: ${{fromJSON('["3.9", "3.10", "3.11", "3.12"]') }} 24 | 25 | steps: 26 | - uses: actions/checkout@v1 27 | - name: Set up Python ${{ matrix.python }} 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: ${{ matrix.python }} 31 | 32 | # Used by ci_test.sh 33 | - name: Install dependencies in ${{ matrix.python }} 34 | run: | 35 | pip install . 36 | - name: Run Tests in ${{ matrix.python }} 37 | run: | 38 | bash scripts/test_algorand_contracts.sh 39 | -------------------------------------------------------------------------------- /tealer/printers/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for tealer printers. 2 | 3 | Printers are information summarizers. Printers make finding and evaluating 4 | interesting information related to the contract easier. There's always abundant 5 | information that can be extracted from a smart contract, manually going 6 | through a smart contract for a every tiny bit of information is hard, boringgg 7 | and error prone. Printers make this process of finding information easier. 8 | 9 | This module contains Abstract base class for printers and implementation of 10 | few printers that can used to retrieve interesting information. 11 | 12 | Modules: 13 | abstract_printer: Defines abstract base class for printers in tealer. 14 | 15 | all_printers: Collects and exports printers defined in tealer. 16 | 17 | call_graph: Printer for exporting call-graph of the contract. 18 | 19 | function_cfg: Printer for exporting contract subroutines in dot format. 20 | 21 | human_summary: Printer to print summary of the contract. 22 | """ 23 | -------------------------------------------------------------------------------- /tests/parsing/teal5-mixed.teal: -------------------------------------------------------------------------------- 1 | #pragma version 5 2 | 3 | txn Sender 4 | txna ApplicationArgs 0 5 | gtxn 0 Sender 6 | gtxna 0 ApplicationArgs 0 7 | global MinTxnFee 8 | arg 0 9 | load 0 10 | loads 11 | store 0 12 | stores 13 | gload 0 0 14 | gloads 0 15 | gaid 0 16 | dig 0 17 | log 18 | cover 0 19 | uncover 0 20 | substring 0 2 21 | asset_params_get AssetTotal 22 | asset_holding_get AssetBalance 23 | gtxns Sender 24 | gtxnsa ApplicationArgs 0 25 | pushint 7272 26 | pushbytes "jojogoodgorilla" 27 | app_params_get AppGlobalNumUint 28 | extract 0 2 29 | txnas ApplicationArgs 30 | gtxnas 0 ApplicationArgs 31 | pop 32 | pop 33 | int 0 34 | int 0 35 | gtxnsas ApplicationArgs 36 | args 37 | itxn_begin 38 | int pay 39 | itxn_field TypeEnum 40 | itxn_submit 41 | itxn CreatedAssetID 42 | ed25519verify 43 | ecdsa_verify 1 44 | ecdsa_pk_recover 1 45 | ecdsa_pk_decompress 1 46 | global CurrentApplicationID 47 | pop 48 | byte 0x01 49 | byte "ZC9KNzlnWTlKZ1pwSkNzQXVzYjNBcG1xTU9YbkRNWUtIQXNKYVk2RzRBdExPakQx" 50 | addr DROUIZXGT3WFJR3QYVZWTR5OJJXJCMOLS7G4FUGZDSJM5PNOVOREH6HIZE 51 | ed25519verify 52 | pop 53 | int 1 54 | txn ApplicationArgs 0x1 55 | -------------------------------------------------------------------------------- /tealer/printers/full_cfg.py: -------------------------------------------------------------------------------- 1 | """Printer for exporting full contract CFG in dot format. 2 | 3 | Classes: 4 | PrinterCFG: Printer to export contract CFG. 5 | """ 6 | 7 | import os 8 | from pathlib import Path 9 | 10 | from tealer.printers.abstract_printer import AbstractPrinter 11 | from tealer.utils.output import full_cfg_to_dot, ROOT_OUTPUT_DIRECTORY 12 | 13 | 14 | class PrinterCFG(AbstractPrinter): # pylint: disable=too-few-public-methods 15 | """Printer to export contract CFG in dot.""" 16 | 17 | NAME = "cfg" 18 | HELP = "Export the CFG of entire contract" 19 | WIKI_URL = "https://github.com/crytic/tealer/wiki/Printer-documentation#cfg" 20 | 21 | def print(self) -> None: 22 | """Export the CFG of entire contract.""" 23 | # outputs a single file: set the dir to {ROOT_DIRECTORY}/{CONTRACT_NAME} 24 | dest = ROOT_OUTPUT_DIRECTORY / Path(self.teal.contract_name) 25 | os.makedirs(dest, exist_ok=True) 26 | 27 | filename = dest / Path("full_cfg.dot") 28 | 29 | print(f"\nCFG exported to file: {filename}") 30 | full_cfg_to_dot(self.teal, filename=filename) 31 | -------------------------------------------------------------------------------- /tests/parsing/transaction_fields.teal: -------------------------------------------------------------------------------- 1 | #pragma version 2 2 | 3 | // https://developer.algorand.org/docs/reference/teal/opcodes/#txn 4 | txn Sender 5 | txn Fee 6 | txn FirstValid 7 | txn FirstValidTime 8 | txn LastValid 9 | txn Note 10 | txn Lease 11 | txn Receiver 12 | txn Amount 13 | txn CloseRemainderTo 14 | txn VotePK 15 | txn SelectionPK 16 | txn VoteFirst 17 | txn VoteLast 18 | txn VoteKeyDilution 19 | txn Type 20 | txn TypeEnum 21 | txn XferAsset 22 | txn AssetAmount 23 | txn AssetSender 24 | txn AssetReceiver 25 | txn AssetCloseTo 26 | txn GroupIndex 27 | txn TxID 28 | txn ApplicationID 29 | txn OnCompletion 30 | txn ApplicationArgs 1 31 | txn NumAppArgs 32 | txn Accounts 1 33 | txn NumAccounts 34 | txn ApprovalProgram 35 | txn ClearStateProgram 36 | txn RekeyTo 37 | txn ConfigAsset 38 | txn ConfigAssetTotal 39 | txn ConfigAssetDecimals 40 | txn ConfigAssetDefaultFrozen 41 | txn ConfigAssetUnitName 42 | txn ConfigAssetName 43 | txn ConfigAssetURL 44 | txn ConfigAssetMetadataHash 45 | txn ConfigAssetManager 46 | txn ConfigAssetReserve 47 | txn ConfigAssetFreeze 48 | txn ConfigAssetClawback 49 | txn FreezeAsset 50 | txn FreezeAssetAccount 51 | txn FreezeAssetFrozen 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/parsing/teal5-app_params_get.teal: -------------------------------------------------------------------------------- 1 | #pragma version 5 2 | // Check the creator is the address provided 3 | int 0 4 | app_params_get AppCreator 5 | assert 6 | txn ApplicationArgs 0 7 | == 8 | assert 9 | 10 | 11 | // Check this program is version 5 12 | int 0 13 | app_params_get AppApprovalProgram 14 | assert 15 | int 0 16 | getbyte 17 | int 5 18 | == 19 | assert 20 | 21 | // Check it's longer than 15 (don't want to try to check all the bytes) 22 | int 0 23 | app_params_get AppApprovalProgram 24 | assert 25 | len 26 | int 15 27 | > 28 | assert 29 | 30 | 31 | // Check the Clear State program precisely 32 | int 0 33 | app_params_get AppClearStateProgram 34 | assert 35 | byte 0x0220010122 36 | == 37 | assert 38 | 39 | int 0 40 | app_params_get AppGlobalNumByteSlice 41 | assert 42 | int 1 43 | == 44 | assert 45 | 46 | int 0 47 | app_params_get AppGlobalNumUint 48 | assert 49 | int 2 50 | == 51 | assert 52 | 53 | int 0 54 | app_params_get AppLocalNumByteSlice 55 | assert 56 | int 3 57 | == 58 | assert 59 | 60 | int 0 61 | app_params_get AppLocalNumUint 62 | assert 63 | int 4 64 | == 65 | assert 66 | 67 | int 0 68 | app_params_get AppExtraProgramPages 69 | assert 70 | int 2 71 | == 72 | assert 73 | 74 | int 1 75 | -------------------------------------------------------------------------------- /examples/printers/human-summary.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=undefined-variable 2 | # type: ignore[name-defined] 3 | from pyteal import * # pylint: disable=wildcard-import, unused-wildcard-import 4 | 5 | 6 | router = Router( 7 | name="HumanSummaryExample", 8 | bare_calls=BareCallActions(), 9 | ) 10 | 11 | 12 | @Subroutine(TealType.none) 13 | def is_creator() -> Expr: 14 | return Assert(Txn.sender() == Global.creator_address()) 15 | 16 | 17 | @router.method(no_op=CallConfig.CREATE) 18 | def create() -> Expr: 19 | return Return() 20 | 21 | 22 | @router.method(update_application=CallConfig.CALL) 23 | def update_application() -> Expr: 24 | return is_creator() 25 | 26 | 27 | @router.method(delete_application=CallConfig.CALL) 28 | def delete_application() -> Expr: 29 | return is_creator() 30 | 31 | 32 | @Subroutine(TealType.none) 33 | def greet() -> Expr: 34 | return Log(Bytes("Hello")) 35 | 36 | 37 | @router.method(no_op=CallConfig.CALL) 38 | def hello() -> Expr: 39 | return greet() 40 | 41 | 42 | pragma(compiler_version="0.22.0") 43 | application_approval_program, _, _ = router.compile_program(version=7) 44 | 45 | if __name__ == "__main__": 46 | print(application_approval_program) 47 | -------------------------------------------------------------------------------- /tealer/teal/instructions/asset_holding_field.py: -------------------------------------------------------------------------------- 1 | """Defines classes to represent asset_holding_get fields. 2 | 3 | ``asset_holding_get`` instruction is used to access holding fields 4 | of an asset for the given account. 5 | 6 | Each field that can be accessed using asset_holding_get is represented 7 | by a class in tealer. All the classes representing the fields must inherit 8 | from AssetHoldingField class. 9 | 10 | """ 11 | 12 | # pylint: disable=too-few-public-methods 13 | class AssetHoldingField: 14 | """Base class to represent asset_holding_get field.""" 15 | 16 | def __init__(self) -> None: 17 | self._version: int = 2 18 | 19 | @property 20 | def version(self) -> int: 21 | """Teal version this field is introduced in and supported from. 22 | 23 | Returns: 24 | Teal version the field is introduced in and supported from. 25 | """ 26 | return self._version 27 | 28 | def __str__(self) -> str: 29 | return self.__class__.__qualname__ 30 | 31 | 32 | class AssetBalance(AssetHoldingField): 33 | """Amount of the asset unit held by this account.""" 34 | 35 | 36 | class AssetFrozen(AssetHoldingField): 37 | """Is asset frozen for this account or not.""" 38 | -------------------------------------------------------------------------------- /examples/printers/call-graph.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=undefined-variable 2 | # type: ignore[name-defined] 3 | from pyteal import * # pylint: disable=wildcard-import, unused-wildcard-import 4 | 5 | 6 | @Subroutine(TealType.none) 7 | def my_subroutine(): 8 | return Approve() 9 | 10 | 11 | @Subroutine(TealType.none) 12 | def recursive_subroutine(n: Expr) -> Expr: 13 | return If( 14 | n == Int(4), 15 | Return(), 16 | recursive_subroutine(n + Int(1)), 17 | ) 18 | 19 | 20 | @Subroutine(TealType.none) 21 | def main_subroutine() -> Expr: 22 | return Seq([my_subroutine(), recursive_subroutine(Int(0))]) 23 | 24 | 25 | router = Router( 26 | name="CallGraphExample", 27 | bare_calls=BareCallActions(), 28 | ) 29 | 30 | 31 | @router.method(no_op=CallConfig.CALL) 32 | def method_a() -> Expr: 33 | return Seq([my_subroutine()]) 34 | 35 | 36 | @router.method(no_op=CallConfig.CALL) 37 | def method_b() -> Expr: 38 | return Seq([main_subroutine()]) 39 | 40 | 41 | @router.method(no_op=CallConfig.CALL) 42 | def method_c() -> Expr: 43 | return Approve() 44 | 45 | 46 | pragma(compiler_version="0.22.0") 47 | application_approval_program, _, _ = router.compile_program(version=7) 48 | 49 | if __name__ == "__main__": 50 | print(application_approval_program) 51 | -------------------------------------------------------------------------------- /tealer/utils/comparable_enum.py: -------------------------------------------------------------------------------- 1 | """Defines a comparable enum class. 2 | 3 | This module implements a comparable enum class which extends 4 | ``Enum`` class with capabilities for comparing two symbolic names 5 | of defined in the same enumeration. 6 | 7 | Classes: 8 | ComparableEnum: Enum subclass with additional comparison capability 9 | i.e enumerations defined with ComparableEnum can be compared. 10 | """ 11 | 12 | from enum import Enum 13 | 14 | from typing import Any 15 | 16 | 17 | # from slither: slither/utils/comparable_enum.py 18 | class ComparableEnum(Enum): 19 | """Class to represent comparable enum types.""" 20 | 21 | def __eq__(self, other: Any) -> bool: 22 | if isinstance(other, ComparableEnum): 23 | return self.value == other.value 24 | return False 25 | 26 | def __ne__(self, other: Any) -> bool: 27 | if isinstance(other, ComparableEnum): 28 | return self.value != other.value 29 | return False 30 | 31 | def __lt__(self, other: Any) -> bool: 32 | if isinstance(other, ComparableEnum): 33 | return self.value < other.value 34 | return False 35 | 36 | def __repr__(self) -> str: 37 | return f"{self.value}" 38 | 39 | def __hash__(self) -> int: 40 | return hash(self.value) 41 | -------------------------------------------------------------------------------- /tealer/teal/instructions/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains functions, classes for parsing and representing teal instructions. 2 | 3 | For representing teal code, this module contains classes for teal 4 | instructions, transaction fields, App parameter fields, Asset parameter 5 | fields and Asset holding fields. 6 | 7 | This module also contains implementations of parsers for instructions, 8 | global fields, transaction fields, App parameter fields, 9 | Asset parameter fields and Asset holding fields. 10 | 11 | Modules: 12 | app_params_field: Defines classes to represent app parameter fields. 13 | 14 | asset_holding_field: Defines classes to represent asset holding fields. 15 | 16 | asset_params_field: Defines classes to represent asset parameter fields. 17 | 18 | instructions: Defines classes to represent teal instructions. 19 | 20 | parse_app_params_field: Parser for app parameter fields. 21 | 22 | parse_asset_holding_field: Parser for asset holding fields. 23 | 24 | parse_asset_params_field: Parser for asset parameter fields. 25 | 26 | parse_global_field: Parser for global fields. 27 | 28 | parse_instruction: Parser for teal instructions. 29 | 30 | parse_transaction_field: Parser for transaction fields. 31 | 32 | transaction_field: Defines classes to represent transaction fields. 33 | """ 34 | -------------------------------------------------------------------------------- /tests/test_regex.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | 4 | from tealer.teal.instructions.instructions import Eq 5 | from tealer.teal.parse_teal import parse_teal 6 | from tealer.utils.regex.regex import parse_regex, match_regex, update_config 7 | from tealer.utils.output import CFGDotConfig, full_cfg_to_dot 8 | 9 | 10 | def test_parse_regex() -> None: 11 | txt = """ 12 | some_label => 13 | gtxn 0 Amount 14 | int 13337 15 | ==""" 16 | 17 | regex = parse_regex(txt) 18 | assert regex.label == "some_label" 19 | assert len(regex.instructions) == 3 20 | assert isinstance(regex.instructions[-1], Eq) 21 | 22 | 23 | def test_match() -> None: 24 | 25 | with open("./tests/regex/vote_approval.teal", "r", encoding="utf8") as f_codebase: 26 | teal = parse_teal(f_codebase.read()) 27 | 28 | with open("./tests/regex/regex.txt", "r", encoding="utf8") as f_regex: 29 | regex = parse_regex(f_regex.read()) 30 | 31 | matches, covered = match_regex(teal, regex) 32 | 33 | assert len(matches) == 1 34 | 35 | assert len(covered) == 21 36 | 37 | # Ensure the CFG generation does not crash 38 | config = CFGDotConfig() 39 | update_config(config, matches, covered) 40 | 41 | with tempfile.NamedTemporaryFile() as fp: 42 | full_cfg_to_dot(teal, config, Path(fp.name)) 43 | -------------------------------------------------------------------------------- /tests/test_parsing_using_pyteal.py: -------------------------------------------------------------------------------- 1 | # type: ignore[unreachable] 2 | import sys 3 | import pytest 4 | 5 | from tealer.teal.instructions import instructions 6 | from tealer.teal.parse_teal import parse_teal 7 | 8 | if not sys.version_info >= (3, 10): 9 | pytest.skip(reason="PyTeal based tests require python >= 3.10", allow_module_level=True) 10 | 11 | # pylint: disable=wrong-import-position 12 | # Place import statements after the version check 13 | from tests.pyteal_parsing.normal_application import normal_application_approval_program 14 | from tests.pyteal_parsing.arc4_application import arc4_application_ap 15 | from tests.pyteal_parsing.control_flow_constructs import control_flow_ap 16 | 17 | 18 | TARGETS = [ 19 | normal_application_approval_program, 20 | arc4_application_ap, 21 | control_flow_ap, 22 | ] 23 | 24 | 25 | @pytest.mark.parametrize("target", TARGETS) # type: ignore 26 | def test_parsing_using_pyteal(target: str) -> None: 27 | teal = parse_teal(target) 28 | # print instruction to trigger __str__ on each ins 29 | # print stack pop/push to trigger the function on each ins 30 | for i in teal.instructions: 31 | assert not isinstance(i, instructions.UnsupportedInstruction), f'ins "{i}" is not supported' 32 | print(i, i.cost) 33 | print(i, i.stack_pop_size) 34 | print(i, i.stack_push_size) 35 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "tealer" 7 | version = "0.1.2" 8 | authors = [{ name = "Trail of Bits" }] 9 | description = "Teal analyzer." 10 | readme = "README.md" 11 | license = { "text" = "AGPL-3.0" } 12 | urls = { "Homepage" = "https://github.com/crytic/tealer" } 13 | requires-python = ">=3.9" 14 | dependencies = [ 15 | "prettytable>=0.7.2", 16 | "py-algorand-sdk", 17 | "pycryptodomex", 18 | "requests", 19 | "pyyaml", 20 | ] 21 | 22 | [project.optional-dependencies] 23 | dev = [ 24 | "pylint==2.13.4", 25 | "black==22.3.0", 26 | "mypy==0.942", 27 | "pytest-cov", 28 | ] 29 | 30 | [project.scripts] 31 | tealer = "tealer.__main__:main" 32 | 33 | [tool.setuptools.packages.find] 34 | where = ["."] 35 | 36 | [tool.black] 37 | target-version = ["py39"] 38 | line-length = 100 39 | [tool.pylint.messages_control] 40 | disable = """ 41 | missing-module-docstring, 42 | missing-class-docstring, 43 | missing-function-docstring, 44 | unnecessary-lambda, 45 | bad-continuation, 46 | cyclic-import, 47 | line-too-long, 48 | invalid-name, 49 | fixme, 50 | too-many-return-statements, 51 | too-many-ancestors, 52 | logging-fstring-interpolation, 53 | logging-not-lazy, 54 | duplicate-code, 55 | import-error, 56 | unsubscriptable-object, 57 | """ 58 | -------------------------------------------------------------------------------- /tests/pyteal_parsing/arc4_application.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=undefined-variable 2 | # type: ignore[name-defined] 3 | from pyteal import * # pylint: disable=wildcard-import, unused-wildcard-import 4 | 5 | barecalls = BareCallActions( 6 | no_op=OnCompleteAction(action=Approve(), call_config=CallConfig.CREATE), 7 | opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.ALL), 8 | close_out=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL), 9 | update_application=OnCompleteAction(call_config=CallConfig.NEVER), 10 | delete_application=OnCompleteAction(call_config=CallConfig.NEVER), 11 | ) 12 | 13 | router = Router( 14 | name="ARC4 application parsing test", 15 | bare_calls=barecalls, 16 | clear_state=Approve(), 17 | ) 18 | 19 | 20 | @ABIReturnSubroutine 21 | def identity(arg: abi.Uint64, *, output: abi.Uint64) -> Expr: 22 | return output.set(arg.get()) 23 | 24 | 25 | @router.method( # type: ignore[misc] 26 | opt_in=CallConfig.ALL, 27 | close_out=CallConfig.CALL, 28 | update_application=CallConfig.NEVER, 29 | delete_application=CallConfig.NEVER, 30 | ) 31 | def hello(name: abi.String, *, output: abi.String) -> Expr: 32 | return output.set(Concat(Bytes("Hello "), name.get())) 33 | 34 | 35 | router.add_method_handler(identity, method_config=MethodConfig(no_op=CallConfig.CALL)) 36 | 37 | 38 | arc4_application_ap, _clear_program, _ = router.compile_program(version=6) 39 | -------------------------------------------------------------------------------- /plugin_example/README.md: -------------------------------------------------------------------------------- 1 | # Tealer plugin example 2 | 3 | Tealer supports plugins, additional detectors and printers can be added to tealer in the form of plugins. 4 | This repository contains template and an actual implementation of tealer plugin. 5 | 6 | ## Architecture 7 | 8 | - plugin have to define a entrypoint in `setup.py`. entrypoint should be defined with group set to `teal_analyzer.plugin` and it should return the tuple containing list of detectors and list of printers when called i.e `Tuple[List[Type[AbstractDetector]], List[Type[AbstractPrinter]]]` 9 | - Detectors should be subclasses of `AbstractDetector`. Detectors have to override `detect` method and return output in one of the supported formats. 10 | - Printers should be subclasses of `AbstractPrinter` and have to override `print` method. 11 | 12 | see `template` folder for skeleton of the plugin 13 | 14 | - `setup.py`: Contains the plugin information. 15 | - `tealer_plugin/__init__.py`: Contains `make_plugin` function which has to return list of detectors and printers. 16 | - `tealer_plugin/detectors/example.py`: Contains detector plugin skeleton. 17 | 18 | plugin can be installed after updating the files in the template by running: 19 | 20 | ```sh 21 | python setup.py develop 22 | ``` 23 | 24 | `rekey_plugin` contains actual implementation of a plugin. 25 | 26 | It is recommended to use a Python virtual environment (for example: [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/)). -------------------------------------------------------------------------------- /tealer/teal/instructions/parse_asset_holding_field.py: -------------------------------------------------------------------------------- 1 | """Parser for fields of asset_holding_get. 2 | 3 | Each ``asset_holding_get`` field is represented as a class. Parsing 4 | the field is creating the class instance representing the field given 5 | it's string representation. 6 | 7 | Fields of ``asset_holding_get`` doesn't have immediate arguments 8 | and have very simple structure. All fields map one-to-one with 9 | their string representation. As a result, asset holding fields are 10 | parsed by first constructing a map(dict) from their string 11 | representation to corresponding class representing the field and 12 | using that to find the correct class for the given field. 13 | 14 | Attributes: 15 | ASSET_HOLDING_FIELD_TXT_TO_OBJECT: Map(dict) from string representation 16 | of asset holding field to the corresponding class. 17 | """ 18 | 19 | from tealer.teal.instructions.asset_holding_field import ( 20 | AssetHoldingField, 21 | AssetBalance, 22 | AssetFrozen, 23 | ) 24 | 25 | ASSET_HOLDING_FIELD_TXT_TO_OBJECT = {"AssetBalance": AssetBalance, "AssetFrozen": AssetFrozen} 26 | 27 | 28 | def parse_asset_holding_field(field: str) -> AssetHoldingField: 29 | """Parse fields of ``asset_holding_get``. 30 | 31 | Args: 32 | field: string representation of the field. 33 | 34 | Returns: 35 | object of class corresponding to the given asset_holding_get field. 36 | """ 37 | 38 | return ASSET_HOLDING_FIELD_TXT_TO_OBJECT[field]() 39 | -------------------------------------------------------------------------------- /tests/parsing/teal4-random-opcodes.teal: -------------------------------------------------------------------------------- 1 | // get the amount of the second transaction 2 | int 1 3 | gtxns Amount 4 | 5 | 6 | // get the first argument of the first transaction 7 | // in a stateful smart contract 8 | int 0 9 | gtxnsa ApplicationArgs 0 10 | 11 | 12 | 13 | // Assume current transaction depends on the transaction 14 | // that is next in the group 15 | // get current group index 16 | // add one and then get that transaction amount 17 | txn GroupIndex 18 | int 1 19 | + 20 | gtxns Amount 21 | 22 | // From https://github.com/algorand/go-algorand/blob/58bc231a0a71c7bccf00881d32790cdd3afe426e/data/transactions/logic/assembler_test.go#L251 23 | int 1 24 | pushint 2000 25 | int 0 26 | int 2 27 | divmodw 28 | callsub stuff 29 | b next 30 | stuff: 31 | retsub 32 | next: 33 | int 1 34 | int 2 35 | shl 36 | int 1 37 | shr 38 | sqrt 39 | int 2 40 | exp 41 | int 2 42 | expw 43 | bitlen 44 | b+ 45 | b- 46 | b/ 47 | b* 48 | b< 49 | b> 50 | b<= 51 | b>= 52 | b== 53 | b!= 54 | b% 55 | b| 56 | b& 57 | b^ 58 | b~ 59 | int 2 60 | bzero 61 | gload 0 0 62 | gloads 0 63 | gaid 0 64 | gaids 65 | int 100 66 | 67 | intc 0 68 | dig 1 69 | swap 70 | getbit 71 | setbit 72 | getbyte 73 | setbyte 74 | select 75 | min_balance 76 | bytec 1 77 | pushbytes 0xb49276bd3ec0977eab86a321c449ead802c96c0bd97c2956131511d2f11eebec 78 | 79 | txna Assets 0 80 | txn NumAssets 81 | txna Applications 0 82 | txn NumApplications 83 | txn GlobalNumUint 84 | txn GlobalNumByteSlice 85 | txn LocalNumUint 86 | txn LocalNumByteSlice -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build-release: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.x' 19 | 20 | - name: Build distributions 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install build 24 | python -m build 25 | 26 | - name: Upload distributions 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: tealer-dists 30 | path: dist/ 31 | 32 | publish: 33 | runs-on: ubuntu-latest 34 | environment: release 35 | permissions: 36 | id-token: write # For trusted publishing + codesigning. 37 | contents: write # For attaching signing artifacts to the release. 38 | needs: 39 | - build-release 40 | steps: 41 | - name: fetch dists 42 | uses: actions/download-artifact@v4 43 | with: 44 | name: tealer-dists 45 | path: dist/ 46 | 47 | - name: publish 48 | uses: pypa/gh-action-pypi-publish@v1.8.11 49 | 50 | - name: sign 51 | uses: sigstore/gh-action-sigstore-python@v2.1.1 52 | with: 53 | inputs: ./dist/*.tar.gz ./dist/*.whl 54 | release-signing-artifacts: true 55 | bundle-only: true -------------------------------------------------------------------------------- /examples/printers/call-graph.teal: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | txna ApplicationArgs 0 3 | method "method_a()void" 4 | == 5 | bnz main_l6 6 | txna ApplicationArgs 0 7 | method "method_b()void" 8 | == 9 | bnz main_l5 10 | txna ApplicationArgs 0 11 | method "method_c()void" 12 | == 13 | bnz main_l4 14 | err 15 | main_l4: 16 | txn OnCompletion 17 | int NoOp 18 | == 19 | txn ApplicationID 20 | int 0 21 | != 22 | && 23 | assert 24 | callsub methodc_5 25 | int 1 26 | return 27 | main_l5: 28 | txn OnCompletion 29 | int NoOp 30 | == 31 | txn ApplicationID 32 | int 0 33 | != 34 | && 35 | assert 36 | callsub methodb_4 37 | int 1 38 | return 39 | main_l6: 40 | txn OnCompletion 41 | int NoOp 42 | == 43 | txn ApplicationID 44 | int 0 45 | != 46 | && 47 | assert 48 | callsub methoda_3 49 | int 1 50 | return 51 | 52 | // my_subroutine 53 | mysubroutine_0: 54 | int 1 55 | return 56 | 57 | // recursive_subroutine 58 | recursivesubroutine_1: 59 | store 0 60 | load 0 61 | int 4 62 | == 63 | bnz recursivesubroutine_1_l2 64 | load 0 65 | int 1 66 | + 67 | load 0 68 | swap 69 | callsub recursivesubroutine_1 70 | store 0 71 | b recursivesubroutine_1_l3 72 | recursivesubroutine_1_l2: 73 | retsub 74 | recursivesubroutine_1_l3: 75 | retsub 76 | 77 | // main_subroutine 78 | mainsubroutine_2: 79 | callsub mysubroutine_0 80 | int 0 81 | callsub recursivesubroutine_1 82 | retsub 83 | 84 | // method_a 85 | methoda_3: 86 | callsub mysubroutine_0 87 | retsub 88 | 89 | // method_b 90 | methodb_4: 91 | callsub mainsubroutine_2 92 | retsub 93 | 94 | // method_c 95 | methodc_5: 96 | int 1 97 | return 98 | -------------------------------------------------------------------------------- /examples/printers/human-summary.teal: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | txna ApplicationArgs 0 3 | method "create()void" 4 | == 5 | bnz main_l8 6 | txna ApplicationArgs 0 7 | method "update_application()void" 8 | == 9 | bnz main_l7 10 | txna ApplicationArgs 0 11 | method "delete_application()void" 12 | == 13 | bnz main_l6 14 | txna ApplicationArgs 0 15 | method "hello()void" 16 | == 17 | bnz main_l5 18 | err 19 | main_l5: 20 | txn OnCompletion 21 | int NoOp 22 | == 23 | txn ApplicationID 24 | int 0 25 | != 26 | && 27 | assert 28 | callsub hello_5 29 | int 1 30 | return 31 | main_l6: 32 | txn OnCompletion 33 | int DeleteApplication 34 | == 35 | txn ApplicationID 36 | int 0 37 | != 38 | && 39 | assert 40 | callsub deleteapplication_3 41 | int 1 42 | return 43 | main_l7: 44 | txn OnCompletion 45 | int UpdateApplication 46 | == 47 | txn ApplicationID 48 | int 0 49 | != 50 | && 51 | assert 52 | callsub updateapplication_2 53 | int 1 54 | return 55 | main_l8: 56 | txn OnCompletion 57 | int NoOp 58 | == 59 | txn ApplicationID 60 | int 0 61 | == 62 | && 63 | assert 64 | callsub create_1 65 | int 1 66 | return 67 | 68 | // is_creator 69 | iscreator_0: 70 | txn Sender 71 | global CreatorAddress 72 | == 73 | assert 74 | retsub 75 | 76 | // create 77 | create_1: 78 | retsub 79 | 80 | // update_application 81 | updateapplication_2: 82 | callsub iscreator_0 83 | retsub 84 | 85 | // delete_application 86 | deleteapplication_3: 87 | callsub iscreator_0 88 | retsub 89 | 90 | // greet 91 | greet_4: 92 | byte "Hello" 93 | log 94 | retsub 95 | 96 | // hello 97 | hello_5: 98 | callsub greet_4 99 | retsub 100 | -------------------------------------------------------------------------------- /tests/parsing/teal7-instructions.teal: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | // new ecdsa curve: ecdsa_verify v, v can be Secp256r1 from Teal v7 3 | 4 | ecdsa_verify Secp256r1 5 | 6 | // new transaction field: FirstValidTime, NumApprovalProgramPages, NumClearStateProgramPages 7 | 8 | txn FirstValidTime 9 | gtxn 0 FirstValidTime 10 | 11 | txn NumApprovalProgramPages 12 | gtxn 0 NumApprovalProgramPages 13 | 14 | txn NumClearStateProgramPages 15 | gtxn 0 NumClearStateProgramPages 16 | 17 | // new array transaction fields: ApprovalProgramPages, ClearStateProgramPages 18 | 19 | txna ApprovalProgramPages 0 20 | txna ClearStateProgramPages 0 21 | 22 | gtxna 1 ApprovalProgramPages 0 23 | gtxna 2 ClearStateProgramPages 1 24 | 25 | gtxnsa ApprovalProgramPages 0 26 | gtxnsa ClearStateProgramPages 1 27 | 28 | gtxnsas ApprovalProgramPages 29 | gtxnsas ClearStateProgramPages 30 | 31 | 32 | // new ins: replace2 s 33 | 34 | byte "abcd" 35 | byte "ef" 36 | replace2 2 37 | 38 | // new ins: replace3 39 | 40 | byte "abcd" 41 | int 2 42 | byte "ef" 43 | replace3 44 | 45 | // new ins: replace 46 | 47 | byte "abcd" 48 | byte "ef" 49 | replace 2 50 | 51 | byte "abcd" 52 | int 2 53 | byte "ef" 54 | replace 55 | 56 | 57 | // new ins: base64_decode e 58 | 59 | byte "AAAA" 60 | base64_decode StdEncoding 61 | 62 | // new ins: json_ref r 63 | json_ref JSONUint64 64 | 65 | // new ins: ed25519verify_bare 66 | byte "A" 67 | byte "B" 68 | byte "C" 69 | ed25519verify_bare 70 | 71 | // new_ins: sha3_256 72 | byte "A" 73 | sha3_256 74 | 75 | // new_ins: vrf_verify s 76 | byte "A" 77 | byte "B" 78 | byte "C" 79 | vrf_verify VrfAlgorand 80 | 81 | // new_ins: block f 82 | int 100 83 | block BlkTimestamp 84 | -------------------------------------------------------------------------------- /tealer/detectors/all_detectors.py: -------------------------------------------------------------------------------- 1 | """Collects and exports detectors defined in tealer. 2 | 3 | Exported detectors are: 4 | 5 | * ``isDeletable``: IsDeletable detects paths missing DeleteApplication check. 6 | * ``isUpdatable``: IsUpdatable detects paths missing UpdateApplication check. 7 | * ``groupSize``: MissingGroupSize detects paths missing GroupSize check. 8 | * ``rekeyTo``: MissingRekeyTo detects paths missing RekeyTo check. 9 | * ``canCloseAccount``: CanCloseAccount detects paths missing CloseRemainderTo check. 10 | * ``canCloseAsset``: CanCloseAsset detects paths missing AssetCloseTo check. 11 | * ``feeCheck``: MissingFeeCheck detects paths missing Fee check. 12 | * ``anyoneCanUpdate``: Detect paths missing validations on sender field AND allows to delete the application. 13 | * ``anyoneCanDelete``: Detect paths missing validations on sender field AND allows to update the application. 14 | """ 15 | 16 | # pylint: disable=unused-import 17 | from tealer.detectors.is_deletable import IsDeletable 18 | from tealer.detectors.is_updatable import IsUpdatable 19 | from tealer.detectors.groupsize import MissingGroupSize 20 | from tealer.detectors.rekeyto import MissingRekeyTo 21 | from tealer.detectors.can_close_account import CanCloseAccount 22 | from tealer.detectors.can_close_asset import CanCloseAsset 23 | from tealer.detectors.fee_check import MissingFeeCheck 24 | from tealer.detectors.anyone_can_update import AnyoneCanUpdate 25 | from tealer.detectors.anyone_can_delete import AnyoneCanDelete 26 | from tealer.detectors.optimizations.constant_gtxn import ConstantGtxn 27 | from tealer.detectors.optimizations.sender_access import SenderAccess 28 | from tealer.detectors.optimizations.self_access import SelfAccess 29 | -------------------------------------------------------------------------------- /plugin_example/template/tealer_plugin/detectors/example.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from tealer.detectors.abstract_detector import ( 4 | AbstractDetector, 5 | DetectorClassification, 6 | DetectorType, 7 | ) 8 | 9 | 10 | if TYPE_CHECKING: 11 | from tealer.utils.output import ListOutput 12 | 13 | 14 | class Example(AbstractDetector): # pylint: disable=too-few-public-methods 15 | """ 16 | Documentation 17 | """ 18 | 19 | NAME = "mydetector" # this is used to select the detector and also while showing output 20 | DESCRIPTION = "Description of the detector." 21 | TYPE: DetectorType = DetectorType.STATELESS # type of detector 22 | 23 | IMPACT: DetectorClassification = DetectorClassification.HIGH 24 | CONFIDENCE: DetectorClassification = DetectorClassification.HIGH 25 | 26 | WIKI_TITLE = "" 27 | WIKI_DESCRIPTION = "" 28 | WIKI_EXPLOIT_SCENARIO = "" 29 | WIKI_RECOMMENDATION = "" # this will be shown in json output as help message 30 | 31 | def detect(self) -> "ListOutput": 32 | """This method will be called if this detector is chosen. 33 | 34 | Implement this method to return output in one of the supported formats (ExecutionPaths). 35 | Use `generate_result` method to generate the output after finding the list of execution 36 | paths considered to be vulnerable or need to be highlighted. `generate_result` also takes 37 | two parameters `description` and `filename`. `description` is supposed to explain about the 38 | execution path and `filename` is the filename prefix of the output dot files. 39 | 40 | See `rekey_plugin` for actual implementation of a plugin. 41 | """ 42 | -------------------------------------------------------------------------------- /tests/test_string_representation.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | import pytest 3 | 4 | from tealer.teal.instructions.parse_app_params_field import APP_PARAMS_FIELD_TXT_TO_OBJECT 5 | from tealer.teal.instructions.parse_asset_holding_field import ASSET_HOLDING_FIELD_TXT_TO_OBJECT 6 | from tealer.teal.instructions.parse_asset_params_field import ASSET_PARAMS_FIELD_TXT_TO_OBJECT 7 | from tealer.teal.instructions.parse_global_field import GLOBAL_FIELD_TXT_TO_OBJECT 8 | from tealer.teal.instructions.parse_transaction_field import TX_FIELD_TXT_TO_OBJECT 9 | from tealer.teal.instructions import instructions 10 | 11 | INSTRUCTION_TXT_TO_OBJECT = { 12 | "err": instructions.Err, 13 | "assert": instructions.Assert, 14 | "gaids": instructions.Gaids, 15 | "loads": instructions.Loads, 16 | "stores": instructions.Stores, 17 | "swap": instructions.Swap, 18 | "getbit": instructions.GetBit, 19 | "setbit": instructions.SetBit, 20 | "getbyte": instructions.GetByte, 21 | "setbyte": instructions.SetByte, 22 | "extract3": instructions.Extract3, 23 | "extract_uint16": instructions.Extract_uint16, 24 | "sha256": instructions.Sha256, 25 | "sha512_256": instructions.Sha512_256, 26 | "retsub": instructions.Retsub, 27 | } 28 | 29 | ALL_TESTS = [ 30 | APP_PARAMS_FIELD_TXT_TO_OBJECT, 31 | ASSET_HOLDING_FIELD_TXT_TO_OBJECT, 32 | ASSET_PARAMS_FIELD_TXT_TO_OBJECT, 33 | GLOBAL_FIELD_TXT_TO_OBJECT, 34 | TX_FIELD_TXT_TO_OBJECT, 35 | INSTRUCTION_TXT_TO_OBJECT, 36 | ] 37 | 38 | 39 | @pytest.mark.parametrize("test", ALL_TESTS) # type: ignore 40 | def test_string_repr(test: Dict[str, Any]) -> None: 41 | for target, obj in test.items(): 42 | assert str(obj()) == target 43 | -------------------------------------------------------------------------------- /tests/pyteal_parsing/normal_application.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # pylint: disable=undefined-variable 3 | # type: ignore[name-defined] 4 | from pyteal import * # pylint: disable=wildcard-import, unused-wildcard-import 5 | 6 | 7 | @Subroutine(TealType.uint64) 8 | def add_upto_n(n: ScratchVar) -> Expr: 9 | i = ScratchVar() 10 | result = ScratchVar() 11 | return Seq( 12 | [ 13 | result.store(Int(0)), 14 | For(i.store(Int(0)), i.load() < n.load(), i.store(i.load() + Int(1))).Do( 15 | result.store(result.load() + i.load()) 16 | ), 17 | Return(result.load()), 18 | ] 19 | ) 20 | 21 | 22 | def on_creation() -> Expr: 23 | x = ScratchVar() 24 | return Seq([x.store(Int(10)), Pop(add_upto_n(x)), Return(Int(1))]) 25 | 26 | 27 | def approval_program() -> Expr: 28 | return Cond( 29 | [Txn.application_id() == Int(0), on_creation()], 30 | [Txn.on_completion() == OnComplete.DeleteApplication, Reject()], 31 | [Txn.on_completion() == OnComplete.UpdateApplication, Reject()], 32 | [Txn.on_completion() == OnComplete.CloseOut, Approve()], 33 | [Txn.on_completion() == OnComplete.OptIn, Approve()], 34 | ) 35 | 36 | 37 | def clear_program() -> Expr: 38 | return Return(Int(1)) 39 | 40 | 41 | normal_application_approval_program = compileTeal( 42 | approval_program(), 43 | mode=Mode.Application, 44 | version=7, 45 | optimize=OptimizeOptions(scratch_slots=False), 46 | assembleConstants=True, 47 | ) 48 | 49 | _clear_program = compileTeal( 50 | clear_program(), mode=Mode.Application, version=7, optimize=OptimizeOptions(scratch_slots=False) 51 | ) 52 | 53 | if __name__ == "__main__": 54 | print(normal_application_approval_program) 55 | -------------------------------------------------------------------------------- /tests/parsing/teal6-instructions.teal: -------------------------------------------------------------------------------- 1 | #pragma version 6 2 | 3 | // new txn fields LastLog, StateProofPK 4 | txn LastLog 5 | txn StateProofPK 6 | 7 | // new global fields OpcodeBudget, CallerApplicationID, CallerApplicationAddress 8 | global OpcodeBudget 9 | global CallerApplicationID 10 | global CallerApplicationAddress 11 | 12 | // new ins: acct_params_get f, f can be AcctBalance, AcctMinBalance, AcctAuthAddr, 13 | 14 | acct_params_get AcctBalance 15 | acct_params_get AcctMinBalance 16 | acct_params_get AcctAuthAddr 17 | 18 | // new ins: bsqrt 19 | int 2 20 | bsqrt 21 | 22 | // new ins: divw 23 | 24 | int 100 25 | int 99 26 | int 2 27 | divw 28 | 29 | // new ins: itxn_next 30 | 31 | itxn_next 32 | 33 | // new ins: gitxn t f, t is transaction index and f is transaction field 34 | 35 | gitxn 1 LastLog 36 | gitxn 2 StateProofPK 37 | gitxn 3 Fee 38 | 39 | // new ins: gitxna t f i, t is transaction index, f is array transaction field and i is array index 40 | 41 | gitxna 1 ApplicationArgs 0 42 | gitxna 2 Accounts 1 43 | gitxna 3 Assets 0 44 | 45 | // new ins: gloadss 46 | 47 | gloadss 48 | 49 | // newins: itxnas f. A(top) th value of array transaction field f. 50 | 51 | int 0 52 | itxnas ApplicationArgs 53 | 54 | // new ins: gitxnas t f, A(top) th value of array transaction field of t th transaction 55 | 56 | gitxnas 1 ApplicationArgs 57 | gitxnas 2 Accounts 58 | gitxnas 3 Assets 59 | 60 | 61 | method "increment(uint64,pay)uint64" 62 | 63 | itxn_begin 64 | byte "app_arg_1" 65 | itxn_field ApplicationArgs // ArrayType argument 66 | byte "app_arg_2" 67 | itxn_field ApplicationArgs 68 | byte "app_arg_3" 69 | itxn_field ApplicationArgs 70 | txn Sender 71 | itxn_field Accounts // ArrayType argument 72 | int 6 73 | itxn_field TypeEnum 74 | int 1 75 | itxn_field OnCompletion 76 | int 0 77 | itxn_field Fee 78 | itxn_submit 79 | -------------------------------------------------------------------------------- /.github/workflows/pytest310.yml: -------------------------------------------------------------------------------- 1 | name: CI (python 3.10) 2 | 3 | defaults: 4 | run: 5 | # To load bashrc 6 | shell: bash -ieo pipefail {0} 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - dev 13 | pull_request: 14 | schedule: 15 | # run CI every day even if no PRs/merges occur 16 | - cron: '0 12 * * *' 17 | 18 | jobs: 19 | tests: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v1 24 | - name: Set up Python 3.10 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: "3.10" 28 | 29 | # Used by ci_test.sh 30 | - name: Install dependencies 31 | run: | 32 | pip install . 33 | pip install pytest 34 | pip install pytest-cov 35 | pip install pyteal==0.22.0 36 | 37 | - name: Run Parsing tests in 3.10 38 | run: | 39 | pytest tests/test_parsing.py tests/test_versions.py tests/test_parsing_using_pyteal.py --cov=tealer/teal/instructions --cov-branch --cov-fail-under=96 40 | 41 | - name: Run string tests 42 | run: | 43 | pytest tests/test_string_representation.py 44 | 45 | - name: Run version tests in 3.10 46 | run: | 47 | pytest tests/test_versions.py 48 | pytest tests/test_mode_detector.py 49 | 50 | - name: Run cfg recovery tests in 3.10 51 | run: | 52 | pytest tests/test_cfg.py 53 | 54 | - name: Run detectors tests in 3.10 55 | run: | 56 | pytest tests/test_detectors.py 57 | pytest tests/test_detectors_using_pyteal.py 58 | 59 | - name: Run dataflow analysis tests in 3.10 60 | run: | 61 | pytest tests/transaction_context/test_group_sizes.py 62 | pytest tests/transaction_context/test_group_indices.py 63 | 64 | - name: Run regex tests in 3.10 65 | run: | 66 | pytest tests/test_regex.py -------------------------------------------------------------------------------- /tealer/printers/function_cfg.py: -------------------------------------------------------------------------------- 1 | """Printer for exporting subroutines in dot format. 2 | 3 | Control Flow Graph of each subroutine defined in the contract 4 | is exported in dot format. As the subroutines are only 5 | supported from teal version 4, for contracts written in version 6 | 3 or less, printer doesn't do anything. Each subroutine is saved 7 | in a new dot file with filename ``function_{subroutine_name}_cfg.dot`` 8 | in the destination directly which will be current directory if not 9 | specified. 10 | 11 | Classes: 12 | PrinterFunctionCFG: Printer to export CFGs of subroutines defined 13 | in the contract. The CFGs will be saved in dot format. 14 | """ 15 | 16 | import os 17 | from pathlib import Path 18 | 19 | from tealer.printers.abstract_printer import AbstractPrinter 20 | from tealer.utils.output import all_subroutines_to_dot, ROOT_OUTPUT_DIRECTORY 21 | 22 | 23 | class PrinterFunctionCFG(AbstractPrinter): # pylint: disable=too-few-public-methods 24 | """Printer to export CFGs of subroutines in dot. 25 | 26 | This printer is supposed to work for contracts written in teal version 4 or greater. 27 | Dot files will be saved as `function_{subroutine_name}_cfg.dot` in destination folder 28 | if given or else in the current directory. 29 | 30 | """ 31 | 32 | NAME = "subroutine-cfg" 33 | HELP = "Export the CFG of each subroutine" 34 | WIKI_URL = "https://github.com/crytic/tealer/wiki/Printer-documentation#subroutine-cfg" 35 | 36 | def print(self) -> None: 37 | """Export CFG of each subroutine defined in the contract.""" 38 | # outputs multiple files: set the dir to {ROOT_DIRECTORY}/{CONTRACT_NAME}/{"print-"PRINTER_NAME} 39 | dest = ROOT_OUTPUT_DIRECTORY / Path(self.teal.contract_name) / Path(f"print-{self.NAME}") 40 | os.makedirs(dest, exist_ok=True) 41 | 42 | all_subroutines_to_dot(self.teal, dest) 43 | -------------------------------------------------------------------------------- /tests/parsing/instructions.teal: -------------------------------------------------------------------------------- 1 | #pragma version 2 2 | 3 | // https://developer.algorand.org/docs/reference/teal/opcodes/ 4 | // byte/int are added to make the compilation works 5 | 6 | byte "A" 7 | sha256 8 | keccak256 9 | sha512_256 10 | byte "A" 11 | byte "A" 12 | ed25519verify 13 | int 0 14 | int 0 15 | int 0 16 | int 0 17 | int 0 18 | int 0 19 | int 0 20 | int 0 21 | int 0 22 | int 0 23 | + 24 | - 25 | / 26 | * 27 | < 28 | > 29 | <= 30 | >= 31 | && 32 | || 33 | == 34 | != 35 | ! 36 | pop 37 | byte "A" 38 | len 39 | itob 40 | btoi 41 | int 0 42 | int 0 43 | int 0 44 | int 0 45 | % 46 | | 47 | & 48 | ^ 49 | ~ 50 | pop 51 | int 0 52 | int 0 53 | mulw 54 | int 0 55 | int 0 56 | addw 57 | intcblock 58 | // intc 59 | intc_0 60 | intc_1 61 | intc_2 62 | intc_3 63 | bytecblock 64 | // bytec 65 | bytec_0 66 | bytec_1 67 | bytec_2 68 | bytec_3 69 | arg 0 70 | arg_0 71 | arg_1 72 | arg_2 73 | arg_3 74 | txn Fee 75 | global MinTxnFee 76 | gtxn 1 Fee 77 | load 1 78 | store 1 79 | txna ApplicationArgs 1 80 | gtxna 1 ApplicationArgs 1 81 | int 0 82 | bnz random_label 83 | int 0 84 | bz random_label 85 | b other_label 86 | other_label: 87 | pop 88 | dup 89 | dup2 90 | concat 91 | substring 0 0 92 | int 0 93 | int 0 94 | substring3 95 | int 0 96 | int 0 97 | balance 98 | app_opted_in 99 | pop 100 | app_local_get 101 | app_local_get_ex 102 | app_global_get 103 | app_global_get_ex 104 | pop 105 | pop 106 | pop 107 | byte "A" 108 | int 0 109 | app_local_put 110 | app_global_put 111 | int 0 112 | byte "A" 113 | app_local_del 114 | app_global_del 115 | int 0 116 | asset_holding_get AssetBalance 117 | asset_params_get AssetTotal 118 | // addr is not documented in https://developer.algorand.org/docs/reference/teal/opcodes 119 | addr OWQEGN2AZIA77YIE7YZEZLUN2JKVRUCTSFY3U3YH7PXWIDIQIPRA4IUKII 120 | byte base64 QQ== 121 | err 122 | random_label: 123 | int 0 124 | return 125 | -------------------------------------------------------------------------------- /tests/detectors/subroutine_patterns.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from tealer.detectors.all_detectors import IsUpdatable 4 | 5 | 6 | MULTIPLE_RETURN_POINTS = """ 7 | #pragma version 7 // B0 8 | callsub subroutine_0 9 | callsub subroutine_0 // B1 10 | int 1 // B2 11 | return 12 | subroutine_0: // B3 13 | retsub 14 | """ 15 | 16 | # There is only one possible execution path B0 -> B3 -> B1 -> B3 -> B2 17 | # 1.) In the CFG B3(subroutine_0) is connected to B1 and B2. And when traversing the CFG, detector 18 | # considers B0 -> B3 -> B2 as a valid path and reports it. 19 | # 20 | # 2.) Path B0 -> B3 -> B1 -> B3 -> B2 is not reported because `subroutine_0` is called twice and 21 | # on the second call, detectors think that they are in a loop because subroutine's blocks are 22 | # are already in the path(after the first call). 23 | 24 | # Before fixing bug (1) and (2) 25 | MULTIPLE_RETURN_POINTS_VULNERABLE_PATHS_1: List[List[int]] = [ 26 | [0, 3, 2], 27 | ] 28 | 29 | # After fixing bug (1) and not (2) 30 | MULTIPLE_RETURN_POINTS_VULNERABLE_PATHS_2: List[List[int]] = [] 31 | 32 | # After fixing bug (1) and (2) 33 | MULTIPLE_RETURN_POINTS_VULNERABLE_PATHS_3: List[List[int]] = [ 34 | [0, 3, 1, 3, 2], 35 | ] 36 | 37 | # Tealer does not identify a subroutine when `callsub` instruction is the last instruction. 38 | # Bug is fixed now 39 | CALLSUB_AT_END = """ 40 | #pragma version 7 41 | b main 42 | sub: 43 | int 1 44 | return 45 | main: 46 | callsub sub 47 | """ 48 | 49 | CALLSUB_AT_END_VULNERABLE_PATHS: List[List[int]] = [ 50 | [0, 2, 1], 51 | ] 52 | 53 | subroutine_patterns_tests = [ 54 | # (MULTIPLE_RETURN_POINTS, IsUpdatable, MULTIPLE_RETURN_POINTS_VULNERABLE_PATHS_1), 55 | # (MULTIPLE_RETURN_POINTS, IsUpdatable, MULTIPLE_RETURN_POINTS_VULNERABLE_PATHS_2), 56 | (MULTIPLE_RETURN_POINTS, IsUpdatable, MULTIPLE_RETURN_POINTS_VULNERABLE_PATHS_3), 57 | (CALLSUB_AT_END, IsUpdatable, CALLSUB_AT_END_VULNERABLE_PATHS), 58 | ] 59 | -------------------------------------------------------------------------------- /tealer/teal/instructions/app_params_field.py: -------------------------------------------------------------------------------- 1 | """Defines classes to represent app_params_get fields. 2 | 3 | ``app_params_get`` instruction is used to access parameter fields 4 | of an application in the contract. 5 | 6 | Each field that can be accessed using app_params_get is represented 7 | by a class in tealer. All the classes representing the fields must 8 | inherit from AppParamsField class. 9 | 10 | """ 11 | 12 | # pylint: disable=too-few-public-methods 13 | class AppParamsField: 14 | """Base class to represent App fields.""" 15 | 16 | def __init__(self) -> None: 17 | self._version: int = 5 18 | 19 | @property 20 | def version(self) -> int: 21 | """Teal version this field is introduced in and supported from. 22 | 23 | Returns: 24 | Teal version the field is introduced in and supported from. 25 | """ 26 | return self._version 27 | 28 | def __str__(self) -> str: 29 | return self.__class__.__qualname__ 30 | 31 | 32 | class AppApprovalProgram(AppParamsField): 33 | """Bytecode of approval program of the App.""" 34 | 35 | 36 | class AppClearStateProgram(AppParamsField): 37 | """Bytecode of clear state program of the App.""" 38 | 39 | 40 | class AppGlobalNumUint(AppParamsField): 41 | """Number of uint64 values allowed in Global State.""" 42 | 43 | 44 | class AppGlobalNumByteSlice(AppParamsField): 45 | """Number of byte array values allowed in Global State.""" 46 | 47 | 48 | class AppLocalNumUint(AppParamsField): 49 | """Number of uint64 values allowed in Local State.""" 50 | 51 | 52 | class AppLocalNumByteSlice(AppParamsField): 53 | """Number of byte array values allowed in Local State.""" 54 | 55 | 56 | class AppExtraProgramPages(AppParamsField): 57 | """Number of extra program pages of code space.""" 58 | 59 | 60 | class AppCreator(AppParamsField): 61 | """Address of Creator(deployer) of the application.""" 62 | 63 | 64 | class AppAddress(AppParamsField): 65 | """Address for which this application has authority.""" 66 | -------------------------------------------------------------------------------- /tealer/teal/instructions/parse_app_params_field.py: -------------------------------------------------------------------------------- 1 | """Parser for fields of app_params_get. 2 | 3 | Each ``app_params_get`` field is represented as a class. Parsing 4 | the field is creating the class instance representing the field given 5 | it's string representation. 6 | 7 | Fields of ``app_params_get`` doesn't have immediate arguments 8 | and have very simple structure. All fields map one-to-one with 9 | their string representation. As a result, app parameter fields are 10 | parsed by first constructing a map(dict) from their string 11 | representation to corresponding class representing the field and 12 | using that to find the correct class for the given field. 13 | 14 | Attributes: 15 | APP_PARAMS_FIELD_TXT_TO_OBJECT: Map(dict) from string representation 16 | of app parameter field to the corresponding class. 17 | """ 18 | 19 | from tealer.teal.instructions.app_params_field import ( 20 | AppParamsField, 21 | AppApprovalProgram, 22 | AppClearStateProgram, 23 | AppGlobalNumUint, 24 | AppGlobalNumByteSlice, 25 | AppLocalNumUint, 26 | AppLocalNumByteSlice, 27 | AppExtraProgramPages, 28 | AppCreator, 29 | AppAddress, 30 | ) 31 | 32 | 33 | APP_PARAMS_FIELD_TXT_TO_OBJECT = { 34 | "AppParamsField": AppParamsField, 35 | "AppApprovalProgram": AppApprovalProgram, 36 | "AppClearStateProgram": AppClearStateProgram, 37 | "AppGlobalNumUint": AppGlobalNumUint, 38 | "AppGlobalNumByteSlice": AppGlobalNumByteSlice, 39 | "AppLocalNumUint": AppLocalNumUint, 40 | "AppLocalNumByteSlice": AppLocalNumByteSlice, 41 | "AppExtraProgramPages": AppExtraProgramPages, 42 | "AppCreator": AppCreator, 43 | "AppAddress": AppAddress, 44 | } 45 | 46 | 47 | def parse_app_params_field(field: str) -> AppParamsField: 48 | """Parse fields of ``app_params_get``. 49 | 50 | Args: 51 | field: string representation of the field. 52 | 53 | Returns: 54 | object of class corresponding to the given app_params_get field. 55 | """ 56 | 57 | return APP_PARAMS_FIELD_TXT_TO_OBJECT[field]() 58 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Pytest 2 | 3 | defaults: 4 | run: 5 | # To load bashrc 6 | shell: bash -ieo pipefail {0} 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - dev 13 | pull_request: 14 | schedule: 15 | # run CI every day even if no PRs/merges occur 16 | - cron: '0 12 * * *' 17 | 18 | jobs: 19 | tests: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | python: ${{fromJSON('["3.9", "3.10", "3.11", "3.12"]') }} 24 | 25 | 26 | steps: 27 | - uses: actions/checkout@v1 28 | - name: Set up Python ${{ matrix.python }} 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python }} 32 | 33 | # Used by ci_test.sh 34 | - name: Install dependencies in ${{ matrix.python }} 35 | run: | 36 | pip install . 37 | pip install pytest 38 | pip install pytest-cov 39 | 40 | - name: Run Parsing tests in ${{ matrix.python }} 41 | run: | 42 | pytest tests/test_parsing.py tests/test_versions.py --cov=tealer/teal/instructions --cov-branch --cov-fail-under=96 43 | 44 | - name: Run string tests in ${{ matrix.python }} 45 | run: | 46 | pytest tests/test_string_representation.py 47 | 48 | - name: Run version tests in ${{ matrix.python }} 49 | run: | 50 | pytest tests/test_versions.py 51 | pytest tests/test_mode_detector.py 52 | 53 | - name: Run cfg recovery tests in ${{ matrix.python }} 54 | run: | 55 | pytest tests/test_cfg.py 56 | 57 | - name: Run detectors tests in ${{ matrix.python }} 58 | run: | 59 | pytest tests/test_detectors.py 60 | 61 | - name: Run dataflow analysis tests 62 | run: | 63 | pytest tests/transaction_context/test_group_sizes.py 64 | pytest tests/transaction_context/test_group_indices.py 65 | pytest tests/transaction_context/test_transaction_types.py 66 | pytest tests/transaction_context/test_addr_fields.py 67 | 68 | - name: Run regex tests 69 | run: | 70 | pytest tests/test_regex.py -------------------------------------------------------------------------------- /tealer/teal/instructions/parse_asset_params_field.py: -------------------------------------------------------------------------------- 1 | """Parser for fields of asset_params_get. 2 | 3 | Each ``asset_params_get`` field is represented as a class. Parsing 4 | the field is creating the class instance representing the field given 5 | it's string representation. 6 | 7 | Fields of ``asset_params_get`` doesn't have immediate arguments 8 | and have very simple structure. All fields map one-to-one with 9 | their string representation. As a result, asset parameter fields are 10 | parsed by first constructing a map(dict) from their string 11 | representation to corresponding class representing the field and 12 | using that to find the correct class for the given field. 13 | 14 | Attributes: 15 | ASSET_PARAMS_FIELD_TXT_TO_OBJECT: Map(dict) from string representation 16 | of asset parameter field to the corresponding class. 17 | """ 18 | 19 | from tealer.teal.instructions.asset_params_field import ( 20 | AssetParamsField, 21 | AssetTotal, 22 | AssetDecimals, 23 | AssetDefaultFrozen, 24 | AssetUnitName, 25 | AssetName, 26 | AssetURL, 27 | AssetMetadataHash, 28 | AssetManager, 29 | AssetReserve, 30 | AssetFreeze, 31 | AssetClawback, 32 | AssetCreator, 33 | ) 34 | 35 | ASSET_PARAMS_FIELD_TXT_TO_OBJECT = { 36 | "AssetTotal": AssetTotal, 37 | "AssetDecimals": AssetDecimals, 38 | "AssetDefaultFrozen": AssetDefaultFrozen, 39 | "AssetUnitName": AssetUnitName, 40 | "AssetName": AssetName, 41 | "AssetURL": AssetURL, 42 | "AssetMetadataHash": AssetMetadataHash, 43 | "AssetManager": AssetManager, 44 | "AssetReserve": AssetReserve, 45 | "AssetFreeze": AssetFreeze, 46 | "AssetClawback": AssetClawback, 47 | "AssetCreator": AssetCreator, 48 | } 49 | 50 | 51 | def parse_asset_params_field(field: str) -> AssetParamsField: 52 | """Parse fields of ``asset_params_get``. 53 | 54 | Args: 55 | field: string representation of the field. 56 | 57 | Returns: 58 | object of class corresponding to the given asset_params_get field. 59 | """ 60 | 61 | return ASSET_PARAMS_FIELD_TXT_TO_OBJECT[field]() 62 | -------------------------------------------------------------------------------- /tests/test_versions.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pytest 3 | 4 | from tealer.teal.parse_teal import parse_teal 5 | from tealer.teal.instructions.transaction_field import TransactionField 6 | from tealer.utils.teal_enums import ExecutionMode 7 | 8 | 9 | INSTRUCTIONS_TARGETS = [ 10 | "tests/parsing/teal-instructions-with-versions.teal", 11 | ] 12 | 13 | FIELDS_TARGETS = [ 14 | "tests/parsing/teal-fields-with-versions.teal", 15 | ] 16 | 17 | instruction_info_pattern = re.compile( 18 | r"\(version: ([0-9]+), mode: (Any|Stateful|Stateless), pop: ([0-9]+), push: ([0-9+])\)" 19 | ) 20 | 21 | field_info_pattern = re.compile(r"\(version: ([0-9]+)\)") 22 | 23 | 24 | @pytest.mark.parametrize("target", INSTRUCTIONS_TARGETS) # type: ignore 25 | def test_instruction_version(target: str) -> None: 26 | with open(target, encoding="utf-8") as f: 27 | teal = parse_teal(f.read()) 28 | for ins in teal.instructions: 29 | match_obj = instruction_info_pattern.search(ins.comment) 30 | if match_obj is None: 31 | continue 32 | version, mode_str, pop_size, push_size = match_obj.groups() 33 | mode = { 34 | "Any": ExecutionMode.ANY, 35 | "Stateful": ExecutionMode.STATEFUL, 36 | "Stateless": ExecutionMode.STATELESS, 37 | }[mode_str] 38 | assert ins.mode == mode 39 | assert ins.version == int(version) 40 | assert ins.stack_push_size == int(push_size) 41 | assert ins.stack_pop_size == int(pop_size) 42 | 43 | 44 | @pytest.mark.parametrize("target", FIELDS_TARGETS) # type: ignore 45 | def test_field_version(target: str) -> None: 46 | with open(target, encoding="utf-8") as f: 47 | teal = parse_teal(f.read()) 48 | for ins in teal.instructions: 49 | field = getattr(ins, "field", None) 50 | if field is None: 51 | continue 52 | if isinstance(field, TransactionField): 53 | match_obj = field_info_pattern.search(ins.comment) 54 | if match_obj is None: 55 | continue 56 | version = match_obj.groups()[0] 57 | assert field.version == int(version) 58 | -------------------------------------------------------------------------------- /tealer/teal/instructions/parse_global_field.py: -------------------------------------------------------------------------------- 1 | """Parser for global fields. 2 | 3 | Each ``global`` field is represented as a class. Parsing 4 | the field is creating the class instance representing the field given 5 | it's string representation. 6 | 7 | Fields of ``global`` doesn't have immediate arguments 8 | and have very simple structure. All fields map one-to-one with 9 | their string representation. As a result, global fields are 10 | parsed by first constructing a map(dict) from their string 11 | representation to corresponding class representing the field and 12 | using that to find the correct class for the given field. 13 | 14 | Attributes: 15 | GLOBAL_FIELD_TXT_TO_OBJECT: Map(dict) from string representation 16 | of global field to the corresponding class. 17 | """ 18 | 19 | from tealer.teal.global_field import ( 20 | GroupSize, 21 | ZeroAddress, 22 | MinTxnFee, 23 | GlobalField, 24 | MinBalance, 25 | MaxTxnLife, 26 | LogicSigVersion, 27 | Round, 28 | LatestTimestamp, 29 | CurrentApplicationID, 30 | CreatorAddress, 31 | CurrentApplicationAddress, 32 | GroupID, 33 | OpcodeBudget, 34 | CallerApplicationID, 35 | CallerApplicationAddress, 36 | ) 37 | 38 | GLOBAL_FIELD_TXT_TO_OBJECT = { 39 | "GroupSize": GroupSize, 40 | "MinTxnFee": MinTxnFee, 41 | "ZeroAddress": ZeroAddress, 42 | "MinBalance": MinBalance, 43 | "MaxTxnLife": MaxTxnLife, 44 | "LogicSigVersion": LogicSigVersion, 45 | "Round": Round, 46 | "LatestTimestamp": LatestTimestamp, 47 | "CurrentApplicationID": CurrentApplicationID, 48 | "CreatorAddress": CreatorAddress, 49 | "CurrentApplicationAddress": CurrentApplicationAddress, 50 | "GroupID": GroupID, 51 | "OpcodeBudget": OpcodeBudget, 52 | "CallerApplicationID": CallerApplicationID, 53 | "CallerApplicationAddress": CallerApplicationAddress, 54 | } 55 | 56 | 57 | def parse_global_field(field: str) -> GlobalField: 58 | """Parse global fields. 59 | 60 | Args: 61 | field: string representation of the field. 62 | 63 | Returns: 64 | object of class corresponding to the given global field. 65 | """ 66 | 67 | return GLOBAL_FIELD_TXT_TO_OBJECT[field]() 68 | -------------------------------------------------------------------------------- /tealer/detectors/optimizations/sender_access.py: -------------------------------------------------------------------------------- 1 | """Detector for finding unoptimized access to the sender.""" 2 | 3 | from typing import TYPE_CHECKING, List 4 | 5 | from tealer.detectors.abstract_detector import ( 6 | AbstractDetector, 7 | DetectorClassification, 8 | DetectorType, 9 | ) 10 | from tealer.teal.instructions.instructions import Instruction, Txna 11 | from tealer.teal.instructions.transaction_field import Accounts 12 | from tealer.utils.output import InstructionsOutput 13 | 14 | if TYPE_CHECKING: 15 | from tealer.utils.output import ListOutput 16 | 17 | 18 | class SenderAccess(AbstractDetector): # pylint: disable=too-few-public-methods 19 | """Detector to find unoptimized access to the sender""" 20 | 21 | NAME = "sender-access" 22 | DESCRIPTION = "Unoptimized Gtxn" 23 | TYPE = DetectorType.STATELESS 24 | 25 | IMPACT = DetectorClassification.OPTIMIZATION 26 | CONFIDENCE = DetectorClassification.HIGH 27 | 28 | WIKI_URL = ( 29 | "https://github.com/crytic/tealer/wiki/Detector-Documentation#Unoptimized-sender-access" 30 | ) 31 | WIKI_TITLE = "Unoptimized sender access" 32 | WIKI_DESCRIPTION = "Txn.accounts[0] can be optimized to be Txn.sender()]" 33 | WIKI_EXPLOIT_SCENARIO = """""" 34 | 35 | WIKI_RECOMMENDATION = """Use Txn.sender().""" 36 | 37 | def detect(self) -> "ListOutput": 38 | """Detector to find unoptimized access to the sender 39 | 40 | Returns: 41 | The instruction to optimized 42 | """ 43 | 44 | detector_output: "ListOutput" = [] 45 | 46 | for contract in self.tealer.contracts.values(): 47 | 48 | all_findings: List[List[Instruction]] = [] 49 | 50 | for function in contract.functions.values(): 51 | for block in function.blocks: 52 | for ins in block.instructions: 53 | if isinstance(ins, Txna) and isinstance(ins.field, Accounts) and ins.field.idx == 0: # type: ignore 54 | all_findings.append([ins]) 55 | 56 | if all_findings: 57 | detector_output.append(InstructionsOutput(contract, self, all_findings)) 58 | 59 | return detector_output 60 | -------------------------------------------------------------------------------- /tealer/teal/instructions/parse_acct_params_field.py: -------------------------------------------------------------------------------- 1 | """Parser for fields of acct_params_get. 2 | 3 | Each ``acct_params_get`` field is represented as a class. Parsing 4 | the field is creating the class instance representing the field given 5 | it's string representation. 6 | 7 | Fields of ``acct_params_get`` doesn't have immediate arguments 8 | and have very simple structure. All fields map one-to-one with 9 | their string representation. As a result, app parameter fields are 10 | parsed by first constructing a map(dict) from their string 11 | representation to corresponding class representing the field and 12 | using that to find the correct class for the given field. 13 | 14 | Attributes: 15 | ACCT_PARAMS_FIELD_TXT_TO_OBJECT: Map(dict) from string representation 16 | of app parameter field to the corresponding class. 17 | """ 18 | 19 | from tealer.teal.instructions.acct_params_field import ( 20 | AcctParamsField, 21 | AcctBalance, 22 | AcctMinBalance, 23 | AcctAuthAddr, 24 | AcctTotalNumUint, 25 | AcctTotalNumByteSlice, 26 | AcctTotalExtraAppPages, 27 | AcctTotalAppsCreated, 28 | AcctTotalAppsOptedIn, 29 | AcctTotalAssetsCreated, 30 | AcctTotalAssets, 31 | AcctTotalBoxes, 32 | AcctTotalBoxBytes, 33 | ) 34 | 35 | 36 | ACCT_PARAMS_FIELD_TXT_TO_OBJECT = { 37 | "AcctBalance": AcctBalance, 38 | "AcctMinBalance": AcctMinBalance, 39 | "AcctAuthAddr": AcctAuthAddr, 40 | "AcctTotalNumUint": AcctTotalNumUint, 41 | "AcctTotalNumByteSlice": AcctTotalNumByteSlice, 42 | "AcctTotalExtraAppPages": AcctTotalExtraAppPages, 43 | "AcctTotalAppsCreated": AcctTotalAppsCreated, 44 | "AcctTotalAppsOptedIn": AcctTotalAppsOptedIn, 45 | "AcctTotalAssetsCreated": AcctTotalAssetsCreated, 46 | "AcctTotalAssets": AcctTotalAssets, 47 | "AcctTotalBoxes": AcctTotalBoxes, 48 | "AcctTotalBoxBytes": AcctTotalBoxBytes, 49 | } 50 | 51 | 52 | def parse_acct_params_field(field: str) -> AcctParamsField: 53 | """Parse fields of ``app_params_get``. 54 | 55 | Args: 56 | field: string representation of the field. 57 | 58 | Returns: 59 | object of class corresponding to the given app_params_get field. 60 | """ 61 | 62 | return ACCT_PARAMS_FIELD_TXT_TO_OBJECT[field]() 63 | -------------------------------------------------------------------------------- /examples/printers/subroutine-cfg_contract_shortened_cfg.dot: -------------------------------------------------------------------------------- 1 | digraph g{ 2 | ranksep = 1 3 | overlap = scale 4 | 0[label=<
| // block_id = 0; cost = 5 |
| 1. #pragma version 7 |
| 2. txna ApplicationArgs 0 |
| // method-selector: 0xf037ec85 3. method "method_foo_bar()void" |
| 4. == |
| 5. bnz main_l2 |
| // block_id = 2; cost = 6 |
| 7. main_l2: |
| 8. txn OnCompletion |
| 9. int NoOp |
| 10. == |
| 11. assert |
| 12. callsub methodfoobar_1 |
| // block_id = 3; cost = 2 |
| 13. int 1 |
| 14. return |
| // block_id = 1; cost = 1 |
| 6. err |
| // block_id = 4; cost = 8 // Subroutine foobar_0 |
| // foo_bar 17. foobar_0: |
| 18. store 0 |
| 19. load 0 |
| 20. int 2 |
| 21. % |
| 22. int 0 |
| 23. == |
| 24. bnz foobar_0_l2 |
| // block_id = 6; cost = 3 |
| 28. foobar_0_l2: |
| 29. byte "Foo" |
| 30. log |
| // block_id = 7; cost = 2 |
| 31. foobar_0_l3: |
| 32. retsub |
| // block_id = 5; cost = 3 |
| 25. byte "Bar" |
| 26. log |
| 27. b foobar_0_l3 |
| // block_id = 0; cost = 5 // GroupIndex: 1 2 // GroupSize: 3 6..10 |
| 1. #pragma version 7 |
| 2. global GroupSize |
| 3. int 4 |
| 4. <= |
| 5. bnz main_l2 |
| // block_id = 1; cost = 13 // GroupIndex: 2 // GroupSize: 6..10 |
| 6. global GroupSize |
| 7. int 5 |
| 8. > |
| 9. global GroupSize |
| 10. int 11 |
| 11. < |
| 12. && |
| 13. txn GroupIndex |
| 14. int 2 |
| 15. == |
| 16. && |
| 17. assert |
| 18. b main_l3 |
| // block_id = 2; cost = 9 // GroupIndex: 1 // GroupSize: 3 |
| 19. main_l2: |
| 20. global GroupSize |
| 21. int 3 |
| 22. == |
| 23. assert |
| 24. txn GroupIndex |
| 25. int 1 |
| 26. == |
| 27. assert |
| // block_id = 3; cost = 3 // GroupIndex: 1 2 // GroupSize: 3 6..10 |
| 28. main_l3: |
| 29. int 1 |
| 30. return |