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 {
public struct Wallet has key, store {
id: UID,
amount: u64
}
}

module conventions::claw_back_wallet {
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)
}
}