Repository: algorand/pyteal

Issue:
Improve `Router` clear state program generation
## Problem

Currently the ABI `Router` class supports methods and bare app calls for the clear state action. Any such bare app call and/or methods get put into the clear state program, following the same routing rules as the approval program.

This is a problem, since clear state programs operate under very different failure rules. A clear state program which wishes to have any effect on the state of the chain must never fail, since doing so reverts any effects made in that program. With the current routing rules, it's possible to fail a program by attempting to call an unsupported ABI method, or by providing any arguments when attempting to issue a bare app call.

## Solution

Based on internal discussion, we've agreed on the following solution:
* Drop support for clear state bare app calls and for ABI methods on clear state.
  * These argument keys should still exist, but an error should be thrown at runtime if someone attempts to use them. The error message should indicate the alternative approach described below.
* Introduce a new top-level `Router` argument, `clear_state`, that takes an `Expr`. If provided, this expression would become the entire clear state program; no additional code would be injected by PyTeal.


Discussion:
jasonpaulos: Note that we will also need to update the docs, notably https://pyteal.readthedocs.io/en/stable/abi.html#registering-bare-app-calls
jasonpaulos: I had some docs improvement ideas, so I opened #641

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da02f72..9ddd6bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
 ## Added
 
 ## Changed
+* Improved Router clear state program generation. ([#636](https://github.com/algorand/pyteal/pull/636))
+  * NOTE: a backwards incompatable change was imposed in this PR: previous Clear State Program (CSP) can be constructed in router by registering ABI methods or bare app calls, now one has to use `clear_state` argument in `Router.__init__` to construct the CSP.
 
 ## Fixed
 
diff --git a/docs/abi.rst b/docs/abi.rst
index 63d6514..5720d9e 100644
--- a/docs/abi.rst
+++ b/docs/abi.rst
@@ -732,13 +732,16 @@ The AVM supports 6 types of OnCompletion options that may be specified on an app
 #. **Update application**, which updates an app, represented by :any:`OnComplete.UpdateApplication`
 #. **Delete application**, which deletes an app, represented by :any:`OnComplete.DeleteApplication`
 
-In PyTeal, you have the ability to register a bare app call handler for each of these actions. Additionally, a bare app call handler must also specify whether the handler can be invoking during an **app creation transaction** (:any:`CallConfig.CREATE`), during a **non-creation app call** (:any:`CallConfig.CALL`), or during **either** (:any:`CallConfig.ALL`).
+.. note::
+    While **clear state** is a valid OnCompletion action, its behavior differs significantly from the others. For this reason, the :any:`Router` does not support bare app calls or methods to be called during clear state. Instead, you may use the :code:`clear_state` argument in :any:`Router.__init__` to do work during clear state.
+
+In PyTeal, you have the ability to register a bare app call handler for each of these actions, except for clear state. Additionally, a bare app call handler must also specify whether the handler can be invoking during an **app creation transaction** (:any:`CallConfig.CREATE`), during a **non-creation app call** (:any:`CallConfig.CALL`), or during **either** (:any:`CallConfig.ALL`).
 
 The :any:`BareCallActions` class is used to define a bare app call handler for on completion actions. Each bare app call handler must be an instance of the :any:`OnCompleteAction` class.
 
 The :any:`OnCompleteAction` class is responsible for holding the actual code for the bare app call handler (an instance of either :code:`Expr` or a subroutine that takes no args and returns nothing) as well as a :any:`CallConfig` option that indicates whether the action is able to be called during a creation app call, a non-creation app call, or either.
 
-All the bare app calls that an application wishes to support must be provided to the :any:`Router.__init__` method.
+All the bare app calls that an application wishes to support must be provided to the :any:`Router.__init__` method. Additionally, if you wish to perform actions during clear state, you can specify the :code:`clear_state` argument.
 
 A brief example is below:
 
@@ -785,6 +788,7 @@ A brief example is below:
                 action=assert_sender_is_creator, call_config=CallConfig.CALL
             ),
         ),
