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=CONP4XZSXVZYA7PGYH7426OCAROGQPBTWBUD2334KPEAZIHY7ZRR653AFYMinimum 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 microAlgosManipulating 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, exists1#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    retsub1    @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_31#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    retsubWriting
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 True1#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    retsubWhen 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 True1#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    retsubDeleting 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    retsubOther 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 == 641#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    retsubYou 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