Smart Contract Example Using Pyteal: Algorand’s Sophisticated Use Case: Overdraft/Loan Contract.


Smart contract hosting is a one of the great features of Algorand technology potentials of which many developers are yet to uncover. If you have written code and deploy on Ethereum network a using solidity, you would attest to the intricacies involve in getting things right. In this article, I will show you how you can write instructions to guard a transaction using teal. This is an intermediate to advanced level contract design, hence you should have good knowledge of python language, and/or statically typed programming languages would be an added advantage.


What you will learn

  • How to write and deploy smart contract on Algorand blockchain.
  • Understanding Pyteal/Teal language
  • Nesting In Pyteal

Tools/Requirement

  • Run a node or use Docker sandbox or an endpoint/API service if you cannot run a node on your machine. It is advisable that you run a  goal-CLI which comes with running a node.  Follow the node installation guide.
  • An editor.  If you would like me suggest one, I’d recommend Pycharm.
  • Python application.
  • Algorand python SDK
  • Algodesk

Join the developer forum to learn more.


Overdraft/Line Of Credit

Consider a scenario, where Alice went to shop (where our asset is accepted as a medium of payment) for groceries amounting to a sum of $2,500. Alice use a card issued by us for the purchase. At this point, transaction could not be completed on the spot because she was left with $1,500 in her wallet. She wished there exist a service that could save her this mess.  Alice suggested this to us. Our team deem fit to integrate such service that allow Alice to overdraw her account provided our back-end can remember ever seen Alice around and if certain conditions are met.  All we need is putting up a smart contract to handle this for us. Teal, an acronym for Transaction Execution Approval Language is very robust for the purpose as the name implies. Teal as an assembly language equivalent to either yes or no which feeds on opcodes. Pyteal was developed to abstract away the complexities of working with binaries since Teal is more of reading assembled 1s and 0s. It is a language binding for Teal. Without further Ado, let’s do the deal.

Things to note

I have imported a few modules from my past tutorial available on github. To keep it short and simple, I will concentrate more on the pyteal codes.

The source code is contained in overdraft.py file. Create one in your editor. Copy and paste the code below. 

from pyteal import *
from algosdk.future import transaction
from connection import algo_client, params
from assetInfo import transferasset, asset_manage_authorized, assetinformation, jointAuthorization, accounts_sk
from algosdk.transaction import AssetTransferTxn, encoding
from printAssetHolding import print_asset_holding
from waitForConfirmation import wait_for_confirmation
from algosdk.future.transaction import AssetTransferTxn, transaction
import  base64

# Set the global variables
tmpl_fee = Int(1000)
tmpl_timeout = Int(6)
tmpl_period = Int(5)
min_algo_bal = Int(1000)
min_Freth_bal = Int(5000)
int_rate = Div(Int(8), Int(100))  # Interest on OD
flat_charge = Int(100)  # Flat charges for overdrawing in custom asset
max_gold_client = Int(300000)
max_ruby_client = Int(70000)
stake = False
assetId = 9604118
noReEntrancy = Bytes("base64", "19faf42c")
tmpl_OD = Sha256
sender = 'N6SJLDQXCXZ7IJMLM5VVEAWJ36JUJSDQVNQFA2I3BKFOWE3QLKKJGATPJ4'

userDict = {
    "user100": "...",
    "user101": "G26HOW2LWOV2PRE3WN7S7JDOWE32GR4SRZ34MLANCJPKVLIX4YNBNBT2XA",
    "user102": "..."
}

# Check if user can enjoy this service i.e is a potential client list and has required amount of asset staked.
def isAClient(addr):
    global stake
    if addr in userDict:
        stake = True
        return int(1)
    else:
        stake = stake
        return int(0)