+        clear_state=Approve(),
     )
 
 .. note::
@@ -820,6 +824,7 @@ The first way to register a method is with the :any:`Router.add_method_handler`
             opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL),
             close_out=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL),
         ),
+        clear_state=Approve(),
     )
 
     @ABIReturnSubroutine
diff --git a/examples/application/abi/algobank.py b/examples/application/abi/algobank.py
index 9bdb00d..34303ff 100644
--- a/examples/application/abi/algobank.py
+++ b/examples/application/abi/algobank.py
@@ -25,9 +25,6 @@ router = Router(
         close_out=OnCompleteAction(
             action=transfer_balance_to_lost, call_config=CallConfig.CALL
         ),
-        clear_state=OnCompleteAction(
-            action=transfer_balance_to_lost, call_config=CallConfig.CALL
-        ),
         # only the creator can update or delete the app
         update_application=OnCompleteAction(
             action=assert_sender_is_creator, call_config=CallConfig.CALL
@@ -36,6 +33,7 @@ router = Router(
             action=assert_sender_is_creator, call_config=CallConfig.CALL
         ),
     ),
+    clear_state=transfer_balance_to_lost,
 )
 
 
diff --git a/examples/application/abi/algobank_clear_state.teal b/examples/application/abi/algobank_clear_state.teal
index a917c87..86c0895 100644
--- a/examples/application/abi/algobank_clear_state.teal
+++ b/examples/application/abi/algobank_clear_state.teal
@@ -1,10 +1,4 @@
 #pragma version 6
-txn NumAppArgs
-int 0
-==
-bnz main_l2
-err
-main_l2:
 byte "lost"
 byte "lost"
 app_global_get
diff --git a/pyteal/ast/router.py b/pyteal/ast/router.py
index 9041c8b..5afbaa8 100644
--- a/pyteal/ast/router.py
+++ b/pyteal/ast/router.py
@@ -63,19 +63,6 @@ class CallConfig(IntFlag):
             case _:
                 raise TealInternalError(f"unexpected CallConfig {self}")
 
-    def clear_state_condition_under_config(self) -> int:
-        match self:
-            case CallConfig.NEVER:
-                return 0
-            case CallConfig.CALL:
-                return 1
-            case CallConfig.CREATE | CallConfig.ALL:
-                raise TealInputError(
-                    "Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation"
-                )
-            case _:
-                raise TealInputError(f"unexpected CallConfig {self}")
-
 
 CallConfig.__module__ = "pyteal"
 
@@ -96,6 +83,15 @@ class MethodConfig:
     update_application: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
     delete_application: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
 
+    def __post_init__(self):
+        if self.clear_state != CallConfig.NEVER:
+            raise TealInputError(
+                "Attempt to construct clear state program from MethodConfig: "
+                "Use Router top level argument `clear_state` instead. "
+                "For more details please refer to "
+                "https://pyteal.readthedocs.io/en/latest/abi.html#registering-bare-app-calls"
+            )
+
     def is_never(self) -> bool:
         return all(map(lambda cc: cc == CallConfig.NEVER, astuple(self)))
 
@@ -128,8 +124,11 @@ class MethodConfig:
                         )
             return Or(*cond_list)
 
-    def clear_state_cond(self) -> Expr | int:
-        return self.clear_state.clear_state_condition_under_config()
+
+MethodConfig.__module__ = "pyteal"
+
+
+ActionType = Expr | SubroutineFnWrapper | ABIReturnSubroutine
 
 
 @dataclass(frozen=True)
