Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Article Thumbnail

Royalty payments using ARC18

Background

The draft [ARC18] ASA Transfer Royalty Enforcement Specification is designed to allow NFT and ASA creators define specific royalty payments when their asset is initially sold or resold on secondary markets. It intends to define a broad specification allowing different deployment and transfer scenarios. In addition, the specification should supply ABI methods to properly transfer ASAs with royalties and to allow marketplaces and wallets to easily integrate ARC18-based ASAs into their offerings.

  • This article discusses the draft specification and content is subject to change.
  • A reference implementation is available on the Algorand DevRel Github, although this contract is for information purposes only and has not been audited.

Request

The Algorand community is actively working to define and finalize a specification to enforce royalty-based payments on transfers of selected ASAs.

Request for comments
Please contribute to the discussion on GitHub regarding [ARC18] ASA Transfer Royalty Enforcement Specification .

Audience

ARC18 should be interesting to NFT buyers, NFT creators, marketplaces, and wallets. The specification is designed to work with existing ASA standards like ARC3 and ARC69.

General Technical Overview

From an Algorand primitive level, ARC18 is designed around the idea that an ASA that needs royalty enforcement is minted with a default frozen status, and the clawback address is used to move the ASA from one wallet to the next. The clawback address is defined as a smart contract that enforces the royalty. The contract provides ABI methods for transferring the ASA (transfer), setting a policy that indicates the size and receiver of royalty (set_policy), an offering method to set (or remove) the ASA for sale (offer), setting acceptable ASAs for payment(set_payment_asset), and an optional move method to allow the ASA to be moved without a royalty under certain conditions(royalty_free_move). The contract uses global storage to store royalty policy information. Local state is only used when an owner places an ASA up for sale. This will require that the seller of the ASA be opt-ed into the royalty smart contract for the brief time that the individual offers the ASA for sale. Once sold, the account can opt-out of the contract.

ARC18 is designed to be used with the Algorand Standard Assets (ASA) primitive. Every ASA has a set of immutable properties and a set of mutable addresses. One immutable property is defaultFrozen. If defaultFrozen is true, an account’s holding of that ASA will start out frozen. An ASA can not be transferred from (or to) a frozen account. The freeze address of the ASA can change the freeze status of an account. The only way an ASA can be moved from (or to) a frozen account is if the clawback account performs the transfer. Any ASA that wants to use ARC18 must be defined with a defaultFrozen value of true. The mutable addresses configure how the ASA can be manipulated. These four addresses (manager, reserve, freeze, and clawback) represent specific roles that an account is responsible for. These addresses are mutable unless they are set to the ZeroAddress, which will lock the address for the life of the ASA. Any ASA wanting to use ARC18 must set the manager and freeze accounts to the ZeroAddress or the address of the smart contract implementing ARC18. The clawback address is used to move the ASA and must be set to the application account of the smart contract implementing ARC18. The reserve address can be set to any address.

ARC18 ASAs are expected to define metadata that extends the ARC3 or ARC69 JSON metadata properties. In ARC3 the URL ASA property points to a JSON file containing additional properties. ARC69 ASAs define JSON properties in the note field of an asset configuration transaction. ARC18 ASAs rely on ARC20, which defines a specification for creating ASAs that require custom transfer logic. So the properties for an ARC18 must contain an arc-20 property which contains the application id of the contract that implements ARC18 and an arc-18 property that optionally contains a rekey-checked property. See the transfer method below for more details on this field.

//...
"properties":{
    //...
    "arc-20":{
        "application-id":123
    },
    "arc-18":{
        "rekey-checked":true // Defaults to false if not set
    }
}
//...

This associates the ASA with the specific instance of an ARC18 compliant application. Allowing this metadata to change is not mandated in the specification, but it is strongly recommended that modification to the metadata be disallowed. This can be done by using a URL that points to a resource that can not be changed for ARC3 ASAs or by locking the manager account for ARC69 ASAs.

EditorImages/2022/04/28 16:54/arc18arch.jpg

Primary Terms

