Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Solution Thumbnail

Understanding the TEAL Stack by Example

Overview

This tutorial helps people who started learning about TEAL understand how the language works and interacts with the stack and different storage mechanisms. We’ll look at two contracts and analyze them step by step while visualizing the stack and storage mechanism to help you understand how TEAL works. The goal is to become better at writing TEAL code and using the Opcodes documentation.

You will learn the following:
- How to use different opcodes and how they interact with the stack
- How to read and write data from scratch space and global storage
- How to access properties from transactions in your code
- How logic operators work and why they are useful

First, let’s analyze a smart contract that implements a simple counter. Next, we analyze a smart signatures where we have to send the correct passphrase to successfully send a transaction from a smart signature account.

1. Smart contract: Counter example

A smart contract always consists of a Clear State program and an Approval program. In this example, we have a smart contract where the contract tracks a counter. Each time the contract gets called, the counter value increases by one. To keep track of this counter value, we store the counter value in the global storage.

This is the full contract code for approval_program.teal:

#pragma version 4

// read global state
byte "counter"
dup
app_global_get

// increment the value
int 1
+

// store to scratch space
dup
store 0

// update global state
app_global_put

// load return value as approval
load 0
return

Let’s analyze this contract step by step.

Step 1

Code: byte "counter" (docs)

This opcode converts the word counter to a []byte (byte array) and pushes it to the stack.

Stack               Scratch             Global
0 []byte "counter"  

Step 2

Code: dup (docs)

The dup opcode duplicates the last value on the stack and adds it back on top.

Stack               Scratch             Global
0 []byte "counter"  
1 []byte "counter"  

Step 3

Code: app_global_get (docs)

The app_global_get opcode retrieves the value at position X from the global storage. It pops one item from the stack, which needs to be a []byte. It will then try to retrieve the item with this specific key from the globa storage. So in this example, we are looking for a counter key.

However, the counter key doesn’t exist. Therefore, the Opcode returns a 0 value because by default the value is zero when a key doesn’t exist. However, that doesn’t mean we can’t store a zero value because that is exactly what will happen during the first execution of this simple counter smart contract.

Stack               Scratch             Global
0 []byte "counter"
1 0

Step 4

Code: int 1 (docs)

The int opcode pushes an integer value to the top of the stack.

Stack               Scratch             Global
0 []byte "counter"
1 0
2 1

Step 5

Code: + (docs)

The + opcode pops two uint64 values from the top of the stack, sums them, and adds the total back to the stack.
In this case, we have 0 and 1, adds them (1), and pushes this result to the stack.

Stack               Scratch             Global
0 []byte "counter"
1 1

Step 6

Code: dup (docs)

The dup opcode duplicates the last value on the stack and adds it back on top.

Stack               Scratch             Global
0 []byte "counter"
1 1
2 1

Step 7

Code: store 0 (docs)

The store command allows you to store a value in the scratch storage. This is temporarily storage that you can only access during the execution of your program. When the execution has finished, this data will be erased. Therefore, only use the scratch space to temporarily store values to use them later on in the program execution. Note that in a group of smart contracts, the scratch space is available across all application transactions in the group.

Here, store pops the first value of the stack, which is 1, and stores it at position 0 in the scratch space. We will use this value to return at the end of the program.

Stack               Scratch             Global
0 []byte "counter"  Pos 0 -> 1
1 1

Step 8

Code: app_global_put (docs)

When looking at the docs, the app_global_put opcode pops two items from the stack. The first item needs to be a []byte and the second item can be of any type ([]byte or uint64). The first item is the key to which we want to write the data, in this case counter. And the second item is the value we want to store.

Now, the stack is empty. The value in the global storage will be accessible all the time, even when the program finishes execution.

Stack               Scratch             Global
(empty)             Pos 0 -> 1          "counter" -> 1

Step 9

Code: load 0 (docs)

The load opcode is the counterpart of the store opcode. load can be used to copy a value from the scratch space.

Stack               Scratch             Global
0 1                 Pos 0 -> 1          "counter" -> 1

Step 10

Code: return (docs)

The return opcode uses the last value on the stack as a success value. If the value is positive, the program ends successfully. This opcode pops one value (uint64) from the stack. Finally, we have an empty stack again.