@@ -138,9 +137,7 @@ class OnCompleteAction:
     OnComplete Action, registers bare calls to one single OnCompletion case.
     """
 
-    action: Optional[Expr | SubroutineFnWrapper | ABIReturnSubroutine] = field(
-        kw_only=True, default=None
-    )
+    action: Optional[ActionType] = field(kw_only=True, default=None)
     call_config: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
 
     def __post_init__(self):
@@ -154,21 +151,15 @@ class OnCompleteAction:
         return OnCompleteAction()
 
     @staticmethod
-    def create_only(
-        f: Expr | SubroutineFnWrapper | ABIReturnSubroutine,
-    ) -> "OnCompleteAction":
+    def create_only(f: ActionType) -> "OnCompleteAction":
         return OnCompleteAction(action=f, call_config=CallConfig.CREATE)
 
     @staticmethod
-    def call_only(
-        f: Expr | SubroutineFnWrapper | ABIReturnSubroutine,
-    ) -> "OnCompleteAction":
+    def call_only(f: ActionType) -> "OnCompleteAction":
         return OnCompleteAction(action=f, call_config=CallConfig.CALL)
 
     @staticmethod
-    def always(
-        f: Expr | SubroutineFnWrapper | ABIReturnSubroutine,
-    ) -> "OnCompleteAction":
+    def always(f: ActionType) -> "OnCompleteAction":
         return OnCompleteAction(action=f, call_config=CallConfig.ALL)
 
     def is_empty(self) -> bool:
@@ -197,6 +188,15 @@ class BareCallActions:
         kw_only=True, default=OnCompleteAction.never()
     )
 
+    def __post_init__(self):
+        if not self.clear_state.is_empty():
+            raise TealInputError(
+                "Attempt to construct clear state program from bare app call: "
+                "Use Router top level argument `clear_state` instead. "
+                "For more details please refer to "
+                "https://pyteal.readthedocs.io/en/latest/abi.html#registering-bare-app-calls"
+            )
+
     def is_empty(self) -> bool:
         for action_field in fields(self):
             action: OnCompleteAction = getattr(self, action_field.name)
@@ -220,7 +220,7 @@ class BareCallActions:
                 continue
             wrapped_handler = ASTBuilder.wrap_handler(
                 False,
-                cast(Expr | SubroutineFnWrapper | ABIReturnSubroutine, oca.action),
+                cast(ActionType, oca.action),
             )
             match oca.call_config:
                 case CallConfig.ALL:
@@ -246,21 +246,6 @@ class BareCallActions:
             )
         return Cond(*[[n.condition, n.branch] for n in conditions_n_branches])
 
-    def clear_state_construction(self) -> Optional[Expr]:
-        if self.clear_state.is_empty():
-            return None
-
-        # call this to make sure we error if the CallConfig is CREATE or ALL
-        self.clear_state.call_config.clear_state_condition_under_config()
-
-        return ASTBuilder.wrap_handler(
-            False,
-            cast(
-                Expr | SubroutineFnWrapper | ABIReturnSubroutine,
-                self.clear_state.action,
-            ),
-        )
-
 
 BareCallActions.__module__ = "pyteal"
 
@@ -280,7 +265,7 @@ class ASTBuilder:
 
     @staticmethod
     def wrap_handler(
-        is_method_call: bool, handler: ABIReturnSubroutine | SubroutineFnWrapper | Expr
+        is_method_call: bool, handler: ActionType, wrap_to_name: str | None = None
     ) -> Expr:
         """This is a helper function that handles transaction arguments passing in bare-app-call/abi-method handlers.
 
