The keywords memory, storage, calldata are a big pain point for new Solidity developers—just look at the sheer number of questions that have been asked and articles that have been written 1234 about the topic, not to mention the multiple, different, sections (and here, and here) the Solidity documentation dedicates to the topic.
Storage locations in Solidity are no joke!
I hope to accomplish two things in this write-up:
Clarify the use of storage, memory, and calldata.
Introduce a more intuitive alternative to the whole idea.
Quick Explanation
Conceptually, there are basically two places that smart contract’s data can live: either it’s concretely stored and saved on-chain (storage), or it exists only during the execution of a transaction (memory). These could be compared to a traditional application either writing a file to disk or just putting a value in a variable: if the application (smart contract) stops running (transaction ends), the variables (memory) will be cleared, but the filesystem (storage) will remain, and when the application runs again (another transaction calls the contract), that data will still be there for use again.
(Technically, there’s also a stack storage location, but the Solidity compiler will handle that for you unless you write EVM assembly.)
For the most part, Solidity manages to figure out where to store variables all by itself. The only times the programmer must specify a storage location is when the data type is complex—usually that means a struct or dynamic array.
That covers the difference between storage and memory on a high level. For more details, check out the documentation linked previously.
What’s calldata? It’s like a special case of memory: when an external function receives a message call, the arguments are stored in calldata, which means they’re immutable, but they also consume slightly less gas than if they were in memory. It could be compared to an immutable reference to another application’s memory since external message calls could potentially come from other contracts as well.
contract StorageTest {
struct ComplexStruct {
uint256 x;
uint256 y;
}
ComplexStruct public myValue;
function reset () public {
myValue.x = 0;
myValue.y = 0;
}
function setStruct1 (ComplexStruct calldata c) public { // 67821
myValue = c; // copy and increment
myValue.x = myValue.x + 1;
myValue.y = myValue.y + 1;
}
function setStruct2 (ComplexStruct calldata c) public { // 66374
// increment without copy
myValue.x = c.x + 1;
myValue.y = c.y + 1;
}
function setStruct3 (ComplexStruct memory c) public { // 67363
myValue = c; // copy and increment
myValue.x = myValue.x + 1;
myValue.y = myValue.y + 1;
}
function setStruct4 (ComplexStruct memory c) public { // 66937
// increment without copy
myValue.x = c.x + 1;
myValue.y = c.y + 1;
}
function getStruct () public view returns (ComplexStruct memory) {
return myValue;
}
}
(The commented-out numbers after the function signatures are the gas units consumed for executing each function, according to Remix IDE.)
Though it’s not explicitly stated in the code listing, myValue is allocated to the contract’s storage.
The first two functions (setStruct1, setStruct2) take in a struct stored in calldata, and thus, the more efficient of the two (setStruct2), is actually the most efficient in the whole contract.
setStruct3 and setStruct4 are identical to their N−2 analogues, with the exception that they use memory instead of calldata.
Note that we could not call setStruct1 or setStruct2 from another function within the contract, because we cannot convert from memory to calldata:
setStruct1(ComplexStruct({ x: 1, y: 2 }));
// TypeError: Invalid type for argument in function call. Invalid implicit conversion from struct StorageTest.ComplexStruct memory to struct StorageTest.ComplexStruct calldata requested.
Remember: calldata is immutable!
The Alternative
This whole memory-calldata fiasco is pretty complicated. Unfortunately, there are many non-ideal design decisions still influencing the modern Solidity language. However, if you must deploy to an EVM-compatible chain, Solidity is still the language of choice. It has the best editor support, tooling, resources, and community of any other smart contract DSL.
That’s a necessary qualifier—“smart contract DSL”—because, as it turns out, smart contracts can be written in general-purpose programming languages as well, even those with richer ecosystems than Solidity’s.
NEAR’s virtual machine runs WebAssembly code, meaning one could conceivably write smart contracts for NEAR in any GPL with a WASM compile target. NEAR provides official SDKs for two of those languages: Rust and AssemblyScript.
AssemblyScript is a dialect of TypeScript, which is a strict superset of JavaScript. This means that traditional web2 developers can pick it up in no time!
Using a general-purpose programming language for smart contracts means that the programming paradigm is much more normalized: smart contracts merely run in the context of a regular WASM VM injected with the blockchain-specific environment globals.
Programmers don’t have to specify a storage location for data anymore: variables are just normal variables, and there’s an SDK for I/O to the key-value storage system.
Check out this simple example contract, written in AssemblyScript. Permanent, on-chain storage is invoked with a put/get API, making it much more obvious what exactly is happening to the data.
When I first encountered the NEAR ecosystem, I was stunned at how developer-friendly the contract programming interface was. In the time since then, it’s only gotten better! (Not to mention the gas fees are minuscule compared to ETH: you’re looking at fractions of a cent.) If you’re interested in learning more about smart contract development on the NEAR ecosystem, head over to NEAR University and sign up for one of the free training bootcamps.