Back to all stories
Blogs
Tech & Dev
Move for Solidity Developers I: Storage and Access Control
4/3/2025
Move for Solidity Developers I: Storage and Access Control

“Move for Solidity Developers” is a series created for experienced Solidity developers who already know a bit about Move. It helps you shift from writing Solidity contracts to developing on Move-based blockchains, such as Aptos and Sui. In this first installment, we’ll explore how Move’s approach to state storage and access control differs from the Solidity/EVM model. By drawing comparisons and analogies, we aim to make these new concepts feel familiar.

From Solidity to Move: A New Mental Model

If you've been developing smart contracts in Solidity, you’re used to Ethereum’s account-based model and msg.sender for access control. Move introduces a fresh paradigm focused on resource-oriented programming. This paradigm was born from lessons learned with Solidity – in fact, Move was explicitly designed to enforce asset scarcity and access control at the language level.

Aptos and Sui both use Move variants, each with a unique spin:

  • Aptos Move closely follows the original Diem (Libra) Move design. It uses an account model where each account (address) can hold certain resources (pieces of state).
  • Sui Move adopts the same core language but with an object-centric model. Sui takes some of Move's rules even further, emphasizing objects with owners instead of generic global storage.

Understanding these distinctions will help reframe concepts we know from Solidity into the Move context.

Storage Model: From Contract-Centric to Resource-Centric

To maintain a better understanding of access control, we will first dive into the storage model. In Ethereum and Solidity, state storage is centered around smart contract instances. In Move (on Aptos and Sui), storage is organized by accounts (addresses) or objects rather than contract instances. This is a fundamental shift. Let’s break it down:

Ethereum’s Contract-Centric Storage (Solidity)

In Solidity, each contract deployed at an address has its own storage. Think of a contract as a ledger book where we track data. For example, an ERC-20 token contract might have a state variable:

Code 1

This mapping holds each address’s token balance. The data is stored inside the contract’s storage at its address. Ethereum’s global state is effectively a map of contract addresses to their storage data. The contract is responsible for managing that mapping (e.g. updating balances on transfers). For example, if Alice sends tokens to Bob, the contract updates two stored entries: decrement Alice’s balance and increment Bob’s balance.

2 Alice and Bob Example

Now, what does Move do differently?

Aptos Move: Resources Stored in Accounts

In Move, modules (the equivalent of smart contracts) do not own storage themselves​. Instead, Move uses a global storage that is indexed by account addresses. Under each address, we can store certain resources (structured values) associated with that account.

On Aptos, each user account is like a personal storage locker. A Move module can define a resource type, and each account (address) can choose to hold an instance of that resource. There’s a built-in mapping from (Address, ResourceType) -> ResourceValue in the blockchain state. By design, an address can have at most one value of a given resource type. This eliminates the need for explicit mappings in many cases because the address itself is the key.

For example, suppose we’re implementing a simple coin module in Aptos Move. Instead of one contract holding a mapping(address => uint64) of balances, we would define a Balance resource struct and publish an instance of it under each user’s address who owns the coin. If Alice has 10 X tokens, there should be a Balance resource in Alice’s account with a value 10. Sending 5 tokens to Bob updates Alice’s Balance resource accordingly, while enabling Bob to create a new Balance resource containing the 5 received tokens.

3 Alice and Bob Second Example

Sui Move: Object-Centric Storage Model

Sui’s version of Move is a variant that takes an object-centric approach. When a transaction runs on Sui, it must explicitly declare which objects it will read or modify. These object references are passed as inputs to the transaction’s function. Any new objects created during the transaction are added to the Sui global state when the transaction finishes, each with a specified owner or ownership type, such as Owned (by an Address), shared, immutable, or wrapped(in another object). In other words, instead of implicitly reading from or writing to a global store, Sui transactions move and update specific objects that are passed in and returned out. For example, if we need a "global storage" pattern in a Sui contract, we would create a shared object to store that global information, and then explicitly pass this shared object as a parameter whenever performing an update.

Code 2

