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:
1boxes=[[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.
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 value9 self.box_ref = BoxRef() # Box reference10 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.abimethod2 def get_box(self) -> UInt64:3 return self.box_int.value4
5 @arc4.abimethod6 def get_item_box_map(self, key: UInt64) -> String:7 return self.box_map[key]8
9 @arc4.abimethod10 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.abimethod15 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.bytes19
20 assert box_ref.delete()21 assert box_ref.key == b"blob"22 assert box_ref.get(default=sender_bytes) == sender_bytes23
24 @arc4.abimethod25 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_exists28
29 @arc4.abimethod30 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, exists36
37 @arc4.abimethod38 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#pragma version 102
3get_box:4 proto 0 15 byte "box_int"6 box_get7 swap8 btoi9 swap10 assert11 retsub12
13get_item_box_map:14 proto 1 115 frame_dig -116 itob17 box_get18 assert19 retsub20
21get_box_map:22 proto 0 123 int 124 itob25 box_get26 byte "default"27 cover 228 select29 retsub30
31get_box_ref:32 proto 0 033 byte "blob"34 int 3235 box_create36 assert37 txn Sender38 byte "blob"39 box_del40 assert41 byte "blob"42 box_get43 dig 244 cover 245 select46 ==47 assert48 retsub49
50maybe_box:51 proto 0 252 byte "box_int"53 box_get54 swap55 btoi56 swap57 retsub58
59maybe_box_map:60 proto 0 261 int 162 itob63 box_get64 dup65 uncover 266 swap67 bnz maybe_box_map_after_if_else@268 byte ""69 frame_bury 170
71maybe_box_map_after_if_else@2:72 frame_dig 173 frame_dig 074 uncover 375 uncover 376 retsub77
78maybe_box_ref:79 proto 0 280 byte "blob"81 int 3282 box_create83 assert84 byte "blob"85 box_get86 dup87 uncover 288 swap89 bnz maybe_box_ref_after_if_else@290 byte 0x91 frame_bury 192
93maybe_box_ref_after_if_else@2:94 frame_dig 195 frame_dig 096 uncover 397 uncover 398 retsub
1 @arc4.abimethod2 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.bytes7 app_address = Global.current_application_address.bytes8 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
1#pragma version 102
3extract_box_ref:4 proto 0 05 byte "blob"6 int 327 box_create8 assert9 global CurrentApplicationAddress10 txn Sender11 byte "blob"12 int 013 dig 214 box_replace15 byte "blob"16 int 017 dup18 dig 419 box_splice20 byte "blob"21 int 6422 byte 0x68656c6c6f23 box_replace24 byte "blob"25 int 026 int 6927 box_extract28 cover 229 concat30 byte 0x68656c6c6f31 concat32 ==33 assert34 retsub
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.abimethod2 def set_box(self, value_int: UInt64) -> None:3 self.box_int.value = value_int4
5 @arc4.abimethod6 def set_box_map(self, key: UInt64, value: String) -> None:7 self.box_map[key] = value8
9 @arc4.abimethod10 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] == value13 return True
1#pragma version 102
3set_box:4 proto 1 05 frame_dig -16 itob7 byte "box_int"8 swap9 box_put10 retsub11
12set_box_map:13 proto 2 014 frame_dig -215 itob16 dup17 box_del18 pop19 frame_dig -120 box_put21 retsub22
23set_box_map_struct:24 proto 2 125 byte "users"26 frame_dig -227 concat28 dup29 box_del30 pop31 dup32 frame_dig -133 box_put34 box_get35 assert36 frame_dig -137 ==38 assert39 int 140 retsub
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.abimethod2 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.abimethod9 def length_box_ref(self) -> UInt64:10 box_ref = BoxRef(key=String("blob"))11 assert box_ref.create(size=32)12 return box_ref.length13
14 @arc4.abimethod15 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.length21 assert self.box_map_struct.length(key_0) == value.bytes.length22 return True
1#pragma version 102
3box_map_length:4 proto 0 15 int 06 itob7 dup8 box_len9 bury 110 bnz box_map_length_after_if_else@211 int 012 swap13 retsub14
15box_map_length_after_if_else@2:16 frame_dig 017 box_len18 assert19 swap20 retsub21
22length_box_ref:23 proto 0 124 byte "blob"25 int 3226 box_create27 assert28 byte "blob"29 box_len30 assert31 retsub32
33box_map_struct_length:34 proto 0 135 byte 0x7573657273000000000000000036 box_del37 pop38 byte 0x7573657273000000000000000039 byte 0x0012000000000000004600000000000000020008746573744e616d6540 box_put41 byte 0x7573657273000000000000000042 box_len43 assert44 int 2845 ==46 assert47 byte 0x7573657273000000000000000048 box_len49 assert50 int 2851 ==52 assert53 int 154 retsub
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.abimethod2 def delete_box(self) -> None:3 del self.box_int.value4 del self.box_dynamic_bytes.value5 del self.box_string.value6
7 assert self.box_int.get(default=UInt64(42)) == 428 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.abimethod14 def delete_box_map(self, key: UInt64) -> None:15 del self.box_map[key]16
17 @arc4.abimethod18 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 exists26 assert value == b""
1#pragma version 102
3delete_box:4 proto 0 05 byte "box_int"6 box_del7 pop8 byte "b"9 box_del10 pop11 byte 0x424f585f4312 box_del13 pop14 byte "box_int"15 box_get16 swap17 btoi18 int 4219 swap20 uncover 221 select22 int 4223 ==24 assert25 byte "b"26 box_get27 byte 0x0002343228 cover 229 select30 extract 2 031 byte 0x343232 ==33 assert34 byte 0x424f585f4335 box_get36 byte 0x0002343237 cover 238 select39 byte 0x0002343240 ==41 assert42 retsub43
44delete_box_map:45 proto 1 046 frame_dig -147 itob48 box_del49 pop50 retsub51
52delete_box_ref:53 proto 0 054 byte "box_ref"55 int 3256 box_create57 pop58 byte "box_ref"59 box_len60 bury 161 assert62 byte "box_ref"63 box_del64 pop65 byte "blob"66 box_get67 !68 assert69 byte 0x70 ==71 assert72 retsub
Other methods for boxes
Here are some methods that can be used with box reference to splice, replace and extract box
1 @arc4.abimethod2 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 data8 sender_bytes = Txn.sender.bytes9 app_address = Global.current_application_address.bytes10 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_316
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
1#pragma version 102
3manipulate_box_ref:4 proto 0 05 byte "blob"6 int 327 box_create8 assert9 byte "blob"10 box_len11 bury 112 assert13 global CurrentApplicationAddress14 txn Sender15 byte "blob"16 int 017 dig 218 box_replace19 byte "blob"20 int 021 dup22 dig 423 box_splice24 byte "blob"25 int 6426 byte 0x68656c6c6f27 box_replace28 byte "blob"29 int 030 int 6931 box_extract32 dig 233 dig 234 concat35 byte 0x68656c6c6f36 concat37 ==38 assert39 byte "blob"40 box_del41 assert42 swap43 concat44 byte "blob"45 swap46 box_put47 byte "blob"48 box_len49 bury 150 assert51 byte "blob"52 box_len53 assert54 int 6455 ==56 assert57 retsub
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
Function | Description |
---|---|
box_create | creates 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_del | deletes a box named A if it exists. It returns 1 if A existed, 0 otherwise |
box_extract | reads 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_get | retrieves the contents of box A if A exists, else ”. Y is 1 if A exists, else 0 |
box_len | retrieves the length of box A if A exists, else 0. Y is 1 if A exists, else 0 |
box_put | replaces 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_replace | writes 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
1class UserStruct(arc4.Struct):2 name: arc4.String3 id: arc4.UInt644 asset: arc4.UInt645
6
7class StructInBoxMap(arc4.ARC4Contract):8 def __init__(self) -> None:9 self.user_map = BoxMap(arc4.UInt64, UserStruct, key_prefix="users")10
11 @arc4.abimethod12 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.length18 assert self.user_map.length(key_0) == value.bytes.length19 return True20
21 @arc4.abimethod22 def box_map_set(self, key: arc4.UInt64, value: UserStruct) -> bool:23 self.user_map[key] = value.copy()24 assert self.user_map[key] == value25 return True26
27 @arc4.abimethod28 def box_map_get(self, key: arc4.UInt64) -> UserStruct:29 return self.user_map[key]30
31 @arc4.abimethod32 def box_map_exists(self, key: arc4.UInt64) -> bool:33 return key in self.user_map