@@ -302,11 +287,13 @@ class ASTBuilder:
                   passed in ABIReturnSubroutine and logged, then approve.
         """
         if not is_method_call:
+            wrap_to_name = "bare appcall" if wrap_to_name is None else wrap_to_name
+
             match handler:
                 case Expr():
                     if handler.type_of() != TealType.none:
                         raise TealInputError(
-                            f"bare appcall handler should be TealType.none not {handler.type_of()}."
+                            f"{wrap_to_name} handler should be TealType.none not {handler.type_of()}."
                         )
                     return handler if handler.has_return() else Seq(handler, Approve())
                 case SubroutineFnWrapper():
@@ -316,7 +303,7 @@ class ASTBuilder:
                         )
                     if handler.subroutine.argument_count() != 0:
                         raise TealInputError(
-                            f"subroutine call should take 0 arg for bare-app call. "
+                            f"subroutine call should take 0 arg for {wrap_to_name}. "
                             f"this subroutine takes {handler.subroutine.argument_count()}."
                         )
                     return Seq(handler(), Approve())
@@ -327,22 +314,23 @@ class ASTBuilder:
                         )
                     if handler.subroutine.argument_count() != 0:
                         raise TealInputError(
-                            f"abi-returning subroutine call should take 0 arg for bare-app call. "
+                            f"abi-returning subroutine call should take 0 arg for {wrap_to_name}. "
                             f"this abi-returning subroutine takes {handler.subroutine.argument_count()}."
                         )
                     return Seq(cast(Expr, handler()), Approve())
                 case _:
                     raise TealInputError(
-                        "bare appcall can only accept: none type Expr, or Subroutine/ABIReturnSubroutine with none return and no arg"
+                        f"{wrap_to_name} can only accept: none type Expr, or Subroutine/ABIReturnSubroutine with none return and no arg"
                     )
         else:
+            wrap_to_name = "method call" if wrap_to_name is None else wrap_to_name
             if not isinstance(handler, ABIReturnSubroutine):
                 raise TealInputError(
-                    f"method call should be only registering ABIReturnSubroutine, got {type(handler)}."
+                    f"{wrap_to_name} should be only registering ABIReturnSubroutine, got {type(handler)}."
                 )
             if not handler.is_abi_routable():
                 raise TealInputError(
-                    f"method call ABIReturnSubroutine is not routable "
+                    f"{wrap_to_name} ABIReturnSubroutine is not routable "
                     f"got {handler.subroutine.argument_count()} args with {len(handler.subroutine.abi_args)} ABI args."
                 )
 
@@ -508,19 +496,28 @@ class Router:
         name: str,
         bare_calls: BareCallActions | None = None,
         descr: str | None = None,
+        *,
+        clear_state: Optional[ActionType] = None,
     ) -> None:
         """
         Args:
             name: the name of the smart contract, used in the JSON object.
             bare_calls: the bare app call registered for each on_completion.
             descr: a description of the smart contract, used in the JSON object.
+            clear_state: an expression describing the behavior of clear state program. This
+                expression will be the entirety of the clear state program; no additional code is
+                inserted by the Router. If not provided, the clear state program will always reject.
         """
 
         self.name: str = name
         self.descr = descr
 
         self.approval_ast = ASTBuilder()
-        self.clear_state_ast = ASTBuilder()
+        self.clear_state: Expr = (
+            Reject()
+            if clear_state is None
+            else ASTBuilder.wrap_handler(False, clear_state, "clear state call")
+        )
 
         self.methods: list[sdk_abi.Method] = []
         self.method_sig_to_selector: dict[str, bytes] = dict()
@@ -535,14 +532,6 @@ class Router:
                         cast(Expr, bare_call_approval),
                     )
                 )
-            bare_call_clear = bare_calls.clear_state_construction()
-            if bare_call_clear:
-                self.clear_state_ast.conditions_n_branches.append(
-                    CondNode(
-                        Txn.application_args.length() == Int(0),
-                        cast(Expr, bare_call_clear),
-                    )
-                )
 
     def add_method_handler(
         self,
@@ -594,13 +583,9 @@ class Router:
         self.method_selector_to_sig[method_selector] = method_signature
 
         method_approval_cond = method_config.approval_cond()
-        method_clear_state_cond = method_config.clear_state_cond()
         self.approval_ast.add_method_to_ast(
             method_signature, method_approval_cond, method_call
         )
-        self.clear_state_ast.add_method_to_ast(
-            method_signature, method_clear_state_cond, method_call
-        )
         return method_call
 
     def method(
@@ -637,6 +622,8 @@ class Router:
             opt_in (optional): The allowed calls during :code:`OnComplete.OptIn`.
             close_out (optional): The allowed calls during :code:`OnComplete.CloseOut`.
             clear_state (optional): The allowed calls during :code:`OnComplete.ClearState`.
+                This argument has been deprecated, and will error on compile time if one wants to access it.
+                Use Router top level argument `clear_state` instead.
             update_application (optional): The allowed calls during :code:`OnComplete.UpdateApplication`.
             delete_application (optional): The allowed calls during :code:`OnComplete.DeleteApplication`.
         """
