Solutions
No Results
Solution

ASA Recurring Withdrawal DApp

Overview

This solution will guide you in developing and deploying an orchestration of Algorand protocol features that addresses the following use case:

Building a decentralized application that regulates recurring withdrawals of tokenized assets.

The solution architecture relies on Algorand Standard Asset (ASA), Atomic Transfers (AT) and Stateful & Stateless Smart Contracts (ASC1), whose source code is provided both in TEAL and PyTeal. We will use goal CLI to deploy the application and interact with it manually. Note that the application’s deployment and interaction steps could be executed programmatically using any Algorand SDK instead of goal CLI, consider this as your next step exercise.

Introduction

Regulating recurring payments or withdrawals is a good use-case for a blockchain application. While an implementation of recurring payment escrow could be achieved just making use of Stateless ASC1, integrating Stateful ASC1 provides higher levels of generalization and customization for withdrawal approval schemes.

In the context of the proposed solution, the Stateful component let us model the following withdrawal approval schema:

Users can book withdrawals locking an amount of ASA in the Asset Escrow Account. Once their booked withdrawal has been processed, users can withdraw double the amount of ASA deposited in the Asset Escrow Account. The dApp prevents both withdrawals double-booking and over-booking.

Unlike purely Stateless solutions, a Stateful component lets the recurring withdrawal interval be even grater than 1000 blocks (that is the greatest validity range for any leased Algorand transaction, which limits pure Stateless verification domain).

The proposed withdrawal approval logic could fit, for example, a simple ASA distribution model. However, nothing prevents you from model more sophisticated withdrawal conditions starting from the proposed solution.

Solution Architecture

Algorand provides great protocol features directly on Layer-1, their composition turns out to be extremely powerful and expressive. Before deep diving into technical details of the implementation, is useful to sketch up the solution architecture on high level, pointing out the role that each little brick plays into the dApp.

  1. Algorand Standard Asset: implements any kind of token over which the dApp applies its withdrawal approval logic.
  2. Atomic Transfers: implements a group of transactions that force the simultaneous interaction between the Stateful and Sateless components (as explained in the prerequisite article linked below). The first transaction calls the Stateful component while the second transaction transfers ASA from or to the Stateless component.
  3. Stateful Algorand Smart Contract: implements the Application, triggered by the first transaction in the AT, that reads both the blockchain global state and users accounts’ local state to check if the withdrawal request matches both the ASA balance and block validity approval conditions.
  4. Stateless Algorand Smart Contract: implements the Contract Account that approves the asset transfer transaction to the users if and only if the previous call to the Stateful component succeeds.

The integration between Stateless and Stateful ASC1 is exhaustively covered in this article, which should be considered as a fundamental prerequisite for the following solution.

Let’s examine each of those bricks gradually.

Stateful ASC1 component

The main purpose of the Stateful component is to store and read permanent on-chain information, according to predefined TEAL logic branches. In the context of the proposed recurring ASA withdrawal solution, the Stateful Application makes use of the following variables:

Name Domain Type
AssetEscrow Global ByteSlice
AssetID Global UInt
Creator Global ByteSlice
WithdrawalBookableAmount Global UInt
WithdrawalProcessingRounds Global UInt
WithdrawalBookedAmount Local UInt
WithdrawalBookingRound Local UInt

Stateful Applications are composed by two distinct TEAL programs:

  1. Approval Program
  2. Clear Program

Their roles are described in the lifecycle of a Stateful Smart Contract.

Clear Program

In the proposed solution the Clear Program will simply approve any clear call from the users to remove the application from their accounts balance.

#pragma version 2
int 1
Int(1)

Approval Program

The Approval Program is the TEAL logic that really define the application behaviour. The interaction between users accounts and Stateful applications is governed by the following five types of application calls:

  1. NoOp
  2. OptIn
  3. DeleteApplication
  4. UpdateApplication
  5. CloseOut

The Stateful Smart Contract should be able to detect any application call type and execute specific branches of TEAL logic accordingly. Other than these five application call types, it is also useful to catch the application create condition, in order to implement a specific branch of logic executed only on the application create transaction.