Royalty Policy - a structure containing the basis points (0-10000) that represents the percentage (0-100%) of the total payment for an ASA that should be paid to the royalty receiver and the specific address of the royalty receiver.

Royalty Receiver - The account that receives the royalty portion of the payment. Note that this can be any address on Algorand, including the address of another smart contract that may further split the payment to multiple parties.

Royalty Enforcer - This is the instance of a smart contract that implements ARC18 and is responsible for enforcing the royalty and dividing the payment between the seller and the royalty receiver.

Royalty Enforcer Administrator - This account can perform administrative functions within the contract. By default this must be set to the contract creator. If the optional set_administrator method is implemented, this account can be changed using set_administrator when called by the current administrator. If the optional method is implemented the associated optional get method must also be implemented. This account is allowed to call the royalty_free_move, set_payment_asset, and set_policy methods. If the set_administrator method is implemented, it must use the global key name administrator.

Global/Local Naming Scheme vs Get Methods

Marketplaces and wallets need to be able to read some information about the smart contract such as the royalty policy and the current offer. ARC18 offers two options to address this need: global/local naming scheme and get methods:

  • A smart contract MUST implement the get_* methods specified in the ABI (discussed below)
  • A smart contract MAY follow the global/local naming scheme. If the global key royalty_basis is defined, then it MUST follow strictly the global/local naming scheme for all the other variables.

The above rules are to balance the following:

  • Not requiring complex designs or payment of fees for today’s marketplaces/wallets: At the time of writing, it is not easy to execute a read-only method such as get_* methods without paying any fee. Hence, this standard provides an additional optional way to read these values without having to use complex mechanisms or pay fees.
  • Allowing more flexibility in the future: global/local naming schemes add strong constraints to the smart contract that prevents smart optimizations like compressing the local/global state and may restrict for example the number of royalty ASAs a given user may offer on a marketplace. The get_* method offers much more flexibility in how values are stored.

Concretely clients such as marketplaces/wallets SHOULD do the following:

  • Before support of free call to read-only functions:

    • Check if the global key royalty_basis is present in the global storage. If it is present, read local/global storage to get all the information needed about a royalty ASA.
    • Otherwise, either fail or pay a transaction fee to execute the get_* method.
  • After support of free call to read-only functions:

    • Systematically use the get_* methods.

Smart contract / royalty ASA designers SHOULD do the following:

  • Before support of free call to read-only functions: follow the global/local naming scheme (and also implement the get_* functions, which are MANDATORY in all cases)
  • After support of free call to read-only functions: if the global/local naming scheme prevents optimizations of the smart contract (e.g., restricting the number of ASAs a user can sell), optimize the storage. Importantly, in this case, the key royalty_basis MUST NOT be used at all.

ABI Methods

The ARC18 specification defines six ABI methods that an ARC18 smart contract must implement. These allow the ASA to be configured for royalties, place the asset for sale, transfer the asset to a buyer with a royalty payment to the royalty receiver, two getter methods to retrieve royalty policy and current offer information, and set additional ASAs that can be used besides the Algo to purchase the ASA. In addition, the specification defines three optional methods that can be implemented to support transferring the ASA without a royalty, and set or get the royalty enforcer administrator account.

For more information about how to use ABI with your smart contracts see this article, the ABI specification, or the developer documentation. Developers should use the Atomic Transaction Composer(ATC) to create proper ABI-based transactions. For more information on ATC, see the developer documentation.

These methods are described below.

The set_policy Method

The set policy method is used by the royalty enforcer administrator to set the basis points and the receiver of the royalty. This method must be called by the royalty enforcer administrator account of the ARC18 enforcer contract. This method should have the following parameters.

royalty_basis (ABI Type uint64) - the basis for the royalty receiver.

royalty_recipient (ABI Type address) - The account of the royalty receiver

