Conventions
The following recommendations are based on 2024 Move.
Add section titles
Use titles in code comments to create sections for your Move code files. Structure your titles using === on either side of the title.
module conventions::comments {
  // === Imports ===
  // === Errors ===
  // === Constants ===
  // === Structs ===
  // === Method Aliases ===
  // === Public-Mutative Functions ===
  // === Public-View Functions ===
  // === Admin Functions ===
  // === Public-Package Functions ===
  // === Private Functions ===
  // === Test Functions ===
}
CRUD functions names
These are the available CRUD functions:
- add: Adds a value.
- new: Creates an object.
- drop: Drops a struct.
- empty: Creates a struct.
- remove: Removes a value.
- exists_: Checks if a key exists.
- contains: Checks if a collection contains a value.
- destroy_empty: Destroys an object or data structure that has values with the drop ability.
- to_object_name: Transforms an Object X to Object Y.
- from_object_name: Transforms an Object Y to Object X.
- property_name: Returns an immutable reference or a copy.
- property_name_mut: Returns a mutable reference.
Potato structs
Do not use 'potato' in the name of structs. The lack of abilities define it as a potato pattern.
module conventions::request {
  // ✅ Right
  struct Request {}
  // ❌ Wrong
  struct RequestPotato {}
}
Read functions
Be mindful of the dot syntax when naming functions. Avoid using the object name on function names.
module conventions::profile {
  struct Profile {
    age: u64
  }
  // ✅ Right
  public fun age(self: &Profile):  u64 {
    self.age
  }
  // ❌ Wrong
  public fun profile_age(self: &Profile): u64 {
    self.age
  }
}
module conventions::defi {
  use conventions::profile::{Self, Profile};
  public fun get_tokens(profile: &Profile) {
   // ✅ Right
   let name = profile.age();
   // ❌ Wrong
   let name2 = profile.profile_age();
  }
}
Empty function
Name the functions that create data structures as empty.
module conventions::collection {
  struct Collection has copy, drop, store {
    bits: vector<u8>
  }
  public fun empty(): Collection {
    Collection {
      bits: vector[]
    }
  }
}
New function
Name the functions that create objects as new.
module conventions::object {
  use sui::object::{Self, UID};
  use sui::tx_context::TxContext;
  struct Object has key, store {
    id: UID
  }
  public fun new(ctx:&mut TxContext): Object {
    Object {
      id: object::new(ctx)
    }
  }
}
Shared objects
Library modules that share objects should provide two functions: one to create the object and another to share it. It allows the caller to access its UID and run custom functionality before sharing it.
module conventions::profile {
  use sui::object::{Self, UID};
  use sui::tx_context::TxContext;
  use sui::transfer::share_object;
  struct Profile has key {
    id: UID
  }
  public fun new(ctx:&mut TxContext): Profile {
    Profile {
      id: object::new(ctx)
    }
  }
  public fun share(profile: Profile) {
    share_object(profile);
  }
}
Reference functions
Name the functions that return a reference as <PROPERTY-NAME>_mut or <PROPERTY-NAME>, replacing with <PROPERTY-NAME\> the actual name of the property.
module conventions::profile {
  use std::string::String;
  use sui::object::UID;
  struct Profile has key {
    id: UID,
    name: String,
    age: u8
  }
  // profile.name()
  public fun name(self: &Profile): &String {
    &self.name
  }
  // profile.age_mut()
  public fun age_mut(self: &mut Profile): &mut u8 {
    &mut self.age
  }
}
Separation of concerns
Design your modules around one object or data structure. A variant structure should have its own module to avoid complexity and bugs.
module conventions::wallet {
  use sui::object::UID;
  struct Wallet has key, store {
    id: UID,
    amount: u64
  }
}
module conventions::claw_back_wallet {
  use sui::object::UID;
  struct Wallet has key {
    id: UID,
    amount: u64
  }
}
Errors
Use PascalCase for errors, start with an E and be descriptive.
module conventions::errors {
  // ✅ Right
  const ENameHasMaxLengthOf64Chars: u64 = 0;
  // ❌ Wrong
  const INVALID_NAME: u64 = 0;
}
Struct property comments
Describe the properties of your structs.
module conventions::profile {
  use std::string::String;
  use sui::object::UID;
  struct Profile has key, store {
    id: UID,
    /// The age of the user
    age: u8,
    /// The first name of the user
    name: String
  }
}
Destroy functions
Provide functions to delete objects. Destroy empty objects with the function destroy_empty. Use the function drop for objects that have types that can be dropped.
module conventions::wallet {
  use sui::object::{Self, UID};
  use sui::balance::{Self, Balance};
  use sui::sui::SUI;
  struct Wallet<Value> has key, store {
    id: UID,
    value: Value
  }
  // Value has drop
  public fun drop<Value: drop>(self: Wallet<Value>) {
    let Wallet { id, value: _ } = self;
    object::delete(id);
  }
  // Value doesn't have drop
  // Throws if the `wallet.value` is not empty.
  public fun destroy_empty(self: Wallet<Balance<SUI>>) {
    let Wallet { id, value } = self;
    object::delete(id);
    balance::destroy_zero(value);
  }
}
Pure functions
Keep your functions pure to maintain composability. Do not use transfer::transfer or transfer::public_transfer inside core functions.
module conventions::amm {
  use sui::transfer;
  use sui::coin::Coin;
  use sui::object::UID;
  use sui::tx_context::{Self, TxContext};
  struct Pool has key {
    id: UID
  }
  // ✅ Right
  // Return the excess coins even if they have zero value.
  public fun add_liquidity<CoinX, CoinY, LpCoin>(pool: &mut Pool, coin_x: Coin<CoinX>, coin_y: Coin<CoinY>): (Coin<LpCoin>, Coin<CoinX>, Coin<CoinY>) {
    // Implementation omitted.
    abort(0)
  }
  // ✅ Right
  public fun add_liquidity_and_transfer<CoinX, CoinY, LpCoin>(pool: &mut Pool, coin_x: Coin<CoinX>, coin_y: Coin<CoinY>, recipient: address) {
    let (lp_coin, coin_x, coin_y) = add_liquidity<CoinX, CoinY, LpCoin>(pool, coin_x, coin_y);
    transfer::public_transfer(lp_coin, recipient);
    transfer::public_transfer(coin_x, recipient);
    transfer::public_transfer(coin_y, recipient);
  }
  // ❌ Wrong
  public fun impure_add_liquidity<CoinX, CoinY, LpCoin>(pool: &mut Pool, coin_x: Coin<CoinX>, coin_y: Coin<CoinY>, ctx: &mut TxContext): Coin<LpCoin> {
    let (lp_coin, coin_x, coin_y) = add_liquidity<CoinX, CoinY, LpCoin>(pool, coin_x, coin_y);
    transfer::public_transfer(coin_x, tx_context::sender(ctx));
    transfer::public_transfer(coin_y, tx_context::sender(ctx));
    lp_coin
  }
}
Coin argument
Pass the Coin object by value with the right amount directly because it's better for transaction readability from the frontend.
module conventions::amm {
  use sui::coin::Coin;
  use sui::object::UID;
  struct Pool has key {
    id: UID
  }
  // ✅ Right
  public fun swap<CoinX, CoinY>(coin_in: Coin<CoinX>): Coin<CoinY> {
    // Implementation omitted.
    abort(0)
  }
  // ❌ Wrong
  public fun exchange<CoinX, CoinY>(coin_in: &mut Coin<CoinX>, value: u64): Coin<CoinY> {
    // Implementation omitted.
    abort(0)
  }
}
Access control
To maintain composability, use capabilities instead of addresses for access control.
module conventions::access_control {
  use sui::sui::SUI;
  use sui::object::UID;
  use sui::balance::Balance;
  use sui::coin::{Self, Coin};
  use sui::table::{Self, Table};
  use sui::tx_context::{Self, TxContext};
  struct Account has key, store {
    id: UID,
    balance: u64
  }
  struct State has key {
    id: UID,
    accounts: Table<address, u64>,
    balance: Balance<SUI>
  }
  // ✅ Right
  // With this function, another protocol can hold the `Account` on behalf of a user.
  public fun withdraw(state: &mut State, account: &mut Account, ctx: &mut TxContext): Coin<SUI> {
    let authorized_balance = account.balance;
    account.balance = 0;
    coin::take(&mut state.balance, authorized_balance, ctx)
  }
  // ❌ Wrong
  // This is less composable.
  public fun wrong_withdraw(state: &mut State, ctx: &mut TxContext): Coin<SUI> {
    let sender = tx_context::sender(ctx);
    let authorized_balance = table::borrow_mut(&mut state.accounts, sender);
    let value = *authorized_balance;
    *authorized_balance = 0;
    coin::take(&mut state.balance, value, ctx)
  }
}
Data storage in owned vs shared objects
If your dApp data has a one to one relationship, it's best to use owned objects.
module conventions::vesting_wallet {
  use sui::sui::SUI;
  use sui::coin::Coin;
  use sui::object::UID;
  use sui::table::Table;
  use sui::balance::Balance;
  use sui::tx_context::TxContext;
  struct OwnedWallet has key {
    id: UID,
    balance: Balance<SUI>
  }
  struct SharedWallet has key {
    id: UID,
    balance: Balance<SUI>,
    accounts: Table<address, u64>
  }
  /*
  * A vesting wallet releases a certain amount of coin over a period of time.
  * If the entire balance belongs to one user and the wallet has no additional functionalities, it is best to store it in an owned object.
  */
  public fun new(deposit: Coin<SUI>, ctx: &mut TxContext): OwnedWallet {
    // Implementation omitted.
    abort(0)
  }
  /*
  * If you wish to add extra functionality to a vesting wallet, it is best to share the object.
  * For example, if you wish the issuer of the wallet to be able to cancel the contract in the future.
  */
  public fun new_shared(deposit: Coin<SUI>, ctx: &mut TxContext) {
    // Implementation omitted.
    // It shares the `SharedWallet`.
    abort(0)
  }
}
Admin capability
In admin-gated functions, the first parameter should be the capability. It helps the autocomplete with user types.
module conventions::social_network {
  use std::string::String;
  use sui::object::UID;
  struct Account has key {
    id: UID,
    name: String
  }
  struct Admin has key {
    id: UID,
  }
  // ✅ Right
  // cap.update(&mut account, b"jose");
  public fun update(_: &Admin, account: &mut Account, new_name: String) {
    // Implementation omitted.
    abort(0)
  }
  // ❌ Wrong
  // account.update(&cap, b"jose");
  public fun set(account: &mut Account, _: &Admin, new_name: String) {
    // Implementation omitted.
    abort(0)
  }
}