The Stateful ASC1 boilerplate is a great structured template that handles the required TEAL branches:

#pragma version 2
txn ApplicationID
int 0
==
bnz branch_create
txn OnCompletion
int OptIn
==
bnz branch_opt_in
txn OnCompletion
int CloseOut
==
bnz branch_close_out
txn OnCompletion
int UpdateApplication
==
bnz branch_update
txn OnCompletion
int DeleteApplication
==
bnz branch_delete
txn OnCompletion
int NoOp
==
bnz branch_app_calls
int 0
return
program = Cond(
  [Txn.application_id() == Int(0), on_creation],
  [Txn.on_completion() == OnComplete.OptIn, handle_optin],
  [Txn.on_completion() == OnComplete.CloseOut, handle_closeout],
  [Txn.on_completion() == OnComplete.UpdateApplication, handle_updateapp],
  [Txn.on_completion() == OnComplete.DeleteApplication, handle_deleteapp],
  [Txn.on_completion() == OnComplete.NoOp, handle_noop]
)

Let’s go through each TEAL branch to understand the application’s behaviour.

Application Create

Usually the application create is the branch in which variables initialization takes place.

branch_create:
byte "Creator"
txn Sender
app_global_put
int 1
return
b end_program
on_creation = Seq([
  App.globalPut(Bytes("Creator"), Txn.sender()),
  Return(Int(1))
])

On application create transaction the global variable Creator is initialized with the transaction sender, so that further management actions over the application can be authorized only by the Creator.

Users Opt-In

In the context of this solution there are no specific restrictions to the application opt-in. Note that the TEAL logic in the opt-in branch can be really useful when the application needs some kind of permission on accounts participation.

branch_opt_in:
int 1
return
b end_program
handle_optin = Return(Int(1))

Users Close-Out

In the context of this solution the colse-out condition does not need any kind of specific restriction, so it will always be simply approved by the TEAL logic.

branch_close_out:
int 1
return
b end_program
handle_closeout = Return(Int(1))

Application Update

The approval program of a Stateful Smart Contract could potentially be updated, unless expressly prevented. Although updating a Smart Contract may seem almost contradictory for someone, this feature expands ASC1 capabilities for those use case who need it and should be handled very carefully. This application does not allow any kind of approval program update, not even from its Creator, rejecting any application update call.

branch_update:
int 0
return
b end_program
handle_updateapp = Return(Int(0))

Application Delete

A Stateful Smart Contract could potentially be deleted, unless expressly prevented. As well as the updating feature, deleting a Stateful ASC1 should be handled very carefully. This application only allows its Creator to delete it.

branch_delete:
byte "Creator"
app_global_get
txn Sender
==
bnz is_creator
int 0
return
b end_program
is_creator:
int 1
return
b end_program
handle_deleteapp = If(
  # Condition
  App.globalGet(Bytes("Creator")) == Txn.sender(),
  # Then
  Return(Int(1)),
  # Else
  Return(Int(0))
)

Application Calls

The application calls trigger the main parts of a Stateful ASC1 approval program. In this solution 3 different actions can be executed, based on Atomic Transfer contents:

  1. Withdrawal Set-Up: if application is called by its Creator;
  2. Booking Withdrawal: if application is called passing "str:Booking" as argument;
  3. Withdrawal: if application is called passing "str:Withdrawal" as argument;
branch_app_calls:
global GroupSize
int 2
==
byte "Creator"
app_global_get
gtxn 0 Sender
==
&&
bnz branch_withdrawal_setup
global GroupSize
int 2
==
gtxna 0 ApplicationArgs 0
byte "Booking"
==
&&
bnz branch_booking
global GroupSize
int 2
==
gtxna 0 ApplicationArgs 0
byte "Withdrawal"
==
&&
bnz branch_withdrawal
int 0
return
handle_noop = Cond(
  [And(
    Global.group_size() == Int(2),
    App.globalGet(Bytes("Creator")) == Gtxn[0].sender()
  ), withdrawal_setup],
  [And(
    Global.group_size() == Int(2),
    Gtxn[0].application_args[0] == Bytes("Booking")
  ), booking],
  [And(
    Global.group_size() == Int(2),
    Gtxn[0].application_args[0] == Bytes("Withdrawal")
  ), withdrawal]
)

