Skip to content

Commit

Permalink
OpUp: Allow specification of fee source (#566)
Browse files Browse the repository at this point in the history
  • Loading branch information
barnjamin committed Oct 20, 2022
1 parent b47b9ce commit 59b51dc
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 65 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Added
* Added `abi.type_spec_is_assignable_to` to check for compatible ABI type assignments. ([#540](https://github.com/algorand/pyteal/pull/540))
* Added option to `OpUp` utility to allow specification of source for fees ([566](https://github.com/algorand/pyteal/pull/566))

## Fixed
* Erroring on constructing an odd length hex string. ([#539](https://github.com/algorand/pyteal/pull/539))
Expand Down
23 changes: 23 additions & 0 deletions docs/opup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ of inner transactions issued in a transaction group so budget cannot be increase
budget when using the OpUp utility will need to be high enough to execute the TEAL code that issues the inner
transactions. A budget of ~20 is enough for most use cases.

The default behavior for fees on Inner Transactions is to first take any excess fee credit from overpayment of fees
in the group transaction and, if necessary, draw the remainder of fees from the application account.
This behavior can be changed by specifying the source that should be used to cover fees by passing a fee source argument.


Usage
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Mode
----

The :any:`pyteal.OpUp` utility is available in two modes: :any:`Explicit` and :any:`OnCall`:

================= ===================================================================================
Expand All @@ -32,6 +40,21 @@ target app ID to be provided in the foreign apps array field of the transaction
constructor in order for it to be accessible. :any:`OnCall` is easier to use, but has slightly more overhead
because the target app must be created and deleted during the evaluation of an app call.

Fee Source
----------

The source of fees to cover the Inner Transactions for the :any:`pyteal.OpUp` utility can be specified by passing the appropriate
argument for the fee source.

==================== ========================================================================================
OpUp Fee Source Description
==================== ========================================================================================
:code:`GroupCredit` Only take from the group transaction excess fees
:code:`AppAccount` Always pay out of the app accounts algo balance
:code:`Any` Default behavior. First take from GroupCredit then, if necessary, take from App Account.
==================== ========================================================================================


Ensure Budget
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
1 change: 1 addition & 0 deletions pyteal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ __all__ = [
"OnCompleteAction",
"Op",
"OpUp",
"OpUpFeeSource",
"OpUpMode",
"OptimizeOptions",
"Or",
Expand Down
3 changes: 2 additions & 1 deletion pyteal/ast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
from pyteal.ast.scratchvar import DynamicScratchVar, ScratchVar
from pyteal.ast.maybe import MaybeValue
from pyteal.ast.multi import MultiValue
from pyteal.ast.opup import OpUp, OpUpMode
from pyteal.ast.opup import OpUp, OpUpMode, OpUpFeeSource
from pyteal.ast.ecdsa import EcdsaCurve, EcdsaVerify, EcdsaDecompress, EcdsaRecover
from pyteal.ast.router import (
Router,
Expand Down Expand Up @@ -291,6 +291,7 @@
"MultiValue",
"OpUp",
"OpUpMode",
"OpUpFeeSource",
"BytesAdd",
"BytesMinus",
"BytesDiv",
Expand Down
103 changes: 69 additions & 34 deletions pyteal/ast/opup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Optional, cast
from pyteal.ast.app import OnComplete
from pyteal.errors import TealInputError
from pyteal.errors import TealInputError, TealTypeError
from pyteal.ast.while_ import While
from pyteal.ast.expr import Expr
from pyteal.ast.global_ import Global
Expand All @@ -13,6 +14,8 @@
from pyteal.types import TealType, require_type
from enum import Enum

ON_CALL_APP = Bytes("base16", "068101") # v6 program "int 1"


class OpUpMode(Enum):
"""An Enum object that defines the mode used for the OpUp utility.
Expand All @@ -22,15 +25,32 @@ class OpUpMode(Enum):
during evaluation.
"""

# The app to call must be provided by the user.
#: The app to call must be provided by the user.
Explicit = 0

# The app to call is created then deleted for each request to increase budget.
#: The app to call is created then deleted for each request to increase budget.
OnCall = 1


ON_CALL_APP = Bytes("base16", "068101") # v6 program "int 1"
MIN_TXN_FEE = Int(1000)
class OpUpFeeSource(Enum):
"""An Enum object that defines the source for fees for the OpUp utility."""

#: Only the excess fee (credit) on the outer group should be used (set inner_tx.fee=0)
GroupCredit = 0
#: The app's account will cover all fees (set inner_tx.fee=Global.min_tx_fee())
AppAccount = 1
#: First the excess will be used, remaining fees will be taken from the app account
Any = 2


def _fee_by_source(source: OpUpFeeSource) -> Optional[Expr]:
match source:
case OpUpFeeSource.GroupCredit:
return Int(0)
case OpUpFeeSource.AppAccount:
return Global.min_txn_fee()
case _:
return None


class OpUp:
Expand Down Expand Up @@ -70,58 +90,64 @@ def __init__(self, mode: OpUpMode, target_app_id: Expr = None):
"""

# With only OnCall and Explicit modes supported, the mode argument
# isn't strictly necessary but it will most likely be required if
# isn't strictly necessary, but it will most likely be required if
# we do decide to add more modes in the future.
if mode == OpUpMode.Explicit:
if target_app_id is None:
raise TealInputError(
"target_app_id must be specified in Explicit OpUp mode"
)
require_type(target_app_id, TealType.uint64)
self.target_app_id = target_app_id
elif mode == OpUpMode.OnCall:
if target_app_id is not None:
raise TealInputError("target_app_id is not used in OnCall OpUp mode")
else:
raise TealInputError("Invalid OpUp mode provided")

self.mode = mode
self.target_app_id = target_app_id

def _construct_itxn(self, inner_fee: Optional[Expr]) -> Expr:
fields: dict[TxnField, Expr | list[Expr]] = {
TxnField.type_enum: TxnType.ApplicationCall
}

# If an inner_fee is specified
# add it to the transaction fields
if inner_fee is not None:
require_type(inner_fee, TealType.uint64)
fields[TxnField.fee] = inner_fee

def _construct_itxn(self) -> Expr:
if self.mode == OpUpMode.Explicit:
return Seq(
InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
{
TxnField.type_enum: TxnType.ApplicationCall,
TxnField.application_id: self.target_app_id,
}
),
InnerTxnBuilder.Submit(),
)
fields |= {TxnField.application_id: cast(Expr, self.target_app_id)}
else:
return Seq(
InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
{
TxnField.type_enum: TxnType.ApplicationCall,
TxnField.on_completion: OnComplete.DeleteApplication,
TxnField.approval_program: ON_CALL_APP,
TxnField.clear_state_program: ON_CALL_APP,
}
),
InnerTxnBuilder.Submit(),
)
fields |= {
TxnField.on_completion: OnComplete.DeleteApplication,
TxnField.approval_program: ON_CALL_APP,
TxnField.clear_state_program: ON_CALL_APP,
}

def ensure_budget(self, required_budget: Expr) -> Expr:
return InnerTxnBuilder.Execute(fields)

def ensure_budget(
self, required_budget: Expr, fee_source: OpUpFeeSource = OpUpFeeSource.Any
) -> Expr:
"""Ensure that the budget will be at least the required_budget.
Args:
required_budget: minimum op-code budget to ensure for the
upcoming execution.
fee_source (optional): source that should be used for covering fees on
the inner transactions that are generated.
Note: the available budget just prior to calling ensure_budget() must be
high enough to execute the budget increase code. The exact budget required
depends on the provided required_budget expression, but a budget of ~20
should be sufficient for most use cases. If lack of budget is an issue then
consider moving the call to ensure_budget() earlier in the pyteal program."""
require_type(required_budget, TealType.uint64)
if not type(fee_source) == OpUpFeeSource:
raise TealTypeError(type(fee_source), OpUpFeeSource)

# A budget buffer is necessary to deal with an edge case of ensure_budget():
# if the current budget is equal to or only slightly higher than the
Expand All @@ -133,24 +159,33 @@ def ensure_budget(self, required_budget: Expr) -> Expr:
return Seq(
buffered_budget.store(required_budget + buffer),
While(buffered_budget.load() > Global.opcode_budget()).Do(
self._construct_itxn()
self._construct_itxn(inner_fee=_fee_by_source(fee_source))
),
)

def maximize_budget(self, fee: Expr) -> Expr:
def maximize_budget(
self, fee: Expr, fee_source: OpUpFeeSource = OpUpFeeSource.Any
) -> Expr:
"""Maximize the available opcode budget without spending more than the given fee.
Args:
fee: fee expenditure cap for the op-code budget maximization.
fee_source (optional): source that should be used for covering fees on
the inner transactions that are generated.
Note: the available budget just prior to calling maximize_budget() must be
high enough to execute the budget increase code. The exact budget required
depends on the provided fee expression, but a budget of ~25 should be
sufficient for most use cases. If lack of budget is an issue then consider
moving the call to maximize_budget() earlier in the pyteal program."""
require_type(fee, TealType.uint64)
if not type(fee_source) == OpUpFeeSource:
raise TealTypeError(type(fee_source), OpUpFeeSource)

i = ScratchVar(TealType.uint64)
n = fee / Global.min_txn_fee()
return For(i.store(Int(0)), i.load() < n, i.store(i.load() + Int(1))).Do(
self._construct_itxn()
self._construct_itxn(inner_fee=_fee_by_source(fee_source))
)


Expand Down

0 comments on commit 59b51dc

Please sign in to comment.