Stack               Scratch             Global
(empty)             Pos 0 -> 1          "counter" -> 1

Second call to smart contract
When calling the contract for a second time, the “counter” value will exist and holds a value of 1. At the end of the program, the “counter” value in the global storage will be set to 2. This value will continue to increase each time you call the smart contract.

2. Smart signatures: Passphrase example

A smart signature holds logic that is used to sign transactions. The logic of the smart signature is submitted with a transaction. If the logic of the transaction fails, the spending transaction will also fail.

Let’s take a look at the passphrase.teal example below:

#pragma version 4

// Description: Verify fee, passphrase
// Valid passphrase input: "weather comfort erupt verb pet range endorse exhibit tree brush crane man"

txn Fee
int 10000
<=

// Check length of passphrase
arg 0
len
int 73
==
&&

// The sha256 value of the passphrase
arg 0
sha256
byte base64 30AT2gOReDBdJmLBO/DgvjC6hIXgACecTpFDcP1bJHU=
==
&&

Let’s analyze this contract step by step. Assume we are sending a transaction with a fee set to 9999 and one argument that matches the passphrase requirement. We can only submit a base64 version of this passphrase string which looks like this d2VhdGhlciBjb21mb3J0IGVydXB0IHZlcmIgcGV0IHJhbmdlIGVuZG9yc2UgZXhoaWJpdCB0cmVlIGJydXNoIGNyYW5lIG1hbg==.

This is a generated dry-run payment transaction to the smart signature with the right arguments.

{
   "accounts":null,
   "apps":null,
   "latest-timestamp":0,
   "protocol-version":"",
   "round":0,
   "sources":null,
   "txns":[
      {
         "lsig":{
            "arg":[
               "d2VhdGhlciBjb21mb3J0IGVydXB0IHZlcmIgcGV0IHJhbmdlIGVuZG9yc2UgZXhoaWJpdCB0cmVlIGJydXNoIGNyYW5lIG1hbg=="
            ],
            "l":"BDEBgZFODi0VgUkSEC0BgCDfQBPaA5F4MF0mYsE78OC+MLqEheAAJ5xOkUNw/VskdRIQMQkxBxIQ"
         },
         "txn":{
            "amt":30000,
            "close":"X3YKNQGGRQJ56TQF53XHTX745LJCR5527X7ACYCBSJ3BAUMFHS3FEFJK4E",
            "fee":9999,
            "fv":814,
            "gen":"sandnet-v1",
            "gh":"B5ucbCXXunVZI3AkYAV3/XfUhUqYlrquBoIDQ8SXK/o=",
            "lv":1814,
            "note":"qxI6xbhmNy4=",
            "rcv":"X3YKNQGGRQJ56TQF53XHTX745LJCR5527X7ACYCBSJ3BAUMFHS3FEFJK4E",
            "snd":"45L2272USBPOCS3QC6PUVKGJRNVWY5GN4MDP2NJNCZFAZ7UWMVM3J5KJKQ",
            "type":"pay"
         }
      }
   ]
}

You can generate this transaction using the goal commandline tool. In particular, using goal clerk send. Here’s the full command you can use to generate a JSON dryrun-dump payload yourself.

./sandbox goal clerk send -a 30000 --from-program passphrase.teal -c <address> --argb64 d2VhdGhlciBjb21mb3J0IGVydXB0IHZlcmIgcGV0IHJhbmdlIGVuZG9yc2UgZXhoaWJpdCB0cmVlIGJydXNoIGNyYW5lIG1hbg==  -t <address> -o out-passphrase.txn --dryrun-dump

./sandbox goal clerk send -a 30000 --from-program passphrase.teal -c X3YKNQGGRQJ56TQF53XHTX745LJCR5527X7ACYCBSJ3BAUMFHS3FEFJK4E --argb64 d2VhdGhlciBjb21mb3J0IGVydXB0IHZlcmIgcGV0IHJhbmdlIGVuZG9yc2UgZXhoaWJpdCB0cmVlIGJydXNoIGNyYW5lIG1hbg==  -t X3YKNQGGRQJ56TQF53XHTX745LJCR5527X7ACYCBSJ3BAUMFHS3FEFJK4E -o out-passphrase.txn --dryrun-dump

Let’s analyze the code!

Step 1

Code: txn Fee (docs with index 1 - More info about the fee property and all other properties in the transaction reference)

The txn opcode expects a field as input. For instance, we can say we want to load the fee paid (9999) for the transaction submitted to our smart signature. The txn opcode will then push this data to the top of the stack. We can use this opcode to access all kinds of properties from our transaction, such as the Sender, Receiver, Amount, or transaction type Type. You can find all possibilities in the documentation.

Stack               Scratch             Global
0 9999  

Step 2

Code: int 10000 (docs)

Push the uint64 value 10000 to the stack.

Stack               Scratch             Global
0 9999
1 10000

Step 3

Code: <= (docs)

Compares the last two values if they are lower than or equal to each other. In this case, 9999 is lower than 10000. Therefore, the operation pushes the success value 1 to the stack.

Stack               Scratch             Global
0 1

Step 4

Code: arg 0 (docs)

Loads the argument at position 0 passed with the transaction. In our example, we’ve sumbitted a single argument which represents the base64 version of the expected passphrase. Each extra argument we pass with the transaction can be accessed using arg 1, arg 2, and so on. As you can see, the stack shows the string version of the base64 argument again.

Stack                           Scratch             Global
0 1
1 "weather comfort erupt ..."

Step 5

Code: len (docs)

The len opcode will pop a []byte and calculate its length. This length is added back to the stack.

Stack                           Scratch             Global
0 1
1 73

Step 6

Code: int 73 (docs)

Push the uint64 value 73 to the stack.

Stack               Scratch             Global
0 1
1 73
2 73

Step 7

Code: == (docs)

Verifies if the last two values on the stack are equal. It can compare both byte[] and uint64 values.

Stack               Scratch             Global
0 1
1 1

Step 8

Code: && (docs)

Consumes two uint64 values, if both are not zero, then it returns 1 (true). If both are zero or one of the two values is equal to zero, then it returns 0 (false). This is a very useful operator for chaining conditions. Because we have two 1 values, the expression will evaluate to 1 (true).

Stack               Scratch             Global
0 1

Step 9

Code: arg 0 (docs)

Loads the argument at position 0 passed with the transaction.

Stack                           Scratch             Global
0 1
1 "weather comfort erupt ..."

Step 10

Code: sha256 (docs)

Calculates the SHA256 value of the top-element of the stack, in our case the passphrase argument. Then, it pushes a [32]byte string back to the stack. In the next step, we will use this to compare it against the expected []byte for our passphrase to see if they match.

Stack                                                                   Scratch             Global
0 1
1 "df4013da039178305d2662c13bf0e0be30ba8485e000279c4e914370fd5b2475"

Step 11

Code: byte base64 30AT2gOReDBdJmLBO/DgvjC6hIXgACecTpFDcP1bJHU= (docs)

First, let’s explain where the “30AT2gOReDBdJmLBO/DgvjC6hIXgACecTpFDcP1bJHU=” value comes from. This is the base64 version of the SHA256 of our expected passphrase “weather comfort erupt …”. Next, we tell the byte opcode to convert this base64 string to a []byte. As expected, this yields the same []byte as the argument we’ve submitted with the transaction.

Stack                                                                   Scratch             Global
0 1
1 "df4013da039178305d2662c13bf0e0be30ba8485e000279c4e914370fd5b2475"
2 "df4013da039178305d2662c13bf0e0be30ba8485e000279c4e914370fd5b2475"

Step 12

Code: == (docs)

Verifies if the last two values on the stack are equal. It can compare both byte[] and uint64 values.

Stack               Scratch             Global
0 1
1 1

Step 13

Code: && (docs)

Consumes two uint64 values, if both are not zero, then it returns 1 (true).

Note: You can make more advanced logic constructions with logic operators, for instance: (condition 1 && condition 2) || (condition 3).

Stack               Scratch             Global
0 1

Here, the program ends. Because there’s only one positive value on the stack left, the program finishes successfully.

Image source: Holly Stratton (Unsplash)