The smart contract should store these values in the Global state with the key names royalty_basis and royalty_receipient. The basis must be stored as a uint64 with a value between 0-10000. This value represents a value of between 0-100%, with two implied decimal points (ie hundredths of a percent). The receiver should be stored as a 32-byte array containing the address of the receiver. The specification does not mandate whether the basis points should be immutable, but it is strongly suggested that some max basis be set in the contract code. If this is not done, a creator could set the basis to the maximum after first selling the ASA to another Algorand account. This in essence would prevent anyone from selling the ASA and receiving any value from the sale. To prevent this situation, in addition to adding maximum value in the contract, implementers should make sure the that the manager account is set to the ZeroAddress, reject any transaction that attempts to delete or update the contract, and use an ARC18 templated code with a verifiable hash that can be used to verify the code has not been changed.

The set_payment_asset Method

This method is used to set acceptable forms of payment for a royalty-based ASA. By default, all royalty payments support the native Algo. In many cases, the form of payment may be another ASA, such as USDC. This method must be supplied the following parameters.

payment_asset (ABI Type asset) - The asset that is acceptable for payment for the ARC18 ASA. This asset must be in the Assets array.

allowed (ABI Type bool) - This parameter can be set to true or false. When set to true, the enforcer contract will opt-in to the ASA. On false the enforcer contract should opt-out of the ASA.

This method must be called by the royalty enforcer administrator account. This method takes two parameters, one of which is the ASA id and a boolean that is either true or false. When this boolean is set to true, the enforcer contract should option into the acceptable ASA. If set to false the contract should option out of the ASA. If this method is not used, only Algo payments will be supported by the royalty enforcer contract.

The offer Method

When an owner of an ASA that conforms to ARC18 wants to offer the ASA for sale, they will need to call this method. This call must be called from the current ASA owner and a set of parameters must be passed to the call. These parameters include the following.

royalty_asset (ABI Type uint64) - This is the ASA that the current owner is interested in selling. This asset must be in the Assets array.

royalty_asset_amount (ABI Type uint64) - This is the quantity of the token the user is interested in selling. For NFTs this value will be one.

auth_address (ABI Type address) - This is the address of the account that is allowed to make the transfer enforcer contract call. Most likely this will be defined as the buyer or a marketplace that represents the buyer’s interest.

offered_amount (ABI Type uint64) - This field represents the currently set amount of the asset for sale. This field is primarily used to prevent certain race conditions. This value should be set to 0 if the user is not updating an existing offer for a specific royalty-based ASA (ie newly listing the ASA for sale). If the caller is updating an existing offer, this value should be set to the current amount that is on-chain. This prevents cases where a sale occurs before the offer is updated in the same block, from causing undesired results.

offered_auth_address (ABI Type address) - Similar to the previous parameter, this represents the current authorized address. If the current owner is listing the ASA and not updating an existing offer, this should be set to the ZeroAddress. If the caller is updating the offer this value should contain the current on-chain authorized address.

ARC18 smart contract implementers should store offers in the current owner’s local state with the following key/value structure.

Key - Should be an 8 byte array containing the ASA ID. This generally will require calling itob on the passed in ASA id.

Value - Should be a 40-byte array containing the authorized address (32 bytes) concatenated with the amount to be offered (8 bytes). The amount will generally require using itob on the passed-in amount to offer and concatenating this byte array to the authorized address byte array.

By using local state, any ARC18 ASA seller must first option into the enforcer contract else the method will fail. If the user options or clears out of the contract after making an offer that has not been completed, the offer will be cleared.

The specification does not define how marketplaces or wallets should get a collection of offers. This operation can be done in many ways, but monitoring application transactions to the enforcer contract is one suggested way of handling this operation.

EditorImages/2022/04/28 15:25/arc18offer.jpg

The transfer Method

The transfer method is intended to be used by prospective buyers of an offered ARC18 ASA. If an offer for the specified ASA does not exist, this method must fail. In addition, the offer method must be called by the address that is specified as the auth address in the offer within the current owner’s local state. The following parameters must be included in the call.

royalty_asset (ABI Type asset) - This parameter should specify the ASA that is being transferred. This asset must be in the Assets array.

from (ABI Type account) - This parameter should contain the address of the current owner of the ASA. This account must be in the Accounts array.

to (ABI Type account) - This parameter should contain the address of the account who is going to receive the ASA. This account must be in the Accounts array.