# Function requesting overdraft
def requestOdrft(addr, amt):

    bal_In_Algo = algo_client.account_info(addr)['amount-without-pending-rewards']  # To check if user is eligible, scrutinize their balance in Both in Freth and Algo so the transaction will not fail due to low balance.
    bal_In_Freth = algo_client.account_info(addr)['assets'][0]['amount']  
    alc_bal_Freth = Int(bal_In_Freth) # Balance of Drawer in Freth
    alc_bal_ALGO = Int(bal_In_Algo) # Balance of Drawer in ALGO
    is_pot_client = isAClient(addr) # Is this user contained in the userList?
    isRubyUser = Le(Int(amt), max_ruby_client) # A Ruby user can obtain loan not greater than 70000 Freth 
    isGoldUser = And(isRubyUser, Gt(Int(amt), max_ruby_client), Le(Int(amt), max_gold_client))  # A Gold user can overdraw up to 300,000 Freth. This is the maximum we can allow subscribers to overdraw. 

    # Check that the account balance is less than requested amount
    # Drawer must be holding at least 20,000 worth of Freth at this time.
    # Balance in ALGO at this time must not go below 2000.
    # To enable for transaction fee and in this contract, balance is considered zero when
    # goes below 2000
    # compute total loan to disburse less interest and other charges if any
    net_loan = Minus(Int(amt), Add(Mul(int_rate, Int(amt)), flat_charge))

    # Check transaction fields are correct
    # Check the status of user's account.
    # User must be holding an amount of Freth as staking at this time.
    # Balance in ALGO at this time must not go below amount for transaction fee.
    # To enable for transaction fee and in this contract, balance is considered zero when
    # goes below 2000

    # Conditions for approving transaction
    ovdrft_trxn_cond = And(  # including
        And(  # including
            And(  # And inclusive of..
                Txn.type_enum() == Int(4),  # Transaction type is asset transfer
                Le(Txn.fee(), tmpl_fee),  # Transaction fee cannot exceed stipulated 
                                           amount to guard against any attack intending to
                                           overstate transaction fee. 
                Eq(Txn.amount(), net_loan),  # Amount requested as overdraft cannot exceed
                                              balance less interest plus other charges
                Eq(Txn.lease(), noReEntrancy),  # Guard for transaction parameters
                Eq(Txn.xfer_asset(), Int(assetId)), # Pointing to specific asset we have 
                                                      access to. 
            ),
            Eq(Int(is_pot_client), Int(1)),  # This caller must be an already subscriber. 
                                               is A user in our list
            Le(alc_bal_ALGO, min_algo_bal),  # and balances of user in ALGO & Freth(custom 
                                               asset) are within requirements. 
            Lt(alc_bal_Freth, min_Freth_bal)
        ),
        Or(
            Eq(isRubyUser, Int(1)),  # check that requested loan is within specified 
                                       windows
            Eq(isGoldUser, Int(1))  # The caller should be either a Ruby user or a Gold
                                      user. 
        ),
        And(
            Le(Txn.last_valid(), Int(6)),  # Transaction valid round should not exceed set 
                                              time, else panic. 
            Eq(Txn.asset_close_to(), Addr(asset_manage_authorized)),  # Asset balance is 
                                                     closed to a contract account so no 
                                           ambiguous interpretation for an asset can pass. 
            Eq(Txn.asset_receiver(), Addr(addr))  # Receiver cannot be any other than 
                                                    caller. 
        )
    )
    opcodes = ovdrft_trxn_cond.teal() # To create teal
    return opcodes

Converting the pyteal codes to teal generates the following opcodes.

txn TypeEnum
int 4
==
txn Fee
int 1000
<=
&&
txn Amount
int 20000
int 8
int 100
/
int 20000
*
int 100
+
-
==
&&
txn Lease
byte base64 19faf42c
==
&&
txn XferAsset
int 10897078
==
&&
int 0
int 1
==
&&
int 99999000
int 1000
<=
&&
int 1000
int 5000
<
&&
int 20000
int 70000
<=
int 1
==
int 20000
int 70000
<=
int 20000
int 70000
>
&&
int 20000
int 300000
<=
&&
int 1
==
||
&&
txn LastValid
int 6
<=
txn AssetCloseTo
addr NFZ3L6E4MLIOQ5RWYYRJLHCELIA4Q2HA2U654DTKHMBCUQASCDVLLBEXAQ
==
&&
txn AssetReceiver
addr G26HOW2LWOV2PRE3WN7S7JDOWE32GR4SRZ34MLANCJPKVLIX4YNBNBT2XA
==
&&
&&

