Solutions
No Results
Solution

MiniBond Issuance with PyTeal and Python SDK

Overview

This solution can be divided into 3 major components:

  • Utility functions
  • Configurations before publishing a new bond contract
  • PyTeal logic that supports interactions with the bond contract and transaction functions that encapsulates common transactions such as purchase of bond, claiming of interest and principles, and claiming of funds in the escrow account.

Warning

For sake of simplicity, functions in this solution directly accept mnemonics to process transactions, it is to be noted that in production a key managing tool such as AlgoSigner or MyAlgo API should be used for such purposes.

Utility Functions

The following codes list some utility functions common to all programs that follow:

# connects to and returns an algod client
def algod_client():
    algod_address = "YOUR ALGOD ADDRESS"
    algod_token = ""
    api_key = "YOUR API-KEY"
    headers = {
        "X-API-KEY": api_key,
    }
    client = algod.AlgodClient(algod_token, algod_address, headers)
    return client

# sends a payment transaction
def payment_transaction(passphrase, amt, rcv, algod_client)->dict:
    params = algod_client.suggested_params()
    add = mnemonic.to_public_key(passphrase)
    key = mnemonic.to_private_key(passphrase)
    params.flat_fee = True
    params.fee = 1000
    unsigned_txn = PaymentTxn(add, params, rcv, amt)
    signed = unsigned_txn.sign(key)
    txid = algod_client.send_transaction(signed)
    pmtx = wait_for_confirmation(algod_client, txid, 4)
    return pmtx

# sends an asset transfer transaction 
def asset_transaction(passphrase, amt, rcv, asset_id, algod_client)->dict:
    params = algod_client.suggested_params()
    add = mnemonic.to_public_key(passphrase)
    key = mnemonic.to_private_key(passphrase)
    params.flat_fee = True
    params.fee = 1000
    unsigned_txn = AssetTransferTxn(add, params, rcv, amt, asset_id)
    signed = unsigned_txn.sign(key)
    txid = algod_client.send_transaction(signed)
    pmtx = wait_for_confirmation(algod_client, txid, 4)
    return pmtx

Pre-Publishing Configurations

Representation of a bond

A bond is a fixed income instrument that represents a loan made by an investor (bond holder) to a borrower (bond issuer). It is characterized by a series of typically fixed periodic interest payment (known as coupon payment) and a repayment of face value (also known as par value) at the maturity date. In this solution, a bond contract will be represented with 2 tokens issued at the discretion of the bond publisher: an Interest Token and a Par Value Token, both implemented as Algorand Standardized Assets (ASA). For each unit of a bond paying 10 total interest payments, a buyer will be rewarded with 1 Par Value Token, and 10 Interest Tokens (1 to be spent at each interest claim). The reasons for this design will be discussed here.

Now that we understand the basic logic behind a bond contract, we make use of AssetConfigTxn() to issue those tokens. We should also allow the bond issuer to make some customizations: proj_name for the publisher to give a name to the interest and par value tokens issued, vol for the total number of bonds issued, and total_payments for number of interest to be claimed. Below shows a sample function for Interest Token issuance:

def interest_token_issuance(algod_client, passphrase, proj_name, vol, url, total_payments) -> (int, int):
    params = algod_client.suggested_params()
    params.fee = 1000
    params.flat_fee = True
    address = mnemonic.to_public_key(passphrase)
    key = mnemonic.to_private_key(passphrase)
    txn = AssetConfigTxn(sender= address, sp=params, total=total_payments * vol,
                         default_frozen=False, unit_name=proj_name[0] + "IC", asset_name=proj_name + "Interest",
                         manager=address, reserve=address, freeze=address, clawback=address, url=url, decimals=0)
    signed = txn.sign(key)
    txid = algod_client.send_transaction(signed)
    wait_for_confirmation(algod_client, txid, 4)
    try:
        ptx = algod_client.pending_transaction_info(txid)
        asset_id = ptx["asset-index"]
        return txid, asset_id
    except Exception as e:
        print(e)

The logic for issuance of a Par Value Token is similar. After those two tokens have been created and confirmed on block, we will get two asset-ids that will be used to create an escrow account.

Creation of Escrow Account

An Escrow Account is essentially a TEAL/PyTeal logic that gains full control over an account: for every transaction that involves a fund to leave the account, the logic is automatically evoked and evaluated to approve or reject the request. In this solution, an escrow account is created for each new bond to be published: after the issuer specifies the interest id and par value id (as created above) and a couple of other parameters, a new TEAL logic is compiled based on a template and submitted to the blockchain.

def create_escrow(mgr_add, proj_name, interest_id, par_id, payment_id, closure,
                  begin_round, end_round, par, coupon, total_payments, period, span, holdup, algod_client) -> (str, str):

    compile(mgr_add, interest_id, par_id, payment_id, closure, par,
                      coupon, holdup, begin_round, end_round, total_payments,
                      period, span, proj_name)
    raw_teal = "./teal/{}.teal".format(proj_name)
    data = open(raw_teal, 'r').read()
    try:
        response = algod_client.compile(data)
        return response['result'], response['hash']
    except Exception as e:
        return str(e)

Here the mgr_add should be set to the bond issuer. The interest_id and par_id fields should correspond with the assets just generated. payment_id specifies the asset-id of the stablecoin accepted as method of payment in this bond, and the par and coupon fields should both be specified in units of payment_id. closure specifies the last round before which a buyer can opt-in the bond contract. begin_round and end_round are the first round and the last round of interest payment. After end_round, a buyer is also allowed to claim his / her par value. The period field specifies the number of blocks a buyer has to wait between 2 adjacent interest payments. span specifies the longest period an interest is available for claim and holdup specifies the first round after which the bond issuer can claim funds out of the escrow account. The compile() function takes in those arguments and compiles the PyTeal logic into a proj_name.teal file stored in the teal folder. The algod client then compiles the teal code and returns 2 strings: the first result field returns the bytes of the program that can be later used in the LogicSig() method, and the hash field returns the public address for the escrow account for the buyer and the issuer to interact with.

PyTeal Logic & Utility Functions

We now turn to the actual logic used in the PyTeal contract and transaction functions. These can be divided into roughly 7 different sections:

  • the escrow account opts-in Interest Tokens
  • the escrow account opts-in Par Value Tokens
  • the escrow account opts-in Payment Tokens
  • the bond issuer claims fund out of the escrow account
  • the buyer purchases bond from the escrow account
  • the buyer claims periodic interest payments
  • the buyer claims par values

The escrow will be switched to the different logics by a parameter passed in when calling the contract, as follows:

program = Cond(
        [Btoi(Arg(0)) == Int(0), optInInterest],
        [Btoi(Arg(0)) == Int(1), optInPar],
        [Btoi(Arg(0)) == Int(2), optInPayment],
        [Btoi(Arg(0)) == Int(3), claim],
        [Btoi(Arg(0)) == Int(4), purchase],
        [Btoi(Arg(0)) == Int(5), interestPayment],
        [Btoi(Arg(0)) == Int(6), parPayment],
    )

Now we turn to how each of those sections are implemented.

Activation and opt-ins

First we issue a payment transaction from issuer to the escrow account to activate the escrow account, we make use of the payment_transaction utility function defined above:

# activating escrow account
print("--------------------------------------------")
print("Activating escrow account......")
try:
    txn = payment_transaction(passphrase, 1000000, escrow_id, cl)
except Exception as e:
    print("Activation failed :{}".format(e))
    return
print("Activated successfully")
print(txn)
print("--------------------------------------------")

Before the escrow account can process transactions relating to Interest Tokens, Par Value Tokens and Payment, it first needs to opt-in those assets. Since opt-in transactions on an account require an asset transfer to itself at the amount of 0, these transactions will also be evaluated by the TEAL logic. These are implemented in PyTeal as follows:

# for EscrowAccount to receive payments in opt-in interest token
# arg_id = 0
optInInterest = And(
    Txn.sender() == Txn.asset_receiver(),
    Txn.type_enum() == TxnType.AssetTransfer,
    Txn.xfer_asset() == Int(interest_id),
    Txn.asset_amount() == Int(0),
    Txn.rekey_to() == Global.zero_address(),
    Txn.asset_close_to() == Global.zero_address(),
    Txn.fee() <= max_fee
)

# for EscrowAccount to receive payments in opt-in par-value token
# arg_id = 1
optInPar = And(
    Txn.sender() == Txn.asset_receiver(),
    Txn.type_enum() == TxnType.AssetTransfer,
    Txn.xfer_asset() == Int(par_id),
    Txn.asset_amount() == Int(0),
    Txn.rekey_to() == Global.zero_address(),
    Txn.asset_close_to() == Global.zero_address(),
    Txn.fee() <= max_fee
)

# escrow account receives payment_id as payment
# arg_id = 2
optInPayment = And(
    Txn.sender() == Txn.asset_receiver(),
    Txn.type_enum() == TxnType.AssetTransfer,
    Txn.xfer_asset() == Int(accepted_payment),
    Txn.asset_amount() == Int(0),
    Txn.rekey_to() == Global.zero_address(),
    Txn.asset_close_to() == Global.zero_address(),
    Txn.fee() <= max_fee
)

Note that all parameters are passed in PyTeal in Byte() format, to interpret a value as an integer, we need explicit type casting with Int(). The rekey_to(), asset_close_to() and fee() specifications are part of a routine checking for any smart contract. More detials on this can be found here. After these logic has been implemented, we should be able to have the escrow opt-in those assets. Below shows the sample code for the escrow account to opt-in the Interest Tokens:

# opt-in the escrow account for interest token
print("--------------------------------------------")
print("Opt-in for interest token......")
try:
    program_str = escrow_result.encode()
    program = base64.decodebytes(program_str)
    arg1 = (0).to_bytes(8, 'big')
    lsig = LogicSig(program, [arg1])
    sp = cl.suggested_params()
    atn = AssetTransferTxn(lsig.address(), sp, lsig.address(), 0, interest_id)
    lstx = LogicSigTransaction(atn, lsig)
    txid = cl.send_transaction(lstx)
    msg = wait_for_confirmation(cl, txid, 5)
except Exception as e:
    print("Opt-in interest token failed :{}".format(e))
    return
print("Opt-in interest token success!")
print(msg)
print("--------------------------------------------")

Notice that the field program_str is the bytes of the TEAL logic compiled on client. The logic for Par Value Tokens and Payment Tokens is similar. Next we will have the bond issuer transfer all the Interest Tokens and Par Value Tokens to fund the escrow account as these tokens will be transferred from the escrow account to buyers when they purchase bonds:

# transferring the interest tokens to escrow account
print("--------------------------------------------")
print("Transfer interest token to escrow account......")
try:
    atn = asset_transaction(passphrase, vol * total_payments, escrow_id, interest_id, cl)
except Exception as e:
    print("Transferred interest token failed :{}".format(e))
    return
print("Transferred interest token successfully")
print(atn)
print("--------------------------------------------")

# transferring the par tokens to escrow account
print("--------------------------------------------")
print("Transfer par token to escrow account......")
try:
    atn = asset_transaction(passphrase, vol, escrow_id, par_id, cl)
except Exception as e:
    print("Transferred par token failed :{}".format(e))
    return
print("Transferred par token successfully")
print(atn)
print("--------------------------------------------")
print("Setup-complete!")

Now that the set up is complete, when now turn to how the escrow account can handle various transaction requests from bond issuer and bond holders.

Claim of fund

The sale of bonds is a means of financing for the bond issuer. Therefore, we should allow for the bond issuer to claim funds out of the escrow account to finance his / her projects. In this solution, we only require that the issuer claims funds after a certain holdup period. But feel free to customize any further constraints that you would like to propose for the issuer to claim funds. (Like for example, the remaining funds in the escrow account should always be able to cover 2 interest payments.)

# arg_id = 3
claim = And(
    Txn.receiver() == Addr(mgr_add),
    Txn.type_enum() == TxnType.AssetTransfer,
    Txn.xfer_asset() == Int(accepted_payment),
    Txn.first_valid() > Int(holdup_period),
    Txn.rekey_to() == Global.zero_address(),
    Txn.asset_close_to() == Global.zero_address(),
    Txn.fee() <= max_fee
)

We can also write a function for the issuer to claim funds out of the escrow account:

def claim_fund(programstr, passphrase, escrow_id, amt, payment_id, first_block, last_block, algod_client):
    add = mnemonic.to_public_key(passphrase)
    key = mnemonic.to_private_key(passphrase)
    sp = algod_client.suggested_params()
    sp.first = first_block
    sp.last = last_block
    sp.flat_fee = True
    sp.fee = 1000
    txn = AssetTransferTxn(escrow_id, sp, add, amt, payment_id)
    t = programstr.encode()
    program = base64.decodebytes(t)
    arg = (3).to_bytes(8, 'big')
    lsig = LogicSig(program, args=[arg])
    stxn = LogicSigTransaction(txn, lsig)
    tx_id = algod_client.send_transaction(stxn)
    wait_for_confirmation(algod_client, tx_id, 10)

Nearing an interest payment or the final par value payment, the issuer may wish to replenish the escrow account, we also provide a template for such functionalities. You can also implement something like a sinking fund for risk management purposes with a MultiSig Account.

def replenish_account(passphrase, escrow_id, amt, payment_id, algod_client):
    add = mnemonic.to_public_key(passphrase)
    key = mnemonic.to_private_key(passphrase)
    sp = algod_client.suggested_params()
    sp.flat_fee = True
    sp.fee = 1000
    txn = AssetTransferTxn(add, sp, escrow_id, amt, payment_id)
    stxn = txn.sign(key)
    tx_id = algod_client.send_transaction(stxn)
    wait_for_confirmation(algod_client, tx_id, 10)

Now we turn to the other side of the story and look at how the buyer can interact with the escrow account.

Purchase of bond

The purchase of bond transaction is essentially just 3 single transactions bundled together as an Atomic Transfer:

  1. Buyer → Escrow, payment_id, amt
  2. Escrow → Buyer, par_id, amt
  3. Escrow → Buyer, interest_id, amt * total_payments