royalty_receiver (ABI Type account) - This parameter is the address of the royalty receiver and must match what is stored in global state for the policy. This account must be in the Accounts array.

royalty_asset_amount (ABI Type uint64) - This parameter should contain the amount of the ASA the buyer is trying to transfer. It must be less than or equal to the offer’s royalty_asset_amount. If the transfer is successful this amount must be deducted from the offer’s royalty_asset_amount.

payment (ABI Type transaction) - This parameter is the payment transaction for the purchase of the ASA. This transaction must be either a standard payment transaction (Algos) or an AssetTransfer transaction if the ASA is being purchased with some other ASA. Note that if this is an AssetTranferTransaction, the enforcer contract must be optioned into the payment ASA. This should have been done with a previous call to the set_payment_asset method. This call will fail if this is not the case. Note that when using the ATC this transaction will be added to the atomic transaction group automatically.

payment _asset (ABI Type asset) - If the payment for the ASA is a normal payment transaction, this parameter should be set to 0. If the payment for the ASA is another ASA, the asset should be passed to this call. This asset must be in the Assets array if set to anything other than 0.

offered_amount (ABI Type uint64) - This parameter is used to prevent race conditions when a buyer may issue a transfer, while another is currently processing. It should be set to the on-chain value of the offered amount by the seller at the time the transaction is submitted.

When this method is called the enforcer contract optionally can first verify that neither the from or to accounts have been rekeyed by checking the authaddr of both accounts. This field can be checked by comparing it to the ZeroAddress. If this check is not done royalty payments can be circumvented by rekeying an existing owner account to the buyer. With this logic, the current owner can rekey, but future owners would never be able to sell the ASA with marketplaces that enforce ARC18. If this check is implemented, the contract developer must add and set the rekey_checked metadata property to true.

The enforcer contract must apply the royalty logic and split the payment transaction between the seller and the royalty receiver according to the basis setting in the policy stored in global state. The enforcer contract at this point can decrement the offer’s amount field by the amount of the ARC18 ASA being transferred OR optionally just clear the offer from the local state of the current seller. If decrementing the amount the enforcer contract should clear the offer if the amount drops to 0.

If using a marketplace, the call architecture may look similar to the following step process, where if any step fails they all fail.

EditorImages/2022/04/28 15:50/arc18transfer.jpg

The get_policy Method

This required method is a read only method that can be used by marketplaces and wallet integrators to retrieve the current policy value for a given royalty enforcer contract. This method requires no parameters as an ARC18 contract only stores 1 policy. This method returns an address for the royalty receiver and the basis point setting. Note that while this is a read-only method, this still requires a transaction. Marketplaces and wallets can read the current global state for the contract with the SDK currently or the dryrun request can be used to call this method and retrieve the policy without submitting a transaction. This method is primarily added to the specification to allow more advanced smart contracts to optimize storage by not using the default global/local name scheme, see Global/Local Naming Scheme vs Get Methods.

The get_offer Method

This required method is a read only method than can be used to retrieve an offer for a specific royalty ASA from a specific account. This method takes the following parameters.

royalty_asset (ABI Type uint64) - This parameter should specify the ASA id that is being checked.

from (ABI Type account) - This parameter is the account of the current royalty ASA owner. This account must be in the Accounts array.

This method retrieves the the local state variables representing an offer (if one exists) from a current royalty ASA owner. It returns an address representing the auth address and the amount of the ASA the owner is currently offering. If no offer exists for the given parameters, this method should fail.

Note that while this is a read-only method, this still requires a transaction. Marketplaces and wallets can read the current local state for an account for the specific contract, and it will be keyed with the royalty ASA id. This can be achieved with the SDK currently or the dryrun request can be used to call this method and retrieve the policy without submitting a transaction. This method is primarily added to the specification to allow more advanced smart contracts to optimize storage by not using the default global/local name scheme, see Global/Local Naming Scheme vs Get Methods.

The royalty_free_move Method

This method is optional and allows a royalty ASA to be transferred without a royalty. It may be useful when the creator of an ASA wants to allow a royalty-free move or a marketplace uses intermediate accounts for moving tokens. This method requires the following parameters.