Application Call 1: Withdrawal Set-Up

Application Creator can set up the following global variables executing an Atomic Transfer, composed by an Application Call transaction and an Asset Transfer transaction:

  1. Asset Escrow Contract Account, passed as application call 0 argument, that is the public key obtained compiling the Stateless Asset Escrow Contract Account TEAL source code.
  2. Withdrawal Processing Rounds, passed as application call 1 argument, that is the number of blocks users must wait at least before executing booked withdrawals.
  3. AssetID, read from asset transfer funding transaction, is the Algorand Standard Asset unique identifier for the Asset Escrow.
  4. Withdrawal Bookable Amount, read from asset transfer funding transaction, is the total bookable amount of Algorand Standard Asset deposited in the Asset Escrow.
branch_withdrawal_setup:
gtxn 0 NumAppArgs
int 2
==
gtxn 1 TypeEnum
int axfer
==
&&
gtxn 1 AssetReceiver
gtxna 0 ApplicationArgs 0
==
&&
gtxn 1 AssetAmount
int 0
>
&&
bnz withdrawal_setup
int 0
return
b end_program
withdrawal_setup:
byte "AssetEscrow"
gtxna 0 ApplicationArgs 0
app_global_put
byte "WithdrawalProcessingRounds"
gtxna 0 ApplicationArgs 1
btoi
app_global_put
byte "AssetID"
gtxn 1 XferAsset
app_global_put
byte "WithdrawalBookableAmount"
gtxn 1 AssetAmount
app_global_put
int 1
return
withdrawal_setup = If(
  # Condition
  And(
    Gtxn[0].application_args.length() == Int(2),
    Gtxn[1].type_enum() == TxnType.AssetTransfer,
    Gtxn[1].asset_receiver() == Gtxn[0].application_args[0],
    Gtxn[1].asset_amount() > Int(0)
  ),
  # Then
  Seq([
    App.globalPut(Bytes("AssetEscrow"),
                  Gtxn[0].application_args[0]),
    App.globalPut(Bytes("WithdrawalProcessingRounds"),
                  Btoi(Gtxn[0].application_args[1])),
    App.globalPut(Bytes("AssetID"),
                  Gtxn[1].xfer_asset()),
    App.globalPut(Bytes("WithdrawalBookableAmount"),
                  Gtxn[1].asset_amount()),
    Return(Int(1))
  ]),
  # Else
  Return(Int(0))
)

Application Call 2: Booking Withdrawals

Users can book ASA withdrawals executing an Atomic Transfer, composed by:

  1. Application Call, with 0 argument "str:Booking", that registers withdrawal booking block in local state.
  2. Asset Transfer transaction, that deposits the booked amount of ASA in the Asset Escrow.

The approval program checks if users already booked a withdrawal, preventing double-booking. If booking attempt succeeds, the global bookable amount is then reduced, preventing withdrawals over-booking.

branch_booking:
int 0
gtxn 0 ApplicationID
byte "WithdrawalBookingRound"
app_local_get_ex
store 0
store 1
load 0
load 1
int 0
>
&&
bnz booking_failure
gtxn 1 TypeEnum
int axfer
==
gtxn 1 XferAsset
byte "AssetID"
app_global_get
==
&&
gtxn 1 Sender
gtxn 0 Sender
==
&&
gtxn 1 AssetReceiver
byte "AssetEscrow"
app_global_get
==
&&
gtxn 1 AssetAmount
byte "WithdrawalBookableAmount"
app_global_get
<=
&&
bnz booking
int 0
return
booking:
int 0
byte "WithdrawalBookingRound"
global Round
app_local_put
int 0
byte "WithdrawalBookedAmount"
gtxn 1 AssetAmount
app_local_put
byte "WithdrawalBookableAmount"
byte "WithdrawalBookableAmount"
app_global_get
gtxn 1 AssetAmount
-
app_global_put
int 1
return
b end_booking
booking_failure:
int 0
return
end_booking:
int 1
return
withdrawal_booking_round = App.localGetEx(
  Int(0), Gtxn[0].application_id(), Bytes("WithdrawalBookingRound")
)

