ARC-20: Smart ASA
An ARC for an ASA controlled by an Algorand Smart Contract
Author | Cosimo Bassi, Adriano Di Luzio, John Jannotti |
---|---|
Discussions-To | https://github.com/algorandfoundation/ARCs/issues/109 |
Status | Final |
Type | Standards Track |
Category | Interface |
Created | 2022-04-27 |
Requires | 4 , 22 |
Table of Contents
Abstract
A “Smart ASA” is an Algorand Standard Asset (ASA) controlled by a Smart Contract that exposes methods to create, configure, transfer, freeze, and destroy the asset.
This ARC defines the ABI interface of such Smart Contract, the required metadata, and suggests a reference implementation.
Motivation
The Algorand Standard Asset (ASA) is an excellent building block for on-chain applications. It is battle-tested and widely supported by SDKs, wallets, and dApps.
However, the ASA lacks in flexibility and configurability. For instance, once issued it can’t be re-configured (its unit name, decimals, maximum supply). Also, it is freely transferable (unless frozen). This prevents developers from specifying additional business logic to be checked while transferring it (think of royalties or vesting https://en.wikipedia.org/wiki/Vesting).
Enforcing transfer conditions require freezing the asset and transferring it through a clawback operation – which results in a process that is opaque to users and wallets and a bad experience for the users.
The Smart ASA defined by this ARC extends the ASA to increase its expressiveness and its flexibility. By introducing this as a standard both developers, users (marketplaces, wallets, dApps, etc.) and SDKs can confidently and consistently recognize Smart ASAs and adjust their flows and user experiences accordingly.
Specification
The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 https://datatracker.ietf.org/doc/html/rfc2119.
The following sections describe:
- The ABI interface for a controlling Smart Contract (the Smart Contract that controls a Smart ASA).
- The metadata required to denote a Smart ASA and define the association between an ASA and its controlling Smart Contract.
ABI Interface
The ABI interface specified here draws inspiration from the transaction reference https://developer.algorand.org/docs/get-details/asa/#asset-functions of an Algorand Standard Asset (ASA).
To provide a unified and familiar interface between the Algorand Standard Asset and the Smart ASA, method names and parameters have been adapted to the ABI types but left otherwise unchanged.
Asset Creation
{
"name": "asset_create",
"args": [
{ "type": "uint64", "name": "total" },
{ "type": "uint32", "name": "decimals" },
{ "type": "bool", "name": "default_frozen" },
{ "type": "string", "name": "unit_name" },
{ "type": "string", "name": "name" },
{ "type": "string", "name": "url" },
{ "type": "byte[]", "name": "metadata_hash" },
{ "type": "address", "name": "manager_addr" },
{ "type": "address", "name": "reserve_addr" },
{ "type": "address", "name": "freeze_addr" },
{ "type": "address", "name": "clawback_addr" }
],
"returns": { "type": "uint64" }
}
Calling asset_create
creates a new Smart ASA and returns the identifier of the
ASA. The metadata section describes its required properties.
Upon a call to
asset_create
, a reference implementation SHOULD:
- Mint an Algorand Standard Asset (ASA) that MUST specify the properties defined in the metadata section. In addition:
- The
manager
,reserve
andfreeze
addresses SHOULD be set to the account of the controlling Smart Contract.- The remaining fields are left to the implementation, which MAY set
total
to2 ** 64 - 1
to enable dynamically increasing the max circulating supply of the Smart ASA.name
andunit_name
MAY be set toSMART-ASA
andS-ASA
, to denote that this ASA is Smart and has a controlling application.- Persist the
total
,decimals
,default_frozen
, etc. fields for later use/retrieval.- Return the ID of the created ASA.
It is RECOMMENDED for calls to this method to be permissioned, e.g. to only approve transactions issued by the controlling Smart Contract creator.
Asset Configuration
[
{
"name": "asset_config",
"args": [
{ "type": "asset", "name": "config_asset" },
{ "type": "uint64", "name": "total" },
{ "type": "uint32", "name": "decimals" },
{ "type": "bool", "name": "default_frozen" },
{ "type": "string", "name": "unit_name" },
{ "type": "string", "name": "name" },
{ "type": "string", "name": "url" },
{ "type": "byte[]", "name": "metadata_hash" },
{ "type": "address", "name": "manager_addr" },
{ "type": "address", "name": "reserve_addr" },
{ "type": "address", "name": "freeze_addr" },
{ "type": "address", "name": "clawback_addr" }
],
"returns": { "type": "void" }
},
{
"name": "get_asset_config",
"readonly": true,
"args": [{ "type": "asset", "name": "asset" }],
"returns": {
"type": "(uint64,uint32,bool,string,string,string,byte[],address,address,address,address)",
"desc": "`total`, `decimals`, `default_frozen`, `unit_name`, `name`, `url`, `metadata_hash`, `manager_addr`, `reserve_addr`, `freeze_addr`, `clawback`"
}
}
]
Calling asset_config
configures an existing Smart ASA.
Upon a call to
asset_config
, a reference implementation SHOULD:
- Fail if
config_asset
does not correspond to an ASA controlled by this smart contract.- Succeed iff the
sender
of the transaction corresponds to themanager_addr
that was previously persisted forconfig_asset
by a previous call to this method or, if never caller, toasset_create
.- Update the persisted
total
,decimals
,default_frozen
, etc. fields for later use/retrieval.The business logic associated to the update of the other parameters is left to the implementation. An implementation that maximizes similarities with ASAs, SHOULD NOT allow modifying the
clawback_addr
orfreeze_addr
after they have been set to the special valueZeroAddress
.The implementation MAY provide flexibility on the fields of an ASA that cannot be updated after initial configuration. For instance, it MAY update the
total
parameter to enable minting of new units or restricting the maximum supply; when doing so, the implementation SHOULD ensure that the updatedtotal
is not lower than the current circulating supply of the asset.
Calling get_asset_config
reads and returns the asset
’s configuration as specified in:
- The most recent invocation of
asset_config
; or - if
asset_config
was never invoked forasset
, the invocation ofasset_create
that originally created it.
Upon a call to
get_asset_config
, a reference implementation SHOULD:
- Fail if
asset
does not correspond to an ASA controlled by this smart contract (seeasset_config
).- Return
total
,decimals
,default_frozen
,unit_name
,name
,url
,metadata_hash
,manager_addr
,reserve_addr
,freeze_addr
,clawback
as persisted byasset_create
orasset_config
.
Asset Transfer
{
"name": "asset_transfer",
"args": [
{ "type": "asset", "name": "xfer_asset" },
{ "type": "uint64", "name": "asset_amount" },
{ "type": "account", "name": "asset_sender" },
{ "type": "account", "name": "asset_receiver" }
],
"returns": { "type": "void" }
}
Calling asset_transfer
transfers a Smart ASA.
Upon a call to
asset_transfer
, a reference implementation SHOULD:
- Fail if
xfer_asset
does not correspond to an ASA controlled by this smart contract.- Succeed if:
- the
sender
of the transaction is theasset_sender
andxfer_asset
is not in a frozen state (see Asset Freeze below) andasset_sender
andasset_receiver
are not in a frozen state (see Asset Freeze below)- Succeed if the
sender
of the transaction corresponds to theclawback_addr
, as persisted by the controlling Smart Contract. This enables clawback operations on the Smart ASA.Internally, the controlling Smart Contract SHOULD issue a clawback inner transaction that transfers the
asset_amount
fromasset_sender
toasset_receiver
. The inner transaction will fail on the usual conditions (e.g. not enough balance).Note that the method interface does not specify
asset_close_to
, because holders of a Smart ASA will need two transactions (RECOMMENDED in an Atomic Transfer) to close their position:
- A call to this method to transfer their outstanding balance (possibly as a
CloseOut
operation if the controlling Smart Contract required opt in); and- an additional transaction to close out of the ASA.
Asset Freeze
[
{
"name": "asset_freeze",
"args": [
{ "type": "asset", "name": "freeze_asset" },
{ "type": "bool", "name": "asset_frozen" }
],
"returns": { "type": "void" }
},
{
"name": "account_freeze",
"args": [
{ "type": "asset", "name": "freeze_asset" },
{ "type": "account", "name": "freeze_account" },
{ "type": "bool", "name": "asset_frozen" }
],
"returns": { "type": "void" }
}
]
Calling asset_freeze
prevents any transfer of a Smart ASA. Calling
account_freeze
prevents a specific account from transferring or receiving a
Smart ASA.
Upon a call to
asset_freeze
oraccount_freeze
, a reference implementation SHOULD:
- Fail if
freeze_asset
does not correspond to an ASA controlled by this smart contract.- Succeed iff the
sender
of the transaction corresponds to thefreeze_addr
, as persisted by the controlling Smart Contract.In addition:
- Upon a call to
asset_freeze
, the controlling Smart Contract SHOULD persist the tuple(freeze_asset, asset_frozen)
(for instance, by setting afrozen
flag in global storage).- Upon a call to
account_freeze
the controlling Smart Contract SHOULD persist the tuple(freeze_asset, freeze_account, asset_frozen)
(for instance by setting afrozen
flag in the local storage of thefreeze_account
). See the security considerations section for how to ensure that Smart ASA holders cannot reset theirfrozen
flag by clearing out their state at the controlling Smart Contract.
[
{
"name": "get_asset_is_frozen",
"readonly": true,
"args": [{ "type": "asset", "name": "freeze_asset" }],
"returns": { "type": "bool" }
},
{
"name": "get_account_is_frozen",
"readonly": true,
"args": [
{ "type": "asset", "name": "freeze_asset" },
{ "type": "account", "name": "freeze_account" }
],
"returns": { "type": "bool" }
}
]
The value return by get_asset_is_frozen
(respectively,
get_account_is_frozen
) tells whether any account (respectively
freeze_account
) can transfer or receive freeze_asset
. A false
value
indicates that the transfer will be rejected.
Upon a call to
get_asset_is_frozen
, a reference implementation SHOULD retrieve the tuple(freeze_asset, asset_frozen)
as stored onasset_freeze
and return the value corresponding toasset_frozen
. Upon a call toget_account_is_frozen
, a reference implementation SHOULD retrieve the tuple(freeze_asset, freeze_account, asset_frozen)
as stored onaccount_freeze
and return the value corresponding toasset_frozen
.
Asset Destroy
{
"name": "asset_destroy",
"args": [{ "type": "asset", "name": "destroy_asset" }],
"returns": { "type": "void" }
}
Calling asset_destroy
destroys a Smart ASA.
Upon a call to
asset_destroy
, a reference implementation SHOULD:
- Fail if
destroy_asset
does not correspond to an ASA controlled by this smart contract.It is RECOMMENDED for calls to this method to be permissioned (see
asset_create
).The controlling Smart Contract SHOULD perform an asset destroy operation on the ASA with ID
destroy_asset
. The operation will fail if the asset is still in circulation.
Circulating Supply
{
"name": "get_circulating_supply",
"readonly": true,
"args": [{ "type": "asset", "name": "asset" }],
"returns": { "type": "uint64" }
}
Calling get_circulating_supply
returns the circulating supply of a Smart ASA.
Upon a call to
get_circulating_supply
, a reference implementation SHOULD:
- Fail if
asset
does not correspond to an ASA controlled by this smart contract.- Return the circulating supply of
asset
, defined by the difference between the ASAtotal
and the balance held by itsreserve_addr
(see Asset Creation).
Full ABI Spec
{
"name": "arc-0020",
"methods": [
{
"name": "asset_create",
"args": [
{
"type": "uint64",
"name": "total"
},
{
"type": "uint32",
"name": "decimals"
},
{
"type": "bool",
"name": "default_frozen"
},
{
"type": "string",
"name": "unit_name"
},
{
"type": "string",
"name": "name"
},
{
"type": "string",
"name": "url"
},
{
"type": "byte[]",
"name": "metadata_hash"
},
{
"type": "address",
"name": "manager_addr"
},
{
"type": "address",
"name": "reserve_addr"
},
{
"type": "address",
"name": "freeze_addr"
},
{
"type": "address",
"name": "clawback_addr"
}
],
"returns": {
"type": "uint64"
}
},
{
"name": "asset_config",
"args": [
{
"type": "asset",
"name": "config_asset"
},
{
"type": "uint64",
"name": "total"
},
{
"type": "uint32",
"name": "decimals"
},
{
"type": "bool",
"name": "default_frozen"
},
{
"type": "string",
"name": "unit_name"
},
{
"type": "string",
"name": "name"
},
{
"type": "string",
"name": "url"
},
{
"type": "byte[]",
"name": "metadata_hash"
},
{
"type": "address",
"name": "manager_addr"
},
{
"type": "address",
"name": "reserve_addr"
},
{
"type": "address",
"name": "freeze_addr"
},
{
"type": "address",
"name": "clawback_addr"
}
],
"returns": {
"type": "void"
}
},
{
"name": "get_asset_config",
"readonly": true,
"args": [
{
"type": "asset",
"name": "asset"
}
],
"returns": {
"type": "(uint64,uint32,bool,string,string,string,byte[],address,address,address,address)",
"desc": "`total`, `decimals`, `default_frozen`, `unit_name`, `name`, `url`, `metadata_hash`, `manager_addr`, `reserve_addr`, `freeze_addr`, `clawback`"
}
},
{
"name": "asset_transfer",
"args": [
{
"type": "asset",
"name": "xfer_asset"
},
{
"type": "uint64",
"name": "asset_amount"
},
{
"type": "account",
"name": "asset_sender"
},
{
"type": "account",
"name": "asset_receiver"
}
],
"returns": {
"type": "void"
}
},
{
"name": "asset_freeze",
"args": [
{
"type": "asset",
"name": "freeze_asset"
},
{
"type": "bool",
"name": "asset_frozen"
}
],
"returns": {
"type": "void"
}
},
{
"name": "account_freeze",
"args": [
{
"type": "asset",
"name": "freeze_asset"
},
{
"type": "account",
"name": "freeze_account"
},
{
"type": "bool",
"name": "asset_frozen"
}
],
"returns": {
"type": "void"
}
},
{
"name": "get_asset_is_frozen",
"readonly": true,
"args": [
{
"type": "asset",
"name": "freeze_asset"
}
],
"returns": {
"type": "bool"
}
},
{
"name": "get_account_is_frozen",
"readonly": true,
"args": [
{
"type": "asset",
"name": "freeze_asset"
},
{
"type": "account",
"name": "freeze_account"
}
],
"returns": {
"type": "bool"
}
},
{
"name": "asset_destroy",
"args": [
{
"type": "asset",
"name": "destroy_asset"
}
],
"returns": {
"type": "void"
}
},
{
"name": "get_circulating_supply",
"readonly": true,
"args": [
{
"type": "asset",
"name": "asset"
}
],
"returns": {
"type": "uint64"
}
}
]
}
Metadata
ASA Metadata
The ASA underlying a Smart ASA:
- MUST be
DefaultFrozen
. - MUST specify the ID of the controlling Smart Contract (see below); and
- MUST set the
ClawbackAddr
to the account of such Smart Contract.
The metadata MUST be immutable.
Specifying the controlling Smart Contract
A Smart ASA MUST specify the ID of its controlling Smart Contract.
If the Smart ASA also conforms to any ARC that supports additional properties
(ARC-3, ARC-69), then it MUST include a
arc-20
key and set the corresponding value to a map, including the ID of the
controlling Smart Contract as a value for the key application-id
. For example:
{
//...
"properties": {
//...
"arc-20": {
"application-id": 123
}
}
//...
}
To avoid ecosystem fragmentation this ARC does NOT propose any new method to specify the metadata of an ASA. Instead, it only extends already existing standards.
Handling opt in and close out
A Smart ASA MUST require users to opt to the ASA and MAY require them to opt in to the controlling Smart Contract. This MAY be performed at two separate times.
The reminder of this section is non-normative.
Smart ASAs SHOULD NOT require users to opt in to the controlling Smart Contract, unless the implementation requires storing information into their local schema (for instance, to implement freezing; also see security considerations).
Clients MAY inspect the local state schema of the controlling Smart Contract to infer whether opt in is required.
If a Smart ASA requires opt in, then clients SHOULD prevent users from closing out the controlling Smart Contract unless they don’t hold a balance for any of the ASAs controlled by the Smart Contract.
Rationale
This ARC builds on the strengths of the ASA to enable a Smart Contract to control its operations and flexibly re-configure its configuration.
The rationale is to have a “Smart ASA” that is as widely adopted as the ASA both by the community and by the surrounding ecosystem. Wallets, dApps, and marketplaces:
- Will display a user’s Smart ASA balance out-of-the-box (because of the underlying ASA).
- SHOULD recognize Smart ASAs and inform the users accordingly by displaying the name, unit name, URL, etc. from the controlling Smart Contract.
- SHOULD enable users to transfer the Smart ASA by constructing the appropriate transactions, which call the ABI methods of the controlling Smart Contract.
With this in mind, this standard optimizes for:
- Community adoption, by minimizing the ASA metadata that need to be set and the requirements of a conforming implementation.
- Developer adoption, by re-using the familiar ASA transaction reference in the methods’ specification.
- Ecosystem integration, by minimizing the amount of work that a wallet, dApp or service should perform to support the Smart ASA.
Backwards Compatibility
Existing ASAs MAY adopt this standard if issued or re-configured to match the requirements in the metadata section.
This requires:
- The ASA to be
DefaultFrozen
. - Deploying a Smart Contract that will manage, control and operate on the asset(s).
- Re-configuring the ASA, by setting its
ClawbackAddr
to the account of the controlling Smart Contract. - Associating the ID of the Smart Contract to the ASA (see metadata).
ARC-18
Assets implementing ARC-18 MAY also be compatible with this ARC if the Smart Contract implementing royalties enforcement exposes the ABI methods specified here. The corresponding ASA and their metadata are compliant with this standard.
Reference Implementation
A reference implementation is available here: https://github.com/algorandlabs/smart-asa.
Security Considerations
Keep in mind that the rules governing a Smart ASA are only in place as long as:
- The ASA remains frozen;
- the
ClawbackAddr
of the ASA is set to a controlling Smart Contract, as specified in the metadata section; - the controlling Smart Contract is not updatable, nor deletable, nor re-keyable.
Local State
If your controlling Smart Contract implementation writes information to a user’s local state, keep in mind that users can close out the application and (worse) clear their state at all times. This requires careful considerations.
For instance, if you determine a user’s freeze state by reading a flag from their local state, you should consider the flag set and the user frozen if the corresponding local state key is missing.
For a default_frozen
Smart ASA this means:
- Set the
frozen
flag (to1
) at opt in. - Explicitly verify that a user’s
frozen
flag is not set (is0
) before approving transfers. - If the key
frozen
is missing from the user’s local state, then considered the flag to be set and reject all transfers.
This prevents users from resetting their frozen
flag by clearing their state
and then opting into the controlling Smart Contract again.
Copyright
Copyright and related rights waived via CCO.
Citation
Please cite this document as:
Cosimo Bassi, Adriano Di Luzio, John Jannotti, "ARC-20: Smart ASA," Algorand Requests for Comments, no. 20, April 2022. [Online serial]. Available: https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0020.md.