royalty_asset (ABI Type asset) - This parameter should specify the ASA that is being moved. This asset must be in the Assets array.

royalty_asset_amount (ABI Type uint64) - This parameter should contain the amount of the ASA the move is trying to transfer.

from (ABI Type account) - This parameter should contain the address of the current owner of the ASA. This account must be in the Accounts array.

to (ABI Type account) - This parameter should contain the address of the account who is going to receive the ASA. This account must be in the Accounts array.

offered_amount (ABI Type uint64) - This parameter is used to control race conditions when a buyer may issue a transfer, while another is currently processing. It should be set to the on-chain value of the offered amount by the seller at the time the transaction is submitted.

If implementing a royalty-free move method, the contract must contain logic that prevents the method from being called without the expressed interest of the ASA creator and the current owner of the ASA. To do this, the current owner, when offering the ASA, must set the authorized address to the royalty enforcer administrator account. This means that the royalty enforcer administrator will be the only account allowed to initiate the move method.

The set_administrator Method

This optional method can be implemented to allow the current royalty enforcer administrator to be changed. If this method is implemented the get_administrator method must also be implemented. This method takes the following arguments.

administrator (ABI Type address) - The address for the new royalty enforcer administrator account.

This method must be called from the current royalty enforcer administrator account. This method must set the global key administrator to the address passed as the parameter.

The get_administrator Method

This method returns the address of the current royalty enforcer administrator account. If the set_administrator method is implemented, this method must be implemented. If this method is implemented but the set_administrator method is not implemented this method should just return the contract creator account. If the set method is implemented, the method should return the address value of the current global key administrator. This method takes no parameters.

Additional Notes

Validity of the ARC18 Contract

In some cases where a contract purports to implement ARC18 but has nefarious code, a reference implementation contract can be created where the hash can be verified by marketplaces and wallets to verify the contract adheres to a specific implementation of ARC18. Once a reference ARC18 contract is created, developers have the option to either just manually make a copy of this contract and use it to create a new application, use the SDKs to create an instance of the ARC18 reference or another contract can be created that generates ARC18 instances.

Transaction Fees and Minimum Balances

The specification does not define how the transaction fees or minimum balance requirements should be handled as it should support many transaction fee/minimum balance models. This should be taken into consideration when creating an ARC18 contract. Additionally, any inner transactions that may be executed when the enforcer contract executes should explicitly set the fee to 0 to force the responsibility of the caller to cover this expense through fee pooling.

Minimum Payment Transactions

If the basis is set to low, calculations may result in a royalty payment of less than the base unit of the royalty payment transaction (either ASA transfer or Algo payment transaction). In this case, the ARC18 contract should set some minimum required royalty payment or not issue the inner transaction to pay the royalty. If this is not done, the enforcer contract could be drained with malicious transactions

Group Checking

In some instances, ARC18 developers may want to check the group size to verify that there is specifically a set of transactions. For example, a user submits a pay transaction and a call to the ARC18 contract (Group size of 2) to transfer the royalty ASA. While this seems straightforward, since TEAL version 5, smart contracts can issue inner transactions. This can be used to circumvent royalty payments by issuing a small payment transaction with the ARC18 application transaction and then make the real payment in another inner transaction within the smart contract. Contract developers can address this problem in multiple ways and the specification does not define a specific solution. One way to handle this is to get the caller id within the contract. If it is set you will know that this is indeed an inner transaction. This can be done with PyTEAL using the caller_app_id() method described in the PyTEAL documentation. The contract could then just reject the transfer if it is an inner transaction or verify that the call is coming from an authorized marketplace contract.

Contract Code Changes

The specification does not define if the ARC18 contract can be updated or deleted. If marketplaces are relying on a specific hash for an ARC18 contract, updates and deletes to the code should be prevented in the contract.

Request

The Algorand community is actively working to define and finalize a specification to enforce royalty-based payments on transfers of selected ASAs.

Request for comments
Please contribute to the discussion on GitHub regarding [ARC18] ASA Transfer Royalty Enforcement Specification .