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 up to 32KB (32768 bytes) of data as byte arrays. The app account (the smart contract) is responsible for funding the box storage, and by default only the application that created a box can read, write, or delete it on-chain. Starting with AVM v13, an owning application can opt in to sharing its boxes with other applications; see Sharing Boxes Between Applications.
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 txn.Boxes array holds at most 8 box references, and those 8 are shared with the transaction’s account, asset, and application references. A transaction may instead use the mutually exclusive txn.Access list, which holds up to 16 references of any kind. Both limits count references rather than boxes, so naming other resources leaves room for fewer boxes. 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 2048 bytes require additional references, as each reference has a 2048-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
Section titled “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
Section titled “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 2kb of data. For example, if a box is sized to 4kb, the transaction must use two 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 2kb 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. Repeating a reference works because references are counted with duplicates while each box’s size is counted once, so a second reference to the same box buys 2kb of budget without adding to the bytes to be covered. If you specify 0 as the app ID, the box ref is for the application being called. To reference a box owned by a different application, supply that application’s ID instead of 0. Note that referencing another application’s box makes it available, but does not by itself grant permission to access it; see Sharing Boxes Between Applications.
For example, suppose the contract needs to read “BoxA” which is 3kb, and “Box B” which is 5kb. The budget is pooled across the references rather than counted per box, so this needs four entries to cover 8kb in total, not the five that rounding each box up on its own would suggest:
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 4kb 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((4kb + 10b) / 2kb) = 3), which can be an “empty” box reference. “Box A” on its own would fit in two references exactly, so the 10-byte box costs a whole third one: the budget is granted per reference, not per byte. This is also why a small read from a large box can fail: box_extract of 10 bytes from a 30kb box still needs 30kb of budget, or fifteen references, because the whole box is counted.
Boxes are not the only thing that draws on this budget. For each application available to the group, including the applications being called, any program bytes beyond the standard 8kb limit also count against the box I/O budget, on both the read and the write side. Referencing another application’s box brings that application into the group as well, so its program bytes count too. Programs can only exceed 8kb from consensus v42 onward, by paying an additional fee, so calling or referencing such an application leaves less budget for boxes than the reference count alone suggests.
Box I/O budgets are summed across multiple application calls in the same transaction group. For example, in a group of two smart contract calls using the txn.Boxes array, there is room for 16 array entries (8 per app call, provided those calls use no other foreign references, since accounts, assets, and applications draw on the same per-call limit of 8), allowing access to 32kb of data. If an application needs to access a box of the maximum 32kb size 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
Section titled “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:
(2500 per box) + (400 * (box size + key size))(2500) + (400 * (1024+4)) = 413,700 microAlgosThe minimum balance requirement always belongs to the application that owns the box, never to the application performing the operation. When a permitted application creates, resizes, or deletes a box owned by another application, it is the owning application’s account that is affected. If that account cannot cover the increased requirement, the operation fails. The box I/O budget works the other way round: the group performing the write must cover the bytes it writes, even though the minimum balance falls on the owning application.
Sharing Boxes Between Applications
Section titled “Sharing Boxes Between Applications”A box can be read or written on-chain by the application that owns it. Starting with AVM v13 (consensus v42), an owning application can share its boxes with other applications. There are two independent mechanisms, both set by the owning application on itself with the app_params_set opcode:
| Mechanism | Parameter | Who gains access | What they can do |
|---|---|---|---|
| Foreign box reads | AppForeignBoxReads | Any application | Read only |
| Family box access | AppFamilyBoxAccess | Applications sharing the owner’s creator address | Read, write, create, resize, and delete |
Only family box access involves any notion of a family. Foreign box reads are open to every application, whoever created it.
The two mechanisms grant these levels of access, where write covers creating, updating, resizing, and deleting a box:
| Operation | Owning application | Same creator, with AppFamilyBoxAccess | Any application, with AppForeignBoxReads |
|---|---|---|---|
| Read | Yes | Yes | Yes |
| Write | Yes | Yes | No |
There is no parameter that grants write access to applications outside the family. AppForeignBoxReads grants reads and nothing else.
An application can only set these parameters on itself. There is no way for one application to open up another application’s boxes, and the owning application does not need to be called or otherwise involved when a permitted application accesses its boxes. Both parameters are revocable: setting the field back to 0 with app_params_set withdraws the grant.
Accessing another application’s boxes
Section titled “Accessing another application’s boxes”Each box opcode has an app_box_* counterpart that performs the same operation but takes an additional argument identifying the application that owns the box. For example, box_len returns the length of a box owned by the currently executing application, while app_box_len returns the length of a box owned by the application whose ID is supplied.
Because these opcodes act on the box directly, no inner transaction to the owning application is required. This differs from the usual pattern for reaching another application’s state, which requires that application to expose a method and to be called.
The box must still be made available in the usual way, by appearing in the box references (txn.Boxes) or access list (txn.Access) of some transaction in the group. Availability and permission are separate requirements, and both must be satisfied:
- If the box is available but the owning application has not set the relevant parameter, the access fails.
- If the owning application has set the parameter but the box is not available, the access fails.
Availability is not the same as budget. Another application’s box consumes the reading group’s box I/O budget exactly as the group’s own boxes do, so reading a 32kb box owned by another application needs sixteen references’ worth of budget, not one.
Foreign box reads
Section titled “Foreign box reads”Setting AppForeignBoxReads allows any application to read the owning application’s boxes on-chain, regardless of who created it. It grants reads and nothing else: writing, creating, resizing, and deleting remain restricted to the owning application and, where enabled, its family.
Because no other application can modify a box through this mechanism, foreign box reads carry no minimum balance consequences and no re-entrancy considerations.
Family box access
Section titled “Family box access”An application’s family is the set of applications that share the same creator address. Membership is resolved when the access happens, not when the parameter is set, so the grant extends to every application that creator deploys in the future as well as the ones that exist today.
Setting AppFamilyBoxAccess allows any application in the family to read and write the owning application’s boxes, including creating, resizing, and deleting them.
Re-entrancy in family box access
Section titled “Re-entrancy in family box access”The AVM already forbids re-entering an application that is on the call stack. That rule gives every application a useful guarantee: while it is suspended waiting on an inner call, no one can change its state, because no one can run its code.
Family box access breaks that guarantee, because a different application can now modify a box the suspended application depends on:
A writes its box "b" A now relies on b == "AA" A → X non-family app (different creator) X → C family member of A (same creator) C writes A's box "b" → "CC"A resumes, still assuming b == "AA" ← silent corruptionThe existing re-entrancy rule does not catch this, because C is not A; the application was never re-entered. AVM v13 therefore adds a second, family-scoped check:
An application cannot write a family-shared box when a non-family application sits on the call stack between it and a family application that has already read or written a family-shared box; the write fails.
How each operation behaves
Section titled “How each operation behaves”Reading a family-shared box marks the current call frame as relying on family state, but does not itself trigger the check. Writing runs the check.
| Operation on a family-shared box | Marks the frame as relying on family state | Runs the re-entrancy check |
|---|---|---|
| Read | Yes | No |
| Write, create, resize, delete | Yes | Yes |
The mark is inherited. When a marked frame returns to a caller with the same creator, the caller becomes marked too, so delegating a write to a sibling still counts as relying on family state.
What is allowed and what is blocked
Section titled “What is allowed and what is blocked”In the following, A and C share a creator address and X does not. Box b is owned by A, which has set AppFamilyBoxAccess.
| Sequence | Verdict | Reasoning |
|---|---|---|
A writes b, then A → X → C, and C writes b | Blocked | A’s write would be clobbered while A is suspended |
A reads b, then A → X → C, and C writes b | Blocked | The value A read goes stale; reads mark the frame even though they do not check |
A never touches b, then A → X → C, C writes b | Allowed | No frame on the stack is relying on b |
A writes b, then A → X → C, and C only reads b | Allowed | Reads never trigger the check |
A writes b, then A → C directly, C writes b | Allowed | No non-family application separates them |
A delegates its write to C, then A → X → C writes again | Blocked | A inherited the mark when C returned |
Why the rule is shaped this way
Section titled “Why the rule is shaped this way”| Property | What it means for you |
|---|---|
| The check fires at the write, not at the call | An unrelated application can still call into a family member to read box data, so patterns like price oracles and registries keep working |
| Reads mark but do not check | A stale read is a hazard for the reader alone; it cannot corrupt anyone else’s state |
| Direct family calls are unrestricted | Applications sharing a creator are treated as one co-designed codebase, so coordinating their own writes is the developer’s responsibility, not the AVM’s |
| The owning application is checked too | Once a box is family-shared, the owner is just one writer among several and runs the same check |
Manipulating Box Storage
Section titled “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
Section titled “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.
If app a has set AppFamilyBoxAccess, applications sharing its creator can also allocate boxes in app a using the app_box_create opcode. The resulting increase to the minimum balance requirement still falls on app a’s account.
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
Section titled “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.
public boxString = Box<string>({ key: 'boxString' })public boxInt = Box<uint64>({ key: 'boxInt' })public boxBytes = Box<bytes>({ key: 'boxBytes' })public boxDynamicBytes = Box<arc4.DynamicBytes>({ key: 'boxDynamicBytes' })public boxMap = BoxMap<uint64, string>({ keyPrefix: 'boxMap' })public boxMapObject = BoxMap<uint64, User>({ keyPrefix: 'users' })def __init__(self) -> None: self.box_int = Box(UInt64) self.box_dynamic_bytes = Box[arc4.DynamicBytes](arc4.DynamicBytes, key="b") self.box_string = Box(arc4.String, key=b"BOX_C") self.box_bytes = Box(Bytes) self.box_map = BoxMap( UInt64, String, key_prefix="" ) # Box map with uint as key and string as value 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
Section titled “Reading”Boxes can be manipulated by the smart contract that owns them. The SDKs and goal cmd tool allow any box to be read off-chain, and box contents are never confidential; the restriction governs only which applications may read or manipulate a box on-chain.
App a can read the contents of its boxes on-chain. Since AVM v13, app a can permit other applications to read its boxes on-chain: AppForeignBoxReads opens reads to any application, and AppFamilyBoxAccess opens reads and writes of boxes to applications sharing a’s creator address; see Sharing Boxes Between Applications. 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 2K bytes of box state - 2K of “box read budget”. To read a box larger than 2K, 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 at least as large as the sum of the sizes of all the individual boxes referenced, plus any program bytes beyond 8kb for the applications available to the group (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.
/** * Retrieves the value stored in the boxInt box * @returns The uint64 value stored in boxInt */@readonlypublic getBox(): uint64 { return this.boxInt.value}
/** * Retrieves the value of the boxInt box */@readonlypublic valueBox(): uint64 { return this.boxInt.value}
/** * Retrieves the value stored in the boxInt box and checks if it exists * @returns A tuple containing the value and a boolean indicating if the box exists */@readonlypublic maybeBox(): [uint64, boolean] { const [boxIntValue, boxIntExists] = this.boxInt.maybe() return [boxIntValue, boxIntExists]}
/** * Retrieves the value stored in the boxMap box * @param key The key of the boxMap to retrieve the value from * @returns The value stored in the boxMap box */@readonlypublic getBoxMap(key: uint64): string { return this.boxMap(key).value}
/** * Retrieves the value stored in the boxMap box with a default value if the key does not exist * @param key The key of the boxMap to retrieve the value from * @returns The value stored in the boxMap box */@readonlypublic getBoxMapWithDefault(key: uint64): string { return this.boxMap(key).get({ default: 'default' })}
/** * Retrieves the value stored in the boxMap box and checks if it exists * @param key The key to check in the boxMap * @returns A tuple containing the value and a boolean indicating if the box exists */@readonlypublic maybeBoxMap(key: uint64): [string, boolean] { const [value, exists] = this.boxMap(key).maybe() return [exists ? value : '', exists]}
/** * Retrieves the key prefix of the boxMap box * @returns The key prefix of the boxMap box */@readonlypublic keyPrefixBoxMap(): bytes { return this.boxMap.keyPrefix}
/** * Checks if the boxMap box exists * @param key The key to check for * @returns true if the box exists, false otherwise */@readonlypublic boxMapExists(key: uint64): boolean { return this.boxMap(key).exists}@arc4.abimethoddef get_box(self) -> UInt64: return self.box_int.value
@arc4.abimethoddef get_item_box_map(self, key: UInt64) -> String: return self.box_map[key]
@arc4.abimethoddef get_box_map(self) -> String: key_1 = UInt64(1) return self.box_map.get(key_1, default=String("default"))
@arc4.abimethoddef maybe_box(self) -> tuple[UInt64, bool]: box_int_value, box_int_exists = self.box_int.maybe() return box_int_value, box_int_exists
@arc4.abimethoddef maybe_box_map(self) -> tuple[String, bool]: key_1 = UInt64(1) value, exists = self.box_map.maybe(key_1) if not exists: value = String("") return value, exists#pragma version 10
get_box: proto 0 1 byte "box_int" box_get swap btoi swap assert retsub
get_item_box_map: proto 1 1 frame_dig -1 itob box_get assert retsub
get_box_map: proto 0 1 int 1 itob box_get byte "default" cover 2 select retsub
get_box_ref: proto 0 0 byte "blob" int 32 box_create assert txn Sender byte "blob" box_del assert byte "blob" box_get dig 2 cover 2 select == assert retsub
maybe_box: proto 0 2 byte "box_int" box_get swap btoi swap retsub
maybe_box_map: proto 0 2 int 1 itob box_get dup uncover 2 swap bnz maybe_box_map_after_if_else@2 byte "" frame_bury 1
maybe_box_map_after_if_else@2: frame_dig 1 frame_dig 0 uncover 3 uncover 3 retsub
maybe_box_ref: proto 0 2 byte "blob" int 32 box_create assert byte "blob" box_get dup uncover 2 swap bnz maybe_box_ref_after_if_else@2 byte 0x frame_bury 1
maybe_box_ref_after_if_else@2: frame_dig 1 frame_dig 0 uncover 3 uncover 3 retsub/** * Extracts a value from the boxRef box * @param key The key to extract from */public extractBox(key: string): void { const senderBytes = Txn.sender.bytes const appAddress = Global.currentApplicationAddress.bytes const totalSize = Uint64(appAddress.length + senderBytes.length) const box = Box<bytes>({ key })
assert(box.create({ size: totalSize }), 'box creation failed')
box.replace(0, senderBytes) box.splice(0, 0, appAddress)
const part1 = box.extract(0, 32) const part2 = box.extract(32, 32)
assert(part1.equals(appAddress), 'First part should match app address') assert(part2.equals(senderBytes), 'Second part should match sender bytes')}@arc4.abimethoddef extract_box(self) -> None: box = Box(Bytes, key=String("blob")) assert box.create(size=UInt64(32))
sender_bytes = Txn.sender.bytes app_address = Global.current_application_address.bytes value_3 = Bytes(b"hello") box.replace(0, sender_bytes) box.splice(0, 0, app_address) box.replace(64, value_3) prefix = box.extract(0, 32 * 2 + value_3.length) assert prefix == app_address + sender_bytes + value_3#pragma version 10
extract_box_ref: proto 0 0 byte "blob" int 32 box_create assert global CurrentApplicationAddress txn Sender byte "blob" int 0 dig 2 box_replace byte "blob" int 0 dup dig 4 box_splice byte "blob" int 64 byte 0x68656c6c6f box_replace byte "blob" int 0 int 69 box_extract cover 2 concat byte 0x68656c6c6f concat == assert retsubWriting
Section titled “Writing”By default, app A is the only app that can write the contents of its boxes. Since AVM v13, app A can permit applications sharing its creator address to write its boxes by setting the AppFamilyBoxAccess parameter; see Family box access.
As with reading, each box ref in the boxes array allows an app call to write 2kb of box state - 2kb of “box write budget”. Reads and writes draw on the same per-reference allowance, each checked separately. The write budget is charged at the box’s full size rather than the number of bytes written, and only once however many times the box is written, so replacing a single byte in a 30kb box costs 30kb of write budget. Deleting a box adds nothing to the write budget, but it must still be referenced to be deleted, so its size counts against the read 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.
/** * Sets the value of the boxInt box * @param valueInt The uint64 value to set in the boxInt box */public setBox(valueInt: uint64): void { this.boxInt.value = valueInt}
/** * Sets the value of the boxString box * @param value The string value to set in the boxString box */public setBoxString(value: string): void { this.boxString.value = value}
/** * Sets the value of the boxDynamicBytes box * @param value The dynamic bytes value to set in the boxDynamicBytes box */public setBoxDynamicBytes(value: arc4.DynamicBytes): void { this.boxDynamicBytes.value = value}
/** * Sets the value of the boxMap box * @param key The key to set the value for * @param value The value to set in the boxMap box */public setBoxMap(key: uint64, value: string): void { this.boxMap(key).value = value}@arc4.abimethoddef set_box(self, value_int: UInt64) -> None: self.box_int.value = value_int
@arc4.abimethoddef set_box_map(self, key: UInt64, value: String) -> None: self.box_map[key] = value
@arc4.abimethoddef set_box_map_struct(self, key: arc4.UInt64, value: UserStruct) -> bool: self.box_map_struct[key] = value.copy() assert self.box_map_struct[key] == value return True#pragma version 10
set_box: proto 1 0 frame_dig -1 itob byte "box_int" swap box_put retsub
set_box_map: proto 2 0 frame_dig -2 itob dup box_del pop frame_dig -1 box_put retsub
set_box_map_struct: proto 2 1 byte "users" frame_dig -2 concat dup box_del pop dup frame_dig -1 box_put box_get assert frame_dig -1 == assert int 1 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
Section titled “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.
/** * Retrieves the length of the boxMap box * @param key The key to get the length for * @returns The length of the boxMap box */@readonlypublic boxMapLength(key: uint64): uint64 { if (!this.boxMap(key).exists) { return Uint64(0) }
return this.boxMap(key).length}@arc4.abimethoddef box_map_length(self) -> UInt64: key_0 = UInt64(0) if key_0 not in self.box_map: return UInt64(0) return self.box_map.length(key_0)
@arc4.abimethoddef box_map_struct_length(self) -> bool: key_0 = arc4.UInt64(0) value = UserStruct(arc4.String("testName"), arc4.UInt64(70), arc4.UInt64(2))
self.box_map_struct[key_0] = value.copy() assert self.box_map_struct[key_0].bytes.length == value.bytes.length assert self.box_map_struct.length(key_0) == value.bytes.length return True#pragma version 10
box_map_length: proto 0 1 int 0 itob dup box_len bury 1 bnz box_map_length_after_if_else@2 int 0 swap retsub
box_map_length_after_if_else@2: frame_dig 0 box_len assert swap retsub
length_box_ref: proto 0 1 byte "blob" int 32 box_create assert byte "blob" box_len assert retsub
box_map_struct_length: proto 0 1 byte 0x75736572730000000000000000 box_del pop byte 0x75736572730000000000000000 byte 0x0012000000000000004600000000000000020008746573744e616d65 box_put byte 0x75736572730000000000000000 box_len assert int 28 == assert byte 0x75736572730000000000000000 box_len assert int 28 == assert int 1 retsubDeleting a Box
Section titled “Deleting a Box”By default, only the app that created a box can delete it. If the owning app has set AppFamilyBoxAccess, applications sharing its creator address can delete it as well. 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.
/** * Deletes the value of the boxInt box */public deleteBox(): void { this.boxInt.delete() this.boxDynamicBytes.delete() this.boxString.delete()
assert(this.boxInt.get({ default: Uint64(42) }) === 42) assert(this.boxDynamicBytes.get({ default: new arc4.DynamicBytes('42') }).native === Bytes('42')) assert(this.boxString.get({ default: '42' }) === '42')}
/** * Deletes the value of the boxMap box * @param key The key to delete the value from */public deleteBoxMap(key: uint64): void { this.boxMap(key).delete()}@arc4.abimethoddef delete_box(self) -> None: del self.box_int.value del self.box_dynamic_bytes.value del self.box_string.value
assert self.box_int.get(default=UInt64(42)) == 42 assert ( self.box_dynamic_bytes.get(default=arc4.DynamicBytes(b"42")).native == b"42" ) assert self.box_string.get(default=arc4.String("42")) == "42"
@arc4.abimethoddef delete_box_map(self, key: UInt64) -> None: del self.box_map[key]#pragma version 10
delete_box: proto 0 0 byte "box_int" box_del pop byte "b" box_del pop byte 0x424f585f43 box_del pop byte "box_int" box_get swap btoi int 42 swap uncover 2 select int 42 == assert byte "b" box_get byte 0x00023432 cover 2 select extract 2 0 byte 0x3432 == assert byte 0x424f585f43 box_get byte 0x00023432 cover 2 select byte 0x00023432 == assert retsub
delete_box_map: proto 1 0 frame_dig -1 itob box_del pop retsub
delete_box_ref: proto 0 0 byte "box_ref" int 32 box_create pop byte "box_ref" box_len bury 1 assert byte "box_ref" box_del pop byte "blob" box_get ! assert byte 0x == assert retsubOther methods for boxes
Section titled “Other methods for boxes”Here are some methods that can be used with box reference to splice, replace and extract box
/** * Extracts a value from the boxRef box * @param key The key to extract from */public extractBox(key: string): void { const senderBytes = Txn.sender.bytes const appAddress = Global.currentApplicationAddress.bytes const totalSize = Uint64(appAddress.length + senderBytes.length) const box = Box<bytes>({ key })
assert(box.create({ size: totalSize }), 'box creation failed')
box.replace(0, senderBytes) box.splice(0, 0, appAddress)
const part1 = box.extract(0, 32) const part2 = box.extract(32, 32)
assert(part1.equals(appAddress), 'First part should match app address') assert(part2.equals(senderBytes), 'Second part should match sender bytes')}@arc4.abimethoddef extract_box(self) -> None: box = Box(Bytes, key=String("blob")) assert box.create(size=UInt64(32))
sender_bytes = Txn.sender.bytes app_address = Global.current_application_address.bytes value_3 = Bytes(b"hello") box.replace(0, sender_bytes) box.splice(0, 0, app_address) box.replace(64, value_3) prefix = box.extract(0, 32 * 2 + value_3.length) assert prefix == app_address + sender_bytes + value_3#pragma version 10
manipulate_box_ref: proto 0 0 byte "blob" int 32 box_create assert byte "blob" box_len bury 1 assert global CurrentApplicationAddress txn Sender byte "blob" int 0 dig 2 box_replace byte "blob" int 0 dup dig 4 box_splice byte "blob" int 64 byte 0x68656c6c6f box_replace byte "blob" int 0 int 69 box_extract dig 2 dig 2 concat byte 0x68656c6c6f concat == assert byte "blob" box_del assert swap concat byte "blob" swap box_put byte "blob" box_len bury 1 assert byte "blob" box_len assert int 64 == assert 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
Section titled “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 |
Since AVM v13, each box opcode has an app_box_* counterpart that performs the same operation on a box owned by another application, taking an additional argument for that application’s ID: app_box_create, app_box_del, app_box_extract, app_box_get, app_box_len, app_box_put, app_box_replace, app_box_resize, and app_box_splice. These require the owning application to have permitted the access, as described in Sharing Boxes Between Applications. For the authoritative opcode signatures, see the Opcodes List.
Different functions of the box can be used. The detailed API reference can be found here
Example: Storing struct in box map
Section titled “Example: Storing struct in box map”type User = { id: uint64 name: string age: uint64}
export default class StructInBoxMap extends Contract { public users = BoxMap<uint64, User>({ keyPrefix: 'users' })
public createNewUser(id: uint64, user: User): boolean { this.users(id).value = clone(user)
assertMatch(this.users(id).value, { name: user.name, age: user.age, })
return true }
@readonly public getUser(id: uint64): User { return this.users(id).value }
@readonly public checkUserExists(id: uint64): boolean { return this.users(id).exists }
public updateUserNameAndAge(id: uint64, name: string, age: uint64): boolean { this.users(id).value.name = name this.users(id).value.age = age
assertMatch(this.users(id).value, { name, age, })
return true }}class UserStruct(arc4.Struct): name: arc4.String id: arc4.UInt64 asset: arc4.UInt64
class StructInBoxMap(arc4.ARC4Contract): def __init__(self) -> None: self.user_map = BoxMap(arc4.UInt64, UserStruct, key_prefix="users")
@arc4.abimethod def box_map_test(self) -> bool: key_0 = arc4.UInt64(0) value = UserStruct(arc4.String("testName"), arc4.UInt64(70), arc4.UInt64(2))
self.user_map[key_0] = value.copy() assert self.user_map[key_0].bytes.length == value.bytes.length assert self.user_map.length(key_0) == value.bytes.length return True
@arc4.abimethod def box_map_set(self, key: arc4.UInt64, value: UserStruct) -> bool: self.user_map[key] = value.copy() assert self.user_map[key] == value return True
@arc4.abimethod def box_map_get(self, key: arc4.UInt64) -> UserStruct: return self.user_map[key]
@arc4.abimethod def box_map_exists(self, key: arc4.UInt64) -> bool: return key in self.user_map