“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.
Building on our previous analysis of basic token functionalities across Solidity, Sui Move, and Aptos Move, this report focuses on the advanced features of fungible tokens. We specifically explore how these platforms implement fungible token standards, with extensions such as whitelisting/blacklisting, fee mechanisms, pausing, and whitelisting/blacklisting.
Solidity’s longevity in the smart contract space has fostered a mature ecosystem, particularly for token standards like ERC-20. This maturity is characterized by well-established patterns and the availability of robust libraries, which significantly shape how developers extend basic token functionalities.
The nature of Solidity, especially its support for inheritance, is fundamental to extending token contracts. Developers typically start with a base ERC-20 contract and then inherit from other contracts that provide specific additional features. Esteemed libraries such as OpenZeppelin, Solmate or Solady play a crucial role by offering standardized, audited, and battle-tested implementations of common extensions. For instance, OpenZeppelin's ERC20.sol
contract provides the core ERC-20 implementation, while extensions like ERC20Pausable.sol
or ERC20Burnable.sol
can be inherited to augment the token’s capabilities. This approach reduces boilerplate code, minimizes the risk of introducing vulnerabilities, and promotes interoperability. The developer experience is thus streamlined, allowing for a focus on application-specific logic rather than re-implementing standard features.
The sample code basically overrides ERC20Pausable
and ERC20Blocklist
to complete the pausable and blacklist feature. Both maintain a flag or map for corresponding functionalities. If users must change the fee during a transfer, they can add logic in the _update
function.
Now, let’s dive into Sui Move to see how it maintains a similar extension like what we see on Solidity. Sui Move offers a distinct approach to fungible tokens and their extensions, primarily through two modules: sui::coin
for "regulated coins" with built-in compliance features, and sui::token
for "closed-loop" tokens that allow for highly customizable, programmable policies.
sui::coin
Module: Built-in Regulation for Fungible AssetsThe sui::coin
module is designed for fungible assets that may require regulatory features, such as stablecoins or other tokens needing access control.
Creating Regulated Coins: Referring to the Regulated Coin Example, the primary function for instantiating such tokens is coin::create_regulated_currency_v2
. This function is significant because, in addition to returning the standard TreasuryCap
(which grants capabilities to mint and burn the coin), it also produces a DenyCapV
2 object. This DenyCapV2
is a capability object—a core concept in Move representing a unique, unforgeable right—that grants its bearer administrative control over the deny list and global pause features for that specific coin type. This capability-based access control is a notable departure from Solidity's common address-centric pattern.
Deny List (Blacklisting) Management: A key feature of regulated coins is the deny list. Unlike Solidity, where a blacklist is typically a mapping within the token contract itself, Sui's deny list for sui::coin
is a system-level, singleton shared object residing at the fixed address @0x403
. The bearer of the DenyCapV2
can interact with this system object using functions like coin::deny_list_v2_add
and coin::deny_list_v2_remove
to manage the list of blocked addresses for their specific coin type.
The effects of these actions are subject to Sui's epoch-based timing (epochs are approximately 24 hours):
This system-level deny list and its epoch-based propagation of state changes are fundamental differences from the immediate, contract-contained state updates familiar to Solidity developers.
allow_global_pause
boolean flag to true
during the call to coin::create_regulated_currency_v2
. If enabled, the holder of the DenyCapV2
can use coin::deny_list_v2_enable_global_pause
to halt all activity for that coin type and coin::deny_list_v2_disable_global_pause
to resume it.The effects of the global pause also have immediate and next-epoch components:
Similar to the deny list, this global pause is managed via a capability, and its full enforcement across all aspects of interaction is subject to epoch progression.
Fee Mechanisms: In its standard form for regulated coins, the sui::coin
module does not natively include logic for implementing fee-on-transfer mechanisms. Developers seeking such functionality would need to consider alternative approaches, such as building custom coin logic from scratch (which sacrifices some of the benefits of the standard module) or utilizing the more flexible sui::token
module, discussed next.
Enforcement Points: Checks related to the deny list are performed at multiple stages within the Sui system. For instance, they occur during transaction signing (as seen in authority.rs references for V1 and V2 deny lists) and, for V2, additional checks can occur during transaction execution (as per context.rs references). This signifies that certain regulatory enforcements are embedded at a deeper layer of the Sui protocol, not solely within the developer-written smart contract code.
sui::token
Module: Customizable Policies with the Hot-Potato PatternFor scenarios demanding more granular control and custom rules beyond the scope of sui::coin
, Sui provides the sui::token
module. This is often used for "closed-loop" tokens, such as loyalty points, in-game currencies, or other assets whose utility might be primarily within a specific application or ecosystem.
Concept of Closed-Loop Tokens and TokenPolicy
: The sui::token
module allows developers to define a TokenPolicy<T>
object for a specific token type T
. This policy object encapsulates the custom rules governing the token's behavior. Administrative control over this policy is granted by a TokenPolicyCap
, another capability object. The policy can dictate rules for various actions, including transfers, spends, and conversions between tokens and coins.
The Hot-Potato Pattern and ActionRequest
: A critical Move pattern utilized by the sui::token
module is the "hot-potato" pattern. An ActionRequest<T>
that has no abilities (like store
or drop
). This means that, if a function returns an ActionRequest<T>
, that ActionRequest<T>
object cannot be simply discarded or stored in an account's state; it must be consumed by another function within the same transaction.
Protected actions on tokens, such as token::transfer
, token::spend
, token::to_coin
, and token::from_coin
, are designed to return an ActionRequest<T>
. This ActionRequest<T>
must then be passed to and processed by a function associated with the token's TokenPolicy
. This mechanism effectively forces all interactions with the token to pass through and adhere to the rules defined in its policy, ensuring that policy checks cannot be inadvertently or maliciously bypassed.
Implementing Extensions via Custom Rules: The TokenPolicy
provides the framework for implementing various extensions:
TokenPolicy
can check sender or receiver addresses against custom lists. These lists could be stored, for example, in a Bag
or Table
object associated with the policy. For example, in simple_token, the rule governing the transfer_action
would consult this list and approve or deny the action accordingly based on sender
or receiver
.TokenPolicy
object can serve as a pause switch. Rules handling ActionRequests
for various operations would first check this flag and proceed only if the token is not paused.transfer_action
(or a custom transfer function that generates an ActionRequest
) can implement fee logic. Upon receiving an ActionRequest
for a transfer, the rule could split the token amount, forwarding a portion to a designated fee collector address and the remainder to the intended recipient. The coffee.move contract example, while not direct fee-on-transfer implementations, demonstrates mechanisms for collecting payments or managing value flows that are conceptually similar to how fees could be managed within a TokenPolicy
.This approach offers high flexibility, but also places the responsibility for designing and securely implementing these rules squarely on the developer.
Aptos Move has also evolved its approach to tokens, moving toward a more flexible and extensible model with its Fungible Asset (FA) standard. This standard is designed to overcome limitations of earlier token models and provide a robust foundation for advanced functionalities.
Initially, Aptos provided a coin
module, similar in concept to Sui's basic coin functionalities. However, to address the need for greater extensibility and overcome the rigidity of the original struct-based coin
, Aptos introduced the Fungible Asset (FA) standard at AIP-21. The FA standard leverages Move Objects, which are inherently more customizable and allow for richer interactions than simple structs. This shift is part of a broader effort to unify token handling on Aptos and provide a more future-proof framework for diverse use cases.
The FA standard is built around several key components:
Metadata
Object: Each distinct type of fungible asset is defined by a Metadata
object. This object, typically created to be non-deletable, stores essential information like the asset's name, symbol, decimals, and potentially a URI for an icon. This Metadata
object serves as the unique identifier for the token type.Ref
s: The FA standard utilizes special reference types, or Ref
s, to manage permissions and control critical operations. These are generated, often starting from a ConstructorRef
obtained during the FA's creation:
ConstructorRef
: Used during the initialization of the fungible asset to create other capability Ref
s.MintRef
: Grants the capability to mint new units of the fungible asset.BurnRef
: Grants the capability to burn (destroy) units of the fungible asset.TransferRef
: A particularly relevant capability for extensions, as it allows an authorized entity to freeze accounts from transferring the specific FA, or to bypass such freezes. This is crucial for implementing regulatory compliance features like pausing or restricting transfers for certain accounts.These Ref
s embody a capability-based security model, where holding a specific Ref
grants the authority to perform the associated action.
A cornerstone of the FA standard's extensibility is its support for dispatchable functions, or "hooks." Token issuers can register custom Move functions that the Aptos Framework will automatically invoke during specific FA operations, such as deposits or withdrawals. This allows developers to inject custom logic without altering the core FA module.
object::register_dispatch_functions
(or the more specific fungible_asset::register_derive_supply_dispatch_function
for supply-related hooks) are used to associate these custom functions with the FA's metadata. When a standard operation like a transfer (which involves a withdrawal from the sender and a deposit to the recipient) occurs, the framework checks for registered hooks. If found, the custom hook is executed instead of, or in addition to, the default logic. The underlying mechanism for this is referred to as native_dispatch
within the Aptos framework sources.deposit
and withdraw
. Hooks for balance
and supply
can also be registered to customize how these values are derived or reported.This hook system provides a structured way to introduce custom behavior, somewhat analogous to Solidity's internal function overrides (like _update
), but more explicitly tied to predefined actions within the token lifecycle.
Using the FA standard's capabilities and dispatch hooks, developers can implement various advanced features:
Pausing/Freezing Accounts: The TransferRef
capability is the primary tool for implementing pausing or freezing functionality for a specific fungible asset. The entity holding the TransferRef
for an FA can invoke functions to freeze an account, preventing that account from transferring units of that particular FA. Developers can also take advantage of hooks for deposit
and withdraw
to globally pause the functionality.
Whitelisting/Blacklisting: Whitelisting or blacklisting can be implemented by embedding custom logic within the dispatch hooks for deposit
and withdraw
operations. For example, a blacklist: the withdraw
hook could check if the sender is on a blacklist, and the deposit
hook could check if the recipient is on a blacklist.
Fee-on-Transfer: Fee-on-transfer mechanisms can also be built using the deposit
and withdraw
dispatch hooks.
withdraw
hook. The hook logic would calculate the fee, transfer the fee amount to a designated treasury account, and then allow the withdrawal of the original amount minus the fee.deposit
hook, or a combination of both. The custom logic within the hook would handle the fee calculation and redirection.For example:
There are several security considerations when working with Fungible Assets:
Metadata
object address of an FA before accepting it or using it in any contract logic. This prevents spoofing, where a malicious actor might try to pass off a fake token as a legitimate one.Ref
Integrity: Ensure that any capability Ref
(e.g., MintRef
or TransferRef
) being used corresponds to the expected Metadata
of the FA it purports to control.Metadata
remains consistent during transfers, especially between wallets or contracts, to avoid unauthorized usage or misuse._update
). The ecosystem is heavily supported by mature libraries like OpenZeppelin, which provide standard implementations for extensions like pausing and blocklisting. Fee mechanisms are typically custom-built within these overrides.sui::coin
module offers built-in regulatory features (deny list, global pause) for "regulated coins," managed via the DenyCapV2
capability, with some operations subject to epoch-based latency. The sui::token
module allows for highly customizable "closed-loop" tokens through programmable TokenPolicy
objects, governed by a TokenPolicyCap
and enforced using the hot-potato pattern with ActionRequest
structs.deposit
and withdraw
, with administrative control managed by various Ref
capabilities (e.g., TransferRef
for freezing).Transitioning to Move-based platforms requires an investment in understanding their core principles: the resource model, ownership, capabilities, and the specific object models of Sui and Aptos. While the learning curve exists, Move languages potentially offer advantages in building secure and robust token systems. By grasping these foundational concepts, Solidity developers can effectively leverage the advanced token extension features available on Sui and Aptos, enabling the creation of innovative and sophisticated decentralized applications.