booking = Seq([
  withdrawal_booking_round,
  If(
    # Condition
    And(
      withdrawal_booking_round.hasValue(),
      withdrawal_booking_round.value() > Int(0)
    ),
    # Then
    Return(Int(0)),
    # Else
    Seq([
      Assert(
        And(
          Gtxn[1].type_enum() == TxnType.AssetTransfer,
          Gtxn[1].xfer_asset() == App.globalGet(
            Bytes("AssetID")),
          Gtxn[1].sender() == Gtxn[0].sender(),
          Gtxn[1].asset_receiver() == App.globalGet(
            Bytes("AssetEscrow")),
          Gtxn[1].asset_amount() <= App.globalGet(
            Bytes("WithdrawalBookableAmount"))
        )
      ),
      App.localPut(Int(0), Bytes("WithdrawalBookingRound"),
                   Global.round()),
      App.localPut(Int(0), Bytes("WithdrawalBookedAmount"),
                   Gtxn[1].asset_amount()),
      App.globalPut(Bytes("WithdrawalBookableAmount"),
                    App.globalGet(Bytes("WithdrawalBookableAmount"))
                    - Gtxn[1].asset_amount()),
      Return(Int(1))
    ])
  ),
  Return(Int(1))
])

Depositing booked ASA amount prevents double booking while registering withdrawal booking block prevents malicious users from exploiting opt-in/close-out actions against the Application. In fact, if not properly addressed, malicious users can try to opt-in the application, execute the first withdrawal and then close-out the application clearing their local state, being potentially able to bypass the duration of withdrawal processing interval and executing multiple withdrawals in series. The booking withdrawals logic ensures that users will always have to wait at least the withdrawal processing interval duration before being able to execute the further withdrawals, avoiding the bypass flaw.

Tips

Whenever an application makes use of users local state, be sure that a sequence of opt-in/close-out (or clear state) cannot be exploited as a way to bypass your application logic in undesired ways.

Application Call 3: Withdrawal

Users who already booked a withdrawal can withdraw ASA executing an Atomic Transfer, composed by:

  1. Application Call, with 0 argument "str:Withdrawal", that verifies users’ booked withdrawals have been already processed at the round of application call transaction.
  2. Asset Transfer transaction, that verifies users’ withdrawal amount is double the amount of ASA deposited in the Asset Escrow.

If withdrawal attempt succeed users can book a new withdrawal.

branch_withdrawal:
int 0
gtxn 0 ApplicationID
byte "WithdrawalBookingRound"
app_local_get_ex
store 0
store 1
int 0
gtxn 0 ApplicationID
byte "WithdrawalBookedAmount"
app_local_get_ex
store 2
store 3
int 0
gtxn 0 ApplicationID
app_opted_in
load 1
int 0
>
&&
load 3
int 0
>
&&
global Round
int 0
byte "WithdrawalBookingRound"
app_local_get
byte "WithdrawalProcessingRounds"
app_global_get
+
>=
&&
gtxn 1 XferAsset
byte "AssetID"
app_global_get
==
&&
gtxn 1 Sender
byte "AssetEscrow"
app_global_get
==
&&
gtxn 1 AssetAmount
load 3
int 2
*
==
&&
bnz withdrawal
int 0
return
withdrawal:
int 0
byte "WithdrawalBookingRound"
int 0
app_local_put
int 0
byte "WithdrawalBookedAmount"
int 0
app_local_put
int 1
return
end_program:
withdrawal_approval_round = Ge(
  Global.round(),
  App.localGet(Int(0), Bytes("WithdrawalBookingRound")) +
  App.globalGet(Bytes("WithdrawalProcessingRounds"))
)

withdrawal_booked_amount = App.localGetEx(
  Int(0), Gtxn[0].application_id(), Bytes("WithdrawalBookedAmount")
)

