├── 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=< 5 | 6 | 7 | 8 | 9 | 10 | 11 |
// 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
> labelloc=top shape=plain 12 | ] 0:s -> 1:6:n [color="#e0182b"]; 13 | 0:s -> 2:7:n [color="#36d899"]; 14 | 15 | 2[label=< 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
// block_id = 2; cost = 6
7. main_l2:
8. txn OnCompletion
9. int NoOp
10. ==
11. assert
12. callsub methodfoobar_1
> labelloc=top shape=plain 24 | ] 25 | x2_3[label="Subroutine methodfoobar_1",style=dashed,shape=box,fontname=bold] 2:s -> x2_3:n; 26 | x2_3:s -> 3:13:n; 27 | 28 | 3[label=< 29 | 30 | 31 | 32 |
// block_id = 3; cost = 2
13. int 1
14. return
> labelloc=top shape=plain 33 | ] 34 | 1[label=< 35 | 36 | 37 |
// block_id = 1; cost = 1
6. err
> labelloc=top shape=plain 38 | ] 39 | } -------------------------------------------------------------------------------- /tests/regex/vote_approval.teal: -------------------------------------------------------------------------------- 1 | #pragma version 2 2 | txn ApplicationID 3 | int 0 4 | == 5 | bnz main_l18 6 | txn OnCompletion 7 | int DeleteApplication 8 | == 9 | bnz main_l17 10 | txn OnCompletion 11 | int UpdateApplication 12 | == 13 | bnz main_l16 14 | txn OnCompletion 15 | int CloseOut 16 | == 17 | bnz main_l13 18 | txn OnCompletion 19 | int OptIn 20 | == 21 | bnz main_l12 22 | txna ApplicationArgs 0 23 | byte "vote" 24 | == 25 | bnz main_l7 26 | err 27 | main_l7: 28 | global Round 29 | byte "VoteBegin" 30 | app_global_get 31 | >= 32 | global Round 33 | byte "VoteEnd" 34 | app_global_get 35 | <= 36 | && 37 | bnz main_l9 38 | err 39 | main_l9: 40 | int 0 41 | global CurrentApplicationID 42 | byte "voted" 43 | app_local_get_ex 44 | store 1 45 | store 0 46 | load 1 47 | bnz main_l11 48 | txna ApplicationArgs 1 49 | txna ApplicationArgs 1 50 | app_global_get 51 | int 1 52 | + 53 | app_global_put 54 | int 0 55 | byte "voted" 56 | txna ApplicationArgs 1 57 | app_local_put 58 | int 1 59 | return 60 | main_l11: 61 | int 0 62 | return 63 | main_l12: 64 | global Round 65 | byte "RegBegin" 66 | app_global_get 67 | >= 68 | global Round 69 | byte "RegEnd" 70 | app_global_get 71 | <= 72 | && 73 | return 74 | main_l13: 75 | int 0 76 | global CurrentApplicationID 77 | byte "voted" 78 | app_local_get_ex 79 | store 1 80 | store 0 81 | global Round 82 | byte "VoteEnd" 83 | app_global_get 84 | <= 85 | load 1 86 | && 87 | bnz main_l15 88 | main_l14: 89 | int 1 90 | return 91 | main_l15: 92 | load 0 93 | load 0 94 | app_global_get 95 | int 1 96 | - 97 | app_global_put 98 | b main_l14 99 | main_l16: 100 | txn Sender 101 | byte "Creator" 102 | app_global_get 103 | == 104 | return 105 | main_l17: 106 | txn Sender 107 | byte "Creator" 108 | app_global_get 109 | == 110 | return 111 | main_l18: 112 | byte "Creator" 113 | txn Sender 114 | app_global_put 115 | txn NumAppArgs 116 | int 4 117 | == 118 | bnz main_l20 119 | err 120 | main_l20: 121 | byte "RegBegin" 122 | txna ApplicationArgs 0 123 | btoi 124 | app_global_put 125 | byte "RegEnd" 126 | txna ApplicationArgs 1 127 | btoi 128 | app_global_put 129 | byte "VoteBegin" 130 | txna ApplicationArgs 2 131 | btoi 132 | app_global_put 133 | byte "VoteEnd" 134 | txna ApplicationArgs 3 135 | btoi 136 | app_global_put 137 | int 1 138 | return -------------------------------------------------------------------------------- /examples/printers/subroutine-cfg_foobar_cfg.dot: -------------------------------------------------------------------------------- 1 | digraph g{ 2 | ranksep = 1 3 | overlap = scale 4 | 4[label=< 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
// 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
> labelloc=top shape=plain 15 | ] 4:s -> 5:25:n [color="#e0182b"]; 16 | 4:s -> 6:28:n [color="#36d899"]; 17 | 18 | 6[label=< 19 | 20 | 21 | 22 | 23 |
// block_id = 6; cost = 3
28. foobar_0_l2:
29. byte "Foo"
30. log
> labelloc=top shape=plain 24 | ] 6:s -> 7:31:n [color="BLACK"]; 25 | 26 | 7[label=< 27 | 28 | 29 | 30 |
// block_id = 7; cost = 2
31. foobar_0_l3:
32. retsub
> labelloc=top shape=plain 31 | ] 32 | 5[label=< 33 | 34 | 35 | 36 | 37 |
// block_id = 5; cost = 3
25. byte "Bar"
26. log
27. b foobar_0_l3
> labelloc=top shape=plain 38 | ] 5:s -> 7:31:n [color="BLACK"]; 39 | 40 | } -------------------------------------------------------------------------------- /examples/printers/transaction-context.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 | def on_create() -> Expr: 7 | return Approve() 8 | 9 | 10 | def op_a() -> Expr: 11 | return Seq( 12 | [ 13 | Assert(And(Txn.group_index() == Int(0), Global.group_size() < Int(3))), 14 | Approve(), 15 | ] 16 | ) 17 | 18 | 19 | def op_b() -> Expr: 20 | return Seq( 21 | [ 22 | If( 23 | Global.group_size() <= Int(4), 24 | If( 25 | Global.group_size() == Int(3), 26 | Assert(Txn.group_index() == Int(1)), 27 | If( 28 | Global.group_size() == Int(2), 29 | Return(Txn.group_index() == Int(0)), 30 | ), 31 | ), 32 | Assert( 33 | And( 34 | Global.group_size() > Int(5), 35 | Global.group_size() < Int(11), 36 | Txn.group_index() == Int(2), 37 | ) 38 | ), 39 | ), 40 | Approve(), 41 | ] 42 | ) 43 | 44 | 45 | # def approval_program() -> Expr: 46 | # return Cond( 47 | # [Txn.application_id() == Int(0), on_create()], 48 | # [Txn.application_args[Int(0)] == Bytes("a"), op_a()], 49 | # [Txn.application_args[Int(0)] == Bytes("b"), op_b()], 50 | # ) 51 | 52 | 53 | def approval_program() -> Expr: 54 | # return op_2() 55 | return Seq( 56 | [ 57 | If( 58 | Global.group_size() <= Int(4), 59 | Seq( 60 | [ 61 | Assert(Global.group_size() == Int(3)), 62 | Assert(Txn.group_index() == Int(1)), 63 | ] 64 | ), 65 | Assert( 66 | And( 67 | Global.group_size() > Int(5), 68 | Global.group_size() < Int(11), 69 | Txn.group_index() == Int(2), 70 | ) 71 | ), 72 | ), 73 | Approve(), 74 | ] 75 | ) 76 | 77 | 78 | pragma(compiler_version="0.22.0") 79 | application_approval_program = compileTeal(approval_program(), mode=Mode.Application, version=7) 80 | 81 | if __name__ == "__main__": 82 | print(application_approval_program) 83 | -------------------------------------------------------------------------------- /tealer/teal/instructions/asset_params_field.py: -------------------------------------------------------------------------------- 1 | """Defines classes to represent asset_params_get fields. 2 | 3 | ``asset_params_get`` instruction is used to access parameter fields 4 | of an asset. 5 | 6 | Each field that can be accessed using asset_params_get is represented 7 | by a class in tealer. All the classes representing the fields must inherit 8 | from AssetParamsField class. 9 | 10 | """ 11 | 12 | # pylint: disable=too-few-public-methods 13 | class AssetParamsField: 14 | """Base class to represent asset_params_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 AssetTotal(AssetParamsField): 33 | """Total number of units of this asset.""" 34 | 35 | 36 | class AssetDecimals(AssetParamsField): 37 | """Number of digits to display after the decimal place.""" 38 | 39 | 40 | class AssetDefaultFrozen(AssetParamsField): 41 | """Whether the asset is frozen by default or not.""" 42 | 43 | 44 | class AssetUnitName(AssetParamsField): 45 | """Unit name of the asset.""" 46 | 47 | 48 | class AssetName(AssetParamsField): 49 | """The asset name.""" 50 | 51 | 52 | class AssetURL(AssetParamsField): 53 | """Url associated with the asset.""" 54 | 55 | 56 | class AssetMetadataHash(AssetParamsField): 57 | """32 byte commitment to some unspecified asset metadata.""" 58 | 59 | 60 | class AssetManager(AssetParamsField): 61 | """Manager address of the asset. 62 | 63 | Manager account is the only account that can authorize transactions 64 | to re-configure or destroy an asset. 65 | """ 66 | 67 | 68 | class AssetReserve(AssetParamsField): 69 | """Reserve address of the asset. 70 | 71 | Non-minted assets will reside in Reserve address instead of creator 72 | address if specified. 73 | """ 74 | 75 | 76 | class AssetFreeze(AssetParamsField): 77 | """Freeze address of the asset. 78 | 79 | The freeze account is allowed to freeze or unfreeze the asset holdings 80 | for a specific account. 81 | """ 82 | 83 | 84 | class AssetClawback(AssetParamsField): 85 | """Clawback address of the asset. 86 | 87 | The clawback address represents an account that is allowed to transfer 88 | assets from and to any asset holder. 89 | """ 90 | 91 | 92 | class AssetCreator(AssetParamsField): 93 | """Creator address of this asset.""" 94 | 95 | def __init__(self) -> None: 96 | super().__init__() 97 | self._version: int = 5 98 | -------------------------------------------------------------------------------- /tealer/detectors/optimizations/self_access.py: -------------------------------------------------------------------------------- 1 | """Detector for finding unoptimized self access""" 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 Gtxns, Instruction, Txn, Gtxnsa, Gtxnsas 11 | from tealer.teal.instructions.transaction_field import GroupIndex 12 | from tealer.utils.output import InstructionsOutput 13 | 14 | if TYPE_CHECKING: 15 | from tealer.utils.output import ListOutput 16 | 17 | 18 | class SelfAccess(AbstractDetector): # pylint: disable=too-few-public-methods 19 | """Detector to find unoptimized self access""" 20 | 21 | NAME = "self-access" 22 | DESCRIPTION = "Unoptimized self access" 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-self-access" 30 | ) 31 | WIKI_TITLE = "Unoptimized self access" 32 | WIKI_DESCRIPTION = "Gtxn[Txn.group_index()].field can be replaced by Txn.field" 33 | WIKI_EXPLOIT_SCENARIO = """""" 34 | 35 | WIKI_RECOMMENDATION = """Use Txn.field.""" 36 | 37 | def detect(self) -> "ListOutput": 38 | """Detector to find unoptimized usage of self access 39 | 40 | Returns: 41 | The two instructions 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 | if len(block.instructions) < 2: 53 | continue 54 | 55 | # Sliding windows 56 | for i in range(0, len(block.instructions) - 1): 57 | first = block.instructions[i] 58 | second = block.instructions[i + 1] 59 | 60 | # Note: first / second are always assigned, as we check for len(block.instructions) first 61 | # pylint: disable=undefined-loop-variable 62 | if ( 63 | isinstance(first, Txn) 64 | and isinstance(first.field, GroupIndex) 65 | and isinstance(second, (Gtxns, Gtxnsa, Gtxnsas)) 66 | ): 67 | # pylint: disable=undefined-loop-variable 68 | all_findings.append([first, second]) 69 | 70 | if all_findings: 71 | detector_output.append(InstructionsOutput(contract, self, all_findings)) 72 | 73 | return detector_output 74 | -------------------------------------------------------------------------------- /tealer/detectors/optimizations/constant_gtxn.py: -------------------------------------------------------------------------------- 1 | """Detector for finding unoptimized usage of constant GTXN.""" 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 Gtxns, Instruction 11 | from tealer.utils.analyses import is_int_push_ins 12 | from tealer.utils.output import InstructionsOutput 13 | 14 | if TYPE_CHECKING: 15 | from tealer.utils.output import ListOutput 16 | 17 | 18 | class ConstantGtxn(AbstractDetector): # pylint: disable=too-few-public-methods 19 | """Detector to find unoptimized usage of constant GTXN""" 20 | 21 | NAME = "constant-gtxn" 22 | DESCRIPTION = "Unoptimized Gtxn" 23 | TYPE = DetectorType.STATELESS 24 | 25 | IMPACT = DetectorClassification.OPTIMIZATION 26 | CONFIDENCE = DetectorClassification.HIGH 27 | 28 | WIKI_URL = "https://github.com/crytic/tealer/wiki/Detector-Documentation#Unoptimized-Gtxn" 29 | WIKI_TITLE = "Unoptimized Gtxn" 30 | WIKI_DESCRIPTION = ( 31 | "- Gtxn[Int(1)].field() produces instructions int 1; gtxns field" 32 | "- Gtxn[1].field() produces only one instruction: gtxn 1 field" 33 | ) 34 | WIKI_EXPLOIT_SCENARIO = """""" 35 | 36 | WIKI_RECOMMENDATION = """Use Gtxn[idx].field().""" 37 | 38 | def detect(self) -> "ListOutput": 39 | """Detector to find unoptimized usage of constant GTXN 40 | 41 | Returns: 42 | The two instructions to optimized 43 | """ 44 | 45 | detector_output: "ListOutput" = [] 46 | 47 | for contract in self.tealer.contracts.values(): 48 | 49 | all_findings: List[List[Instruction]] = [] 50 | 51 | for function in contract.functions.values(): 52 | for block in function.blocks: 53 | if len(block.instructions) < 2: 54 | continue 55 | 56 | # Sliding windows 57 | for i in range(0, len(block.instructions) - 1): 58 | first = block.instructions[i] 59 | second = block.instructions[i + 1] 60 | 61 | # Note: first / second are always assigned, as we check for len(block.instructions) first 62 | # pylint: disable=undefined-loop-variable 63 | first_ins_is_int, _ = is_int_push_ins(first) 64 | # pylint: disable=undefined-loop-variable 65 | if first_ins_is_int and isinstance(second, Gtxns): 66 | # pylint: disable=undefined-loop-variable 67 | all_findings.append([first, second]) 68 | 69 | if all_findings: 70 | detector_output.append(InstructionsOutput(contract, self, all_findings)) 71 | 72 | return detector_output 73 | -------------------------------------------------------------------------------- /tests/test_detectors_using_pyteal.py: -------------------------------------------------------------------------------- 1 | # type: ignore[unreachable] 2 | import sys 3 | from typing import Tuple, List, Type 4 | 5 | import pytest 6 | 7 | from tealer.detectors.abstract_detector import AbstractDetector 8 | from tealer.utils.command_line.common import init_tealer_from_single_contract 9 | from tealer.utils.output import ExecutionPaths 10 | 11 | if not sys.version_info >= (3, 10): 12 | pytest.skip(reason="PyTeal based tests require python >= 3.10", allow_module_level=True) 13 | 14 | # Place import statements after the version check 15 | from tests.detectors.router_with_assembled_constants import ( # pylint: disable=wrong-import-position 16 | router_with_assembled_constants, 17 | ) 18 | from tests.detectors.pyteal_can_close import ( # pylint: disable=wrong-import-position 19 | txn_type_based_tests, 20 | ) 21 | from tests.detectors.pyteal_group_size import ( # pylint: disable=wrong-import-position 22 | group_size_tests_pyteal, 23 | ) 24 | 25 | from tests.detectors.pyteal_subroutine_recursion import ( # pylint: disable=wrong-import-position 26 | subroutine_recursion_patterns_tests, 27 | ) 28 | 29 | from tests.detectors.multiple_calls_to_subroutine import ( # pylint: disable=wrong-import-position 30 | multiple_calls_to_subroutine_tests, 31 | ) 32 | 33 | TESTS: List[Tuple[str, Type[AbstractDetector], List[List[int]]]] = [ 34 | *router_with_assembled_constants, 35 | *txn_type_based_tests, 36 | *group_size_tests_pyteal, 37 | *subroutine_recursion_patterns_tests, 38 | *multiple_calls_to_subroutine_tests, 39 | ] 40 | 41 | 42 | @pytest.mark.parametrize("test", TESTS) # type: ignore 43 | def test_detectors_using_pyteal(test: Tuple[str, Type[AbstractDetector], List[List[int]]]) -> None: 44 | code, detector, expected_paths = test 45 | tealer = init_tealer_from_single_contract(code.strip(), "test") 46 | tealer.register_detector(detector) 47 | result = tealer.run_detectors()[0] 48 | if not isinstance(result, ExecutionPaths): 49 | result = result[0] 50 | 51 | # for bi in teal.bbs: 52 | # print( 53 | # bi, 54 | # bi.idx, 55 | # bi.transaction_context.transaction_types, 56 | # bi.transaction_context.group_indices, 57 | # ) 58 | # print( 59 | # bi.transaction_context.gtxn_context(0).transaction_types, 60 | # bi.transaction_context.group_indices, 61 | # ) 62 | # print( 63 | # bi.transaction_context.gtxn_context(1).transaction_types, 64 | # bi.transaction_context.group_indices, 65 | # ) 66 | print("result paths =", result.paths) 67 | print("expected paths =", expected_paths) 68 | assert len(result.paths) == len(expected_paths) 69 | for path, expected_path in zip(result.paths, expected_paths): 70 | assert len(path) == len(expected_path) 71 | for bi, expected_idx in zip(path, expected_path): 72 | assert bi.idx == expected_idx 73 | -------------------------------------------------------------------------------- /tests/group_transactions/basic/expected_output.yaml: -------------------------------------------------------------------------------- 1 | name: BasicRekeyTo 2 | operations: 3 | - operation: case_2 4 | vulnerable_transactions: 5 | - txn_id: T1 6 | contracts: 7 | - contract: LogicSig_1 8 | function: lsig1_case_2 9 | - operation: case_3 10 | vulnerable_transactions: 11 | - txn_id: T1 12 | - operation: case_9 13 | vulnerable_transactions: 14 | - txn_id: T1 15 | contracts: 16 | - contract: LogicSig_1 17 | function: lsig1_case_9 18 | - operation: case_12 19 | vulnerable_transactions: 20 | - txn_id: T1 21 | - operation: case_13 22 | vulnerable_transactions: 23 | - txn_id: T2 24 | contracts: 25 | - contract: LogicSig_2 26 | function: lsig2_case_13 27 | - operation: case_14 28 | vulnerable_transactions: 29 | - txn_id: T1 30 | contracts: 31 | - contract: LogicSig_1 32 | function: lsig1_case_14 33 | - txn_id: T2 34 | contracts: 35 | - contract: LogicSig_2 36 | function: lsig2_case_14 37 | - operation: case_17 38 | vulnerable_transactions: 39 | - txn_id: T1 40 | contracts: 41 | - contract: LogicSig_1 42 | function: lsig1_case_17 43 | - operation: case_21 44 | vulnerable_transactions: 45 | - txn_id: T1 46 | contracts: 47 | - contract: LogicSig_1 48 | function: lsig1_case_21 49 | - operation: case_24 50 | vulnerable_transactions: 51 | - txn_id: T1 52 | - operation: case_25 53 | vulnerable_transactions: 54 | - txn_id: T2 55 | contracts: 56 | - contract: LogicSig_2 57 | function: lsig2_case_25 58 | - operation: case_26 59 | vulnerable_transactions: 60 | - txn_id: T1 61 | contracts: 62 | - contract: LogicSig_1 63 | function: lsig1_case_26 64 | - txn_id: T2 65 | contracts: 66 | - contract: LogicSig_2 67 | function: lsig2_case_26 68 | - operation: case_28 69 | vulnerable_transactions: 70 | - txn_id: T1 71 | contracts: 72 | - contract: LogicSig_1 73 | function: lsig1_case_28 74 | - operation: case_29 75 | vulnerable_transactions: 76 | - txn_id: T1 77 | contracts: 78 | - contract: LogicSig_1 79 | function: lsig1_case_29 80 | - operation: case_32 81 | vulnerable_transactions: 82 | - txn_id: T1 83 | contracts: 84 | - contract: LogicSig_1 85 | function: lsig1_case_32 86 | - operation: case_34 87 | vulnerable_transactions: 88 | - txn_id: T1 89 | contracts: 90 | - contract: LogicSig_1 91 | function: lsig1_case_34 92 | - operation: case_35 93 | vulnerable_transactions: 94 | - txn_id: T1 95 | contracts: 96 | - contract: LogicSig_1 97 | function: lsig1_case_35 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Tealer 2 | First, thanks for your interest in contributing to Tealer! We welcome and appreciate all contributions, including bug reports, feature suggestions, tutorials/blog posts, and code improvements. 3 | 4 | If you're unsure where to start, we recommend our [`good first issue`](https://github.com/crytic/tealer/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and [`help wanted`](https://github.com/crytic/tealer/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) issue labels. 5 | 6 | ## Bug reports and feature suggestions 7 | Bug reports and feature suggestions can be submitted to our issue tracker. For bug reports, attaching the contract that caused the bug will help us in debugging and resolving the issue quickly. If you find a security vulnerability, do not open an issue; email opensource@trailofbits.com instead. 8 | 9 | ## Questions 10 | Questions can be submitted to the issue tracker, but you may get a faster response if you ask in our [chat room](https://slack.empirehacking.nyc/) (in the #ethereum channel). 11 | 12 | ## One punch editable developement version 13 | Do something like this to clone the repository and install all development dependencies in a python virtual environment 14 | ```bash 15 | git clone https://github.com/crytic/tealer.git 16 | cd tealer 17 | make dev 18 | ``` 19 | 20 | Update `tealer` by running `git pull` from the `tealer/` directory. 21 | 22 | 23 | ## Code 24 | Tealer uses the pull request contribution model. Please make an account on Github, fork this repo, and submit code contributions via pull request. For more documentation, look [here](https://guides.github.com/activities/forking/). 25 | 26 | Some pull request guidelines: 27 | 28 | - Work from the [`dev`](https://github.com/crytic/tealer/tree/dev) branch. We performed extensive tests prior to merging anything to `master`, working from `dev` will allow us to merge your work faster. 29 | - Minimize irrelevant changes (formatting, whitespace, etc) to code that would otherwise not be touched by this patch. Save formatting or style corrections for a separate pull request that does not make any semantic changes. 30 | - When possible, large changes should be split up into smaller focused pull requests. 31 | - Fill out the pull request description with a summary of what your patch does, key changes that have been made, and any further points of discussion, if applicable. 32 | - Title your pull request with a brief description of what it's changing. "Fixes #123" is a good comment to add to the description, but makes for an unclear title on its own. 33 | 34 | ### Linters 35 | 36 | Several linters and security checkers are run on the PRs. 37 | 38 | To run them locally in the root dir of the repository: 39 | 40 | - `pylint tealer --rcfile pyproject.toml` 41 | - `black tealer --config pyproject.toml` 42 | - `mypy tealer --config mypy.ini` 43 | 44 | Install the linters with `pip install .[dev]`. 45 | We use pylint `2.13.4`, black `22.3.0`, mypy `0.942`. 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/detectors/router_with_assembled_constants.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # mypy: ignore-errors 3 | from pyteal import * # pylint: disable=wildcard-import, unused-wildcard-import 4 | 5 | from pyteal import * 6 | 7 | from tealer.detectors.all_detectors import IsUpdatable, IsDeletable 8 | 9 | router = Router( 10 | name="Example", 11 | bare_calls=BareCallActions(), 12 | ) 13 | 14 | """ 15 | Pattern for method config having CallConfig.CALL: 16 | txn OnCompletion 17 | int NoOp 18 | == 19 | txn ApplicationID 20 | int 0 21 | != 22 | && 23 | assert 24 | 25 | Pattern for method config having CallConfig.CREATE: 26 | txn OnCompletion 27 | int NoOp 28 | == 29 | txn ApplicationID 30 | int 0 31 | == 32 | && 33 | assert 34 | 35 | Pattern for method config having CallConfig.ALL: 36 | txn OnCompletion 37 | int NoOp 38 | == 39 | assert 40 | 41 | Tealer cannot calculate the information from first two patterns. For that reason, all of the 42 | methods have CallConfig.ALL. 43 | """ 44 | 45 | 46 | @router.method(no_op=CallConfig.ALL) 47 | def echo(input: abi.Uint64, *, output: abi.Uint64) -> Expr: 48 | """ 49 | Method config validations Teal pattern: 50 | txn OnCompletion 51 | int NoOp 52 | == 53 | txn ApplicationID 54 | int 0 55 | != 56 | && 57 | assert // Assert(NoOp, CALL) 58 | 59 | Args: 60 | input: Input argument 61 | output: Return value of the echo method. 62 | 63 | Returns: 64 | Body of the echo method as PyTeal expr. 65 | """ 66 | return output.set(input.get()) 67 | 68 | 69 | @router.method(opt_in=CallConfig.ALL) 70 | def deposit() -> Expr: 71 | return Return() 72 | 73 | 74 | @router.method(close_out=CallConfig.ALL) 75 | def getBalance() -> Expr: 76 | return Return() 77 | 78 | 79 | @router.method(update_application=CallConfig.ALL) 80 | def update() -> Expr: 81 | return Err() 82 | 83 | 84 | @router.method(delete_application=CallConfig.ALL) 85 | def delete() -> Expr: 86 | return Err() 87 | 88 | 89 | # does not matter even if multiple actions are allowed as it fails for all calls. 90 | @router.method(no_op=CallConfig.CALL, opt_in=CallConfig.CREATE, close_out=CallConfig.ALL) 91 | def some(input: abi.Uint64) -> Expr: 92 | return Err() 93 | 94 | 95 | # PyTeal creates intcblock, bytecblock if assemble_constants = True 96 | # int NoOp, OptIn, ... are all replaced by intc_* instructions. 97 | # pragma(compiler_version="0.22.0") 98 | approval_program, clear_state_program, contract = router.compile_program( 99 | version=7, 100 | assemble_constants=True, # use intcblock, bytecblock 101 | # optimize=OptimizeOptions(scratch_slots=True), 102 | ) 103 | 104 | router_with_assembled_constants = [ 105 | (approval_program, IsUpdatable, []), 106 | (approval_program, IsDeletable, []), 107 | ] 108 | -------------------------------------------------------------------------------- /tealer/printers/abstract_printer.py: -------------------------------------------------------------------------------- 1 | """Defines abstract base class for printers in tealer. 2 | 3 | This module contains implementation of abstract class which 4 | defines the attributes, common methods and must implement methods 5 | of printers. All printers in tealer must inherit from this class 6 | and set attributes, override abstract methods. 7 | 8 | Classes: 9 | AbstractPrinter: Abstract class to represent printers in tealer. 10 | """ 11 | 12 | import abc 13 | 14 | from typing import TYPE_CHECKING 15 | 16 | if TYPE_CHECKING: 17 | from tealer.teal.teal import Teal 18 | from pathlib import Path 19 | 20 | 21 | class IncorrectPrinterInitialization(Exception): 22 | """Exception class to represent incorrect printer intialization. 23 | 24 | This exception will be used if any of the necessary attributes 25 | of the AbstractPrinter are not set by the inheriting printer class. 26 | """ 27 | 28 | 29 | class AbstractPrinter(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods 30 | """Abstract class to represent printers in tealer. 31 | 32 | All printers in tealer must inherit from this class and override the 33 | abstract methods with functionality specific to them. This class defines 34 | the api methods that must be implemented by every printers. ``print`` 35 | method of each printer will be called to execute the printer. Every 36 | printer must implement ``print`` method. 37 | 38 | Attrributes: 39 | NAME: Name of the printer. printer name will be used while 40 | displaying information about the printer and also for selecting 41 | the printer from command line. 42 | HELP: Description of the printer. 43 | 44 | Args: 45 | teal: Teal instance representing the contract. Printer will run on 46 | the contract represented by :teal: 47 | 48 | Raises: 49 | IncorrectPrinterInitialization: Exception is raised if NAME, HELP 50 | attributes are not set by the inheriting printer class. 51 | """ 52 | 53 | NAME = "" 54 | HELP = "" 55 | WIKI_URL = "" 56 | 57 | def __init__(self, teal: "Teal"): 58 | self.teal = teal 59 | 60 | if not self.NAME: 61 | raise IncorrectPrinterInitialization( 62 | f"NAME is not initialized {self.__class__.__name__}" 63 | ) 64 | 65 | if not self.HELP: 66 | raise IncorrectPrinterInitialization( 67 | f"HELP is not initialized {self.__class__.__name__}" 68 | ) 69 | 70 | if not self.WIKI_URL: 71 | raise IncorrectPrinterInitialization( 72 | f"WIKI_URL is not initialized for {self.__class__.__name__}" 73 | ) 74 | 75 | @abc.abstractmethod 76 | def print(self) -> None: 77 | """entry method of the printer. 78 | 79 | All printers must override this method with the functionality 80 | specific to them. This method is the entry point of the printer 81 | and will be called to execute it. 82 | """ 83 | -------------------------------------------------------------------------------- /tealer/execution_context/transactions.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Optional, Dict, List 2 | 3 | from tealer.utils.teal_enums import TransactionType 4 | 5 | # from tealer.exceptions import TealerException 6 | 7 | if TYPE_CHECKING: 8 | from tealer.teal.functions import Function 9 | 10 | # pylint: disable=too-few-public-methods, too-many-instance-attributes 11 | class Transaction: 12 | def __init__(self) -> None: 13 | self.type: TransactionType = TransactionType.Any 14 | self.has_logic_sig: bool = False 15 | self.logic_sig: Optional["Function"] = None 16 | self.application: Optional["Function"] = None 17 | self.absoulte_index: Optional[int] = None 18 | # t1.relative_indexes[-1] = t2 => t2.group_index() == t1.group_index() - 1 19 | self.relative_indexes: Dict[int, Transaction] = {} 20 | self.group_transaction: Optional[GroupTransaction] = None 21 | self.transacton_id: str = "" 22 | 23 | 24 | # pylint: disable=too-few-public-methods 25 | class GroupTransaction: 26 | def __init__(self) -> None: 27 | self.transactions: List[Transaction] = [] 28 | self.absolute_indexes: Dict[int, Transaction] = {} 29 | # {t1: (t2, -2)} => t1.group_index() == t2.group_index() - 2 30 | self.group_relative_indexes: Dict[Transaction, Dict[Transaction, int]] = {} 31 | self.operation_name: str = "" 32 | 33 | 34 | def fill_group_relative_indexes(group: "GroupTransaction") -> None: 35 | # group_relative_indexes: Dict[Transaction, Dict[Transaction, int]] = {} 36 | for txn in group.transactions: 37 | group.group_relative_indexes[txn] = {} 38 | 39 | for txn in group.transactions: 40 | for offset in txn.relative_indexes: 41 | other_txn = txn.relative_indexes[offset] 42 | # other_txn.group_index() = txn.group_index() + offset 43 | group.group_relative_indexes[other_txn][txn] = offset 44 | 45 | 46 | # def fill_indexes(group: GroupTransaction): 47 | # """Calculate the absolute indexes and relative indexes using the indexes from the config. 48 | 49 | # """ 50 | 51 | # for txn in group.transactions: 52 | # if txn.absoulte_index is not None: 53 | # for offset in txn.relative_indexes: 54 | # other_txn = txn.relative_indexes[offset] 55 | # other_txn_abs = txn.absoulte_index + offset 56 | # if other_txn.absoulte_index is None: 57 | # other_txn.absoulte_index = other_txn_abs 58 | # group.absolute_indexes[other_txn_abs] = other_txn 59 | # # else: 60 | # # if other_txn.absoulte_index != other_txn_abs: 61 | # # raise TealerException("Invalid group configuration") 62 | # for offset in txn.relative_indexes: 63 | # other_txn = txn.relative_indexes[offset] 64 | # this_txn_offset = -1 * offset 65 | # if this_txn_offset not in other_txn.relative_indexes: 66 | # other_txn.relative_indexes[this_txn_offset] = txn 67 | -------------------------------------------------------------------------------- /tests/group_transactions/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List 2 | from dataclasses import dataclass 3 | from pathlib import Path 4 | import yaml 5 | 6 | 7 | class InvalidTestCase(Exception): 8 | pass 9 | 10 | 11 | @dataclass 12 | class OutputContract: 13 | contract_name: str 14 | function_name: str 15 | 16 | @staticmethod 17 | def from_yaml(contract: Dict[str, Any]) -> "OutputContract": 18 | if "contract" not in contract or "function" not in contract: 19 | raise InvalidTestCase( 20 | f"Missing required fields in the contract description: {contract}" 21 | ) 22 | return OutputContract(contract["contract"], contract["function"]) 23 | 24 | 25 | @dataclass 26 | class OutputTransaction: 27 | transaction_id: str 28 | contracts: List[OutputContract] 29 | 30 | @staticmethod 31 | def from_yaml(transaction: Dict[str, Any]) -> "OutputTransaction": 32 | if "txn_id" not in transaction: 33 | raise InvalidTestCase(f'Missing "txn_id" field for the transaction {transaction}') 34 | 35 | contracts = [] 36 | if "contracts" in transaction: 37 | for contract in transaction["contracts"]: 38 | contracts.append(OutputContract.from_yaml(contract)) 39 | return OutputTransaction( 40 | transaction["txn_id"], 41 | sorted(contracts, key=lambda x: (x.contract_name, x.function_name)), 42 | ) 43 | 44 | 45 | @dataclass 46 | class OutputOperation: 47 | operation: str 48 | vulnerable_transactions: List[OutputTransaction] 49 | 50 | @staticmethod 51 | def from_yaml(operation: Dict[str, Any]) -> "OutputOperation": 52 | if "operation" not in operation or "vulnerable_transactions" not in operation: 53 | raise InvalidTestCase(f"Missing required fields in the operation: {operation}") 54 | 55 | operation_name = operation["operation"] 56 | 57 | vulnerable_transactions = [] 58 | for transaction in operation["vulnerable_transactions"]: 59 | vulnerable_transactions.append(OutputTransaction.from_yaml(transaction)) 60 | 61 | return OutputOperation(operation_name, vulnerable_transactions) 62 | 63 | 64 | @dataclass 65 | class ExpectedOutput: 66 | name: str 67 | operations: List[OutputOperation] 68 | 69 | @staticmethod 70 | def from_yaml(output: Dict[str, Any]) -> "ExpectedOutput": 71 | if "name" not in output or "operations" not in output: 72 | raise InvalidTestCase(f"Missing required fields in the expected output: {output}") 73 | 74 | name = output["name"] 75 | operations = [] 76 | for operation in output["operations"]: 77 | operations.append(OutputOperation.from_yaml(operation)) 78 | return ExpectedOutput(name, operations) 79 | 80 | 81 | def expected_output_from_file(file_path: Path) -> "ExpectedOutput": 82 | with open(file_path, encoding="utf-8") as f: 83 | # print(yaml.safe_load(f.read())) 84 | return ExpectedOutput.from_yaml(yaml.safe_load(f.read())) 85 | -------------------------------------------------------------------------------- /tests/group_transactions/basic/expected_output_update_delete.yaml: -------------------------------------------------------------------------------- 1 | name: BasicRekeyTo 2 | operations: 3 | - operation: case_2 4 | vulnerable_transactions: 5 | - txn_id: T1 6 | contracts: 7 | - contract: App_1 8 | function: app1_case_2 9 | - operation: case_3 10 | vulnerable_transactions: 11 | - txn_id: T1 12 | contracts: 13 | - contract: App_1 14 | function: app1_case_3 15 | - operation: case_4 16 | vulnerable_transactions: 17 | - txn_id: T1 18 | contracts: 19 | - contract: App_1 20 | function: app1_case_4 21 | - operation: case_9 22 | vulnerable_transactions: 23 | - txn_id: T1 24 | contracts: 25 | - contract: App_1 26 | function: app1_case_9 27 | - operation: case_12 28 | vulnerable_transactions: 29 | - txn_id: T1 30 | contracts: 31 | - contract: App_1 32 | function: app1_case_12 33 | - operation: case_13 34 | vulnerable_transactions: 35 | - txn_id: T1 36 | contracts: 37 | - contract: App_1 38 | function: app1_case_13 39 | - operation: case_14 40 | vulnerable_transactions: 41 | - txn_id: T1 42 | contracts: 43 | - contract: App_1 44 | function: app1_case_14 45 | - operation: case_17 46 | vulnerable_transactions: 47 | - txn_id: T1 48 | contracts: 49 | - contract: App_1 50 | function: app1_case_17 51 | - operation: case_21 52 | vulnerable_transactions: 53 | - txn_id: T1 54 | contracts: 55 | - contract: App_1 56 | function: app1_case_21 57 | - operation: case_24 58 | vulnerable_transactions: 59 | - txn_id: T1 60 | contracts: 61 | - contract: App_1 62 | function: app1_case_24 63 | - operation: case_25 64 | vulnerable_transactions: 65 | - txn_id: T1 66 | contracts: 67 | - contract: App_1 68 | function: app1_case_25 69 | - operation: case_26 70 | vulnerable_transactions: 71 | - txn_id: T1 72 | contracts: 73 | - contract: App_1 74 | function: app1_case_26 75 | - operation: case_28 76 | vulnerable_transactions: 77 | - txn_id: T1 78 | contracts: 79 | - contract: App_1 80 | function: app1_case_28 81 | - operation: case_29 82 | vulnerable_transactions: 83 | - txn_id: T1 84 | contracts: 85 | - contract: App_1 86 | function: app1_case_29 87 | - operation: case_32 88 | vulnerable_transactions: 89 | - txn_id: T1 90 | contracts: 91 | - contract: App_1 92 | function: app1_case_32 93 | - operation: case_34 94 | vulnerable_transactions: 95 | - txn_id: T1 96 | contracts: 97 | - contract: App_1 98 | function: app1_case_34 99 | - operation: case_35 100 | vulnerable_transactions: 101 | - txn_id: T1 102 | contracts: 103 | - contract: App_1 104 | function: app1_case_35 105 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Code Base 3 | 4 | defaults: 5 | run: 6 | # To load bashrc 7 | shell: bash -ieo pipefail {0} 8 | 9 | on: 10 | push: 11 | branches: [main, dev] 12 | pull_request: 13 | schedule: 14 | # run CI every day even if no PRs/merges occur 15 | - cron: '0 12 * * *' 16 | 17 | jobs: 18 | build: 19 | name: Lint Code Base 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout Code 24 | uses: actions/checkout@v2 25 | 26 | - name: Set up Python 3.9 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: 3.9 30 | 31 | - name: Install dependencies 32 | run: | 33 | pip install . 34 | mkdir -p .github/linters 35 | cp pyproject.toml .github/linters 36 | cp mypy.ini .github/linters 37 | pip install pytest 38 | - name: Pylint 39 | uses: github/super-linter/slim@v4.9.2 40 | if: always() 41 | env: 42 | # run linter on everything to catch preexisting problems 43 | VALIDATE_ALL_CODEBASE: true 44 | DEFAULT_BRANCH: master 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | # Run only pylint 47 | VALIDATE_PYTHON: true 48 | VALIDATE_PYTHON_PYLINT: true 49 | PYTHON_PYLINT_CONFIG_FILE: pyproject.toml 50 | 51 | - name: Black 52 | uses: github/super-linter/slim@v4.9.2 53 | if: always() 54 | env: 55 | # run linter on everything to catch preexisting problems 56 | VALIDATE_ALL_CODEBASE: true 57 | DEFAULT_BRANCH: master 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | # Run only black 60 | VALIDATE_PYTHON_BLACK: true 61 | PYTHON_BLACK_CONFIG_FILE: pyproject.toml 62 | 63 | - name: Mypy 64 | uses: github/super-linter/slim@v4.9.2 65 | if: always() 66 | env: 67 | # run linter on everything to catch preexisting problems 68 | VALIDATE_ALL_CODEBASE: true 69 | DEFAULT_BRANCH: master 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | # Run only mypy 72 | VALIDATE_PYTHON_MYPY: true 73 | PYTHON_MYPY_CONFIG_FILE: mypy.ini 74 | 75 | - name: Lint everything else 76 | uses: github/super-linter/slim@v4.9.2 77 | if: always() 78 | env: 79 | # run linter on everything to catch preexisting problems 80 | VALIDATE_ALL_CODEBASE: true 81 | DEFAULT_BRANCH: master 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | # Always false 84 | VALIDATE_PYTHON: false 85 | VALIDATE_PYTHON_PYLINT: false 86 | VALIDATE_PYTHON_BLACK: false 87 | VALIDATE_PYTHON_ISORT: false 88 | # Always false 89 | VALIDATE_JSON: false 90 | VALIDATE_JAVASCRIPT_STANDARD: false 91 | VALIDATE_PYTHON_FLAKE8: false 92 | VALIDATE_DOCKERFILE: false 93 | VALIDATE_DOCKERFILE_HADOLINT: false 94 | VALIDATE_EDITORCONFIG: false 95 | VALIDATE_JSCPD: false 96 | VALIDATE_PYTHON_MYPY: false 97 | SHELLCHECK_OPTS: "-e SC1090 -e SC2181 -e SC2103" 98 | -------------------------------------------------------------------------------- /examples/printers/transaction-context.dot: -------------------------------------------------------------------------------- 1 | digraph g{ 2 | ranksep = 1 3 | overlap = scale 4 | 5 | 0[label=< 6 | 7 | 8 | 9 | 10 | 11 | 12 |
// 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
> labelloc=top shape=plain 13 | ] 0:s -> 1:6:n [color="#e0182b"]; 14 | 0:s -> 2:19:n [color="#36d899"]; 15 | 16 | 1[label=< 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
// 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
> labelloc=top shape=plain 32 | ] 1:s -> 3:28:n [color="BLACK"]; 33 | 34 | 2[label=< 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
// 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
> labelloc=top shape=plain 46 | ] 2:s -> 3:28:n [color="BLACK"]; 47 | 48 | 3[label=< 49 | 50 | 51 | 52 | 53 |
// block_id = 3; cost = 3
// GroupIndex: 1 2
// GroupSize: 3 6..10
28. main_l3:
29. int 1
30. return
> labelloc=top shape=plain 54 | ] 55 | } -------------------------------------------------------------------------------- /tealer/teal/instructions/acct_params_field.py: -------------------------------------------------------------------------------- 1 | """Defines classes to represent acct_params_get fields. 2 | 3 | ``acct_params_get`` instruction is used to access fields 4 | of an account in the contract. 5 | 6 | Each field that can be accessed using acct_params_get is represented 7 | by a class in tealer. All the classes representing the fields must 8 | inherit from AcctParamsField class. 9 | 10 | """ 11 | 12 | # pylint: disable=too-few-public-methods 13 | class AcctParamsField: 14 | """Base class to represent Acct fields.""" 15 | 16 | def __init__(self) -> None: 17 | self._version: int = 6 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 AcctBalance(AcctParamsField): 33 | """Account balance in microalgos.""" 34 | 35 | 36 | class AcctMinBalance(AcctParamsField): 37 | """Minimum required balance for account, in microalgos.""" 38 | 39 | 40 | class AcctAuthAddr(AcctParamsField): 41 | """Address the account is rekeyed to.""" 42 | 43 | 44 | class AcctTotalNumUint(AcctParamsField): 45 | """The total number of uint64 values allocated by this account in Global and Local States.""" 46 | 47 | def __init__(self) -> None: 48 | super().__init__() 49 | self._version: int = 8 50 | 51 | 52 | class AcctTotalNumByteSlice(AcctParamsField): 53 | """The total number of byte array values allocated by this account in Global and Local States.""" 54 | 55 | def __init__(self) -> None: 56 | super().__init__() 57 | self._version: int = 8 58 | 59 | 60 | class AcctTotalExtraAppPages(AcctParamsField): 61 | """The number of extra app code pages used by this account.""" 62 | 63 | def __init__(self) -> None: 64 | super().__init__() 65 | self._version: int = 8 66 | 67 | 68 | class AcctTotalAppsCreated(AcctParamsField): 69 | """The number of existing apps created by this account""" 70 | 71 | def __init__(self) -> None: 72 | super().__init__() 73 | self._version: int = 8 74 | 75 | 76 | class AcctTotalAppsOptedIn(AcctParamsField): 77 | """The number of apps this account is opted into.""" 78 | 79 | def __init__(self) -> None: 80 | super().__init__() 81 | self._version: int = 8 82 | 83 | 84 | class AcctTotalAssetsCreated(AcctParamsField): 85 | """The number of existing ASAs created by this account.""" 86 | 87 | def __init__(self) -> None: 88 | super().__init__() 89 | self._version: int = 8 90 | 91 | 92 | class AcctTotalAssets(AcctParamsField): 93 | """The numbers of ASAs held by this account (including ASAs this account created).""" 94 | 95 | def __init__(self) -> None: 96 | super().__init__() 97 | self._version: int = 8 98 | 99 | 100 | class AcctTotalBoxes(AcctParamsField): 101 | """The number of existing boxes created by this account's app.""" 102 | 103 | def __init__(self) -> None: 104 | super().__init__() 105 | self._version: int = 8 106 | 107 | 108 | class AcctTotalBoxBytes(AcctParamsField): 109 | """The total number of bytes used by this account's app's box keys and values.""" 110 | 111 | def __init__(self) -> None: 112 | super().__init__() 113 | self._version: int = 8 114 | -------------------------------------------------------------------------------- /tealer/teal/functions.py: -------------------------------------------------------------------------------- 1 | """Defines a class to represent a Function in Teal contract. 2 | 3 | Functions are different from subroutine. Functions are abstract blocks of code, each callable 4 | by a end-user. Function represents an operation of the contract. 5 | 6 | Classes: 7 | Function: Represents a Function. 8 | """ 9 | 10 | from typing import TYPE_CHECKING, List, Dict 11 | 12 | from tealer.teal.context.block_transaction_context import BlockTransactionContext 13 | 14 | if TYPE_CHECKING: 15 | from tealer.teal.basic_blocks import BasicBlock 16 | from tealer.teal.teal import Teal 17 | from tealer.teal.subroutine import Subroutine 18 | 19 | 20 | # pylint: disable=too-few-public-methods,too-many-instance-attributes 21 | class Function: 22 | """Class to represent a function in a contract.""" 23 | 24 | def __init__( # pylint: disable=too-many-arguments 25 | self, 26 | function_name: str, 27 | entry: "BasicBlock", 28 | blocks: List["BasicBlock"], 29 | contract: "Teal", 30 | main: "Subroutine", 31 | subroutines: Dict[str, "Subroutine"], 32 | ) -> None: 33 | self.function_name: str = function_name 34 | self.entry: "BasicBlock" = entry 35 | self._blocks: List["BasicBlock"] = blocks 36 | self.contract: "Teal" = contract 37 | self.main: "Subroutine" = main 38 | self.subroutines: Dict[str, "Subroutine"] = subroutines 39 | # A subroutine is represented using a single CFG. A subroutine can be part of multiple 40 | # functions. Basic Blocks part of the subroutine CFG will be part of all functions the subroutine 41 | # is used in. But, block transaction context is specific for each function. As a result, 42 | # We cannot store the block transaction context in basic block object. 43 | self._transaction_contexts: Dict["BasicBlock", "BlockTransactionContext"] = { 44 | block: BlockTransactionContext() for block in self._blocks 45 | } 46 | # caller blocks of a subroutine depend on the function __main__ CFG. As a result, they 47 | # cannot be computed without considering the function. 48 | self._subroutine_caller_blocks: Dict["Subroutine", List["BasicBlock"]] = { 49 | sub: [] for sub in subroutines.values() 50 | } 51 | 52 | for block in self._blocks: 53 | if block.is_callsub_block: 54 | self._subroutine_caller_blocks[block.called_subroutine].append(block) 55 | 56 | self._subroutine_return_point_blocks: Dict["Subroutine", List["BasicBlock"]] = {} 57 | for sub, caller_blocks in self._subroutine_caller_blocks.items(): 58 | self._subroutine_return_point_blocks[sub] = [ 59 | bi.next[0] for bi in caller_blocks if len(bi.next) == 1 60 | ] 61 | 62 | # print(self._subroutine_caller_blocks) 63 | 64 | # TODO: add exit_blocks 65 | 66 | @property 67 | def blocks(self) -> List["BasicBlock"]: 68 | return self._blocks 69 | 70 | def transaction_context(self, block: "BasicBlock") -> "BlockTransactionContext": 71 | return self._transaction_contexts[block] 72 | 73 | def caller_blocks(self, subroutine: "Subroutine") -> List["BasicBlock"]: 74 | """BasicBlock with callsub instructions which call the subroutine. 75 | 76 | Args: 77 | subroutine: the subroutine 78 | 79 | Returns: 80 | List of caller callsub basic blocks. This blocks have "callsub {subroutine.name}" 81 | as exit instruction. 82 | """ 83 | return self._subroutine_caller_blocks[subroutine] 84 | 85 | def return_point_blocks(self, subroutine: "Subroutine") -> List["BasicBlock"]: 86 | return self._subroutine_return_point_blocks[subroutine] 87 | -------------------------------------------------------------------------------- /tests/transaction_context/test_group_indices.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | import pytest 3 | 4 | from tealer.utils.command_line.common import init_tealer_from_single_contract 5 | from tests.utils import order_basic_blocks 6 | 7 | 8 | MULTIPLE_RETSUB = """ 9 | #pragma version 5 10 | b main 11 | is_even: 12 | int 2 13 | % 14 | bz return_1 15 | int 0 16 | txn GroupIndex 17 | int 2 18 | == 19 | assert 20 | retsub 21 | return_1: 22 | txn GroupIndex 23 | int 3 24 | < 25 | assert 26 | int 1 27 | retsub 28 | main: 29 | txn GroupIndex 30 | int 1 31 | != 32 | assert 33 | int 4 34 | callsub is_even 35 | return 36 | """ 37 | 38 | MULTIPLE_RETSUB_GROUP_INDICES = [[0, 2], [0, 2], [2], [0, 2], [0, 2], [0, 2]] 39 | 40 | SUBROUTINE_BACK_JUMP = """ 41 | #pragma version 5 42 | b main 43 | getmod: 44 | % 45 | retsub 46 | is_odd: 47 | txn GroupIndex 48 | int 4 49 | < 50 | assert 51 | txn GroupIndex 52 | int 2 53 | != 54 | assert 55 | int 2 56 | b getmod 57 | main: 58 | int 5 59 | callsub is_odd 60 | return 61 | """ 62 | 63 | SUBROUTINE_BACK_JUMP_GROUP_INDICES = [ 64 | [0, 1, 3], 65 | [0, 1, 3], 66 | [0, 1, 3], 67 | [0, 1, 3], 68 | [0, 1, 3], 69 | [0, 1, 3], 70 | ] 71 | 72 | BRANCHING = """ 73 | #pragma version 4 74 | txn GroupIndex 75 | int 2 76 | >= 77 | assert 78 | txn GroupIndex 79 | int 4 80 | > 81 | bz fin 82 | txn GroupIndex 83 | int 1 84 | == 85 | bnz check_second_arg 86 | int 0 87 | return 88 | check_second_arg: 89 | txn ApplicationArgs 1 90 | btoi 91 | int 100 92 | > 93 | bnz fin 94 | int 0 95 | return 96 | fin: 97 | int 1 98 | return 99 | """ 100 | 101 | BRANCHING_GROUP_INDICES = [[2, 3, 4], [], [], [], [], [2, 3, 4]] 102 | 103 | LOOPS = """ 104 | #pragma version 5 105 | txn GroupIndex 106 | int 4 107 | != 108 | assert 109 | int 0 110 | loop: 111 | dup 112 | txn GroupIndex 113 | int 3 114 | >= 115 | bz end 116 | int 1 117 | + 118 | txn GroupIndex 119 | int 3 120 | < 121 | assert 122 | b loop 123 | end: 124 | int 2 125 | txn GroupIndex 126 | == 127 | assert 128 | int 1 129 | return 130 | """ 131 | 132 | LOOPS_GROUP_INDICES = [[2], [2], [], [2]] 133 | 134 | LOOPS_GROUP_SIZES = """ 135 | #pragma version 5 136 | txn GroupIndex 137 | int 4 138 | != 139 | assert 140 | global GroupSize 141 | int 6 142 | <= 143 | int 0 144 | loop: 145 | dup 146 | txn GroupIndex 147 | int 3 148 | > 149 | bz end 150 | int 1 151 | + 152 | txn GroupIndex 153 | int 6 154 | < 155 | assert 156 | b loop 157 | end: 158 | int 2 159 | txn GroupIndex 160 | > 161 | assert 162 | int 5 163 | global GroupSize 164 | <= 165 | assert 166 | int 1 167 | return 168 | """ 169 | 170 | LOOPS_GROUP_SIZES_GROUP_INDICES = [[3], [3], [], [3]] 171 | 172 | ALL_TESTS = [ 173 | (MULTIPLE_RETSUB, MULTIPLE_RETSUB_GROUP_INDICES), 174 | (SUBROUTINE_BACK_JUMP, SUBROUTINE_BACK_JUMP_GROUP_INDICES), 175 | (BRANCHING, BRANCHING_GROUP_INDICES), 176 | (LOOPS, LOOPS_GROUP_INDICES), 177 | (LOOPS_GROUP_SIZES, LOOPS_GROUP_SIZES_GROUP_INDICES), 178 | ] 179 | 180 | 181 | @pytest.mark.parametrize("test", ALL_TESTS) # type: ignore 182 | def test_group_indices(test: Tuple[str, List[List[int]]]) -> None: 183 | code, group_indices = test 184 | tealer = init_tealer_from_single_contract(code.strip(), "test") 185 | function = tealer.contracts["test"].functions["test"] 186 | 187 | bbs = order_basic_blocks(function.blocks) 188 | for b, indices in zip(bbs, group_indices): 189 | assert function.transaction_context(b).group_indices == indices 190 | -------------------------------------------------------------------------------- /tealer/utils/code_complexity.py: -------------------------------------------------------------------------------- 1 | """Util functions to compute code complexity of the contracts. 2 | 3 | Module implements functions to calculate cyclomatic complexity of 4 | the contract for now. 5 | 6 | Functions: 7 | compute_cyclomatic_complexity(cfg: List["BasicBlock"]) -> int: 8 | Calculates the cyclomatic complexity of the contract given 9 | it's Control Flow Graph(CFG) :cfg:. 10 | 11 | """ 12 | # pylint: skip-file 13 | # mypy: ignore-errors 14 | # TODO: Functions in this module are not used anywhere. Have to decide on how to calculate the code complexity. 15 | 16 | from typing import List, TYPE_CHECKING 17 | from tealer.utils.analyses import next_blocks_global, prev_blocks_global 18 | 19 | if TYPE_CHECKING: 20 | from tealer.teal.basic_blocks import BasicBlock 21 | 22 | 23 | # from slither: slither/utils/code_complexity.py 24 | def _compute_number_edges(cfg: List["BasicBlock"]) -> int: 25 | """compute number of edges in the control flow graph of the contract. 26 | 27 | Args: 28 | cfg: Control Flow Graph(CFG) of the contract. 29 | 30 | Returns: 31 | number of edges in the CFG. 32 | """ 33 | 34 | n = 0 35 | for bb in cfg: 36 | n += len(next_blocks_global(bb)) 37 | return n 38 | 39 | 40 | # from slither: slither/utils/code_complexity.py 41 | def _compute_strongly_connected_components(cfg: List["BasicBlock"]) -> List[List["BasicBlock"]]: 42 | """Compute strongly connected components in the control flow graph. 43 | 44 | Implementation uses Kosaraju algorithm to find strongly connected 45 | components in the control flow graph of the contract. This follows 46 | the implementation described in the `Kosaraju algorithm wikipedia page`_. 47 | 48 | Args: 49 | cfg : Control Flow Graph(CFG) of the contract 50 | 51 | Returns: 52 | list of strongly connected components in CFG. each connected component 53 | is a list of basic blocks(nodes in CFG). 54 | 55 | .. _Kosaraju algorithm wikipedia page: 56 | https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm#The_algorithm 57 | """ 58 | 59 | visited = {bb: False for bb in cfg} 60 | assigned = {bb: False for bb in cfg} 61 | components = [] 62 | 63 | l = [] 64 | 65 | def visit(bb: "BasicBlock") -> None: 66 | if not visited[bb]: 67 | visited[bb] = True 68 | for next_bb in next_blocks_global(bb): 69 | visit(next_bb) 70 | l.append(bb) 71 | 72 | for bb in cfg: 73 | visit(bb) 74 | 75 | def assign(bb: "BasicBlock", root: List["BasicBlock"]) -> None: 76 | if not assigned[bb]: 77 | assigned[bb] = True 78 | root.append(bb) 79 | for prev_bb in prev_blocks_global(bb): 80 | assign(prev_bb, root) 81 | 82 | for bb in l: 83 | component: List["BasicBlock"] = [] 84 | assign(bb, component) 85 | if component: 86 | components.append(component) 87 | 88 | return components 89 | 90 | 91 | # from slither: slither/utils/code_complexity.py 92 | def compute_cyclomatic_complexity(cfg: List["BasicBlock"]) -> int: 93 | """Compute cyclomatic complexity of the contract. 94 | 95 | Args: 96 | cfg: Control Flow Graph(CFG) of the contract. 97 | 98 | Returns: 99 | Cyclomatic complexity of the contract. 100 | """ 101 | 102 | # from https://en.wikipedia.org/wiki/Cyclomatic_complexity 103 | # M = E - N + 2P 104 | # where M is the complexity 105 | # E number of edges 106 | # N number of nodes 107 | # P number of connected components 108 | 109 | E = _compute_number_edges(cfg) 110 | N = len(cfg) 111 | P = len(_compute_strongly_connected_components(cfg)) 112 | return E - N + 2 * P 113 | -------------------------------------------------------------------------------- /tests/transaction_context/test_addr_fields.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List 2 | import pytest 3 | 4 | from tealer.utils.command_line.common import init_tealer_from_single_contract 5 | from tests.utils import order_basic_blocks 6 | 7 | 8 | BRANCHING_ADDR = """ 9 | #pragma version 2 10 | txn Receiver 11 | addr 6ZIOGDXGSQSL4YINHLKCHYRV64FSN4LTUIQ6A4VWYK36FXFF42VI2UV7SM 12 | == 13 | bz wrongreceiver 14 | txn Fee 15 | int 10000 16 | < 17 | bz highfee 18 | txn CloseRemainderTo 19 | global ZeroAddress 20 | == 21 | bz closing_account 22 | txn AssetCloseTo 23 | global ZeroAddress 24 | == 25 | bz closing_asset 26 | global GroupSize 27 | int 1 28 | == 29 | bz unexpected_group_size 30 | int 1 31 | return 32 | wrongreceiver: 33 | highfee: 34 | closing_account: 35 | closing_asset: 36 | unexpected_group_size: 37 | err 38 | """ 39 | 40 | rekeyto_any = [True] * 6 + [False] * 5 41 | rekeyto_none = [False] * 6 + [True] * 5 42 | rekeyto: List[List[str]] = [[] for _ in range(11)] 43 | rekeyto_values = (rekeyto_any, rekeyto_none, rekeyto) 44 | 45 | closeto_any = [False] * 11 46 | closeto_none = [True] * 11 47 | closeto: List[List[str]] = [[] for _ in range(11)] 48 | closeto_values = (closeto_any, closeto_none, closeto) 49 | 50 | assetcloseto_any = [False] * 11 51 | assetcloseto_none = [True] * 11 52 | assetcloseto: List[List[str]] = [[] for _ in range(11)] 53 | assetcloseto_values = (assetcloseto_any, assetcloseto_none, assetcloseto) 54 | 55 | BRANCHING_ADDR_VALUES = [ 56 | rekeyto_values, 57 | closeto_values, 58 | assetcloseto_values, 59 | ] 60 | 61 | BRANCHING_ADDR_GTXN = """ 62 | #pragma version 2 63 | txn Receiver 64 | addr 6ZIOGDXGSQSL4YINHLKCHYRV64FSN4LTUIQ6A4VWYK36FXFF42VI2UV7SM 65 | == 66 | bz wrongreceiver 67 | txn Fee 68 | int 10000 69 | < 70 | bz highfee 71 | gtxn 0 CloseRemainderTo 72 | global ZeroAddress 73 | == 74 | bz closing_account 75 | gtxn 0 AssetCloseTo 76 | addr AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEVAL4QAJS7JHB4 77 | == 78 | bz closing_asset 79 | global GroupSize 80 | int 1 81 | == 82 | bz unexpected_group_size 83 | int 1 84 | return 85 | wrongreceiver: 86 | highfee: 87 | closing_account: 88 | closing_asset: 89 | unexpected_group_size: 90 | err 91 | """ 92 | 93 | ALL_TESTS_TXN = [ 94 | (BRANCHING_ADDR, BRANCHING_ADDR_VALUES, -1), 95 | (BRANCHING_ADDR_GTXN, BRANCHING_ADDR_VALUES, 0), 96 | ] 97 | 98 | 99 | @pytest.mark.parametrize("test", ALL_TESTS_TXN) # type: ignore 100 | def test_addr_fields( # pylint: disable=too-many-locals 101 | test: Tuple[str, List[Tuple[List[bool], List[bool], List[List[str]]]], int] 102 | ) -> None: 103 | code, values, idx = test 104 | tealer = init_tealer_from_single_contract(code.strip(), "test") 105 | function = tealer.contracts["test"].functions["test"] 106 | ex_rekeyto_any, ex_rekeyto_none, ex_rekeyto = values[0] 107 | ex_closeto_any, ex_closeto_none, ex_closeto = values[1] 108 | ex_assetcloseto_any, ex_assetcloseto_none, ex_assetcloseto = values[2] 109 | 110 | bbs = order_basic_blocks(function.blocks) 111 | for b in bbs: 112 | if idx == -1: 113 | ctx = function.transaction_context(b) 114 | else: 115 | ctx = function.transaction_context(b).gtxn_context(idx) 116 | 117 | assert ctx.rekeyto.any_addr == ex_rekeyto_any[b.idx] 118 | assert ctx.rekeyto.no_addr == ex_rekeyto_none[b.idx] 119 | assert set(ctx.rekeyto.possible_addr) == set(ex_rekeyto[b.idx]) 120 | 121 | assert ctx.closeto.any_addr == ex_closeto_any[b.idx] 122 | assert ctx.closeto.no_addr == ex_closeto_none[b.idx] 123 | assert set(ctx.closeto.possible_addr) == set(ex_closeto[b.idx]) 124 | 125 | assert ctx.assetcloseto.any_addr == ex_assetcloseto_any[b.idx] 126 | assert ctx.assetcloseto.no_addr == ex_assetcloseto_none[b.idx] 127 | assert set(ctx.assetcloseto.possible_addr) == set(ex_assetcloseto[b.idx]) 128 | -------------------------------------------------------------------------------- /tests/regex/atomic_swap.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # Example from https://pyteal.readthedocs.io/en/stable/examples.html 4 | 5 | from pyteal import * 6 | 7 | 8 | def approval_program(): 9 | on_creation = Seq( 10 | [ 11 | App.globalPut(Bytes("Creator"), Txn.sender()), 12 | Assert(Txn.application_args.length() == Int(4)), 13 | App.globalPut(Bytes("RegBegin"), Btoi(Txn.application_args[0])), 14 | App.globalPut(Bytes("RegEnd"), Btoi(Txn.application_args[1])), 15 | App.globalPut(Bytes("VoteBegin"), Btoi(Txn.application_args[2])), 16 | App.globalPut(Bytes("VoteEnd"), Btoi(Txn.application_args[3])), 17 | Return(Int(1)), 18 | ] 19 | ) 20 | 21 | is_creator = Txn.sender() == App.globalGet(Bytes("Creator")) 22 | 23 | get_vote_of_sender = App.localGetEx(Int(0), App.id(), Bytes("voted")) 24 | 25 | on_closeout = Seq( 26 | [ 27 | get_vote_of_sender, 28 | If( 29 | And( 30 | Global.round() <= App.globalGet(Bytes("VoteEnd")), 31 | get_vote_of_sender.hasValue(), 32 | ), 33 | App.globalPut( 34 | get_vote_of_sender.value(), 35 | App.globalGet(get_vote_of_sender.value()) - Int(1), 36 | ), 37 | ), 38 | Return(Int(1)), 39 | ] 40 | ) 41 | 42 | on_register = Return( 43 | And( 44 | Global.round() >= App.globalGet(Bytes("RegBegin")), 45 | Global.round() <= App.globalGet(Bytes("RegEnd")), 46 | ) 47 | ) 48 | 49 | choice = Txn.application_args[1] 50 | choice_tally = App.globalGet(choice) 51 | on_vote = Seq( 52 | [ 53 | Assert( 54 | And( 55 | Global.round() >= App.globalGet(Bytes("VoteBegin")), 56 | Global.round() <= App.globalGet(Bytes("VoteEnd")), 57 | ) 58 | ), 59 | get_vote_of_sender, 60 | If(get_vote_of_sender.hasValue(), Return(Int(0))), 61 | App.globalPut(choice, choice_tally + Int(1)), 62 | App.localPut(Int(0), Bytes("voted"), choice), 63 | Return(Int(1)), 64 | ] 65 | ) 66 | 67 | program = Cond( 68 | [Txn.application_id() == Int(0), on_creation], 69 | [Txn.on_completion() == OnComplete.DeleteApplication, Return(is_creator)], 70 | [Txn.on_completion() == OnComplete.UpdateApplication, Return(is_creator)], 71 | [Txn.on_completion() == OnComplete.CloseOut, on_closeout], 72 | [Txn.on_completion() == OnComplete.OptIn, on_register], 73 | [Txn.application_args[0] == Bytes("vote"), on_vote], 74 | ) 75 | 76 | return program 77 | 78 | 79 | def clear_state_program(): 80 | get_vote_of_sender = App.localGetEx(Int(0), App.id(), Bytes("voted")) 81 | program = Seq( 82 | [ 83 | get_vote_of_sender, 84 | If( 85 | And( 86 | Global.round() <= App.globalGet(Bytes("VoteEnd")), 87 | get_vote_of_sender.hasValue(), 88 | ), 89 | App.globalPut( 90 | get_vote_of_sender.value(), 91 | App.globalGet(get_vote_of_sender.value()) - Int(1), 92 | ), 93 | ), 94 | Return(Int(1)), 95 | ] 96 | ) 97 | 98 | return program 99 | 100 | 101 | if __name__ == "__main__": 102 | with open("vote_approval.teal", "w") as f: 103 | compiled = compileTeal(approval_program(), mode=Mode.Application, version=2) 104 | f.write(compiled) 105 | 106 | with open("vote_clear_state.teal", "w") as f: 107 | compiled = compileTeal(clear_state_program(), mode=Mode.Application, version=2) 108 | f.write(compiled) 109 | -------------------------------------------------------------------------------- /tealer/printers/transaction_context.py: -------------------------------------------------------------------------------- 1 | """Printer to output information about transaction fields in the CFG.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from typing import List, TYPE_CHECKING 6 | 7 | from tealer.printers.abstract_printer import AbstractPrinter 8 | from tealer.utils.output import ( 9 | CFGDotConfig, 10 | full_cfg_to_dot, 11 | all_subroutines_to_dot, 12 | ROOT_OUTPUT_DIRECTORY, 13 | ) 14 | 15 | if TYPE_CHECKING: 16 | from tealer.teal.basic_blocks import BasicBlock 17 | 18 | 19 | class PrinterTransactionContext(AbstractPrinter): # pylint: disable=too-few-public-methods 20 | """Printer to output information about transaction fields in the CFG. 21 | 22 | Transaction field analysis finds possible values for transaction fields. This printer 23 | adds this information on top of each of the basic blocks in the output CFG. 24 | """ 25 | 26 | NAME = "transaction-context" 27 | # TODO: Add TxnType, Sender, RekeyTo, CloseRemainderTo, AssetCloseTo field values 28 | HELP = "Output possible values of GroupIndices, GroupSize" 29 | WIKI_URL = "https://github.com/crytic/tealer/wiki/Printer-documentation#transaction-context" 30 | 31 | @staticmethod 32 | def _repr_num_list(values: List[int]) -> str: 33 | """Return short string representation of range of integers. 34 | 35 | intergers are space-separated and represented in ascending order. 36 | 37 | Continous sequence of more than 3 integers are represented using short-form(a..b). 38 | e.g 39 | 5 6 7 8 40 | => 5..8 41 | 42 | 1 2 3 5 6 7 8 9 11 13 14 15 16 43 | => 1 2 3 5..9 11 13..16 44 | 45 | Args: 46 | values: Sorted list of integers. smaller value is first. 47 | 48 | Returns: 49 | Returns short string representation of the :values: 50 | """ 51 | values = sorted(values) 52 | sequences: List[List[int]] = [[]] 53 | for i in values: 54 | if not sequences[-1]: 55 | sequences[-1].append(i) 56 | elif sequences[-1][-1] == (i - 1): 57 | sequences[-1].append(i) 58 | else: 59 | sequences.append([i]) 60 | str_seqs = [] 61 | for seq in sequences: 62 | if len(seq) >= 4: 63 | str_seqs.append(f"{seq[0]}..{seq[-1]}") 64 | else: 65 | str_seqs.append(" ".join(str(i) for i in seq)) 66 | return " ".join(str_seqs) 67 | 68 | def print(self) -> None: 69 | filename = Path("transaction-context.dot") 70 | 71 | # outputs multiple files: set the dir to {ROOT_DIRECTORY}/{CONTRACT_NAME}/{"print-"PRINTER_NAME} 72 | dest = ROOT_OUTPUT_DIRECTORY / Path(self.teal.contract_name) / Path(f"print-{self.NAME}") 73 | os.makedirs(dest, exist_ok=True) 74 | 75 | filename = dest / filename 76 | function = list(self.teal.functions.values())[0] 77 | 78 | def get_info(bb: "BasicBlock") -> List[str]: 79 | # NOTE: use the first function for now as `init_tealer_from_single_contract` uses entire contract as single function. 80 | group_indices_str = self._repr_num_list(function.transaction_context(bb).group_indices) 81 | group_sizes_str = self._repr_num_list(function.transaction_context(bb).group_sizes) 82 | return [f"GroupIndex: {group_indices_str}", f"GroupSize: {group_sizes_str}"] 83 | 84 | config = CFGDotConfig() 85 | config.bb_additional_comments = get_info 86 | # generate a Full CFG with all group-size, group-index comments 87 | full_cfg_to_dot(self.teal, config, filename) 88 | # Also generate shortened CFGs with group-size and group-index comments. 89 | all_subroutines_to_dot( 90 | self.teal, dest, config, "txn_ctx" 91 | ) # set prefix differentiate from other printers 92 | print(f"\nExported CFG with transaction context information to {filename}") 93 | -------------------------------------------------------------------------------- /tealer/teal/global_field.py: -------------------------------------------------------------------------------- 1 | """Defines classes to represent global fields. 2 | 3 | Global fields are fields that are common to all the transactions 4 | in the group. Teal supports access to this fields using ``global`` 5 | instruction. 6 | 7 | Each global field is represented by a class in tealer. All the 8 | classes representing the fields must inherit from GlobalField class. 9 | 10 | """ 11 | 12 | # pylint: disable=too-few-public-methods 13 | class GlobalField: 14 | """Base class representing a global field""" 15 | 16 | def __init__(self) -> None: 17 | self._version: int = 1 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 GroupSize(GlobalField): 33 | """Number of transactions in this atomic transaction group.""" 34 | 35 | 36 | class MinTxnFee(GlobalField): 37 | """Minimum transaction fee in micro algos.""" 38 | 39 | 40 | class ZeroAddress(GlobalField): 41 | """32 byte address of all zero bytes""" 42 | 43 | 44 | class MinBalance(GlobalField): 45 | """Minimum balance in micro algos.""" 46 | 47 | 48 | class MaxTxnLife(GlobalField): 49 | """Maximum Transaction Life in number of rounds.""" 50 | 51 | 52 | class LogicSigVersion(GlobalField): 53 | """Maximum supported TEAL version.""" 54 | 55 | def __init__(self) -> None: 56 | super().__init__() 57 | self._version = 2 58 | 59 | 60 | class Round(GlobalField): 61 | """Current round number.""" 62 | 63 | def __init__(self) -> None: 64 | super().__init__() 65 | self._version = 2 66 | 67 | 68 | class LatestTimestamp(GlobalField): 69 | """Last confirmed block unix timestamp. Fails if negative.""" 70 | 71 | def __init__(self) -> None: 72 | super().__init__() 73 | self._version = 2 74 | 75 | 76 | class CurrentApplicationID(GlobalField): 77 | """ID of the current application executing.""" 78 | 79 | def __init__(self) -> None: 80 | super().__init__() 81 | self._version = 2 82 | 83 | 84 | class CreatorAddress(GlobalField): 85 | """Address of the creator of the current application.""" 86 | 87 | def __init__(self) -> None: 88 | super().__init__() 89 | self._version = 3 90 | 91 | 92 | class CurrentApplicationAddress(GlobalField): 93 | """Address that the current application controls.""" 94 | 95 | def __init__(self) -> None: 96 | super().__init__() 97 | self._version = 5 98 | 99 | 100 | class GroupID(GlobalField): 101 | """ID of the transaction group. 102 | 103 | ID will be 32 zero bytes if the transaction is not part 104 | of the group. 105 | """ 106 | 107 | def __init__(self) -> None: 108 | super().__init__() 109 | self._version = 5 110 | 111 | 112 | class OpcodeBudget(GlobalField): 113 | """The remaining cost that can be spent by opcodes in the program""" 114 | 115 | def __init__(self) -> None: 116 | super().__init__() 117 | self._version = 6 118 | 119 | 120 | class CallerApplicationID(GlobalField): 121 | """The application ID of the application that called this application. 122 | 123 | ID wil be 0 if the current application is at the top-level. Application mode only. 124 | """ 125 | 126 | def __init__(self) -> None: 127 | super().__init__() 128 | self._version = 6 129 | 130 | 131 | class CallerApplicationAddress(GlobalField): 132 | """The application address of the application that called this application. 133 | 134 | ZeroAddress if this application is at the top-level. Application mode only. 135 | """ 136 | 137 | def __init__(self) -> None: 138 | super().__init__() 139 | self._version = 6 140 | -------------------------------------------------------------------------------- /tealer/printers/call_graph.py: -------------------------------------------------------------------------------- 1 | """Printer for exporting call-graph of the contract.""" 2 | 3 | import html 4 | import os 5 | from pathlib import Path 6 | from typing import Dict, Set 7 | 8 | from tealer.printers.abstract_printer import AbstractPrinter 9 | from tealer.utils.output import ROOT_OUTPUT_DIRECTORY 10 | 11 | 12 | class PrinterCallGraph(AbstractPrinter): # pylint: disable=too-few-public-methods 13 | """Printer to export call-graph of the contract. 14 | 15 | This printer is supposed to work for contracts written in teal version 4 or greater. 16 | Call graph dot file will be saved as `call-graph.dot` in destination folder if given 17 | or else in the current directory. The entry point of the contract is 18 | treated as another function("__entry__") while constructing the call graph. 19 | 20 | """ 21 | 22 | NAME = "call-graph" 23 | HELP = "Export the call graph of contract to a dot file" 24 | WIKI_URL = "https://github.com/crytic/tealer/wiki/Printer-documentation#call-graph" 25 | 26 | def _construct_call_graph(self) -> Dict[str, Set[str]]: 27 | """construct call graph for the contract. 28 | 29 | entry point is treated as a separate subroutine/function `__main__`. 30 | For each subroutine, label(subroutine name) represents the node and each callsub 31 | instruction in the subroutine corresponds to directed edge to the target subroutine. 32 | 33 | Returns: 34 | Dict[str, Set[str]]: dictionary representing the graph. The graph has edges from each 35 | subroutine in d[key] to key. The source node of the edge are values and destination node is the key. 36 | if d["S1"] = ["S2", "S3"] then graph has edges "S3 -> S1", "S2 -> S1". 37 | """ 38 | 39 | graph: Dict[str, Set[str]] = {} 40 | for _, subroutine in self.teal.subroutines.items(): 41 | graph[subroutine.name] = set( 42 | map(lambda bi: bi.subroutine.name, subroutine.caller_blocks) 43 | ) 44 | 45 | # no need to handle __main__ subroutine cause it is program entry point and there won't be caller blocks. 46 | return graph 47 | 48 | def print(self) -> None: 49 | """Export call graph of the contract in dot format. 50 | 51 | Each node in the call graph corresponds to a subroutine and edge representing call from 52 | source subroutine to called subroutine. Entry point of the contract is treated as another 53 | function and represented with label `__entry__`. Call graph will be saved as `call-graph.dot` 54 | in the destination directory if given or else in the current directory. 55 | Subroutines are supported from teal version 4. so, for contracts written in lesser version, this 56 | function only prints the error message stating the same.""" 57 | 58 | if self.teal.version < 4: 59 | print("subroutines are not supported in teal version 3 or less") 60 | return 61 | 62 | dot_output = "digraph g{\n" 63 | graph = self._construct_call_graph() 64 | 65 | # construct dot representation of the graph 66 | graph_edges = "" 67 | for destination_sub, source_subs in graph.items(): 68 | destination_sub = html.escape(destination_sub, quote=True) 69 | dot_output += f"{destination_sub}[label={destination_sub}];\n" 70 | for source_sub in source_subs: 71 | source_sub = html.escape(source_sub, quote=True) 72 | graph_edges += f"{source_sub} -> {destination_sub};\n" 73 | dot_output += graph_edges 74 | dot_output += "}\n" 75 | 76 | # outputs a single file: set the dir to {ROOT_DIRECTORY}/{CONTRACT_NAME} 77 | dest = ROOT_OUTPUT_DIRECTORY / Path(self.teal.contract_name) 78 | os.makedirs(dest, exist_ok=True) 79 | 80 | filename = Path("call-graph.dot") 81 | filename = dest / filename 82 | 83 | print(f"\nExported call graph to {filename}") 84 | with open(filename, "w", encoding="utf-8") as f: 85 | f.write(dot_output) 86 | -------------------------------------------------------------------------------- /tealer/detectors/is_deletable.py: -------------------------------------------------------------------------------- 1 | """Detector for finding execution paths missing DeleteApplication check.""" 2 | 3 | from typing import List, TYPE_CHECKING, Tuple 4 | 5 | from tealer.detectors.abstract_detector import ( 6 | AbstractDetector, 7 | DetectorClassification, 8 | DetectorType, 9 | ) 10 | 11 | from tealer.detectors.utils import ( 12 | detect_missing_tx_field_validations_group, 13 | detect_missing_tx_field_validations_group_complete, 14 | ) 15 | from tealer.utils.teal_enums import TealerTransactionType 16 | from tealer.utils.output import ExecutionPaths 17 | 18 | if TYPE_CHECKING: 19 | from tealer.utils.output import ListOutput 20 | from tealer.teal.basic_blocks import BasicBlock 21 | from tealer.teal.context.block_transaction_context import BlockTransactionContext 22 | from tealer.teal.teal import Teal 23 | 24 | 25 | class IsDeletable(AbstractDetector): # pylint: disable=too-few-public-methods 26 | """Detector to find execution paths missing DeleteApplication check. 27 | 28 | Stateful smart contracts(application) can be deleted in algorand. If the 29 | application transaction of type DeleteApplication is approved by the application, 30 | then the application will be deleted. Contracts can check the application 31 | transaction type using OnCompletion field. 32 | 33 | This detector tries to find execution paths that approve the application 34 | transaction("return 1") and doesn't check the OnCompletion field against 35 | DeleteApplication value. Execution paths that only execute if the application 36 | transaction is not DeleteApplication are excluded. 37 | """ 38 | 39 | NAME = "is-deletable" 40 | DESCRIPTION = "Deletable Applications" 41 | TYPE = DetectorType.STATEFULL 42 | 43 | IMPACT = DetectorClassification.HIGH 44 | CONFIDENCE = DetectorClassification.HIGH 45 | 46 | WIKI_URL = "https://github.com/crytic/tealer/wiki/Detector-Documentation#deletable-application" 47 | WIKI_TITLE = "Deletable Application" 48 | WIKI_DESCRIPTION = ( 49 | "Application can be deleted by sending an `DeleteApplication` type application call. " 50 | ) 51 | WIKI_EXPLOIT_SCENARIO = """ 52 | ```py 53 | @router.method(delete_application=CallConfig.CALL) 54 | def delete_application() -> Expr: 55 | return Assert(Txn.sender() == Global.creator_address()) 56 | ``` 57 | 58 | Eve steals application creator's private key and deletes the application. Application's assets are permanently lost. 59 | """ 60 | 61 | WIKI_RECOMMENDATION = """ 62 | Do not approve `DeleteApplication` type application calls. 63 | """ 64 | 65 | def detect(self) -> "ListOutput": 66 | """Detect execution paths with missing DeleteApplication check. 67 | 68 | Returns: 69 | ExecutionPaths instance containing the list of vulnerable execution 70 | paths along with name, check, impact, confidence and other detector 71 | information. 72 | """ 73 | 74 | def checks_field(block_ctx: "BlockTransactionContext") -> bool: 75 | # return False if Txn Type can be DeleteApplication. 76 | # return True if Txn Type cannot be DeleteApplication. 77 | return not TealerTransactionType.ApplDeleteApplication in block_ctx.transaction_types 78 | 79 | # there should be a better to decide which function to call ?? 80 | if self.tealer.output_group: 81 | # mypy complains if the value is returned directly. Uesd the second suggestion mentioned here: 82 | # https://mypy.readthedocs.io/en/stable/common_issues.html#variance 83 | return list( 84 | detect_missing_tx_field_validations_group_complete(self.tealer, self, checks_field) 85 | ) 86 | 87 | output: List[ 88 | Tuple["Teal", List[List["BasicBlock"]]] 89 | ] = detect_missing_tx_field_validations_group(self.tealer, checks_field) 90 | detector_output: "ListOutput" = [] 91 | for contract, vulnerable_paths in output: 92 | detector_output.append(ExecutionPaths(contract, self, vulnerable_paths)) 93 | 94 | return detector_output 95 | -------------------------------------------------------------------------------- /tests/test_subroutine_identification.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Dict 2 | import pytest 3 | 4 | from tealer.teal.subroutine import Subroutine 5 | from tealer.teal.instructions import instructions 6 | from tealer.teal.parse_teal import parse_teal 7 | 8 | from tests.utils import cmp_cfg, construct_cfg 9 | 10 | 11 | MULTIPLE_RETSUB = """ 12 | #pragma version 5 13 | b main 14 | push_zero: 15 | int 0 16 | retsub 17 | is_even: 18 | int 2 19 | % 20 | bz return_1 21 | callsub push_zero 22 | retsub 23 | return_1: 24 | int 1 25 | retsub 26 | main: 27 | int 4 28 | callsub is_even 29 | return 30 | """ 31 | 32 | ins_list = [ 33 | instructions.Pragma(5), 34 | instructions.B("main"), 35 | instructions.Label("push_zero"), 36 | instructions.Int(0), 37 | instructions.Retsub(), 38 | instructions.Label("is_even"), 39 | instructions.Int(2), 40 | instructions.Modulo(), 41 | instructions.BZ("return_1"), 42 | instructions.Callsub("push_zero"), 43 | instructions.Retsub(), 44 | instructions.Label("return_1"), 45 | instructions.Int(1), 46 | instructions.Retsub(), 47 | instructions.Label("main"), 48 | instructions.Int(4), 49 | instructions.Callsub("is_even"), 50 | instructions.Return(), 51 | ] 52 | 53 | ins_partitions = [(0, 2), (2, 5), (5, 9), (9, 10), (10, 11), (11, 14), (14, 17), (17, 18)] 54 | bbs_links = [(0, 6), (2, 3), (2, 5), (3, 4), (6, 7)] 55 | 56 | bbs = construct_cfg(ins_list, ins_partitions, bbs_links) 57 | MULTIPLE_RETSUB_MAIN = Subroutine("", bbs[0], [bbs[0], bbs[6], bbs[7]]) 58 | MULTIPLE_RETSUB_SUBROUTINES = { 59 | "push_zero": Subroutine("push_zero", bbs[1], [bbs[1]]), 60 | "is_even": Subroutine("is_even", bbs[2], [bbs[2], bbs[3], bbs[4], bbs[5]]), 61 | } 62 | 63 | SUBROUTINE_BACK_JUMP = """ 64 | #pragma version 5 65 | b main 66 | getmod: 67 | % 68 | retsub 69 | is_odd: 70 | int 2 71 | b getmod 72 | main: 73 | int 5 74 | callsub is_odd 75 | return 76 | """ 77 | 78 | ins_list = [ 79 | instructions.Pragma(5), 80 | instructions.B("main"), 81 | instructions.Label("getmod"), 82 | instructions.Modulo(), 83 | instructions.Retsub(), 84 | instructions.Label("is_odd"), 85 | instructions.Int(2), 86 | instructions.B("getmod"), 87 | instructions.Label("main"), 88 | instructions.Int(5), 89 | instructions.Callsub("is_odd"), 90 | instructions.Return(), 91 | ] 92 | 93 | ins_partitions = [(0, 2), (2, 5), (5, 8), (8, 11), (11, 12)] 94 | bbs_links = [(0, 3), (3, 4), (2, 1)] 95 | 96 | bbs = construct_cfg(ins_list, ins_partitions, bbs_links) 97 | SUBROUTINE_BACK_JUMP_MAIN = Subroutine("", bbs[0], [bbs[0], bbs[3], bbs[4]]) 98 | SUBROUTINE_BACK_JUMP_SUBROUTINES = {"is_odd": Subroutine("is_odd", bbs[2], [bbs[1], bbs[2]])} 99 | 100 | ALL_TESTS = [ 101 | (MULTIPLE_RETSUB, MULTIPLE_RETSUB_MAIN, MULTIPLE_RETSUB_SUBROUTINES), 102 | (SUBROUTINE_BACK_JUMP, SUBROUTINE_BACK_JUMP_MAIN, SUBROUTINE_BACK_JUMP_SUBROUTINES), 103 | ] 104 | 105 | 106 | @pytest.mark.parametrize("test", ALL_TESTS) # type: ignore 107 | def test_subroutine_identification(test: Tuple[str, Subroutine, Dict[str, Subroutine]]) -> None: 108 | code, expected_main, expected_subroutines = test 109 | teal = parse_teal(code.strip()) 110 | test_main = teal.main 111 | 112 | assert cmp_cfg([test_main.entry], [expected_main.entry]) 113 | assert cmp_cfg(test_main.blocks, expected_main.blocks) 114 | 115 | subroutines = teal.subroutines 116 | assert len(subroutines.keys()) == len(expected_subroutines.keys()) 117 | assert sorted(subroutines.keys()) == sorted(expected_subroutines.keys()) 118 | for ex_name in expected_subroutines: 119 | subroutine = subroutines[ex_name] 120 | expected_subroutine = expected_subroutines[ex_name] 121 | assert subroutine.name == expected_subroutine.name 122 | assert cmp_cfg([subroutine.entry], [expected_subroutine.entry]) 123 | assert cmp_cfg(subroutine.blocks, expected_subroutine.blocks) 124 | assert len(subroutines) == len(expected_subroutines) 125 | -------------------------------------------------------------------------------- /tealer/detectors/anyone_can_delete.py: -------------------------------------------------------------------------------- 1 | """Detect paths missing validations on sender field AND allows to delete the application.""" 2 | 3 | from typing import List, TYPE_CHECKING, Tuple 4 | 5 | from tealer.detectors.abstract_detector import ( 6 | AbstractDetector, 7 | DetectorClassification, 8 | DetectorType, 9 | ) 10 | from tealer.detectors.utils import ( 11 | detect_missing_tx_field_validations_group, 12 | detect_missing_tx_field_validations_group_complete, 13 | ) 14 | from tealer.utils.teal_enums import TealerTransactionType 15 | from tealer.utils.output import ExecutionPaths 16 | 17 | 18 | if TYPE_CHECKING: 19 | from tealer.teal.basic_blocks import BasicBlock 20 | from tealer.utils.output import ListOutput 21 | from tealer.teal.context.block_transaction_context import BlockTransactionContext 22 | from tealer.teal.teal import Teal 23 | 24 | 25 | class AnyoneCanDelete(AbstractDetector): # pylint: disable=too-few-public-methods 26 | """Detector to find execution paths missing validations on sender field AND allows to delete the application. 27 | 28 | Stateful smart contracts(application) can be deleted in Algorand. If the 29 | application transaction of type DeleteApplication is approved by the application, 30 | then the application will be deleted. 31 | 32 | This detector tries to find execution paths for which 33 | - OnCompletion can be DeleteApplication And 34 | - Transaction sender can be any address. 35 | """ 36 | 37 | NAME = "unprotected-deletable" 38 | DESCRIPTION = "Unprotected Deletable Applications" 39 | TYPE = DetectorType.STATEFULL 40 | 41 | IMPACT = DetectorClassification.HIGH 42 | CONFIDENCE = DetectorClassification.HIGH 43 | 44 | WIKI_URL = "https://github.com/crytic/tealer/wiki/Detector-Documentation#unprotected-deletable-application" 45 | WIKI_TITLE = "Unprotected Deletable Application" 46 | WIKI_DESCRIPTION = ( 47 | "Application can be deleted by anyone. " 48 | "More at [building-secure-contracts/not-so-smart-contracts/algorand/access_controls]" 49 | "(https://github.com/crytic/building-secure-contracts/tree/master/not-so-smart-contracts/algorand/access_controls)." 50 | ) 51 | WIKI_EXPLOIT_SCENARIO = """\ 52 | ```py 53 | @router.method(delete_application=CallConfig.CALL) 54 | def delete_application() -> Expr: 55 | return Approve() 56 | ``` 57 | 58 | Eve calls `delete_application` method and deletes the application making its assets permanently inaccesible. 59 | """ 60 | 61 | WIKI_RECOMMENDATION = """ 62 | - Avoid deletable applications. 63 | - Add access controls to the vulnerable method. 64 | """ 65 | 66 | def detect(self) -> "ListOutput": 67 | """Detect execution paths missing validations on sender field AND can delete the application . 68 | 69 | Returns: 70 | ExecutionPaths instance containing the list of vulnerable execution 71 | paths along with name, check, impact, confidence and other detector 72 | information. 73 | """ 74 | 75 | def checks_field(block_ctx: "BlockTransactionContext") -> bool: 76 | # return False if Txn Type can be DeleteApplication AND sender can be any address. 77 | # return True otherwise 78 | return not ( 79 | TealerTransactionType.ApplDeleteApplication in block_ctx.transaction_types 80 | and block_ctx.sender.any_addr 81 | ) 82 | 83 | # there should be a better to decide which function to call ?? 84 | if self.tealer.output_group: 85 | # mypy complains if the value is returned directly. Uesd the second suggestion mentioned here: 86 | # https://mypy.readthedocs.io/en/stable/common_issues.html#variance 87 | return list( 88 | detect_missing_tx_field_validations_group_complete(self.tealer, self, checks_field) 89 | ) 90 | 91 | output: List[ 92 | Tuple["Teal", List[List["BasicBlock"]]] 93 | ] = detect_missing_tx_field_validations_group(self.tealer, checks_field) 94 | detector_output: "ListOutput" = [] 95 | for contract, vulnerable_paths in output: 96 | detector_output.append(ExecutionPaths(contract, self, vulnerable_paths)) 97 | 98 | return detector_output 99 | -------------------------------------------------------------------------------- /tealer/detectors/anyone_can_update.py: -------------------------------------------------------------------------------- 1 | """Detect paths missing validations on sender field AND allows to update the application.""" 2 | 3 | from typing import List, TYPE_CHECKING, Tuple 4 | 5 | from tealer.detectors.abstract_detector import ( 6 | AbstractDetector, 7 | DetectorClassification, 8 | DetectorType, 9 | ) 10 | from tealer.detectors.utils import ( 11 | detect_missing_tx_field_validations_group, 12 | detect_missing_tx_field_validations_group_complete, 13 | ) 14 | from tealer.utils.teal_enums import TealerTransactionType 15 | from tealer.utils.output import ExecutionPaths 16 | 17 | if TYPE_CHECKING: 18 | from tealer.teal.basic_blocks import BasicBlock 19 | from tealer.utils.output import ListOutput 20 | from tealer.teal.context.block_transaction_context import BlockTransactionContext 21 | from tealer.teal.teal import Teal 22 | 23 | 24 | class AnyoneCanUpdate(AbstractDetector): # pylint: disable=too-few-public-methods 25 | """Detector to find execution paths missing validations on sender field AND allows to update the application. 26 | 27 | Stateful smart contracts(application) can be updated in algorand. If the 28 | application transaction of type UpdateApplication is approved by the application, 29 | then the application's code will be updated with the new code. 30 | 31 | This detector tries to find execution paths for which 32 | - OnCompletion can be UpdateApplication And 33 | - Transaction sender can be any address. 34 | """ 35 | 36 | NAME = "unprotected-updatable" 37 | DESCRIPTION = "Unprotected Upgradable Applications" 38 | TYPE = DetectorType.STATEFULL 39 | 40 | IMPACT = DetectorClassification.HIGH 41 | CONFIDENCE = DetectorClassification.HIGH 42 | 43 | WIKI_URL = "https://github.com/crytic/tealer/wiki/Detector-Documentation#unprotected-updatable-application" 44 | WIKI_TITLE = "Unprotected Upgradable Application" 45 | WIKI_DESCRIPTION = ( 46 | "Application can be updated by anyone. " 47 | "More at [building-secure-contracts/not-so-smart-contracts/algorand/access_controls]" 48 | "(https://github.com/crytic/building-secure-contracts/tree/master/not-so-smart-contracts/algorand/access_controls)." 49 | ) 50 | WIKI_EXPLOIT_SCENARIO = """ 51 | ```py 52 | @router.method(update_application=CallConfig.CALL) 53 | def update_application() -> Expr: 54 | return Approve() 55 | ``` 56 | 57 | Eve updates the application by calling `update_application` method and steals all of its assets. 58 | """ 59 | 60 | WIKI_RECOMMENDATION = """ 61 | - Avoid upgradable applications. 62 | - Add access controls to the vulnerable method. 63 | """ 64 | 65 | def detect(self) -> "ListOutput": 66 | """Detect paths missing validations on sender field AND allows to update the application. 67 | 68 | Returns: 69 | ExecutionPaths instance containing the list of vulnerable execution 70 | paths along with name, check, impact, confidence and other detector 71 | information. 72 | """ 73 | 74 | def checks_field(block_ctx: "BlockTransactionContext") -> bool: 75 | # return False if Txn Type can be UpdateApplication AND sender can be any address. 76 | # return True otherwise. 77 | return not ( 78 | TealerTransactionType.ApplUpdateApplication in block_ctx.transaction_types 79 | and block_ctx.sender.any_addr 80 | ) 81 | 82 | # there should be a better to decide which function to call ?? 83 | if self.tealer.output_group: 84 | # mypy complains if the value is returned directly. Uesd the second suggestion mentioned here: 85 | # https://mypy.readthedocs.io/en/stable/common_issues.html#variance 86 | return list( 87 | detect_missing_tx_field_validations_group_complete(self.tealer, self, checks_field) 88 | ) 89 | 90 | output: List[ 91 | Tuple["Teal", List[List["BasicBlock"]]] 92 | ] = detect_missing_tx_field_validations_group(self.tealer, checks_field) 93 | detector_output: "ListOutput" = [] 94 | for contract, vulnerable_paths in output: 95 | detector_output.append(ExecutionPaths(contract, self, vulnerable_paths)) 96 | 97 | return detector_output 98 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | from tealer.teal.basic_blocks import BasicBlock 4 | from tealer.teal.instructions.instructions import Instruction 5 | 6 | 7 | def cmp_instructions(ins1: Instruction, ins2: Instruction) -> bool: 8 | return str(ins1) == str(ins2) 9 | 10 | 11 | def order_basic_blocks(bbs: List[BasicBlock]) -> List[BasicBlock]: 12 | key = lambda bb: bb.entry_instr.line 13 | return sorted(bbs, key=key) 14 | 15 | 16 | def cmp_basic_blocks(b1: BasicBlock, b2: BasicBlock) -> bool: 17 | if len(b1.instructions) != len(b2.instructions): 18 | print( 19 | f"number of instructions are different: {len(b1.instructions)}, {len(b2.instructions)}" 20 | ) 21 | return False 22 | 23 | for ins1, ins2 in zip(b1.instructions, b2.instructions): 24 | if not cmp_instructions(ins1, ins2): 25 | print(f"instructions comparison faile: {ins1}, {ins2}") 26 | return False 27 | 28 | return True 29 | 30 | 31 | def cmp_cfg(bbs1: List[BasicBlock], bbs2: List[BasicBlock]) -> bool: 32 | if len(bbs1) != len(bbs2): 33 | print(f"number of blocks are different: {len(bbs1)}, {len(bbs2)}") 34 | return False 35 | bbs1, bbs2 = map(order_basic_blocks, (bbs1, bbs2)) 36 | 37 | for bb1, bb2 in zip(bbs1, bbs2): 38 | print(f"comparing bb1: {bb1.idx}, bb2: {bb2.idx}\nbb1:\n{bb1}\nbb2:\n{bb2}\n") 39 | if not cmp_basic_blocks(bb1, bb2): 40 | return False 41 | 42 | if len(bb1.prev) != len(bb2.prev): 43 | print(f"number of prev blocks are different: {len(bb1.prev)}, {len(bb2.prev)}") 44 | print(bb1.prev) 45 | print(bb2.prev) 46 | return False 47 | 48 | if len(bb1.next) != len(bb2.next): 49 | print(f"number of next blocks are different: {len(bb1.next)}, {len(bb2.next)}") 50 | print(bb1.next) 51 | print(bb2.next) 52 | return False 53 | 54 | bbs_prev_1, bbs_prev_2 = map(order_basic_blocks, (bb1.prev, bb2.prev)) 55 | for bbp1, bbp2 in zip(bbs_prev_1, bbs_prev_2): 56 | if bbp1.entry_instr.line != bbp2.entry_instr.line: 57 | print("entry lines of previous blocks is different") 58 | print(bbp1.entry_instr.line, bbp2.entry_instr.line) 59 | return False 60 | 61 | bbs_next_1, bbs_next_2 = map(order_basic_blocks, (bb1.next, bb2.next)) 62 | for bbn1, bbn2 in zip(bbs_next_1, bbs_next_2): 63 | if bbn1.entry_instr.line != bbn2.entry_instr.line: 64 | print("entry lines of next blocks is different") 65 | print(bbn1.entry_instr.line, bbn2.entry_instr.line) 66 | return False 67 | 68 | return True 69 | 70 | 71 | def construct_cfg( 72 | ins_list: List[Instruction], 73 | ins_partitions: List[Tuple[int, int]], 74 | bbs_links: List[Tuple[int, int]], 75 | ) -> List[BasicBlock]: 76 | """cfg construction helper function. 77 | 78 | construct cfg using the list of instructions, basic block partitions 79 | and their relation. 80 | 81 | Args: 82 | ins_list: list of instructions of the program. 83 | ins_partitions (List[Tuple[int]]): list of tuple indicating starting and 84 | ending index into ins_list where ins_list[starting: ending] belong to a 85 | single basic_block. 86 | bbs_links (List[Tuple[int]]): links two basic blocks (b1, b2). indexes of basic block 87 | corresponds to index in ins_partition. order of indexes is important as this adds 88 | a directed edge from b1 to b2. 89 | 90 | Returns: 91 | List[BasicBlock]: returns a list of basic blocks representing the CFG. 92 | """ 93 | for idx, ins in enumerate(ins_list, start=1): 94 | ins.line = idx 95 | 96 | bbs = [] 97 | for (idx, (start, end)) in enumerate(ins_partitions): 98 | bb = BasicBlock() 99 | for ins in ins_list[start:end]: 100 | bb.add_instruction(ins) 101 | bb.idx = idx 102 | bbs.append(bb) 103 | 104 | for idx1, idx2 in bbs_links: 105 | bbs[idx1].add_next(bbs[idx2]) 106 | bbs[idx2].add_prev(bbs[idx1]) 107 | 108 | return bbs 109 | -------------------------------------------------------------------------------- /tests/pyteal_parsing/control_flow_constructs.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # pylint: disable=undefined-variable 3 | # type: ignore[name-defined] 4 | 5 | """ 6 | Template for pyteal test: 7 | 8 | File containing the pyteal code 9 | ``` 10 | # pylint: disable=undefined-variable 11 | # type: ignore[name-defined] 12 | from pyteal import * # pylint: disable=wildcard-import, unused-wildcard-import 13 | 14 | def approval_program() -> Expr: 15 | return Return(Int(1)) 16 | 17 | def clear_program() -> Expr: 18 | return Return(Int(1)) 19 | 20 | approval_program_str = compileTeal( 21 | approval_program(), 22 | mode=Mode.Application, 23 | version=7, 24 | optimize=OptimizeOptions(scratch_slots=False), 25 | ) 26 | 27 | _clear_program = compileTeal( 28 | clear_program(), mode=Mode.Application, version=7, optimize=OptimizeOptions(scratch_slots=False) 29 | ) 30 | ``` 31 | 32 | Test file that imports the pyteal code 33 | ``` 34 | # type: ignore[unreachable] 35 | import sys 36 | import pytest 37 | 38 | from tealer.teal.instructions import instructions 39 | from tealer.teal.parse_teal import parse_teal 40 | 41 | if not sys.version_info >= (3, 10): 42 | pytest.skip(reason="PyTeal based tests require python >= 3.10", allow_module_level=True) 43 | 44 | # pylint: disable=wrong-import-position 45 | # Place import statements after the version check 46 | from tests.pyteal_parsing.normal_application import normal_application_approval_program 47 | ``` 48 | """ 49 | 50 | from pyteal import * # pylint: disable=wildcard-import, unused-wildcard-import 51 | 52 | 53 | @Subroutine(TealType.none) # type: ignore[misc] 54 | def foo_bar(n: Expr) -> Expr: 55 | return While(n < Int(10)).Do( 56 | If(n < Int(5)) 57 | .Then( 58 | Log(Bytes("Foo")), 59 | ) 60 | .Else( 61 | Log(Bytes("Bar")), 62 | ) 63 | ) 64 | 65 | 66 | @Subroutine(TealType.none) # type: ignore[misc] 67 | def break_continue(n: ScratchVar) -> Expr: 68 | i = ScratchVar() 69 | return While(Int(1)).Do( 70 | If(n.load() % Int(11) == Int(0)).Then(Break()), 71 | For( 72 | i.store(n.load()), 73 | i.load() < Int(25), 74 | i.store(i.load() + Int(3)), 75 | ).Do(If(n.load() % Int(4) == Int(0)).Then(Continue()), Log(Itob(i.load()))), 76 | n.store(n.load() + i.load() - Int(1)), 77 | ) 78 | 79 | 80 | @Subroutine(TealType.uint64) # type: ignore[misc] 81 | def factorial(n: Expr) -> Expr: 82 | # Note: Recursion is not supported if the subroutine takes ScratchVar type argument 83 | return If( 84 | n == Int(0), # condition 85 | Return(Int(1)), # then expression 86 | Return(n * factorial(n - Int(1))), # else expression 87 | ) 88 | 89 | 90 | def call_all_subroutines() -> Expr: 91 | n = ScratchVar() 92 | return Pop( 93 | Seq( 94 | [ 95 | Pop( 96 | factorial(Int(10)) 97 | ), # All Expressions in the Seq must return TealType.none. Last expression is a exception. 98 | foo_bar(Int(1)), 99 | n.store(Int(0)), 100 | break_continue(n), # ScratchVar types are taken as references. 101 | factorial(Int(5)), # Last expression can return a value 102 | ] 103 | ) # Seq returns value of the last expression. Pop it to return none. 104 | ) 105 | 106 | 107 | def some_subroutines(c: Expr) -> Expr: 108 | n = ScratchVar() 109 | return Cond( 110 | [c == Int(0), Pop(factorial(Int(7)))], # All branches should return same type 111 | [ 112 | c == Int(1), 113 | Seq( 114 | n.store(Int(1)), 115 | foo_bar(n.load()), 116 | ), 117 | ], 118 | [c == Int(2), Seq([n.store(Int(0)), break_continue(n)])], 119 | ) 120 | 121 | 122 | def approval_program() -> Expr: 123 | return Seq( 124 | [ 125 | call_all_subroutines(), 126 | some_subroutines(Int(1)), 127 | Approve(), 128 | ] 129 | ) 130 | 131 | 132 | control_flow_ap = compileTeal( 133 | approval_program(), 134 | mode=Mode.Application, 135 | version=7, 136 | optimize=OptimizeOptions(scratch_slots=True), 137 | ) 138 | 139 | if __name__ == "__main__": 140 | print(control_flow_ap) 141 | -------------------------------------------------------------------------------- /tealer/detectors/is_updatable.py: -------------------------------------------------------------------------------- 1 | """Detector for finding execution paths missing UpdateApplication.""" 2 | 3 | from typing import List, TYPE_CHECKING, Tuple 4 | 5 | from tealer.detectors.abstract_detector import ( 6 | AbstractDetector, 7 | DetectorClassification, 8 | DetectorType, 9 | ) 10 | from tealer.detectors.utils import ( 11 | detect_missing_tx_field_validations_group, 12 | detect_missing_tx_field_validations_group_complete, 13 | ) 14 | from tealer.utils.teal_enums import TealerTransactionType 15 | from tealer.utils.output import ExecutionPaths 16 | 17 | if TYPE_CHECKING: 18 | from tealer.teal.basic_blocks import BasicBlock 19 | from tealer.utils.output import ListOutput 20 | from tealer.teal.context.block_transaction_context import BlockTransactionContext 21 | from tealer.teal.teal import Teal 22 | 23 | 24 | class IsUpdatable(AbstractDetector): # pylint: disable=too-few-public-methods 25 | """Detector to find execution paths missing UpdateApplication check. 26 | 27 | Stateful smart contracts(application) can be updated with the new code 28 | in algorand. If the application transaction of type UpdateApplication is 29 | approved by the contract then the application's approval, clear programs 30 | will be replaced by the ones sent along with the transaction. Contracts 31 | can check the application transaction type using OnCompletion field. 32 | 33 | This detector tries to find execution paths that approve the application 34 | transactions("return 1") and doesn't check the OnCompletion field against 35 | UpdateApplication value. Execution paths that only execute if the application 36 | transaction is not UpdateApplication are excluded. 37 | """ 38 | 39 | NAME = "is-updatable" 40 | DESCRIPTION = "Upgradable Applications" 41 | TYPE = DetectorType.STATEFULL 42 | 43 | IMPACT = DetectorClassification.HIGH 44 | CONFIDENCE = DetectorClassification.HIGH 45 | 46 | WIKI_URL = "https://github.com/crytic/tealer/wiki/Detector-Documentation#upgradable-application" 47 | WIKI_TITLE = "Upgradable Application" 48 | WIKI_DESCRIPTION = ( 49 | "Application can be updated by sending an `UpdateApplication` type application call." 50 | ) 51 | WIKI_EXPLOIT_SCENARIO = """ 52 | ```py 53 | @router.method(update_application=CallConfig.CALL) 54 | def update_application() -> Expr: 55 | return Assert(Txn.sender() == Global.creator_address()) 56 | ``` 57 | 58 | Creator updates the application and steals all of its assets. 59 | """ 60 | 61 | WIKI_RECOMMENDATION = """ 62 | Do not approve `UpdateApplication` type application calls. 63 | """ 64 | 65 | def detect(self) -> "ListOutput": 66 | """Detect execution paths with missing UpdateApplication check. 67 | 68 | Returns: 69 | ExecutionPaths instance containing the list of vulnerable execution 70 | paths along with name, check, impact, confidence and other detector 71 | information. 72 | """ 73 | 74 | def checks_field(block_ctx: "BlockTransactionContext") -> bool: 75 | # return False if Txn Type can be UpdateApplication. 76 | # return True if Txn Type cannot be UpdateApplication. 77 | return not TealerTransactionType.ApplUpdateApplication in block_ctx.transaction_types 78 | 79 | # there should be a better to decide which function to call ?? 80 | if self.tealer.output_group: 81 | # mypy complains if the value is returned directly. Uesd the second suggestion mentioned here: 82 | # https://mypy.readthedocs.io/en/stable/common_issues.html#variance 83 | return list( 84 | detect_missing_tx_field_validations_group_complete(self.tealer, self, checks_field) 85 | ) 86 | 87 | # paths_without_check: List[List[BasicBlock]] = detect_missing_tx_field_validations( 88 | # self.teal, checks_field 89 | # ) 90 | 91 | # return ExecutionPaths(self.teal, self, paths_without_check) 92 | output: List[ 93 | Tuple["Teal", List[List["BasicBlock"]]] 94 | ] = detect_missing_tx_field_validations_group(self.tealer, checks_field) 95 | detector_output: "ListOutput" = [] 96 | for contract, vulnerable_paths in output: 97 | detector_output.append(ExecutionPaths(contract, self, vulnerable_paths)) 98 | 99 | return detector_output 100 | -------------------------------------------------------------------------------- /plugin_example/rekey_plugin/tealer_rekey_plugin/detectors/rekeyto_stateless.py: -------------------------------------------------------------------------------- 1 | """Detector for finding execution paths missing RekeyTo check.""" 2 | 3 | from typing import List, TYPE_CHECKING, Tuple 4 | 5 | from tealer.detectors.abstract_detector import ( 6 | AbstractDetector, 7 | DetectorClassification, 8 | DetectorType, 9 | ) 10 | from tealer.detectors.utils import detect_missing_tx_field_validations_group 11 | from tealer.utils.output import ExecutionPaths 12 | 13 | 14 | if TYPE_CHECKING: 15 | from tealer.teal.basic_blocks import BasicBlock 16 | from tealer.utils.output import ListOutput 17 | from tealer.teal.context.block_transaction_context import BlockTransactionContext 18 | from tealer.teal.teal import Teal 19 | 20 | 21 | class CanRekey(AbstractDetector): # pylint: disable=too-few-public-methods 22 | """Detector to find execution paths missing RekeyTo check. 23 | 24 | TEAL, from version 2 onwards supports rekeying of accounts. 25 | An account can be rekeyed to a different address. Once rekeyed, 26 | rekeyed address has entire authority over the account. Contract 27 | Accounts can also be rekeyed. If RekeyTo field of the transaction 28 | is set to malicious actor's address, then they can control the account 29 | funds, assets directly bypassing the contract's restrictions. 30 | 31 | This detector tries to find execution paths that approve the algorand 32 | transaction("return 1") and doesn't check the RekeyTo transaction field. 33 | Additional to checking rekeying of it's own contract, detector also finds 34 | execution paths that doesn't check RekeyTo field of other transactions 35 | in the atomic group. 36 | """ 37 | 38 | NAME = "rekey-to" 39 | DESCRIPTION = "Rekeyable Logic Signatures" 40 | TYPE = DetectorType.STATELESS 41 | 42 | IMPACT = DetectorClassification.HIGH 43 | CONFIDENCE = DetectorClassification.HIGH 44 | 45 | WIKI_URL = "https://github.com/crytic/tealer/wiki/Detector-Documentation#rekeyable-logicsig" 46 | WIKI_TITLE = "Rekeyable LogicSig" 47 | WIKI_DESCRIPTION = ( 48 | "Logic signature does not validate `RekeyTo` field." 49 | " Attacker can submit a transaction with `RekeyTo` field set to their address and take control over the account." 50 | " More at [building-secure-contracts/not-so-smart-contracts/algorand/rekeying]" 51 | "(https://github.com/crytic/building-secure-contracts/tree/master/not-so-smart-contracts/algorand/rekeying)" 52 | ) 53 | WIKI_EXPLOIT_SCENARIO = """ 54 | ```py 55 | def withdraw(...) -> Expr: 56 | return Seq( 57 | [ 58 | Assert( 59 | And( 60 | Txn.type_enum() == TxnType.Payment, 61 | Txn.first_valid() % period == Int(0), 62 | Txn.last_valid() == Txn.first_valid() + duration, 63 | Txn.receiver() == receiver, 64 | Txn.amount() == amount, 65 | Txn.first_valid() < timeout, 66 | ) 67 | ), 68 | Approve(), 69 | ] 70 | ) 71 | ``` 72 | 73 | Alice signs the logic-sig to allow recurring payments to Bob.\ 74 | Eve uses the logic-sig and submits a valid transaction with `RekeyTo` field set to her address.\ 75 | Eve takes over Alice's account. 76 | """ 77 | 78 | WIKI_RECOMMENDATION = """ 79 | Validate `RekeyTo` field in the LogicSig. 80 | """ 81 | 82 | def detect(self) -> "ListOutput": 83 | """Detect execution paths with missing CloseRemainderTo check. 84 | 85 | Returns: 86 | ExecutionPaths instance containing the list of vulnerable execution 87 | paths along with name, check, impact, confidence and other detector 88 | information. 89 | """ 90 | 91 | def checks_field(block_ctx: "BlockTransactionContext") -> bool: 92 | # return False if RekeyTo field can have any address. 93 | # return True if RekeyTo should have some address or zero address 94 | return not block_ctx.rekeyto.any_addr 95 | 96 | output: List[ 97 | Tuple["Teal", List[List["BasicBlock"]]] 98 | ] = detect_missing_tx_field_validations_group(self.tealer, checks_field) 99 | detector_output: "ListOutput" = [] 100 | for contract, vulnerable_paths in output: 101 | detector_output.append(ExecutionPaths(contract, self, vulnerable_paths)) 102 | 103 | return detector_output 104 | -------------------------------------------------------------------------------- /tealer/teal/subroutine.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, List, Optional 2 | 3 | from tealer.exceptions import TealerException 4 | from tealer.teal.instructions.instructions import Retsub 5 | 6 | if TYPE_CHECKING: 7 | from tealer.teal.basic_blocks import BasicBlock 8 | from tealer.teal.teal import Teal 9 | 10 | 11 | class Subroutine: # pylint: disable=too-many-instance-attributes 12 | """Represent a Teal subroutine. 13 | 14 | Main entry point code, code that is not part of any subroutine, is represented as a separate subroutine. 15 | """ 16 | 17 | def __init__(self, name: str, entry: "BasicBlock", blocks: List["BasicBlock"]) -> None: 18 | self._name = name 19 | self._entry = entry 20 | self._blocks = blocks 21 | self._exit_blocks = [ 22 | b for b in blocks if len(b.next) == 0 or isinstance(b.exit_instr, Retsub) 23 | ] 24 | self._contract: Optional["Teal"] = None 25 | self._caller_callsub_blocks: List["BasicBlock"] = [] 26 | self._return_point_blocks: List["BasicBlock"] = [] 27 | 28 | @property 29 | def name(self) -> str: 30 | """Name of the subroutine 31 | 32 | Returns: 33 | Returns the name of the subroutine. The name of the CFG with non-subroutine blocks is "__main__" 34 | """ 35 | return self._name 36 | 37 | @property 38 | def entry(self) -> "BasicBlock": 39 | """Entry block of the subroutine. 40 | 41 | Returns: 42 | entry block of the subroutine. 43 | """ 44 | return self._entry 45 | 46 | @property 47 | def exit_blocks(self) -> List["BasicBlock"]: 48 | """Exit blocks of the subroutine. 49 | 50 | Returns: 51 | Returns the exit blocks of the subroutine. Exit blocks include blocks which return from the subroutine(retsub) 52 | and also blocks which exit from the contract. 53 | """ 54 | return self._exit_blocks 55 | 56 | @property 57 | def blocks(self) -> List["BasicBlock"]: 58 | """List of all basic blocks of the subroutine. 59 | 60 | Returns: 61 | Returns list of all basic blocks that are part of this subroutine. 62 | """ 63 | return self._blocks 64 | 65 | @property 66 | def contract(self) -> "Teal": 67 | """The Teal contract this subroutine is a part of 68 | 69 | Returns: 70 | Returns the contract. 71 | 72 | Raises: 73 | TealerException: Raises error if contract is not set during parsing. 74 | """ 75 | if self._contract is None: 76 | raise TealerException(f"Contract of subroutine {self._name} is not set") 77 | return self._contract 78 | 79 | @contract.setter 80 | def contract(self, contract_obj: "Teal") -> None: 81 | self._contract = contract_obj 82 | 83 | @property 84 | def caller_blocks(self) -> List["BasicBlock"]: 85 | """BasicBlock with callsub instructions which call this subroutine. 86 | 87 | Returns: 88 | List of caller callsub basic blocks. This blocks have "callsub {self.name}" 89 | as exit instruction. 90 | """ 91 | return self._caller_callsub_blocks 92 | 93 | @caller_blocks.setter 94 | def caller_blocks(self, caller_callsub_blocks: List["BasicBlock"]) -> None: 95 | """Set caller_blocks and return point blocks 96 | 97 | Args: 98 | caller_callsub_blocks: basic blocks with callsub instruction calling this subroutine. 99 | """ 100 | self._caller_callsub_blocks = caller_callsub_blocks 101 | self._return_point_blocks = [ 102 | bi.next[0] for bi in caller_callsub_blocks if len(bi.next) == 1 103 | ] 104 | 105 | @property 106 | def return_point_blocks(self) -> List["BasicBlock"]: 107 | return self._return_point_blocks 108 | 109 | @property 110 | def retsub_blocks(self) -> List["BasicBlock"]: 111 | """List of basic blocks of the subroutine containing `Retsub`. 112 | 113 | Returns: 114 | Returns the list of retsub blocks. This blocks return the execution from the 115 | subroutine to the caller. 116 | """ 117 | return [b for b in self._exit_blocks if isinstance(b.exit_instr, Retsub)] 118 | 119 | @property 120 | def called_subroutines(self) -> List["Subroutine"]: 121 | """List of subroutines called by this subroutine. 122 | 123 | Returns: 124 | Returns a list of subroutines called by the subroutine. 125 | """ 126 | return list(set(bi.called_subroutine for bi in self._blocks if bi.is_callsub_block)) 127 | --------------------------------------------------------------------------------