In the first transaction we require the buyer to transfer funds in units of payment_id (the stablecoin accepted as payment), the amount of transfer should be the number of contracts to purchase times the par value of bonds (we assume that bonds are sold at par value), this is validated in the contract with a modulo operation. The second and third transactions transfers Par Value Tokens and Interest Tokens to the buyer. The number of Par Value Tokens transferred should be exactly the number of contracts purchased as computed above, while the number of Interest Tokens should be multiplied with total times of future interest payments.

# user purchases token from escrow
# arg_id = 4
purchase =  And(
    Global.group_size() == Int(3),
    Gtxn[0].type_enum() == TxnType.AssetTransfer,
    Gtxn[0].xfer_asset() == Int(accepted_payment),
    Gtxn[0].asset_amount() % Int(par) == Int(0),
    Gtxn[0].first_valid() < Int(closure),
    Gtxn[1].type_enum() == TxnType.AssetTransfer,
    Gtxn[1].xfer_asset() == Int(par_id),
    Gtxn[1].asset_amount() == Gtxn[0].asset_amount() / Int(par),
    Gtxn[1].asset_receiver() == Gtxn[0].sender(),
    Gtxn[1].rekey_to() == Global.zero_address(),
    Gtxn[1].asset_close_to() == Global.zero_address(),
    Gtxn[1].fee() <= max_fee,
    Gtxn[2].type_enum() == TxnType.AssetTransfer,
    Gtxn[2].xfer_asset() == Int(interest_id),
    Gtxn[2].asset_amount() == Gtxn[1].asset_amount() * Int(total_payment),
    Gtxn[2].asset_receiver() == Gtxn[0].sender(),
    Gtxn[2].rekey_to() == Global.zero_address(),
    Gtxn[2].asset_close_to() == Global.zero_address(),
    Gtxn[2].fee() <= max_fee
)

Notice that the escrow account can not only access the information related to the logic transaction itself, but also all information that has been bundled in to Gtxn[0]. Now we turn to the side of the buyer and implement the transaction function the user has to call when purchasing a bond:

def purchase_bond(programstr, escrow_id, passphrase, amt, payment_id, par, interest_id, par_id, total_payments, algod_client: algod_client(), first_block, last_block):
    add = mnemonic.to_public_key(passphrase)
    key = mnemonic.to_private_key(passphrase)
    sp = algod_client.suggested_params()
    sp.first = first_block
    sp.last = last_block
    sp.flat_fee = True
    sp.fee = 1000
    print("--------------------------------------------")
    print("Opt-in the buyer account for interest and par token......")
    txn0_1 = AssetTransferTxn(add, sp, add, 0, interest_id)
    txn0_2 = AssetTransferTxn(add, sp, add, 0, par_id)
    sign0_1 = txn0_1.sign(key)
    sign0_2 = txn0_2.sign(key)
    txn0_1_id = algod_client.send_transaction(sign0_1)
    wait_for_confirmation(algod_client, txn0_1_id, 5)
    print("Successfully opt-in")
    print("--------------------------------------------")
    print("--------------------------------------------")
    print("Bundling purchase transactions and submitting......")
    txn0_2_id = algod_client.send_transaction(sign0_2)
    wait_for_confirmation(algod_client, txn0_2_id, 5)
    txn1 = AssetTransferTxn(add, sp, escrow_id, amt * par, payment_id)
    txn2 = AssetTransferTxn(escrow_id, sp, add, amt, par_id)
    txn3 = AssetTransferTxn(escrow_id, sp, add, amt * total_payments, interest_id)
    t = programstr.encode()
    program = base64.decodebytes(t)
    arg = (4).to_bytes(8, 'big')
    lsig = LogicSig(program, args=[arg])
    grp_id = calculate_group_id([txn1, txn2, txn3])
    txn1.group = grp_id
    txn2.group = grp_id
    txn3.group = grp_id
    stxn1 = txn1.sign(key)
    stxn2 = LogicSigTransaction(txn2, lsig)
    stxn3 = LogicSigTransaction(txn3, lsig)
    signed_group = [stxn1, stxn2, stxn3]
    tx_id = algod_client.send_transactions(signed_group)
    wait_for_confirmation(algod_client, tx_id, 200)
    print("Successfulley commited transaction!")
    print("--------------------------------------------")

Notice that we first have to opt the buyer in for Interest and Par Value Tokens. Next we assemble the 3 transactions specified as above separately, and then join them together to compute a grp_id. We then define this id in each of the transactions with .group field. Then we sign each single transaction with its sender field (for escrow account, we useLogicSig()). These transactions will then bundled as group and sent via the client with send_transactions().

Claim of interest

