Skip to content

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.

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.

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.

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

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 microAlgos

The 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.

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:

MechanismParameterWho gains accessWhat they can do
Foreign box readsAppForeignBoxReadsAny applicationRead only
Family box accessAppFamilyBoxAccessApplications sharing the owner’s creator addressRead, 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:

OperationOwning applicationSame creator, with AppFamilyBoxAccessAny application, with AppForeignBoxReads
ReadYesYesYes
WriteYesYesNo

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.

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.

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.

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.

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 corruption

The 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.

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 boxMarks the frame as relying on family stateRuns the re-entrancy check
ReadYesNo
Write, create, resize, deleteYesYes

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.

In the following, A and C share a creator address and X does not. Box b is owned by A, which has set AppFamilyBoxAccess.

SequenceVerdictReasoning
A writes b, then A → X → C, and C writes bBlockedA’s write would be clobbered while A is suspended
A reads b, then A → X → C, and C writes bBlockedThe 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 bAllowedNo frame on the stack is relying on b
A writes b, then A → X → C, and C only reads bAllowedReads never trigger the check
A writes b, then A → C directly, C writes bAllowedNo non-family application separates them
A delegates its write to C, then A → X → C writes againBlockedA inherited the mark when C returned
PropertyWhat it means for you
The check fires at the write, not at the callAn 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 checkA stale read is a hazard for the reader alone; it cannot corrupt anyone else’s state
Direct family calls are unrestrictedApplications 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 tooOnce a box is family-shared, the owner is just one writer among several and runs the same check

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.

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.

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' })

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.

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
*/
@readonly
public getBox(): uint64 {
return this.boxInt.value
}
/**
* Retrieves the value of the boxInt box
*/
@readonly
public 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
*/
@readonly
public 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
*/
@readonly
public 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
*/
@readonly
public 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
*/
@readonly
public 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
*/
@readonly
public 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
*/
@readonly
public boxMapExists(key: uint64): boolean {
return this.boxMap(key).exists
}
/**
* 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')
}

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
}

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.

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
*/
@readonly
public boxMapLength(key: uint64): uint64 {
if (!this.boxMap(key).exists) {
return Uint64(0)
}
return this.boxMap(key).length
}

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()
}

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')
}

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

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

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

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
}
}