Skip to main content

Conventions

The following recommendations are based on 2024 Move.

Add Section Titles

Use section titles in code comments to organize your Move code files. Structure your titles using === on either side of the title.

module conventions::comments {
// === Errors ===

// === Constants ===

// === Structs ===

// === Method Aliases ===

// === Public-Mutative Functions ===

// === Public-View Functions ===

// === Admin Functions ===

// === Public-Package Functions ===

// === Private Functions ===

// === Test Functions ===
}

CRUD Function Names

The following are recommended names for 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 contains values with the drop ability.
  • to_object_name: Transforms Object X to Object Y.
  • from_object_name: Transforms Object Y to Object X.
  • property_name: Returns an immutable reference or a copy.
  • property_name_mut: Returns a mutable reference.

Potato Structs

Avoid using 'potato' in the names of structs. The absence of abilities defines a potato pattern.

module conventions::request {
// ✅ Correct
public struct Request {}

// ❌ Incorrect
public struct RequestPotato {}
}

Read Functions

When naming functions, be mindful of the dot syntax. Avoid using the object name in function names.

module conventions::profile {

public struct Profile {
age: u64
}

// ✅ Correct
public fun age(self: &Profile): u64 {
self.age
}

// ❌ Incorrect
public fun profile_age(self: &Profile): u64 {
self.age
}
}

module conventions::defi {

use conventions::profile::Profile;

public fun get_tokens(profile: &Profile) {

// ✅ Correct
let name = profile.age();

// ❌ Incorrect
let name2 = profile.profile_age();
}
}

Empty Function

Name functions that create data structures as empty.

module conventions::collection {

public struct Collection has copy, drop, store {
bits: vector<u8>
}

public fun empty(): Collection {
Collection {
bits: vector[]
}
}
}

New Function

Name functions that create objects as new.

module conventions::object {

public 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. This allows the caller to access its UID and execute custom functionality before sharing it.

`

module conventions::profile {

use iota::transfer::share_object;

public 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 functions that return a reference as <PROPERTY-NAME>_mut or <PROPERTY-NAME>, replacing <PROPERTY-NAME> with the actual name of the property.

module conventions::profile {

use std::string::String;

public 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 a single object or data structure. A variant structure should have its own module to minimize complexity and reduce the likelihood of bugs.

module conventions::wallet {

use iota::object::UID;

public struct Wallet has key, store {
id: UID,
amount: u64
}
}

module conventions::claw_back_wallet {

use iota::object::UID;

public struct Wallet has key {
id: UID,
amount: u64
}
}

Errors

Use PascalCase for errors, starting with an E and ensuring they are descriptive.

module conventions::errors {
// ✅ Correct
const ENameHasMaxLengthOf64Chars: u64 = 0;

// ❌ Incorrect
const INVALID_NAME: u64 = 0;
}

Struct Property Comments

Document the properties of your structs with comments.

module conventions::profile {
use std::string::String;

public 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. Use the destroy_empty function to destroy empty objects, and the drop function for objects containing types that can be dropped.


module conventions::wallet {

use iota::balance::{Self, Balance};
use iota::iota::IOTA;

public 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<IOTA>>) {
let Wallet { id, value } = self;
object::delete(id);
balance::destroy_zero(value);
}
}

Pure Functions

Keep your functions pure to maintain composability. Avoid using transfer::transfer or transfer::public_transfer within core functions.

module conventions::amm {

use iota::coin::Coin;

public struct Pool has key {
id: UID
}

// ✅ Correct
// 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)
}

// ✅ Correct
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);
}

// ❌ Incorrect
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 correct amount directly, as this improves transaction readability from the frontend.

module conventions::amm {

use iota::coin::Coin;

public struct Pool has key {
id: UID
}

// ✅ Correct
public fun swap<CoinX, CoinY>(coin_in: Coin<CoinX>): Coin<CoinY> {
// Implementation omitted.
abort(0)
}

// ❌ Incorrect
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 iota::iota::IOTA;
use iota::balance::Balance;
use iota::coin::{Self, Coin};
use iota::table::{Self, Table};

public struct Account has key, store {
id: UID,
balance: u64
}

public struct State has key {
id: UID,
accounts: Table<address, u64>,
balance: Balance<IOTA>
}

// ✅ Correct
// 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<IOTA> {
let authorized_balance = account.balance;

account.balance = 0;

coin::take(&mut state.balance, authorized_balance, ctx)
}

// ❌ Incorrect
// This is less composable.
public fun wrong_withdraw(state: &mut State, ctx: &mut TxContext): Coin<IOTA> {
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

For data with a one-to-one relationship, it's best to use owned objects.


module conventions::vesting_wallet {

use iota::iota::IOTA;
use iota::coin::Coin;
use iota::table::Table;
use iota::balance::Balance;

public struct OwnedWallet has key {
id: UID,
balance: Balance<IOTA>
}

public struct SharedWallet has key {
id: UID,
balance: Balance<IOTA>,
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<IOTA>, 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<IOTA>, ctx: &mut TxContext) {
// Implementation omitted.
// It shares the `SharedWallet`.
abort(0)
}
}

Admin Capability

In admin-gated functions, the first parameter should be the capability. This helps with autocomplete for user types.

module conventions::social_network {

use std::string::String;

public struct Account has key {
id: UID,
name: String
}

public struct Admin has key {
id: UID,
}

// ✅ Correct
// cap.update(&mut account, b"jose");
public fun update(_: &Admin, account: &mut Account, new_name: String) {
// Implementation omitted.
abort(0)
}

// ❌ Incorrect
// account.update(&cap, b"jose");
public fun set(account: &mut Account, _: &Admin, new_name: String) {
// Implementation omitted.
abort(0)
}
}

Question 1/4

What is the recommended naming convention for functions that create objects in Move?