Contract-Level Storage and Account-Centric Storage

When Solidity developers switch to Aptos, they face an initial hurdle: adapting to new data storage and ownership concepts like resources and objects. These are different from Solidity but serve similar purposes. Understanding these differences is key for a smooth transition.


Contract-Level Storage

In Solidity, data is stored in a contract's storage slots. To create an NFT, you design a contract based on the ERC-721 standard. This contract serves as a hub, tracking ownership with data structures such as mappings. It's like a digital ledger that stores both the code (the logic) and the data (the state), ensuring every NFT is accounted for and linked to its owner.


contract MyNFT is ERC721 {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// Mapping from token ID to owner address
// This mapping is inherently part of the ERC721 implementation
// mapping(uint256 => address) private _owners; // In ERC721
constructor() ERC721("MyNFT", "MNFT") {
// code
}
// Function to mint a new NFT
function mintNFT(address recipient) public returns (uint256) {
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
// The _mint function updates the internal _owners mapping in ERC-721
_mint(recipient, newItemId);
return newItemId;
}
// Additional functions and contract logic...
}

Every contract contains specific storage slots for its state variables. The storage root, calculated from these slots using a Patricia Merkle Tree, is recorded in the global state tree, keyed by the contract's address.


Account-Centric Storage: Containers

On Aptos, the Move language stores data in a unified, tree-shaped global storage. Instead of accounts being two different types (EoAs and Contract Accounts), each account on Aptos acts as a container for both modules (contract code) and resources (data). The idea of ‘containers’ is key to understanding how to effectively program in Move.

Instead of using inheritance from a contract like ERC-721, NFTs are created as objects. Consider objects as 'containers' for resources. Essentially, resources are grouped together to create an object. That object represents an NFT. Each object is stored at a single address.

From a developer’s perspective on Aptos, ownership is more directly associated with the account, and less about contract state. This is the main idea behind the ‘container-like’ global storage system of Aptos against the contract level storage system on Ethereum.

To drive this concept home, let’s use an overly simplified example of representing ownership of an NFT using Move. In this scenario, we are NOT using the Digital Asset Standard. Instead we will abstract away those complexities and use a simple resource to represent an NFT.


module 0x1::my_module {
use std::signer; // Library in Move to use 'signers'
use std::string; // Library in Move to use strings
// Define a simple NFT resource with an id and some metadata
// IN PRACTICE, YOU HAVE TO USE THE DIGITAL ASSET STANDARD TO DEFINE AN NFT
struct NFT has key {
id: u64,
name: string::utf8("My NFT"),
}
// Public entry function to create a new NFT and store it in the creator's account
public entry fun create_nft(account: &signer, id: u64, metadata: vector<u8>) {
let nft = NFT { id, metadata };
move_to(account, nft);
}
}

In the example above, the struct named NFT is a resource. Notice the use of move_to(account, nft). This is one of the global storage operators discussed in the previous section. This account parameter effectively serves as a storage 'container' for the freshly minted NFT resource. This resource can be viewed under an account on the Aptos Block Explorer. Overall, this is intuitively and structurally different from storing ownership information at the contract level using state variables (ownership mapping).