“Move for Solidity Developers” is a series created for experienced Solidity developers with a basic understanding of Move. It helps you shift from writing Solidity contracts to developing on Move-based blockchains, such as Aptos and Sui.
Building on our previous analysis of access control and storage model differences among Solidity, Sui Move, and Aptos Move, this second installment shifts focus toward a practical application of those foundational concepts. We now examine the most common token (ERC20 on Solidity), which is an essential component of value transfer and functionality across the DeFi ecosystem.
In this post, we particularly discuss how basic token standards are implemented across these platforms, focusing solely on minting, burning, and transferring functionalities. Advanced features such as pausing, whitelisting, and freezing will be discussed in detail in a subsequent series.
To understand the unified token standard across Solidity and Move (Aptos and Sui), let's begin by outlining the creation process.
In Solidity, developers can easily create ERC20 tokens by importing OpenZeppelin’s ERC20.sol library. The following is a basic ERC20 token contract that extends OpenZeppelin’s standard ERC20 implementation:
Unlike Solidity, Sui Move adopts an object-based storage model. Sui provides a standardized module, sui::coin, for the creation and management of fungible tokens. The following example demonstrates the basic process of creating a custom coin in Sui Move.
Creating a token in Sui Move involves several key components:
coin
module from the Sui framework must be imported to access core coin functionalities.init
): The init()
function acts like a constructor, executed at module deployment. It registers the custom coin type and initializes its associated metadata.T
(e.g., MY_COIN
) can be registered only once on-chain, enforcing type uniqueness.TreasuryCap<T>
: Grants mint and burn privileges for the token type T
. This is usually held by the token issuer or an authorized module.CoinMetadata
: Contains descriptive metadata such as name, symbol, decimals, description, and optional icon URL.Best Practices for TreasuryCap
and CoinMetadata
Management in Initialization
TreasuryCap<T>
should not be shared, as it enables unrestricted minting. If shared, it should be controlled by well defined delegate logic to avoid public mint/burn.TreasuryCap<T>
should not be frozen in order to preserve authorized minting and burning capabilities.CoinMetadata
should be frozen in order to prevent tampering with token attributes, ensuring consistency and trust.Aptos' Move package provides the coin module, similar to Sui, for basic coin creation. managed_coin encapsulates the functionalities of the basic coin
module, providing essential scripts to initialize, mint, burn, and transfer tokens. The managed_coin::initialize
method is designed for basic token functionality only. The following example demonstrates a basic implementation of a coin named Moon Coin:
managed_coin::initialize
in Aptos Move calls coin::initialize
to set the coin metadata and transfers the burn_cap, freeze_cap
, and mint_cap
to the contract deployer (sender
).
Minting and burning are core mechanisms for managing token supply. Though functionally similar, their implementations in Solidity and Move differ in how they enforce security and authority. This section examines these differences.
OpenZeppelin’s ERC20 token contract includes internal _mint
and _burn
functions, allowing developers to design custom public functions within their extended contracts to suit different use cases and business logic. Burning is usually controlled by privileged roles or permitted for users to destroy tokens from their own accounts. The contract should enforce strict access control on mint and burn functions to prevent unauthorized token issuance or destruction.
The sui::coin
package provides built-in functions for minting and burning tokens:
coin::mint_and_transfer
coin::burn
However, both operations are strictly permissioned and can only be invoked by accounts or modules that hold access to the corresponding TreasuryCap<T>
object. When burning occurs, the function destroys the coin object and updates the total supply counter.
Notably, once TreasuryCap<T>
is frozen, both minting and burning operations become impossible, as both mint_and_transfer
and burn
require a &mut TreasuryCap<T>
(a mutable reference), consequently affecting other functions that depend on these core capabilities.
It is worth noting that, in contrast to Solidity, Move does not support direct inheritance. Users or administrators in Move could interact directly with the native coin module, rather than through intermediary modules or contracts like my_coin
.
The managed_coin
package provides entry functions for minting and burning tokens: managed_coin::mint
and managed_coin::burn
. Similar to the design in Sui Move, these functions are strictly governed by the mint_cap
and burn_cap
, and perform functionality through the coin module, which enforces access control.
Unlike Sui Coin, we can see that the mint_cap
and burn_cap
are wrapped inside Capabilities
. Due to the feature of the move language, the unwrapped operation can only be performed in the managed_coin
module, which declares Capabilities
. As a result, coins can only be minted through the managed_coin
with the resource holder as the signer.
This means that the admin cannot bypass contract constraints because they need both the signer authority and the capability resource, which is account-bound rather than being a transferable object as in Sui.
DeFi relies on token transfers and balance queries as fundamental operations. This section contrasts the Solidity and Move approaches to these core functionalities, highlighting differences in their design principles, security implications, and execution mechanisms.
In the standard ERC20 contract, all token holder balances are centrally recorded and managed in the mapping _balances
within the contract itself.
The ERC20 standard allows token holders to transfer tokens from their own accounts using the transfer()
function. Additionally, holders can authorize other addresses to operate on their behalf by calling the approve()
function. Once approved, the designated address can transfer tokens from the owner’s account using the transferFrom()
function.
Sui Move uses the Coin<T>
object to represent token balances. Each Coin<T>
instance contains a unique object ID (UID
) and a Balance<T>
field that encapsulates the actual token amount. Each token type is uniquely identified by the type parameter T
(e.g., SUI, USDC).
To transfer tokens to a specific account, the sui::pay::split_and_transfer
function is typically used. This function allows a Coin<T>
object to be split into a specified amount and transferred directly to a recipient.
Transferring tokens results in the creation of a new Coin<T>
object. The transferred amount can be verified on-chain by querying the UID
of the newly created Coin<T>
object. For example, when Alice transfers 50 SUI to Bob, a new Coin<T> object
is created with a unique id and balance of 50 SUI, and transferred to B's ownership. The original sender retains the remaining balance (if any) in a separate Coin<T>
. Rather than maintaining balances in a centralized ledger, user holdings are represented as collections of individual Coin<T>
objects. A single account may hold multiple Coin<T>
objects, each with its own UID and balance.
Due to Sui Move’s object-based model, the approve()
and transferFrom()
pattern commonly used in ERC20 is not supported. In Sui Move, only the object owner has the authority to access or transfer the resources contained within a Coin<T>
object.
Aptos Move uses CoinStore<T>
and Coin<T>
to store user’s coin balance and account status:
Historically, Aptos required an account to explicitly register to receive a custom token type by creating a CoinStore
for it. AIP-13 shifted this from an explicit opt-in to an implicit opt-out model. The aptos_account::transfer_coins
function now typically handles the automatic creation of a CoinStore for the recipient if one doesn't already exist for that CoinType, simplifying the user experience. This function encapsulates the underlying withdraw
and deposit
operations implemented within the coin package.
Users can still choose to opt-out of receiving direct coin transfers via aptos_account::set_allow_direct_coin_transfers(false)
.
Feature | ERC20 (Solidity) | Sui Move Coin | Aptos Move Coin |
---|---|---|---|
Balance Representation | Uses global state variable _balances mapping(address => uint256) to track balances | Coin<T> owned by the user | CoinStore<T> and Coin<T> owned by the user |
Minting/Burning Authority | Controlled via onlyOwner or role-based access with additional extensions | Controlled by exclusive access to TreasuryCap<T> | Controlled by exclusive access to MintRef , BurnRef , TransferRef |
Transfer | Update _balances mapping | Create new object and transfer the object owner to the coin receiver | Create a CoinStore for receivers if they do not have one, update the balance field of CoinStore |
This article presented the foundational aspects of the Coin standard, including core interfaces, transfer logic, and module interactions. In the next article, we will cover advanced features for regulated coin and fungible assets, including:
mint()
with custom logic, while preventing unauthorized bypasses through direct module access.