withdrawal = Seq([
  withdrawal_booking_round,
  withdrawal_booked_amount,
  Assert(
    And(
      App.optedIn(Int(0), Gtxn[0].application_id()),
      withdrawal_booking_round.value() > Int(0),
      withdrawal_booked_amount.value() > Int(0),
      withdrawal_approval_round,
      Gtxn[1].xfer_asset() == App.globalGet(Bytes("AssetID")),
      Gtxn[1].sender() == App.globalGet(Bytes("AssetEscrow")),
      Gtxn[1].asset_amount() == Mul(
        withdrawal_booked_amount.value(), Int(2)
      )
    )
  ),
  App.localPut(Int(0), Bytes("WithdrawalBookingRound"), Int(0)),
  App.localPut(Int(0), Bytes("WithdrawalBookedAmount"), Int(0)),
  Return(Int(1))
])

Stateless ASC1 component

The main purpose of the Stateless component is to approve or reject transactions, according to predefined TEAL logic branches. In the context of the proposed recurring ASA withdrawal solution, the Stateless Escrow Contract Account approves or rejects the following transactions:

Name Type
Escrow Contract ASA Opt-In Single Transaction
Escrow Contract ASA withdrawal by Stateful Application Users Group Transaction
#pragma version 2
global GroupSize
int 1
==
bnz branch_opt_in
global GroupSize
int 2
==
bnz branch_withdrawal
int 0
return
program = Cond(
  [Global.group_size() == Int(1), asa_opt_in],
  [Global.group_size() == Int(2), asa_withdraw]
)

While the Stateful Component’s parameters can be updated over time passing arguments through application call transactions (as the Asset Escrow Contract Account address or the Withdrawal Processing Rounds), the Stateless Component’s parameters must be hardcoded into the TEAL logic, so must be know in advance. For the Escrow Contract Account these parameter must be know:

  1. TMPL_APP_ID
  2. TMPL_ASSET_ID

Tips

Read carefully the guidelines before stating developing your Stateless ASC1.

Escrow Contract Account ASA Opt-In or Close-Out

The Asset Escrow Contract Account only approves ASA opt-in transaction for one specific AssedID, which must be equal to the AssetID global variable of the Stateful Component.

branch_opt_in:
txn TypeEnum
int axfer
==
txn XferAsset
int TMPL_ASSET_ID
==
&&
txn AssetAmount
int 0
==
&&
txn Fee
int 1000
<=
&&
txn RekeyTo
global ZeroAddress
==
&&
txn AssetCloseTo
global ZeroAddress
==
&&
b end_contract
fee = Int(1000)

asa_opt_in = And(
  Txn.type_enum() == TxnType.AssetTransfer,
  Txn.xfer_asset() == Int(asa_id),
  Txn.asset_amount() == Int(0),
  Txn.fee() <= fee,
  Txn.rekey_to() == Global.zero_address(),
  Txn.asset_close_to() == Global.zero_address(),
)

Escrow Contract Account withdrawal

The Asset Escrow Contract Account only approves ASA withdrawals of AssedID, if and only if withdrawals transactions are submitted as Atomic Transfer, composed by of 2 transactions, that:

  1. Calls the Stateful Withdrawal Application, ensuring that only processed withdrawals are approved.
  2. Transfers the right amount of ASA from the Asset Escrow Contract Account to the User.
branch_withdrawal:
gtxn 0 TypeEnum
int appl
==
gtxn 0 ApplicationID
int TMPL_APP_ID
==
&&
gtxn 0 OnCompletion
int NoOp
==
&&
gtxn 1 TypeEnum
int axfer
==
&&
gtxn 1 XferAsset
int TMPL_ASSET_ID
==
&&
gtxn 1 Fee
int 1000
<=
&&
gtxn 1 AssetCloseTo
global ZeroAddress
==
&&
gtxn 1 RekeyTo
global ZeroAddress
==
&&
end_contract:
asa_withdraw = And(
  Gtxn[0].type_enum() == TxnType.ApplicationCall,
  Gtxn[0].application_id() == Int(app_id),
  Gtxn[0].on_completion() == OnComplete.NoOp,
  Gtxn[1].type_enum() == TxnType.AssetTransfer,
  Gtxn[1].xfer_asset() == Int(asa_id),
  Gtxn[1].fee() <= fee,
  Gtxn[1].asset_close_to() == Global.zero_address(),
  Gtxn[1].rekey_to() == Global.zero_address()
)