If you are familiar with goal, I have provided a reference to help you through the compilation stage and how the results are manipulated to create a logic signature. You may use this boilerplate if you are able to access the goal-CLi or algodesk which return base64 encoded program bytes and base32 SHA512_256 hash of program bytes (Address style), exactly what we need.

Importing from the boilerplate:

Now that the basic contract is implemented we need to build an example of instantiating the contract. We will do this by creating a python file named ‘odrft_deploy.py’. We first set the template variables and then call the ovdrft function we created in the previous steps. Next, we use python to call out to the command line to compile the TEAL program by saving the TEAL code to a file and using the execute function. Finally we read the file containing the compiled TEAL bytes back into a local variable.

Note: I made some changes. Swap the htlc file with ovdrft

#!/usr/bin/env python3

import uuid, base64
from algosdk import algod, transaction, account, mnemonic
from overdraft import requestOdrft

#--------- compile & send transaction using Goal and Python SDK ----------
tmpl_hash_fn = Sha256
tmpl_hash_img = Bytes("base64", "QzYhq9JlYbn2QdOMrhyxVlNtNjeyvyJc/I8d8VAGfGc=")
teal_source = 
requestOdrft("G26HOW2LWOV2PRE3WN7S7JDOWE32GR4SRZ34MLANCJPKVLIX4YNBNBT2XA", 20000)
# compile teal
teal_file = str(uuid.uuid4()) + ".teal"
with open(teal_file, "w+") as f:
    f.write(teal_source)
lsig_fname = str(uuid.uuid4()) + ".tealc"

stdout, stderr = execute(["goal", "clerk", "compile", "-o", lsig_fname,
                      teal_file])
if stderr != "":
    print(stderr)
    raise
elif len(stdout) < 59:
    print("error in compile teal")
    raise

with open(lsig_fname, "rb") as f:
    teal_bytes = f.read()

Additional information

  • Import all utilities from the pyteal module.
  • from python import *​
  • Every pyteal expression must evaluate to a binary (either a [TealType.bytes]() or TealType.uint64 expression. 
  • Both the right and left operands must be of the same type.
  • [Int(2) == Int(4)](). This is right:
  • [Bytes(address) == Bytes(address)](). This is right:
  • [Int(2) == Bytes(address)]() . This is wrong
  • Comparison between the operands must result in either True or False equals to Int(1) or Int(0)
  • Pyteal supports operator overloading for example: Le(Int(3), Int(2))
    simply says: check that 3 of type TealType.uint64 equate with 2 of the same type. More information is
    found in Pyteal documentation.

Create the Logic Signature And Sign A Transaction With It.

  • Get the program and parameters and use them to create an lsig
  • For the contract account to be used in a transaction
  • In this example ‘hype sense black soap loop lucky king number’
  • hashed with sha256 will produce our a byte hash

# This creates a lock for the ovdrft contract
args = "hype sense black soap loop lucky king number".encode()
# Add the program bytes and args to a LogicSig object
lsig = transaction.LogicSig(teal_bytes, args)
print( lsig.address() )
# Transfer asset
def transferAssets(rec, amount):
    params.fee = 1000
    params.flat_fee = True

    txn = AssetTransferTxn(
        sender=sender,
        sp=params,
        receiver=rec,
        amt=amount,
        index=assetId
    )

    lstx = transaction.LogicSigTransaction(txn, lgsig)
    txns = [lstx]
    transaction.write_to_file(txns, "ovrdrft.stxn", False)

    # Submit transaction to the network
    tx_id = algo_client.send_transaction(lstx, headers={'content-type': 'application/x-binary'})
    message = "Transaction was signed with: {}.".format(tx_id)
    wait = wait_for_confirmation(tx_id)
    isSuccessful = bool(wait is not None)

    # Now check the asset holding for receiver.
    # This should now show a holding with the sent balance.
    assetHolding = print_asset_holding(rec, assetId)
    return isSuccessful

Conclusion

Pyteal enables Algorand developers to express contract logic more naturally using python. The Pyteal code is then converted to teal opcodes using `.teal()` member function. It gives a direct and single interpretation to program logic thereby avoiding ambiguity. Meanwhile, developers also have the onus of writing efficient, effective and secure code.  Interact with the developers resources to learn more about Algorand Smart contract.


You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *