Skip to content
This new developer portal is under construction. For complete documentation, please refer to the old developer portal.

Box Storage

Box storage in Algorand is a feature that provides additional on-chain storage options for smart contracts, allowing them to store and manage larger amounts of data beyond the limitations of global and local state. Unlike the fixed sizes of global and local state storages, box storage offers dynamic flexibility for creating, resizing, and deleting storage units

These storage units, called boxes, are key-value storage segments associated with individual applications, each capable of storing upto 32KB (32768 bytes) of data as byte arrays. Boxes are only visible and accessible to the application that created them, ensuring data integrity and security.

Both the box key and data are stored as byte arrays, requiring any uint64 variables to be converted before storage. While box storage expands the capabilities of Algorand smart contracts, it does incur additional costs in terms of minimum balance requirements (MBR) to cover the network storage space. The maximum number of box references is currently set to 8, allowing an app to create and reference up to 8 boxes simultaneously. Each box is a fixed-length structure but can be resized using the App.box_resize method or by deleting and recreating the box. Boxes over 1024 bytes require additional references, as each reference has a 1024-byte operational budget. The app account’s MBR increases with each additional box and byte in the box’s name and allocated size. If an application with outstanding boxes is deleted, the MBR is not recoverable, so it’s recommended to delete all box storage and withdraw funds before app deletion.

Usage of Boxes

Boxes are helpful in many scenarios:

  • Applications that need more extensive or unbound contract storage.
  • Applications that want to store data per user but do not wish to require users to opt in to the contract or need the account data to persist even after the user closes or clears out of the application.
  • Applications that have dynamic storage requirements.
  • Applications requiring larger storage blocks that can not fit the existing global state key-value pairs.
  • Applications that require storing arbitrary maps or hash tables.

Box Array

When interacting with apps via app call transactions, developers need a way to specify which boxes an application will access during execution. The box array is part of the smart contract reference arrays alongside the apps, accounts, and assets arrays. These arrays define the objects the app call will interact with (read, write, or send transactions to).

The box array is an array of pairs: the first element of each pair is an integer specifying the index into the foreign application array, and the second element is the key name of the box to be accessed.

Each entry in the box array allows access to only 1kb of data. For example, if a box is sized to 4kb, the transaction must use four entries in this array. To claim an allotted entry, a corresponding app ID and box name must be added to the box ref array. If you need more than the 1kb associated with that specific box name, you can either specify the box ref entry more than once or, preferably, add “empty” box refs [0,””] into the array. If you specify 0 as the app ID, the box ref is for the application being called.

For example, suppose the contract needs to read “BoxA” which is 1.5kb, and “Box B” which is 2.5kb. This would require four entries in the box ref array and would look something like:

1
boxes=[[0, "BoxA"],[0,"BoxB"], [0,""],[0,""]]

The required box I/O budget is based on the sizes of the boxes accessed rather than the amount of data read or written. For example, if a contract accesses “Box A” with a size of 2kb and “Box B” with a size of 10 bytes, this requires both boxes to be in the box reference array and one additional reference ( ceil((2kb + 10b) / 1kb), which can be an “empty” box reference.

Access budgets are summed across multiple application calls in the same transaction group. For example, in a group of two smart contract calls, there is room for 16 array entries (8 per app call), allowing access to 16kb of data. If an application needs to access a 16kb box named “Box A”, it will need to be grouped with one additional application call, and the box reference array for each transaction in the group should look similar to this:

Transaction 0: [0,”Box A”],[0,””],[0,””],[0,””],[0,””],[0,””],[0,””],[0,””] Transaction 1: [0,””],[0,””],[0,””],[0,””],[0,””],[0,””],[0,””],[0,””]

Box refs can be added to the boxes array using goal or any SDKs.

Terminal window
goal app method --app-id=53 --method="add_member2()void" --box="53,str:BoxA" --from=CONP4XZSXVZYA7PGYH7426OCAROGQPBTWBUD2334KPEAZIHY7ZRR653AFY

Minimum Balance Requirement For Boxes

Boxes are created by a smart contract and raise the minimum balance requirement (MBR) in the contract’s ledger balance. This means that a contract intending to use boxes must be funded beforehand.

When a box with name n and size s is created, the MBR is raised by 2500 + 400 * (len(n)+s) microAlgos. When the box is destroyed, the minimum balance requirement is decremented by the same amount.

Notice that the key (name) is included in the MBR calculation.

For example, if a box is created with the name “BoxA” (a 4-byte long key) and with a size of 1024 bytes, the MBR for the app account increases by 413,700 microAlgos:

1
(2500 per box) + (400 * (box size + key size))
2
(2500) + (400 * (1024+4)) = 413,700 microAlgos

Manipulating Box Storage

Box storage offers several abstractions for efficient data handling:

Box: Box abstracts the reading and writing of a single value to a single box. The box size will be reconfigured dynamically to fit the size of the value being assigned to it.

BoxRef: BoxRef abstracts the reading and writing of boxes containing raw binary data. The size is configured manually and can be set to values larger than the AVM can handle in a single value.

BoxMap: BoxMap abstracts the reading and writing of a set of boxes using a common key and content type. Each composite key (prefix + key) still needs to be made available to the application via the boxes property of the Transaction.

Allocation

App A can allocate as many boxes as needed when needed. App a allocates a box using the box_create opcode in its TEAL program, specifying the name and the size of the allocated box. Boxes can be any size from 0 to 32K bytes. Box names must be at least 1 byte, at most 64 bytes, and unique within app a. The app account(the smart contract) is responsible for funding the box storage (with an increase to its minimum balance requirement; see below for details). The app call’s boxes array must reference a box name and app ID to be allocated.

Boxes may only be accessed (whether reading or writing) in a Smart Contract’s approval program, not in a clear state program.

Creating a Box

The AVM supports two opcodes box_create and box_put that can be used to create a box. The box_create opcode takes two parameters, the name and the size in bytes for the created box. The box_put opcode takes two parameters as well. The first parameter is the name and the second is a byte array to write. Because the AVM limits any element on the stack to 4kb, box_put can only be used for boxes with length <= 4kb.

Boxes can be created and deleted, but once created, they cannot be resized. At creation time, boxes are filled with 0 bytes up to their requested size. The box’s contents can be changed, but the size is fixed at that point. If a box needs to be resized, it must first be deleted and then recreated with the new size.

1
def __init__(self) -> None:
2
self.box_int = Box(UInt64)
3
self.box_dynamic_bytes = Box[arc4.DynamicBytes](arc4.DynamicBytes, key="b")
4
self.box_string = Box(arc4.String, key=b"BOX_C")
5
self.box_bytes = Box(Bytes)
6
self.box_map = BoxMap(
7
UInt64, String, key_prefix=""
8
) # Box map with uint as key and string as value
9
self.box_ref = BoxRef() # Box reference
10
self.box_map_struct = BoxMap(arc4.UInt64, UserStruct, key_prefix="users")

Box names must be unique within an application. If using box_create, and an existing box name is passed with a different size, the creation will fail. If an existing box name is used with the existing size, the call will return a 0 without modifying the box contents. When creating a new box, the call will return a 1. When using box_put with an existing key name, the put will fail if the size of the second argument (data array) is different from the original box size.

Reading

Boxes can only be manipulated by the smart contract that owns them. While the SDKs and goal cmd tool allow these boxes to be read off-chain, only the smart contract that owns them can read or manipulate them on-chain. App a is the only app that can read the contents of its boxes on-chain. This on-chain privacy is unique to box storage. Recall that anybody can read everything from off-chain using the algod or indexer APIs. To read box b from app a, the app call must include b in its boxes array. Read budget: Each box reference in the boxes array allows an app call to access 1K bytes of box state - 1K of “box read budget”. To read a box larger than 1K, multiple box references must be put in the boxes arrays. The box read budget is shared across the transaction group. The total box read budget must be larger than the sum of the sizes of all the individual boxes referenced (it is not possible to use this read budget for a part of a box - the whole box is read in). Box data is unstructured. This is unique to box storage. A box is referenced by including its app ID and box name.

The AVM provides two opcodes for reading the contents of a box, box_get and box_extract. The box_get opcode takes one parameter,: the key name of the box. It reads the entire contents of a box. The box_get opcode returns two values. The top-of-stack is an integer that has the value of 1 or 0. A value of 1 means that the box was found and read. A value of 0 means that the box was not found. The next stack element contains the bytes read if the box exists; otherwise, it contains an empty byte array. box_get fails if the box length exceeds 4kb.

1
@arc4.abimethod
2
def get_box(self) -> UInt64:
3
return self.box_int.value
4
5
@arc4.abimethod
6
def get_item_box_map(self, key: UInt64) -> String:
7
return self.box_map[key]
8
9
@arc4.abimethod
10
def get_box_map(self) -> String:
11
key_1 = UInt64(1)
12
return self.box_map.get(key_1, default=String("default"))
13
14
@arc4.abimethod
15
def get_box_ref(self) -> None:
16
box_ref = BoxRef(key=String("blob"))
17
assert box_ref.create(size=32)
18
sender_bytes = Txn.sender.bytes
19
20
assert box_ref.delete()
21
assert box_ref.key == b"blob"
22
assert box_ref.get(default=sender_bytes) == sender_bytes
23
24
@arc4.abimethod
25
def maybe_box(self) -> tuple[UInt64, bool]:
26
box_int_value, box_int_exists = self.box_int.maybe()
27
return box_int_value, box_int_exists
28
29
@arc4.abimethod
30
def maybe_box_map(self) -> tuple[String, bool]:
31
key_1 = UInt64(1)
32
value, exists = self.box_map.maybe(key_1)
33
if not exists:
34
value = String("")
35
return value, exists
36
37
@arc4.abimethod
38
def maybe_box_ref(self) -> tuple[Bytes, bool]:
39
box_ref = BoxRef(key=String("blob"))
40
assert box_ref.create(size=32)
41
42
value, exists = box_ref.maybe()
43
if not exists:
44
value = Bytes(b"")
45
return value, exists
1
@arc4.abimethod
2
def extract_box_ref(self) -> None:
3
box_ref = BoxRef(key=String("blob"))
4
assert box_ref.create(size=32)
5
6
sender_bytes = Txn.sender.bytes
7
app_address = Global.current_application_address.bytes
8
value_3 = Bytes(b"hello")
9
box_ref.replace(0, sender_bytes)
10
box_ref.splice(0, 0, app_address)
11
box_ref.replace(64, value_3)
12
prefix = box_ref.extract(0, 32 * 2 + value_3.length)
13
assert prefix == app_address + sender_bytes + value_3

Writing

App A is the only app that can write the contents of its boxes. As with reading, each box ref in the boxes array allows an app call to write 1kb of box state - 1kb of “box write budget”.

The AVM provides two opcodes, box_put and box_replace, to write data to a box. The box_put opcode is described in the previous section. The box_replace opcode takes three parameters: the key name, the starting location and replacement bytes.

1
@arc4.abimethod
2
def set_box(self, value_int: UInt64) -> None:
3
self.box_int.value = value_int
4
5
@arc4.abimethod
6
def set_box_map(self, key: UInt64, value: String) -> None:
7
self.box_map[key] = value
8
9
@arc4.abimethod
10
def set_box_map_struct(self, key: arc4.UInt64, value: UserStruct) -> bool:
11
self.box_map_struct[key] = value.copy()
12
assert self.box_map_struct[key] == value
13
return True

When using box_replace, the box size can not increase. This means the call will fail if the replacement bytes, when added to the start byte location, exceed the box’s upper bounds.

The following sections cover the details of manipulating boxes within a smart contract.

Getting a Box Length

The AVM offers the box_len opcode to retrieve the length of a box and verify its existence. The opcode takes the box key name and returns two unsigned integers (uint64). The top-of-stack is either a 0 or 1, where 1 indicates the box’s existence, and 0 indicates it does not exist. The next is the length of the box if it exists; otherwise, it is 0.

1
@arc4.abimethod
2
def box_map_length(self) -> UInt64:
3
key_0 = UInt64(0)
4
if key_0 not in self.box_map:
5
return UInt64(0)
6
return self.box_map.length(key_0)
7
8
@arc4.abimethod
9
def length_box_ref(self) -> UInt64:
10
box_ref = BoxRef(key=String("blob"))
11
assert box_ref.create(size=32)
12
return box_ref.length
13
14
@arc4.abimethod
15
def box_map_struct_length(self) -> bool:
16
key_0 = arc4.UInt64(0)
17
value = UserStruct(arc4.String("testName"), arc4.UInt64(70), arc4.UInt64(2))
18
19
self.box_map_struct[key_0] = value.copy()
20
assert self.box_map_struct[key_0].bytes.length == value.bytes.length
21
assert self.box_map_struct.length(key_0) == value.bytes.length
22
return True

Deleting a Box

Only the app that created a box can delete it. If an app is deleted, its boxes are not deleted. The boxes will not be modifiable but can still be queried using the SDKs. The minimum balance will also be locked. (The correct cleanup design is to look up the boxes from off-chain and call the app to delete all its boxes before deleting the app itself.)

The AVM offers the box_del opcode to delete a box. This opcode takes the box key name. The opcode returns one unsigned integer (uint64) with a value of 0 or 1. A value of 1 indicates the box existed and was deleted. A value of 0 indicates the box did not exist.

1
@arc4.abimethod
2
def delete_box(self) -> None:
3
del self.box_int.value
4
del self.box_dynamic_bytes.value
5
del self.box_string.value
6
7
assert self.box_int.get(default=UInt64(42)) == 42
8
assert (
9
self.box_dynamic_bytes.get(default=arc4.DynamicBytes(b"42")).native == b"42"
10
)
11
assert self.box_string.get(default=arc4.String("42")) == "42"
12
13
@arc4.abimethod
14
def delete_box_map(self, key: UInt64) -> None:
15
del self.box_map[key]
16
17
@arc4.abimethod
18
def delete_box_ref(self) -> None:
19
box_ref = BoxRef(key=String("blob"))
20
self.box_ref.create(size=UInt64(32))
21
assert self.box_ref, "has data"
22
23
self.box_ref.delete()
24
value, exists = box_ref.maybe()
25
assert not exists
26
assert value == b""

Other methods for boxes

Here are some methods that can be used with box reference to splice, replace and extract box

1
@arc4.abimethod
2
def manipulate_box_ref(self) -> None:
3
box_ref = BoxRef(key=String("blob"))
4
assert box_ref.create(size=32)
5
assert box_ref, "has data"
6
7
# manipulate data
8
sender_bytes = Txn.sender.bytes
9
app_address = Global.current_application_address.bytes
10
value_3 = Bytes(b"hello")
11
box_ref.replace(0, sender_bytes)
12
box_ref.splice(0, 0, app_address)
13
box_ref.replace(64, value_3)
14
prefix = box_ref.extract(0, 32 * 2 + value_3.length)
15
assert prefix == app_address + sender_bytes + value_3
16
17
assert box_ref.delete()
18
assert box_ref.key == b"blob"
19
20
box_ref.put(sender_bytes + app_address)
21
assert box_ref, "Blob exists"
22
assert box_ref.length == 64

You must delete all boxes before deleting a contract. If this is not done, the minimum balance for that box is not recoverable.

Summary of Box Operations

For manipulating box storage data like reading, writing, deleting and checking if it exists:

TEAL: Different opcodes can be used

FunctionDescription
box_createcreates a box named A of length B. It fails if the name A is empty or B exceeds 32,768. It returns 0 if A already exists else 1
box_deldeletes a box named A if it exists. It returns 1 if A existed, 0 otherwise
box_extractreads C bytes from box A, starting at offset B. It fails if A does not exist or the byte range is outside A’s size
box_getretrieves the contents of box A if A exists, else ”. Y is 1 if A exists, else 0
box_lenretrieves the length of box A if A exists, else 0. Y is 1 if A exists, else 0
box_putreplaces the contents of box A with byte-array B. It fails if A exists and len(B) != len(box A). It creates A if it does not exist
box_replacewrites byte-array C into box A, starting at offset B. It fails if A does not exist or the byte range is outside A’s size

Different functions of the box can be used. The detailed API reference can be found here

Example: Storing struct in box map

1
class UserStruct(arc4.Struct):
2
name: arc4.String
3
id: arc4.UInt64
4
asset: arc4.UInt64
5
6
7
class StructInBoxMap(arc4.ARC4Contract):
8
def __init__(self) -> None:
9
self.user_map = BoxMap(arc4.UInt64, UserStruct, key_prefix="users")
10
11
@arc4.abimethod
12
def box_map_test(self) -> bool:
13
key_0 = arc4.UInt64(0)
14
value = UserStruct(arc4.String("testName"), arc4.UInt64(70), arc4.UInt64(2))
15
16
self.user_map[key_0] = value.copy()
17
assert self.user_map[key_0].bytes.length == value.bytes.length
18
assert self.user_map.length(key_0) == value.bytes.length
19
return True
20
21
@arc4.abimethod
22
def box_map_set(self, key: arc4.UInt64, value: UserStruct) -> bool:
23
self.user_map[key] = value.copy()
24
assert self.user_map[key] == value
25
return True
26
27
@arc4.abimethod
28
def box_map_get(self, key: arc4.UInt64) -> UserStruct:
29
return self.user_map[key]
30
31
@arc4.abimethod
32
def box_map_exists(self, key: arc4.UInt64) -> bool:
33
return key in self.user_map