All About Anchor Account Size
April 6, 2023

0. TL;DR

Smart contracts using Anchor require developers to allocate space for new accounts and specify the account size. Anchor provides guidelines for calculating the size based on the account structure, but many developers use std::mem::size_of instead, as they don’t have to manually update the size when making changes to the account structure. Are they equivalent? In this blog post, we conduct a systematic comparison of the results produced by std::mem::size_of and the Anchor space reference.

While std::mem::size_of generally works well and allocates more space than necessary, there are some scenarios where it may return a smaller size. Therefore, developers should understand the differences between these two methods before selecting the appropriate method.

1. The Comparison

In Anchor, developers commonly utilize std::mem::size_of<T>() to determine the size of the Account struct. However, this function may not produce an exact match with the size of the data stored in the Account after serialization with BorshSerialize.

The table below provides a summary of the size calculation for different data types using both methods. While most types give the same results, discrepancies may arise for certain data types, such as Vector and Enum, where std::mem::size_of<T>() may return a smaller value.

Note: all experiments in this blog post were done on a 64-bit machine.

Type std::mem::size_of<T>() Anchor Space Reference
bool 1 1
u8/i8 1 1
u16/i16 2 2
u32/i32 4 4
u64/i64 8 8
u128/i128 16 16
[T;amount] space(T) * amount space(T) * amount
Pubkey 32 32
     
Vec<T> 24 4 + space(T) * amount
String 24 4 + string length
Option<T> pad(space(T)), when T is String, Vec<...>, Box<...>
pad(1 + space(T)), otherwise
1 + space(T)
Enum 0, zero-variant enum
space(variant), signle-variant enum
pad(space(max discriminant) + space(max variant)), otherwise
1 + largest variant size

2. The Differences

The size_of function in Rust returns a fixed value based on the size information of the type and may not provide an accurate calculation of the memory occupied for some objects.

2.1. Vec and String

  • Vector: when calculating the size of a Vector using size_of, the function returns a value of 24 since the Vector in memory comprises three fields: the pointer to the data, the capacity, and the length, each having a size of 8 bytes.
  • String: similarly, for a string, the size_of function returns a value of 24, which is due to the fact that a String in memory consists of three fields: the pointer to the data, the capacity, and the length, each having a size of 8 bytes.
Example

When calculating the size of the Example account that contains a vector of type T, some developers may use the formula std::mem::size_of::<Example>() + (std::mem::size_of::<T>() * example.len()).

This approach is valid and may even result in an account size that exceeds the original structure size of 20 bytes, because after serialization with BorshSerialize, the content of the vector becomes the length (4 bytes) plus the size of the actual content.

Please note that it is necessary to account for the additional bytes required to store the length of the vector when calculating the total account size.

2.2. Option<T>

The Option<T> has two variants: None or Some. In memory, the None variant doesn't store any values but just a "tag" of 0, while the Some variant stores values together with the "tag" of 1.

However, the 0/1 "tag" is not needed if T is a Box or other smart pointer types. Since smart pointers in Rust cannot be 0, None can be represented as 0 and Some can be directly represented by the value of the pointer itself.

Therefore, when calculating the size of an Option<T> using the size_of function,  the result will depend on the type T:

  • If it's a Box<...>, a Vec<...> or a String, the result is the size of T after the proper alignment padding.
  • Otherwise, the result is the size of T plus 1 after the alignment padding.

In comparison, after serialization, the actual space occupied is 1 + space(T), because Option uses 1 byte to represent Some or None.

Example

2.3. Enum

Typically, when the size_of function is applied to an enum, the result equals to the space of the discriminant plus the space of the largest variant, taking into account the proper memory alignment padding.

  • Discriminant space. For a zero-variant enum or a single variant enum, the discriminant is not needed such that the space is 0. For others, it's the space needed by the largest discriminant or the space specified by "#[repr(inttype)]".
  • Variant space. For a non-data-carrying enum, it's 0. Otherwise, it's the same space needed by the variant field. For more details, please refer to the visualizing memory layout of Rust's data types tutorial.
Example

3. Conclusion

In Rust, the size_of function returns a fixed value based on the size information of a given type, but it may not accurately calculate the memory occupied by the object.

Therefore, when calculating the size of an account, it is important to note that using the size_of function may lead to inconsistencies between the calculated size and the actual size of the account. To ensure accuracy, it is recommended to manually calculate the account size using the corresponding value from the official Anchor documentation.


About sec3 (Formerly Soteria)

sec3 is a security research firm that prepares Web3 projects for millions of users. The Launch Audit is a rigorous, researcher-led code examination that investigates and certifies mainnet-grade smart contracts; sec3’s continuous auditing software platform, X-ray, integrates with Github to progressively scan pull requests, helping projects fortify code before deployment; and sec3’s post-deployment security solution, WatchTower, ensures funds stay safe. sec3 is building technology-based scalable solutions for Web3 projects to ensure protocols stay safe as they scale.

To learn more about sec3, please visit https://www.sec3.dev