Application Deployment

The application is deployed constructing and issuing transactions manually through goal CLI, according a specific timeline. As mentioned in the overview, the application could be deployed programmatically using any Algorand SDK instead of goal CLI, consider this as your next step exercise.

Creating Stateful ASA Withdrawal Application

The recurring withdrawal dApp deployment starts with the creation of its Stateful component, providing the both the withdrawal_approval.teal and the withdrawal_clear.teal and declaring the global state schema and the local state schema consumed by the application.

Input

$ ./goal app create --creator TMPL_CREATOR_ADDRESS --approval-prog withdrawal_approval.teal --clear-prog withdrawal_clear.teal --global-byteslices 2 --global-ints 3 --local-byteslices 0 --local-ints 2

Output

Created app with app index TMPL_APP_ID

Querying existing applications on the testing private network with the Indexer we get an overview of its properties:

Input

curl "localhost:8980/v2/applications?pretty"

Output

{
  "application": {
    "created-at-round": ...,
    "id": TMPL_APP_ID,
    "params": {
      "approval-program": "...",
      "clear-state-program": "...",
      "creator": "TMPL_CREATOR_ADDRESS",
      "global-state": [
        {
          "key": "Q3JlYXRvcg==",
          "value": {
            "bytes": "pZwQR2IXbUaqt+fGQ+cmcuVXGz/W407o0AsCi6DA+Fg=",
            "type": 1,
            "uint": 0
          }
        }
      ],
      "global-state-schema": {
        "num-byte-slice": 2,
        "num-uint": 3
      },
      "local-state-schema": {
        "num-byte-slice": 0,
        "num-uint": 2
      }
    }
  },
  "current-round": ...
}

Then, reading the application global sate, we can check that the Creator global variable has been correctly initialized.

Input

$ ./goal app read --app-id TMPL_APP_ID --global

Output

{
  "Creator": {
    "tb": "TMPL_CREATOR_ADDRESS",
    "tt": 1
  }
}% 

Initializing Stateless Asset Escrow Contract Account

We can now assign values to Stateless ASC1 parameters, hard-coding them in its TEAL source code:

  1. TMPL_ASSET_ID
  2. TMPL_APP_ID

and compile the withdrawal_escrow.teal

Input

$ ./goal clerk compile withdrawal_escrow.teal

Output

withdrawal_escrow.teal: TMPL_ESCROW_CONTRACT_ADDRESS

As any other account on Algorand, also Contracts Accounts must be activated funding them at least with a minimum balance of 0,1 ALGO. Let’s fund it with 0,3 ALGO.

Input

$ ./goal clerk send -f TMPL_CREATOR_ADDRESS -t TMPL_ASSET_ESCROW_ADDRESS -a 300000

Once the Asset Escrow Contract Address has been funded, it can now opt-in the ASA (the only one allowed by definition in its TEAL logic). Transactions that involve Stateless ASC1 must be signed with Logic Signature, it means that we have to:

write the unsigned transaction on file

Input

$ ./goal asset send --assetid TMPL_ASSET_ID -f TMPL_ASSET_ESCROW_ADDRESS -t TMPL_ASSET_ESCROW_ADDRESS -a 0 -o escrow_optin.txn

sign it providing its TEAL logic

Input

$ ./goal clerk sign -i escrow_optin.txn -p withdrawal_escrow.teal -o escrow_optin.ltxn

submit it to the network

Input

$ ./goal clerk rawsend -f escrow_optin.ltxn

Once the transaction has been committed, we can see its effect reading the Asset Escrow Contract account info:

Input

$ ./goal account info -a TMPL_ESCROW_CONTRACT_ADDRESS

Output

Created Assets:
    <none>
Held Assets:
    ID TMPL_ASSET_ID, Test, balance 0.0 TST
Created Apps:
    <none>
Opted In Apps:
    <none>

We are finally ready to fund the Asset Escrow Contract with an amount of ASA in order to back users withdrawals.

Setting Up Stateful ASA Withdrawal Application