@@ -644,6 +631,15 @@ class Router:
         # - None
         # - CallConfig.Never
         # both cases evaluate to False in if statement.
+
+        if clear_state is not None:
+            raise TealInputError(
+                "Attempt to register ABI method for clear state program: "
+                "Use Router top level argument `clear_state` instead. "
+                "For more details please refer to "
+                "https://pyteal.readthedocs.io/en/latest/abi.html#registering-bare-app-calls"
+            )
+
         def wrap(_func) -> ABIReturnSubroutine:
             wrapped_subroutine = ABIReturnSubroutine(_func, overriding_name=name)
             call_configs: MethodConfig
@@ -651,7 +647,6 @@ class Router:
                 no_op is None
                 and opt_in is None
                 and close_out is None
-                and clear_state is None
                 and update_application is None
                 and delete_application is None
             ):
@@ -664,7 +659,6 @@ class Router:
                 _no_op = none_to_never(no_op)
                 _opt_in = none_to_never(opt_in)
                 _close_out = none_to_never(close_out)
-                _clear_state = none_to_never(clear_state)
                 _update_app = none_to_never(update_application)
                 _delete_app = none_to_never(delete_application)
 
@@ -672,7 +666,6 @@ class Router:
                     no_op=_no_op,
                     opt_in=_opt_in,
                     close_out=_close_out,
-                    clear_state=_clear_state,
                     update_application=_update_app,
                     delete_application=_delete_app,
                 )