In the original Move (as used in platforms like Aptos), developers use global storage operations such as move_to or borrow_global to store and access resources in an account’s storage. Sui does not use these global storage functions. Instead, every piece of data lives as an object with a unique identifier (UID) in Sui’s global state. Even Move packages (which are sets of modules) are deployed as objects with unique IDs. This means there isn’t a single “account container” holding resources, but rather a collection of objects identified by UIDs. Sui’s object model eliminates the need for the acquires keyword and global storage operators, because contracts operate directly on the passed-in objects rather than looking up data by an address in a global storage.

To illustrate Sui’s object-centric model, consider how coin balances are handled. In Sui’s standard coin implementation, there isn’t one big “balance” field stored in a single account object. Instead, each individual coin balance is its own object. If Alice has 10 X tokens, she might have an object in the Sui state that represents 10 X. If she wants to send 5 X tokens to Bob, the transaction would move a portion of that coin object to Bob (this could mean splitting her 10 X object into, say, a 5 X object that goes to Bob and a 5 X object that remains with Alice). Bob would then receive a new coin object worth 5 X, owned by his address.

5 Flow Chart

Summary of Storage

Solidity/Ethereum: Data lives in contract storage. Use mappings/arrays to organize data, keyed by addresses if needed. Any contract can call another to get or set data (if the contract exposes functions for it).

Aptos Move: Data lives as resources in user accounts. No need for explicit mappings for per-user data; the blockchain state natively stores one resource per address per type​. We use global storage operations (move_to, borrow_global, etc.) to manage resources under addresses.

Sui Move: Data lives as objects. There is no generalized “read anything by address” during execution. Either an object is owned by someone (and then only they can use it by including it in a transaction), it’s shared (and accessible to any transaction by ID, if declared), immutable, or wrapped. Addresses are mostly identifiers for ownership​, not buckets of data.

6 Flow Chart Storage Models between Solidity | Aptos move | Sui move

Now that we understand where data lives, the next question is: Who can access or modify that data? This brings us to access control.

Access Control: Who Can Do What, and How?

Controlling who can call certain functions or perform certain actions is central to smart contract security, which also brings one of the root causes for severe security incidents. In Solidity, we use patterns like onlyOwner modifiers or role-based access via msg.sender. In Move, the same goals are achieved with different primitives and patterns. We’ll compare approaches in Solidity, Aptos Move, and Sui Move:

Solidity’s Approach to Access Control

In Solidity, any external function can be called by anyone (unless we restrict it). Therefore, it’s up to the contract code to enforce rules about who can call what. Common patterns include:

  • Owner-only functions: The contract has an owner state variable set at deployment. Critical functions do require(msg.sender == owner, "not authorized") to ensure only the owner can call them.
  • Role-based access: Using mappings of addresses to roles or libraries like OpenZeppelin’s AccessControl, we can assign multiple roles (admin, minter, etc.) and check require(hasRole(MINTER_ROLE, msg.sender), "needs minter role").
  • Function visibility: Functions can be declared public, external, internal, or private. However, visibility in Solidity mainly controls what can be called from other contracts or transactions, not which user address can call. For example, internal just means only this contract or its inheritors can call the function, which isn’t about user permission but about code organization.

The key point is that Solidity trusts the code to perform the right checks. The Ethereum protocol itself doesn’t know that onlyOwner should be enforced – it’s all in the contract logic.

(Aptos) Move’s Approach: Signers and Modules

Move, being resource-oriented, introduces the concept of a signer and ties certain actions to an account by design. A function can accept an argument of type signer which represents the calling account’s authorization. Only the real transaction sender can create a signer for their account. This means if a Move function is defined as public fun execution(account: &signer), only the owner of account (who can sign transactions for that address) can call it. In effect, &signer is analogous to tx.origin or the sender from a transaction but with type safety – you can’t forge a signer (except the resource account), it should come from the transaction.

For example, in Aptos core code we might see a function ensuring it’s only called by a specific address:

Code 3