The application can be setted up only by its Creator, who must:

  1. Provide application call arguments in this specific order: "addr:TMPL_ASSET_ESCROW_ADDRESS", "int:TMPL_WITHDRAWALS_PROCESSING_BLOCKS"
  2. Fund the Asset Escrow with the total bookable amount of ASA.

Unsigned standalone transactions creation

$ ./goal app call --app-id TMPL_APP_ID -f TMPL_CREATOR_ADDRESS --app-arg "addr:TMPL_ASSET_ESCROW_ADDRESS" --app-arg "int:TMPL_WITHDRAWALS_PROCESSING_BLOCKS" -o withdrawal_app_call.txn

$ ./goal asset send --assetid TMPL_ASSET_ID -f TMPL_CREATOR_ADDRESS -t TMPL_ASSET_ESCROW_ADDRESS -a TMPL_BOOKABLE_AMOUNT -o escrow_funding.txn

Grouping unsigned standalone transactions

$ cat withdrawal_app_call.txn escrow_funding.txn > setup.txn

$ ./goal clerk group -i setup.txn -o setup.gtxn

Splitting unsigned group transaction, transactions are no longer valid if submitted as standalone.

Input

$ ./goal clerk split -i setup.gtxn -o unsigned_setup.txn

Output

Wrote transaction 0 to unsigned_setup-0.txn
Wrote transaction 1 to unsigned_setup-1.txn

Signing standalone transactions

$ ./goal clerk sign -i unsigned_setup-0.txn -o setup-0.stxn

$ ./goal clerk sign -i unsigned_setup-1.txn -o setup-1.stxn

Grouping signed standalone transactions, they can be committed only together.

$ cat setup-0.stxn setup-1.stxn > setup.sgtxn

Submitting signed group transaction

$ ./goal clerk rawsend -f setup.sgtxn

Reading the application global sate, we can now check that the other global variables have been correctly initialized.

Input

$ ./goal app read --app-id TMPL_APP_ID --global

Output

{
  "AssetEscrow": {
    "tb": "TMPL_ASSET_ESCROW_ADDRESS",
    "tt": 1
  },
  "AssetID": {
    "tt": 2,
    "ui": TMPL_ASSET_ID
  },
  "Creator": {
    "tb": "TMPL_CREATOR_ADDRESS",
    "tt": 1
  },
  "WithdrawalBookableAmount": {
    "tt": 2,
    "ui": TMPL_BOOKABLE_AMOUNT
  },
  "WithdrawalProcessingRounds": {
    "tt": 2,
    "ui": TMPL_WITHDRAWALS_PROCESSING_BLOCKS
  }
}

The recurring withdrawal application is now fully deployed and ready to be used.

Application Usage

From users perspective ASA recurring withdrawals dApp consists of the following 3 steps:

  1. Withdrawal Application Opt-In
  2. Withdrawal Booking
  3. Withdrawal

Each one of those requires the submission of different kind of transaction or group transactions.

User Application Opt-In

Any Stateful application that makes use of account’s local state must be opted-in by users in order to be able to interact with them. Application opt-in is a specific kind of application transaction call. In the context of this solution the approval program does not perform any kind of verification on opt-in application calls, so anyone can opt-in:

Input

$ ./goal app optin --app-id TMPL_APP_ID -f TMPL_USER_ADDRESS

User Account’s state has been updated with Stateful application local sate schema that consists only of two uint variables.

Input

$ ./goal account info -a TMPL_USER_ADDRESS

Output

Created Assets:
    <none>
Held Assets:
    ID TMPL_ASSET_ID, Test, balance 10.0 TST
Created Apps:
    <none>
Opted In Apps:
    ID TMPL_APP_ID, local state used 0/2 uints, 0/0 byte slices

Withdrawal Booking

Users have to book their withdrawals in advance and wait until they have been processed before being able to actually execute them. Users book a withdrawal through an Atomic Transfer, composed by an Application Call transaction (with argument "str:Booking") and an Asset Transfer transaction with which they lock the booked amount into the Asset Escrow.

Unsigned standalone transactions creation

$ ./goal app call --app-id TMPL_APP_ID -f TMPL_USER_ADDRESS --app-arg "str:Booking" -o withdrawal_booking.txn