@@ -714,7 +707,7 @@ class Router:
         """
         return (
             self.approval_ast.program_construction(),
-            self.clear_state_ast.program_construction(),
+            self.clear_state,
             self.contract_construct(),
         )
 


Test patch:
diff --git a/pyteal/ast/router_test.py b/pyteal/ast/router_test.py
index 6091618..065c4ef 100644
--- a/pyteal/ast/router_test.py
+++ b/pyteal/ast/router_test.py
@@ -252,7 +252,6 @@ GOOD_SUBROUTINE_CASES: list[pt.ABIReturnSubroutine | pt.SubroutineFnWrapper] = [
 ON_COMPLETE_CASES: list[pt.EnumInt] = [
     pt.OnComplete.NoOp,
     pt.OnComplete.OptIn,
-    pt.OnComplete.ClearState,
     pt.OnComplete.CloseOut,
     pt.OnComplete.UpdateApplication,
     pt.OnComplete.DeleteApplication,
@@ -361,6 +360,36 @@ def camel_to_snake(name: str) -> str:
     return "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_")
 
 
+def test_method_config_clear_state_failure():
+    with pytest.raises(pt.TealInputError) as tie:
+        pt.MethodConfig(clear_state=pt.CallConfig.CALL)
+
+    assert "Attempt to construct clear state program from MethodConfig" in str(tie)
+
+
+def test_bare_call_config_clear_state_failure():
+    with pytest.raises(pt.TealInputError) as tie:
+        pt.BareCallActions(
+            clear_state=pt.OnCompleteAction(
+                action=pt.Seq(), call_config=pt.CallConfig.CALL
+            )
+        )
+
+    assert "Attempt to construct clear state program from bare app call" in str(tie)
+
+
+def test_router_register_method_clear_state_failure():
+    router = pt.Router("doomedToFail")
+
+    with pytest.raises(pt.TealInputError) as tie:
+
+        @router.method(clear_state=pt.CallConfig.CALL)
+        def incr_by_1(a: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr:
+            return output.set(a.get() + pt.Int(1))
+
+    assert "Attempt to register ABI method for clear state program" in str(tie)
+
+
 def test_call_config():
     for cc in pt.CallConfig:
         approval_cond_on_cc: pt.Expr | int = cc.approval_condition_under_config()
@@ -382,31 +411,11 @@ def test_call_config():
                     f"unexpected approval_cond_on_cc {approval_cond_on_cc}"
                 )
 
-        if cc in (pt.CallConfig.CREATE, pt.CallConfig.ALL):
-            with pytest.raises(
-                pt.TealInputError,
-                match=r"Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation$",
-            ):
-                cc.clear_state_condition_under_config()
-            continue
-
-        clear_state_cond_on_cc: int = cc.clear_state_condition_under_config()
-        match clear_state_cond_on_cc:
-            case 0:
-                assert cc == pt.CallConfig.NEVER
-            case 1:
-                assert cc == pt.CallConfig.CALL
-            case _:
-                raise pt.TealInternalError(
-                    f"unexpected clear_state_cond_on_cc {clear_state_cond_on_cc}"
-                )
-
 
 def test_method_config_call_config_never():
     never_mc = pt.MethodConfig(no_op=pt.CallConfig.NEVER)
     assert never_mc.is_never()
     assert never_mc.approval_cond() == 0
-    assert never_mc.clear_state_cond() == 0
 
 
 def _gen_method_configs(sample_count: int = 10):
@@ -427,17 +436,6 @@ def test_method_config(mc: pt.MethodConfig):
         if str(oc) != str(pt.OnComplete.ClearState)
     ]
 
-    match mc.clear_state:
-        case pt.CallConfig.NEVER:
-            assert mc.clear_state_cond() == 0
-        case pt.CallConfig.CALL:
-            assert mc.clear_state_cond() == 1
-        case pt.CallConfig.CREATE | pt.CallConfig.ALL:
-            with pytest.raises(
-                pt.TealInputError,
-                match=r"Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation$",
-            ):
-                mc.clear_state_cond()
     if mc.is_never() or all(
         getattr(mc, i) == pt.CallConfig.NEVER for i, _ in approval_check_names_n_ocs
     ):
@@ -520,7 +518,7 @@ def test_wrap_handler_bare_call():
         ),
         (
             mult_over_u64_and_log,
-            "subroutine call should take 0 arg for bare-app call. this subroutine takes 2.",
+            "subroutine call should take 0 arg for bare appcall. this subroutine takes 2.",
         ),
         (
             eine_constant,
@@ -528,7 +526,7 @@ def test_wrap_handler_bare_call():
         ),
         (
             take_abi_and_log,
-            "abi-returning subroutine call should take 0 arg for bare-app call. this abi-returning subroutine takes 1.",
+            "abi-returning subroutine call should take 0 arg for bare appcall. this abi-returning subroutine takes 1.",
         ),
         (
             1,
@@ -724,10 +722,7 @@ def test_contract_json_obj():
         filter(lambda x: isinstance(x, pt.ABIReturnSubroutine), GOOD_SUBROUTINE_CASES)
     )
     contract_name = "contract_name"
-    on_complete_actions = pt.BareCallActions(
-        clear_state=pt.OnCompleteAction.call_only(safe_clear_state_delete)
-    )
-    router = pt.Router(contract_name, on_complete_actions)
+    router = pt.Router(contract_name, clear_state=safe_clear_state_delete)
     method_list: list[sdk_abi.Method] = []
     for subroutine in abi_subroutines:
 
@@ -779,10 +774,7 @@ def test_build_program_all_empty():
 
 
 def test_build_program_approval_empty():
-    router = pt.Router(
-        "test",
-        pt.BareCallActions(clear_state=pt.OnCompleteAction.call_only(pt.Approve())),
-    )
+    router = pt.Router("test", clear_state=pt.Approve())
 
     approval, clear_state, contract = router.build_program()
 
@@ -823,48 +815,16 @@ def test_build_program_clear_state_empty():
     assert contract == expected_contract
 
 
-def test_build_program_clear_state_invalid_config():
-    for config in (pt.CallConfig.CREATE, pt.CallConfig.ALL):
-        bareCalls = pt.BareCallActions(
-            clear_state=pt.OnCompleteAction(action=pt.Approve(), call_config=config)
-        )
-        with pytest.raises(
-            pt.TealInputError,
-            match=r"Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation$",
-        ):
-            pt.Router("test", bareCalls)
-
-        router = pt.Router("test")
-
-        @pt.ABIReturnSubroutine
-        def clear_state_method():
-            return pt.Approve()
-
-        with pytest.raises(
-            pt.TealInputError,
-            match=r"Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation$",
-        ):
-            router.add_method_handler(
-                clear_state_method,
-                method_config=pt.MethodConfig(clear_state=config),
-            )
-
-
 def test_build_program_clear_state_valid_config():
     action = pt.If(pt.Txn.fee() == pt.Int(4)).Then(pt.Approve()).Else(pt.Reject())
-    config = pt.CallConfig.CALL
 
     router_with_bare_call = pt.Router(
         "test",
-        pt.BareCallActions(
-            clear_state=pt.OnCompleteAction(action=action, call_config=config)
-        ),
+        clear_state=action,
     )
     _, actual_clear_state_with_bare_call, _ = router_with_bare_call.build_program()
 
-    expected_clear_state_with_bare_call = assemble_helper(
-        pt.Cond([pt.Txn.application_args.length() == pt.Int(0), action])
-    )
+    expected_clear_state_with_bare_call = assemble_helper(action)
 
     with pt.TealComponent.Context.ignoreExprEquality():
         assert (
@@ -872,34 +832,6 @@ def test_build_program_clear_state_valid_config():
             == expected_clear_state_with_bare_call
         )
 
-    router_with_method = pt.Router("test")
-
-    @pt.ABIReturnSubroutine
-    def clear_state_method():
-        return action
-
-    router_with_method.add_method_handler(
-        clear_state_method, method_config=pt.MethodConfig(clear_state=config)
-    )
-
-    _, actual_clear_state_with_method, _ = router_with_method.build_program()
-
-    expected_clear_state_with_method = assemble_helper(
-        pt.Cond(
-            [
-                pt.Txn.application_args[0]
-                == pt.MethodSignature("clear_state_method()void"),
-                pt.Seq(clear_state_method(), pt.Approve()),
-            ]
-        )
-    )
-
-    with pt.TealComponent.Context.ignoreExprEquality():
-        assert (
-            assemble_helper(actual_clear_state_with_method)
-            == expected_clear_state_with_method
-        )
-
 
 def test_override_names():
     r1 = pt.Router("test")
diff --git a/pyteal/compiler/compiler_test.py b/pyteal/compiler/compiler_test.py
index 7031384..ae7c911 100644
--- a/pyteal/compiler/compiler_test.py
+++ b/pyteal/compiler/compiler_test.py
@@ -2297,7 +2297,6 @@ def test_router_app():
             method_config=pt.MethodConfig(
                 no_op=pt.CallConfig.CALL,
                 opt_in=pt.CallConfig.ALL,
-                clear_state=pt.CallConfig.CALL,
             ),
         )
         assert meth.method_signature() == "empty_return_subroutine()void"
@@ -2311,7 +2310,6 @@ def test_router_app():
             method_config=pt.MethodConfig(
                 no_op=pt.CallConfig.CALL,
                 opt_in=pt.CallConfig.CALL,
-                clear_state=pt.CallConfig.CALL,
             ),
         )
 
@@ -2326,25 +2324,8 @@ def test_router_app():
         )
         assert meth.method_signature() == "log_creation()string"
 
-        @pt.ABIReturnSubroutine
-        def approve_if_odd(condition_encoding: pt.abi.Uint32) -> pt.Expr:
-            return (
-                pt.If(condition_encoding.get() % pt.Int(2))
-                .Then(pt.Approve())
-                .Else(pt.Reject())
-            )
-
-        meth = router.add_method_handler(
-            approve_if_odd,
-            method_config=pt.MethodConfig(
-                no_op=pt.CallConfig.NEVER, clear_state=pt.CallConfig.CALL
-            ),
-        )
-        assert meth.method_signature() == "approve_if_odd(uint32)void"
-
     on_completion_actions = pt.BareCallActions(
         opt_in=pt.OnCompleteAction.call_only(pt.Log(pt.Bytes("optin call"))),
-        clear_state=pt.OnCompleteAction.call_only(pt.Approve()),
     )
 
     with pytest.raises(pt.TealInputError) as e:
@@ -2355,7 +2336,9 @@ def test_router_app():
     assert "Frame pointers aren't available" in str(e.value)
 
     _router_with_oc = pt.Router(
-        "ASimpleQuestionablyRobustContract", on_completion_actions
+        "ASimpleQuestionablyRobustContract",
+        on_completion_actions,
+        clear_state=pt.Approve(),
     )
     add_methods_to_router(_router_with_oc)
     (
@@ -2828,78 +2811,13 @@ retsub""".strip()
     assert expected_ap_with_oc == actual_ap_with_oc_compiled
 
     expected_csp_with_oc = """#pragma version 6
-txn NumAppArgs
-int 0
-==
-bnz main_l8
-txna ApplicationArgs 0
-method "empty_return_subroutine()void"
-==
-bnz main_l7
-txna ApplicationArgs 0
-method "log_1()uint64"
-==
-bnz main_l6
-txna ApplicationArgs 0
-method "approve_if_odd(uint32)void"
-==
-bnz main_l5
-err
-main_l5:
-txna ApplicationArgs 1
-int 0
-extract_uint32
-store 2
-load 2
-callsub approveifodd_2
-int 1
-return
-main_l6:
-callsub log1_1
-store 1
-byte 0x151f7c75
-load 1
-itob
-concat
-log
-int 1
-return
-main_l7:
-callsub emptyreturnsubroutine_0
-int 1
-return
-main_l8:
-int 1
-return
-
-// empty_return_subroutine
-emptyreturnsubroutine_0:
-byte "appear in both approval and clear state"
-log
-retsub
-
-// log_1
-log1_1:
-int 1
-store 0
-load 0
-retsub
-
-// approve_if_odd
-approveifodd_2:
-store 3
-load 3
-int 2
-%
-bnz approveifodd_2_l2
-int 0
-return
-approveifodd_2_l2:
 int 1
 return""".strip()
     assert expected_csp_with_oc == actual_csp_with_oc_compiled
 