However, Move offers a more flexible pattern beyond hard-coding addresses: capabilities (the "cap" pattern). A capability is a resource that signifies permission to do something. Only the module that defines a capability resource can create it and give it out, which means we can’t fake it. This is like handing someone a token or key that they can later present to prove they have a certain right.

Code 5

In this pattern, the AdminCap resource is like a unique key. The module's initial setup gives that key to, say, the contract deployer. Later, when update_settings is called, the Move runtime ensures that only the account possessing the AdminCap can call it successfully (because borrow_global<AdminCap will fail for others).

(Aptos) Move’s module system also inherently restricts access in some ways:

  • Only the module that defines a resource type can directly modify or destroy that resource. This is enforced by the VM. So, if we have a Balance resource which is declared in our module, another module cannot just remove coins from someone’s balance; it has to call our module’s public functions (which can check permissions) to do so.
  • Functions can have different visibility: public, public(script) (entry functions), friend (can only be called by specified modules), or private. For example, we might mark an internal helper as private so nobody outside our module (not even other modules) can call it. Solidity’s internal is similar, but Move’s friend mechanism lets us explicitly allow specific other modules to call our functions, a bit like granting selective cross-contract permissions. It is worth noting that Move has a special keywords entry which will also influence the visibility of the function. We have discovered a severe bug related to this pattern, which is detailed here.

Sui Move: Object Ownership as Access Control

Sui, with its object-based design, follows the same general principles but emphasizes them even more. Since Sui doesn’t allow arbitrary global reads/writes, the very act of having an object in the transaction is part of access control. If an object is owned by Alice’s address, then for a transaction to use that object, it must be signed by Alice (because only Alice can initiate a transaction that includes her owned objects). This is implicitly an access control: Bob can’t just decide to act on Alice's owned object.

Code 6

For functions directly involved in a transaction, a TxContext is attached as an argument, which will contain important transaction-related information, such as the sender’s address(it is worth noting that the signer of the transaction, which is similar to the tx.origin) and the current transaction epoch. This means in Sui we can still do an explicit check if needed, e.g. assert!(TxContext::sender(ctx) == expected_addr, EACCES). However, we don't need to in many cases since having access to the objects themselves is typically enough control.

Code 7

Sui Move uses a function visibility system closely resembling Aptos Move, providing three main visibility levels:

  • private: The default visibility; functions are accessible only within their defining module.
  • public: Functions can be accessed from any module, both internally and externally.
  • public(package): Functions are accessible only by modules within the same package, but not externally.

Additionally, Sui Move features the special keyword entry, which explicitly marks functions callable from external transactions. This keyword directly influences function accessibility and invocation patterns.

Moreover, similar to how Aptos restricts resource modification exclusively to the defining module, Sui leverages an analogous approach through its witness pattern. This pattern provides a secure way to verify struct ownership, ensuring that only authorized modules or accounts can modify or transfer important objects, thus enforcing ownership rules at the language level. More details here.

Summary of Access Control

Solidity/Ethereum:

  • Use msg.sender in require statements for access checks.
  • Implement roles via code (owner var, role mappings).

Aptos Move:

  • Functions can explicitly require a signer (auth) from the caller; no one else can fake that.
  • Use Move’s resource types to create capabilities (permission tokens) that grant rights. Only those who hold the token (resource) can perform the action.

Sui Move:

  • Object ownership serves as a gate: only an object’s owner can initiate its use in a transaction.
  • The transaction context provides the signer’s address if needed for checks, similar to tx.origin but not implicitly available unless asked.

Beyond: Embracing the Move Mindset

Transitioning from Solidity to Move (on Aptos or Sui) requires a shift in the mental model. We learned that storage in Move is organized by accounts and objects rather than contract instances, and access control is often handled with first-class concepts like signers and capability resources instead of only ad-hoc checks. The good news is that many patterns from Solidity still apply — they’re just achieved through different means.

This is just the beginning of our Move journey. With the fundamentals of storage and access control under our belt, we’re ready to get to more interesting topics. See you in the next chapter, and keep building!