$ ./goal asset send --assetid TMPL_ASSET_ID -f TMPL_USER_ADDRESS -t TMPL_ASSET_ESCROW -a TMPL_BOOKED_AMOUT -o withdrawal_locking.txn

Grouping unsigned standalone transactions

$ cat withdrawal_booking.txn withdrawal_locking.txn > booking.txn

$ ./goal clerk group -i booking.txn -o booking.gtxn

Splitting unsigned group transaction, transactions are no longer valid if submitted as standalone.

Input

$ ./goal clerk split -i booking.gtxn -o unsigned_booking.txn

Output

Wrote transaction 0 to unsigned_booking-0.txn
Wrote transaction 1 to unsigned_booking-1.txn

Signing standalone transactions

$ ./goal clerk sign -i unsigned_booking-0.txn -o booking-0.stxn

$ ./goal clerk sign -i unsigned_booking-1.txn -o booking-1.stxn

Grouping signed standalone transactions, they can be committed only together.

$ cat booking-0.stxn booking-1.stxn > booking.sgtxn

Submitting signed group transaction

$ ./goal clerk rawsend -f booking.sgtxn

Once the Atomic Transfer has been committed the WITHDRAWAL_BOOKING_BLOCK and TMPL_BOOKED_AMOUT are saved into User’s local state and the ASA TMPL_BOOKED_AMOUT deposited into the Asset Escrow until the withdrawal.

Input

$ ./goal app read --app-id TMPL_APP_ID --local -f TMPL_USER_ADDRESS

Output

{
  "WithdrawalBookedAmount": {
    "tt": 2,
    "ui": TMPL_BOOKED_AMOUT
  },
  "WithdrawalBookingRound": {
    "tt": 2,
    "ui": WITHDRAWAL_BOOKING_BLOCK
  }
}

The total bookable amount is now reduced, you can check it with

Input

$ ./goal app read --app-id TMPL_APP_ID --global

The user must now wait TMPL_WITHDRAWALS_PROCESSING_BLOCKS to claim the booked withdrawal.

Withdrawal

Withdrawals are executed as a group transaction, consisting of a Stateful Application call transaction and Stateless Asset Escrow booked amount transfer.

Unsigned standalone transactions creation

$ ./goal app call --app-id TMPL_APP_ID -f TMPL_USER_ADDRESS --app-arg "str:Withdrawal" -o withdrawal_app_call.txn

$ ./goal asset send --assetid TMPL_ASSET_ID -f TMPL_ASSET_ESCROW_ADDRESS -t TMPL_USER_ADDRESS -a DOUBLE_TMPL_BOOKED_AMOUT -o withdrawal_escrow_transfer.txn

Grouping unsigned standalone transactions

$ cat withdrawal_app_call.txn withdrawal_escrow_transfer.txn > withdrawal.txn

$ ./goal clerk group -i withdrawal.txn -o withdrawal.gtxn

Splitting unsigned group transaction, transactions are no longer valid if submitted as standalone.

Input

$ ./goal clerk split -i withdrawal.gtxn -o unsigned_withdrawal.txn

Output

Wrote transaction 0 to unsigned_withdrawal-0.txn
Wrote transaction 1 to unsigned_withdrawal-1.txn

Signing standalone transactions

$ ./goal clerk sign -i unsigned_withdrawal-0.txn -o withdrawal-0.stxn

$ ./goal goal clerk sign -i unsigned_withdrawal-1.txn -p withdrawal_escrow.teal -o withdrawal-1.ltxn

Grouping signed standalone transactions, they can be committed only together.

$ cat withdrawal-0.stxn withdrawal-1.ltxn > withdrawal.sgtxn

Submitting signed group transaction

$ ./goal clerk rawsend -f withdrawal.sgtxn

Once the Atomic Transfer as been committed, the user withdraws from the Asset Escrow both the amount locked on booking process plus the booked amount.

Conclusions

You are now able to create and deploy your own ASA recurring withdrawal dApp, customizing the withdrawal amount approval with the logic that best fit your use case.

dapp

algorand standard assets

asc1

atomic transfer

pyteal

goal

teal

stateful smart contracts

February 22, 2021