-    _router_without_oc = pt.Router("yetAnotherContractConstructedFromRouter")
+    _router_without_oc = pt.Router(
+        "yetAnotherContractConstructedFromRouter", clear_state=pt.Approve()
+    )
     add_methods_to_router(_router_without_oc)
     (
         actual_ap_without_oc_compiled,
@@ -3350,66 +3268,6 @@ retsub""".strip()
     assert actual_ap_without_oc_compiled == expected_ap_without_oc
 
     expected_csp_without_oc = """#pragma version 6
-txna ApplicationArgs 0
-method "empty_return_subroutine()void"
-==
-bnz main_l6
-txna ApplicationArgs 0
-method "log_1()uint64"
-==
-bnz main_l5
-txna ApplicationArgs 0
-method "approve_if_odd(uint32)void"
-==
-bnz main_l4
-err
-main_l4:
-txna ApplicationArgs 1
-int 0
-extract_uint32
-store 2
-load 2
-callsub approveifodd_2
-int 1
-return
-main_l5:
-callsub log1_1
-store 1
-byte 0x151f7c75
-load 1
-itob
-concat
-log
-int 1
-return
-main_l6:
-callsub emptyreturnsubroutine_0
-int 1
-return
-
-// empty_return_subroutine
-emptyreturnsubroutine_0:
-byte "appear in both approval and clear state"
-log
-retsub
-
-// log_1
-log1_1:
-int 1
-store 0
-load 0
-retsub
-
-// approve_if_odd
-approveifodd_2:
-store 3
-load 3
-int 2
-%
-bnz approveifodd_2_l2
-int 0
-return
-approveifodd_2_l2:
 int 1
 return""".strip()
     assert actual_csp_without_oc_compiled == expected_csp_without_oc