Claim of interest is a feature fundamental to bond contracts. There are 2 special discretions when implementing a bond contract: first, we want to make sure that the buyer claims the exact amount that he / she is entitled to: a buyer who holds 2 bond contracts should be allowed to spend at most 2 Interest Tokens at any given interest claim period. This is enforced by the introduction of a Par Value Token as a proxy for ownership of 1 bond contract. So each time a buyer wants to claim interest on the bond he / she holds, besides sending the interest tokens to the escrow account, we also require them to transfer all Par Value Token they own to themselves, this way we can validate that they are not claiming all interests in one payment. These 2 transactions are then bundled together with the interest payment transaction as an Atomic Transfer:

  1. Buyer → Escrow, interest_id, amt
  2. Buyer → Buyer, par_id, amt
  3. Escrow → Buyer, payment_id, amt * interest

The second thing we should pay attention to is how the periodic feature is implemented: we enforce this with a specification on first_valid() and last_valid() fields of the interest claim transaction. If a user tries to claim an interest too early or too late (the block height when submitted falls out of the first-last interval), the transaction would be considered invalid. We then make use of the modulo operator to specify to period, that is, an interest is only available when the first_valid() field of interest claim divides a stipulated period. Finally to prevent multiple claims in one period, we tag the interest payment transaction with a lease field, which essentially excludes any other transactions bearing the same {sender: lease} pair during this first_valid() to last_valid() period. A detailed explanation on periodic payment can be found in this tutorial. Combining all of the above, we get something like:

# user receives interest from escrow
# arg_id = 5
interestPayment =  And(
    Global.group_size() == Int(3),
    Gtxn[0].type_enum() == TxnType.AssetTransfer,
    Gtxn[0].xfer_asset() == Int(interest_id),
    Gtxn[1].type_enum() == TxnType.AssetTransfer,
    Gtxn[1].sender() == Gtxn[0].sender(),
    Gtxn[1].asset_receiver() == Gtxn[0].sender(),
    Gtxn[1].xfer_asset() == Int(par_id),
    Gtxn[1].asset_amount() == Gtxn[0].asset_amount(),
    Gtxn[2].type_enum() == TxnType.AssetTransfer,
    Gtxn[2].xfer_asset() == Int(accepted_payment),
    Gtxn[2].asset_receiver() == Gtxn[0].sender(),
    Gtxn[2].first_valid() >= Int(begin_round),
    Gtxn[2].first_valid() % Int(period) == Int(0),
    Gtxn[2].last_valid() == Gtxn[1].first_valid() + Int(span),
    Gtxn[2].asset_amount() == Gtxn[0].asset_amount() * Int(coupon),
    Gtxn[2].lease() == lease,
    Gtxn[2].rekey_to() == Global.zero_address(),
    Gtxn[2].asset_close_to() == Global.zero_address(),
    Gtxn[2].fee() <= max_fee
)

We also implement an interest payment function that automatically bundles all 3 transactions together for the convenience of the buyer.

def claim_interest(programstr, escrow_id, passphrase, amt, coupon, payment_id, interest_id, par_id, first_block, last_block, algod_client: algod_client()):
    add = mnemonic.to_public_key(passphrase)
    key = mnemonic.to_private_key(passphrase)
    sp = algod_client.suggested_params()
    sp.first = first_block
    sp.last = last_block
    sp.flat_fee = True
    sp.fee = 1000
    lease_str = "YOUR LEASE"
    lease = base64.b64decode(lease)
    print("--------------------------------------------")
    print("Bundling interest claim and submitting......")
    txn1 = AssetTransferTxn(add, sp, escrow_id, amt, interest_id)
    txn2 = AssetTransferTxn(add, sp, add, amt, par_id)
    txn3 = AssetTransferTxn(escrow_id, sp, add, amt * coupon, payment_id, lease=lease)
    t = programstr.encode()
    program = base64.decodebytes(t)
    arg = (5).to_bytes(8, 'big')
    lsig = LogicSig(program, args=[arg])
    grp_id = calculate_group_id([txn1, txn2, txn3])
    txn1.group = grp_id
    txn2.group = grp_id
    txn3.group = grp_id
    stxn1 = txn1.sign(key)
    stxn2 = txn2.sign(key)
    stxn3 = LogicSigTransaction(txn3, lsig)
    signed_group = [stxn1, stxn2, stxn3]
    tx_id = algod_client.send_transactions(signed_group)
    wait_for_confirmation(algod_client, tx_id, 200)
    print("Successfully committed transaction!")
    print("--------------------------------------------")

claim of par value

Claim of par value follows the same logic as interest claim, only easier. For par value payments, we require the buyer to transfer the Par Value Token directly to the escrow account, thus terminating his / her ownership status; Simultaneously the par value will be transferred back to the buyer’s account. Once again, these transactions are bundled into an Atomic Transfer:

  1. Buyer → Escrow, par_id, amt
  2. Escrow → Buyer, payment_id, amt * par

The PyTeal Logic is implemented as follows:

# user receives par value from escrow
# arg_id = 6
parPayment = And(
    Global.group_size() == Int(2),
    Gtxn[0].type_enum() == TxnType.AssetTransfer,
    Gtxn[0].xfer_asset() == Int(par_id),
    Gtxn[1].type_enum() == TxnType.AssetTransfer,
    Gtxn[1].first_valid() > Int(end_round),
    Gtxn[1].asset_receiver() == Gtxn[0].sender(),
    Gtxn[1].xfer_asset() == Int(accepted_payment),
    Gtxn[1].asset_amount() == Gtxn[0].asset_amount() * Int(par),
    Gtxn[1].rekey_to() == Global.zero_address(),
    Gtxn[1].asset_close_to() == Global.zero_address(),
    Gtxn[1].fee() <= max_fee
)

And the transaction function is also available:

def claim_par(programstr, escrow_id, passphrase, amt, par, payment_id, par_id, first_block, last_block, algod_client):
    add = mnemonic.to_public_key(passphrase)
    key = mnemonic.to_private_key(passphrase)
    sp = algod_client.suggested_params()
    sp.first = first_block
    sp.last = last_block
    sp.flat_fee = True
    sp.fee = 1000
    txn1 = AssetTransferTxn(add, sp, escrow_id, amt, par_id)
    txn2 = AssetTransferTxn(escrow_id, sp, add, amt * par, payment_id)
    t = programstr.encode()
    program = base64.decodebytes(t)
    arg = (6).to_bytes(8, 'big')
    lsig = LogicSig(program, args=[arg])
    grp_id = calculate_group_id([txn1, txn2])
    txn1.group = grp_id
    txn2.group = grp_id
    stxn1 = txn1.sign(key)
    stxn2 = LogicSigTransaction(txn2, lsig)
    signed_group = [stxn1, stxn2]
    tx_id = algod_client.send_transactions(signed_group)
    wait_for_confirmation(algod_client, tx_id, 10)

Assembling PyTeal Logics & Transaction Functions

complete escrow account

We can assemble all 7 separate logics for an escrow account as described above and bundle them in to one function EscrowAccount() as follows:

def EscrowAccount(mgr_add, interest_id, par_id, accepted_payment, closure, par, coupon,
                  holdup_period, begin_round, end_round, total_payment,
                  period, span):
    max_fee = Int(1000)
    lease = Bytes("base64", "YOUR LEASE")

    # for EscrowAccount to receive payments in opt-in interest token
    # arg_id = 0
    optInInterest = And(
        Txn.sender() == Txn.asset_receiver(),
        Txn.type_enum() == TxnType.AssetTransfer,
        Txn.xfer_asset() == Int(interest_id),
        Txn.asset_amount() == Int(0),
        Txn.rekey_to() == Global.zero_address(),
        Txn.asset_close_to() == Global.zero_address(),
        Txn.fee() <= max_fee
    )

    # for EscrowAccount to receive payments in opt-in par-value token
    # arg_id = 1
    optInPar = And(
        Txn.sender() == Txn.asset_receiver(),
        Txn.type_enum() == TxnType.AssetTransfer,
        Txn.xfer_asset() == Int(par_id),
        Txn.asset_amount() == Int(0),
        Txn.rekey_to() == Global.zero_address(),
        Txn.asset_close_to() == Global.zero_address(),
        Txn.fee() <= max_fee
    )

    # escrow account receives payment_id as payment
    # arg_id = 2
    optInPayment = And(
        Txn.sender() == Txn.asset_receiver(),
        Txn.type_enum() == TxnType.AssetTransfer,
        Txn.xfer_asset() == Int(accepted_payment),
        Txn.asset_amount() == Int(0),
        Txn.rekey_to() == Global.zero_address(),
        Txn.asset_close_to() == Global.zero_address(),
        Txn.fee() <= max_fee
    )

    # arg_id = 3
    claim = And(
        Txn.receiver() == Addr(mgr_add),
        Txn.type_enum() == TxnType.AssetTransfer,
        Txn.xfer_asset() == Int(accepted_payment),
        Txn.first_valid() > Int(holdup_period),
        Txn.rekey_to() == Global.zero_address(),
        Txn.asset_close_to() == Global.zero_address(),
        Txn.fee() <= max_fee
    )

    # user purchases token from escrow
    # arg_id = 4
    purchase =  And(
        Global.group_size() == Int(3),
        Gtxn[0].type_enum() == TxnType.AssetTransfer,
        Gtxn[0].xfer_asset() == Int(accepted_payment),
        Gtxn[0].asset_amount() % Int(par) == Int(0),
        Gtxn[0].first_valid() < Int(closure),
        Gtxn[1].type_enum() == TxnType.AssetTransfer,
        Gtxn[1].xfer_asset() == Int(par_id),
        Gtxn[1].asset_amount() == Gtxn[0].asset_amount() / Int(par),
        Gtxn[1].asset_receiver() == Gtxn[0].sender(),
        Gtxn[1].rekey_to() == Global.zero_address(),
        Gtxn[1].asset_close_to() == Global.zero_address(),
        Gtxn[1].fee() <= max_fee,
        Gtxn[2].type_enum() == TxnType.AssetTransfer,
        Gtxn[2].xfer_asset() == Int(interest_id),
        Gtxn[2].asset_amount() == Gtxn[1].asset_amount() * Int(total_payment),
        Gtxn[2].asset_receiver() == Gtxn[0].sender(),
        Gtxn[2].rekey_to() == Global.zero_address(),
        Gtxn[2].asset_close_to() == Global.zero_address(),
        Gtxn[2].fee() <= max_fee
    )

    # user receives interest from escrow
    # arg_id = 5
    interestPayment =  And(
        Global.group_size() == Int(3),
        Gtxn[0].type_enum() == TxnType.AssetTransfer,
        Gtxn[0].xfer_asset() == Int(interest_id),
        Gtxn[1].type_enum() == TxnType.AssetTransfer,
        Gtxn[1].sender() == Gtxn[0].sender(),
        Gtxn[1].asset_receiver() == Gtxn[0].sender(),
        Gtxn[1].xfer_asset() == Int(par_id),
        Gtxn[1].asset_amount() == Gtxn[0].asset_amount(),
        Gtxn[2].type_enum() == TxnType.AssetTransfer,
        Gtxn[2].xfer_asset() == Int(accepted_payment),
        Gtxn[2].asset_receiver() == Gtxn[0].sender(),
        Gtxn[2].first_valid() >= Int(begin_round),
        Gtxn[2].first_valid() % Int(period) == Int(0),
        Gtxn[2].last_valid() == Gtxn[1].first_valid() + Int(span),
        Gtxn[2].asset_amount() == Gtxn[0].asset_amount() * Int(coupon),
        Gtxn[2].lease() == lease,
        Gtxn[2].rekey_to() == Global.zero_address(),
        Gtxn[2].asset_close_to() == Global.zero_address(),
        Gtxn[2].fee() <= max_fee
    )

    # user receives par value from escrow
    # arg_id = 6
    parPayment = And(
        Global.group_size() == Int(2),
        Gtxn[0].type_enum() == TxnType.AssetTransfer,
        Gtxn[0].xfer_asset() == Int(par_id),
        Gtxn[1].type_enum() == TxnType.AssetTransfer,
        Gtxn[1].first_valid() > Int(end_round),
        Gtxn[1].asset_receiver() == Gtxn[0].sender(),
        Gtxn[1].xfer_asset() == Int(accepted_payment),
        Gtxn[1].asset_amount() == Gtxn[0].asset_amount() * Int(par),
        Gtxn[1].rekey_to() == Global.zero_address(),
        Gtxn[1].asset_close_to() == Global.zero_address(),
        Gtxn[1].fee() <= max_fee
    )


    program = Cond(
        [Btoi(Arg(0)) == Int(0), optInInterest],
        [Btoi(Arg(0)) == Int(1), optInPar],
        [Btoi(Arg(0)) == Int(2), optInPayment],
        [Btoi(Arg(0)) == Int(3), claim],
        [Btoi(Arg(0)) == Int(4), purchase],
        [Btoi(Arg(0)) == Int(5), interestPayment],
        [Btoi(Arg(0)) == Int(6), parPayment],
    )

    return program

We now want to compile the PyTeal code into TEAL contract and store it in the teal folder, this is encapsulated in compile():

def compile(mgr_add, interest_id, par_id, accepted_payment, closure, par, coupon, holdup_period, begin_round, end_round, total_payment, period, span, name):
    program = EscrowAccount(mgr_add, interest_id, par_id, accepted_payment, closure, par, coupon, holdup_period, begin_round, end_round, total_payment, period, span)
    with open("./teal/" + name + ".teal", "w") as f:
        compiled = compileTeal(program, Mode.Signature)
        f.write(compiled)

Publisher Function

It is also useful to group all the pre-publishing configurations into 1 function that sets up the escrow account. This function is implemented as follows:

def main_pub(passphrase, proj_name, vol, url, par, coupon, payment_id,
             closure, start_round, period, total_payments, span, hold_up, client):
    # ensuring that buyer will be able to claim coupon on start_round
    add = mnemonic.to_public_key(passphrase)
    key = mnemonic.to_private_key(passphrase)
    print("Checking configurations......")
    print("--------------------------------------------")
    cl = client
    if start_round % period != 0:
        start_round = (start_round + period) - (start_round % period)
        print("Start round for interest payment refactored to {}".format(start_round))
    end_round = start_round + (total_payments-1) * period
    print("--------------------------------------------")

    # issuance of tokens
    print("Issuing tokens......")
    print("--------------------------------------------")
    try:
        interest_txid, interest_id = interest_token_issuance(cl, passphrase, proj_name, vol, url, total_payments)
        par_txid, par_id = par_token_issuance(cl, passphrase, proj_name, vol, url)
    except Exception as e:
        print("Issuance failed :{}".format(e))
        return
    print("Issued tokens successfully")
    print("Interest token id: {}, recorded in {}".format(interest_id, interest_txid))
    print("Par token id: {}, recorded in {}".format(par_id, par_txid))
    print("--------------------------------------------")

    # creating escrow account
    print("--------------------------------------------")
    print("Creating escrow account......")
    try:
        escrow_result, escrow_id = create_escrow(add, proj_name, interest_id, par_id, payment_id,
                                  closure, start_round, end_round, par, coupon,
                                  total_payments, period, span, hold_up, cl)
    except Exception as e:
        print("Escrow account creation failed :{}".format(e))
        return
    print("Created escrow account successfully")
    print("Escrow account result :{}".format(escrow_result))
    print("Escrow account public address: {}".format(escrow_id))
    print("--------------------------------------------")

    # activating escrow account
    print("--------------------------------------------")
    print("Activating escrow account......")
    try:
        txn = payment_transaction(passphrase, 1000000, escrow_id, cl)
    except Exception as e:
        print("Activation failed :{}".format(e))
        return
    print("Activated successfully")
    print(txn)
    print("--------------------------------------------")

    # opt-in the escrow account for interest token
    print("--------------------------------------------")
    print("Opt-in for interest token......")
    try:
        program_str = escrow_result.encode()
        program = base64.decodebytes(program_str)
        arg1 = (0).to_bytes(8, 'big')
        lsig = LogicSig(program, [arg1])
        sp = cl.suggested_params()
        atn = AssetTransferTxn(lsig.address(), sp, lsig.address(), 0, interest_id)
        lstx = LogicSigTransaction(atn, lsig)
        txid = cl.send_transaction(lstx)
        msg = wait_for_confirmation(cl, txid, 5)
    except Exception as e:
        print("Opt-in interest token failed :{}".format(e))
        return
    print("Opt-in interest token success!")
    print(msg)
    print("--------------------------------------------")

    # opt-in the escrow account for par token
    print("--------------------------------------------")
    print("Opt-in for par token......")
    try:
        program_str = escrow_result.encode()
        program = base64.decodebytes(program_str)
        arg1 = (1).to_bytes(8, 'big')
        lsig = LogicSig(program, [arg1])
        sp = cl.suggested_params()
        atn = AssetTransferTxn(lsig.address(), sp, lsig.address(), 0, par_id)
        lstx = LogicSigTransaction(atn, lsig)
        txid = cl.send_transaction(lstx)
        msg = wait_for_confirmation(cl, txid, 5)
    except Exception as e:
        print("Opt-in par token failed :{}".format(e))
        return
    print("Opt-in par token success!")
    print(msg)
    print("--------------------------------------------")

    # opt-in the escrow account for payment token
    print("--------------------------------------------")
    print("Opt-in for payment token......")
    try:
        program_str = escrow_result.encode()
        program = base64.decodebytes(program_str)
        arg1 = (2).to_bytes(8, 'big')
        lsig = LogicSig(program, [arg1])
        sp = cl.suggested_params()
        atn = AssetTransferTxn(lsig.address(), sp, lsig.address(), 0, payment_id)
        lstx = LogicSigTransaction(atn, lsig)
        txid = cl.send_transaction(lstx)
        msg = wait_for_confirmation(cl, txid, 5)
    except Exception as e:
        print("Opt-in payment token failed :{}".format(e))
        return
    print("Opt-in payment token success!")
    print(msg)
    print("--------------------------------------------")

    # transferring the interest tokens to escrow account
    print("--------------------------------------------")
    print("Transfer interest token to escrow account......")
    try:
        atn = asset_transaction(passphrase, vol * total_payments, escrow_id, interest_id, cl)
    except Exception as e:
        print("Transferred interest token failed :{}".format(e))
        return
    print("Transferred interest token successfully")
    print(atn)
    print("--------------------------------------------")

    # transferring the par tokens to escrow account
    print("--------------------------------------------")
    print("Transfer par token to escrow account......")
    try:
        atn = asset_transaction(passphrase, vol, escrow_id, par_id, cl)
    except Exception as e:
        print("Transferred par token failed :{}".format(e))
        return
    print("Transferred par token successfully")
    print(atn)
    print("--------------------------------------------")
    print("Setup-complete!")
    return interest_id, par_id, escrow_result, escrow_id

Sample Rundown

Conclusion

In this solution, we explored the possibility for a stateless escrow account to host a bond contract. We start from the bond issuer defining the terms for the bond contract to eventually how a buyer can claim interest and par values on bonds that he / she holds. Further features of the bond contract such as protective covenants, call provisions, sinking funds etc. are left for the reader to customize.

algorand standard assets

python

logicsignature

asa

pyteal

March 25, 2021