Smart Contract to manage NFT Royalties
This is an explanation of a smart contract PROTOTYPE for artistic NTFs on Algorand. The NFTs can be traded on any Algorand NFT marketplace (DEX) based on agreed standards for asset identification and metadata. This prototype is to explore how one can manage royalties ("Droit de Suite") on initial NFT transaction, but also on all future transactions.
THIS IS NOT PRODUCTION-READY.
Also, please note that Uncopied.Art is neither an online art shop, nor a market place / DEX. Uncopied intends to issue secure 'proof of provenance' for physical and digital art (NFTs). Uncopied's vision is to make original art truly unique, with physical and digitally immutable certificates of authenticity, expertise and inventory that will outlive us. Uncopied assets represent an immutable proof of provenance for a physical or digital object, as such they are non-tradable and inalienable. Learn more about Uncopied here https://uncopied.art/
Uncopied ASA != Tradable NFT
What does this contract Achieve
The purpose of this contract is to enable royalty transactions for Uncopied and to also guide the creation of NFTs, for this purpose, the Algorand blockchain and the TEAL(Transaction Execution Approval Language) has been chosen as the desired Blockchain and smart contract language.
Algorand examples or tutorials used as a reference
- Assets and Custom Transfer Logic by Jason Weathersby, Algorand (https://developer.algorand.org/solutions/assets-and-custom-transfer-logic/)
- AlgoRealm, a NFT Royalty Game by Cosimo Bassi, Algorand (https://developer.algorand.org/solutions/algorealm-nft-royalty-game/)
Things to understand before proceeding
A royalty transaction is a transaction that allows a creator of a content to be rewarded for his content by a buyer of such content, in our case, the content is the NFT.
NFTs are created as ASA (Algorand Standard Assets) under the Algorand public blockchain.
Atomic transactions are transactions that are grouped together such that if one fails, all fail.
How do we achieve this?
We are going to solve this particular problem using an Atomic transaction that contains three transactions and another that contains four transactions, but first, it is good that you understand that there are two types of royalty transactions in our case :
- A creator selling his NFT to a buyer.
- A buyer giving or selling his NFT to a new Buyer
A creator selling his NFT to a buyer.
What are the 3 transactions for this case?
The first transaction is an Asset Transfer Transaction that transfers the NFT(Asset) from the creator to the buyer of the NFT
The second transaction is a Payment Transaction from the Buyer of the NFT to the creator of the NFT; this transaction allows the buyer of the NFT to pay or reward (send royalties to) the creator of the NFT
A call to a stateful smart contract, which confirms that the amount the buyer of the NFT pays in transaction 2 is the correct amount set by the creator and that the buyer of the NFT is also sending this amount to the right creator.
A buyer transferring his NFT
- The first transaction sends the royalties to the creator
- The second transaction sends the NFT to the new buyer.
- The third transaction is a call to the stateful smart contract
- The fourth transaction is the transaction where the new buyer pays the previous buyer for the NFT.
Order Of Transactions For A Royalty Transaction To Take Place
An atomic transaction that creates an asset with default frozen true and also a stateful smart contract which stores the price of a single unit of the NFT, the percentage gain on any transfer of this NFT along with the address of the creator
A compilation of a stateless smart contract that includes the application id of the stateful smart contract, will give us an address.
Funding the stateless smart contract address.
Performing an asset configuration transaction of the created asset to make the clawback address be the stateless smart contract address
Performing an opt-in transaction to opt the buyer of the NFT(asset) into the asset
An atomic transaction groups the 3 transactions described earlier together in order to have a successful transaction
Code And Explanation
Stateful Smart Contract
A stateful smart contract allows us to store certain values on the Algorand blockchain, for our use case, we will be storing the price of the NFT along with the address of the creator of the NFT. Let's write the TEAL code for this in a file and call it Price.teal
#pragma version 3 int 0 txn ApplicationID == bnz creation
the first line defines the version of the Teal programming language we are using. In our case, its version 3, the next three lines load 0 and the current application ID to the stack and compare them, this returns 1 if they are equal and 0 if they are not. Make sure to note that for a stateful smart contract that is just being created, the Application ID is always 0, so, with this, we can always specify the actions we intend to take when our Stateful smart contract is just being created. so the fourth line in the code above uses the
bnz(Branch Not Zero) command to jump to a branch in our code called creation if the comparison is not zero(false). And when we call this Teal program for the first time, the comparison of 0 and the application id will definitely be zero since it is just being created. So let's proceed to create this branch of our code called creation.
creation: byte "Creator" txn Sender app_global_put byte "Price" gtxna 1 ApplicationArgs 0 btoi app_global_put byte "Percent" gtxna 1 ApplicationArgs 1 btoi app_global_put global GroupSize int 2 >= gtxn 0 TypeEnum int acfg == && gtxn 0 ConfigAssetDefaultFrozen int 1 == && return
The first line above is what allows the Teal runtime to know that the codes after that line are a part of the
creation branch, the second to fourth line save the Address of the Creator of the NFT, the fifth to eighth line save the Price of the NFT; we assume that the price is in microalgos. The ninth to 12th line saves the percentage the creator wants to receive on each transfer of this asset, the thirteenth to seventeenth line make sure that the stateful smart contract is created along with an asset, the eighteenth to Twentieth line make sure that the asset is frozen by default. That's presently all for our Stateful smart contract creation logic, let's look at the rest of our code after the
bnz creation line earlier:
int UpdateApplication txn OnCompletion == bnz updateApp
In the code above, we check if the application is being updated and we send program execution to the
updateApp branch, lets take a look at this branch
updateApp: byte "Creator" app_global_get txn Sender == return
updateApp branch simply checks if the creator of the contract is the one calling the contract and allows the app(contract) to be updated, if not, it doesn't allow the app to be updated.
Let's proceed with our code after the
bnz updateApp Line:
int DeleteApplication txn OnCompletion == bnz DeleteApp
In the code above, we check if the application(contract) is being deleted and we send program execution to the
DeleteApp branch, lets take a look at this branch
DeleteApp: byte "Creator" app_global_get txn Sender == return
DeleteApp branch simply checks if the creator of the contract is the one calling the contract and allows the app(contract) to be deleted, if not, it doesn't allow the app to be deleted.
Let's proceed with our code after the
bnz DeleteApp Line:
byte "Creator" app_global_get gtxn 0 AssetSender == bnz txSentFromCreator
For you to understand the code above, you need to remember that we have two kinds of royalty transactions in our case, that line above decides which of them it is. We check if the asset sender is equal to the creator which is the case when the creator is the one selling the asset to a buyer and then we move to a branch called
txSentFromCreator. Let's look at the code in this branch:
txSentFromCreator: global GroupSize int 3 == byte "Price" app_global_get gtxn 1 Amount == && gtxn 0 AssetAmount int 1 == && byte "Creator" app_global_get gtxn 0 AssetSender == && gtxn 1 Receiver gtxn 0 AssetSender == && return
The first three lines make sure that this transaction is being called along with at least two other transactions, the next four lines get the price of this stateful smart contract that was stored during creation and compares it with the amount the buyer wants to pay the creator in the second transaction, the next three lines make sure it's a single unit of this asset that is being transferred, and the next four lines make sure that the sender of the asset in the first transaction is the same as the creator of the contract that was stored when the contract was created. The next lines make sure that the receiver in the second transaction is equal to the sender in the first transaction. Notice the return statement at the end, this is so that our code does not drop into the creation branch again when it is being called
Let's look at the code after our
bnz txSentFromCreator line, this will be the case if the creator is not the one sending the asset meaning the asset is being set from someone who has bought the NFT to another person, which is our second case
global GroupSize int 4 == byte "Creator" app_global_get gtxn 0 Receiver == && gtxn 0 Amount int 100 * store 10 gtxn 3 Amount store 11 load 10 load 11 / byte "Percent" app_global_get == && return
We first make sure that there are at least 4 transactions, then we check that the creator of the NFT is the receiver in the first transaction, we further take the amount in the first transaction, multiply it by 100 and divide it by the amount the new buyer is paying for it, then check if its equal to the percentage set by the creator and allow the contract call to take place if this is the case or reject the transaction.
A simple clarification: suppose a creator sets
P as the royalties percentage he will receive on a transfer of a single unit of an NFT at a variable price
A then the function f(A) that a buyer should pay to the Creator when transferring an asset is
(P/100)*A, lets call f(A)
Y in our case, this means
Y = (P/100)*A. This means that
P should always be equal to
(Y*100)/A; that's why in the code above, we multiply the amount(Y) paid to the creator by 100 and divide it by the amount(A) that the new buyer is willing to pay for the NFT.
And that's all for our stateful smart contract.
Stateless Smart Contract
Let's create a file and call it uncopied.teal, this file will house our stateless smart contract which when compiled will be used as our clawback address, let's look at the code for this file
#pragma version 3 txn Fee int 10000 <= global GroupSize int 3 == && gtxn 2 TypeEnum int appl == && gtxn 2 ApplicationID int 15831798 == &&
So we first make sure the transaction fee is not more than 10000 micro algos so a malicious party does not try to exhaust our funds by setting a high transaction fee, we make sure this transaction is grouped with three other transactions, then we make sure the third transaction is an application call transaction and lastly we check if the application id of the third transaction is equal to the application id of our stateful smart contract address, the nos
15831798 should be changed to the application id of the stateful smart contract before this is compiled, a simple find and replace transaction with any programming language should do this fine.
So now, let's try to run through the order of transactions above using the goal command-line tool.
- An atomic transaction that creates an asset with default frozen true and also a stateful smart contract that stores the price of a single unit of the NFT, the percentage gain on each transfer of the NFT along with the address of the creator
#Two Transactions #AssetCreate Transaction goal asset create --assetmetadatab64 "16efaa3924a6fd9d3a4824799a4ac65d" --asseturl "www.coolade.com" --creator "GJ2KFYI723EMIS76SNSG3TKHDSW7322AZZJXJNV3J35B4TIQVXFXJLB3PI" --decimals 0 --defaultfrozen=true --total 1000 --unitname nljh --name myas --out=unsginedtransaction1.tx #Stateful smart contract create transaction goal app create --creator GJ2KFYI723EMIS76SNSG3TKHDSW7322AZZJXJNV3J35B4TIQVXFXJLB3PI --app-arg "int:200" --app-arg "int:5" --approval-prog ../contracts/Price.teal --global-byteslices 2 --global-ints 2 --local-byteslices 1 --local-ints 1 --clear-prog ../contracts/clearprice.teal --out=unsginedtransaction2.tx # group both transactions cat unsginedtransaction1.tx unsginedtransaction2.tx > combinedtransactions.tx goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx goal clerk split -i groupedtransactions.tx -o split.tx goal clerk sign -i split-0.tx -o signout-0.tx goal clerk sign -i split-1.tx -o signout-1.tx cat signout-0.tx signout-1.tx > signout.tx goal clerk rawsend -f signout.tx
When i run the commands above on my PC presently, the atomic transaction is successful with the following transaction IDs
When i inspect both of them on testnet.algoexplorer.io, I find out the following information
Asset Id: 15976209
Application Id : 15976210
- A compilation of a stateless smart contract which includes the application id of the stateful smart contract, this will give us an address.
So now, replace the application id in the uncopied.teal file with the one above(15836076) and compile it
goal clerk compile ./uncopied.teal
when I run the command above, I get
ICQFYYDKNB6LHNLBMNWDLJBRR67ZJOBA6WEPAXZZHPIDCHMFOSL2XOSYXQ as the address.
Funding the stateless smart contract address you can fund the address on https://bank.testnet.algorand.network/
Performing an asset configuration transaction of the created asset to make the clawback address be the stateless smart contract address. Make sure to pass in the right clawback address and the asset id
goal asset config --manager GJ2KFYI723EMIS76SNSG3TKHDSW7322AZZJXJNV3J35B4TIQVXFXJLB3PI --new-clawback ICQFYYDKNB6LHNLBMNWDLJBRR67ZJOBA6WEPAXZZHPIDCHMFOSL2XOSYXQ --assetid 15976209
When I do this, I get a successful transaction with a transaction id of
O7ULQ3WZBCWPFJGNVFK6O6H5BTXMWRMNXRYABFTQ6EG6HWRBWJ2A, feel free to inspect this id on https://testnet.algoexplorer.io/
- Performing an opt-in transaction to opt the buyer of the NFT(asset) into the asset. Next, we need to opt-in the account of the buyer so he can receive the NFTs.
goal asset send -a 0 -f ZIJ5DOBG3GBMRJ7CGRQENUK5Z746YXEPAATYPFRKIU3WSMEWTJJ43DOMVA -t ZIJ5DOBG3GBMRJ7CGRQENUK5Z746YXEPAATYPFRKIU3WSMEWTJJ43DOMVA --assetid 15976209
After running this, I get a transaction with the following id
GA5CXWNPFOESYOPFQB6JRHGAC2I57H7QKMRTDWUN3B3F73G4CXFQ, feel free to inspect this id on https://testnet.algoexplorer.io/
- An atomic transaction grouping the 3 transactions described earlier together in order to have a successful transaction. This is the final transaction where the buyer receives his NFT and the Creator receives his royalty in algo. Make sure to use the right asset id, application id and clawback address where necessary.
goal asset send --amount 1 --assetid 15976209 --from GJ2KFYI723EMIS76SNSG3TKHDSW7322AZZJXJNV3J35B4TIQVXFXJLB3PI --to ZIJ5DOBG3GBMRJ7CGRQENUK5Z746YXEPAATYPFRKIU3WSMEWTJJ43DOMVA --clawback ICQFYYDKNB6LHNLBMNWDLJBRR67ZJOBA6WEPAXZZHPIDCHMFOSL2XOSYXQ --out unsignedAssetSend.tx goal clerk send --from ZIJ5DOBG3GBMRJ7CGRQENUK5Z746YXEPAATYPFRKIU3WSMEWTJJ43DOMVA --to GJ2KFYI723EMIS76SNSG3TKHDSW7322AZZJXJNV3J35B4TIQVXFXJLB3PI --amount 200 --out unsignedSend.tx goal app call --from GJ2KFYI723EMIS76SNSG3TKHDSW7322AZZJXJNV3J35B4TIQVXFXJLB3PI --app-id 15976210 --out unsignedPriceCall.tx cat unsignedAssetSend.tx unsignedSend.tx unsignedPriceCall.tx > combinedNftTransactions.tx goal clerk group -i combinedNftTransactions.tx -o groupedNftTransactions.tx goal clerk split -i groupedNftTransactions.tx -o splitNft.tx goal clerk sign -i splitNft-0.tx --program ../contracts/uncopied.teal -o signoutNft-0.tx goal clerk sign -i splitNft-1.tx -o signoutNft-1.tx goal clerk sign -i splitNft-2.tx -o signoutNft-2.tx cat signoutNft-0.tx signoutNft-1.tx signoutNft-2.tx > signoutNft.tx goal clerk rawsend -f signoutNft.tx
After doing this, I get three successful transactions with ids,
Meaning our transactions were successful.
Up next is the second type of royalty transaction, where someone who has already bought the NFT sends it to a new buyer:
The first step here is to opt the new user into the nft:
goal asset send -a 0 -f XHFRWIODL7MFJNCWURZY6USPIWUZTQ2X6EWCJ7SWSRKJPUN4LYHO5XK2BY -t XHFRWIODL7MFJNCWURZY6USPIWUZTQ2X6EWCJ7SWSRKJPUN4LYHO5XK2BY --assetid 15974170
I get a successful transaction with the transaction id :
Then we can send the new buyer the NFT while also sending the percent of the amount transferred to the Creator :
goal clerk send --from ZIJ5DOBG3GBMRJ7CGRQENUK5Z746YXEPAATYPFRKIU3WSMEWTJJ43DOMVA --to GJ2KFYI723EMIS76SNSG3TKHDSW7322AZZJXJNV3J35B4TIQVXFXJLB3PI --amount=400000 --out unsignedSend.tx goal asset send --amount 1 --assetid 15976209 --from ZIJ5DOBG3GBMRJ7CGRQENUK5Z746YXEPAATYPFRKIU3WSMEWTJJ43DOMVA --to XHFRWIODL7MFJNCWURZY6USPIWUZTQ2X6EWCJ7SWSRKJPUN4LYHO5XK2BY --clawback ICQFYYDKNB6LHNLBMNWDLJBRR67ZJOBA6WEPAXZZHPIDCHMFOSL2XOSYXQ --out unsignedAssetSend.tx goal app call --from ZIJ5DOBG3GBMRJ7CGRQENUK5Z746YXEPAATYPFRKIU3WSMEWTJJ43DOMVA --app-id 15976210 --out unsignedPriceCall.tx goal clerk send --from XHFRWIODL7MFJNCWURZY6USPIWUZTQ2X6EWCJ7SWSRKJPUN4LYHO5XK2BY --to ZIJ5DOBG3GBMRJ7CGRQENUK5Z746YXEPAATYPFRKIU3WSMEWTJJ43DOMVA --amount=8000000 --out unsignedSend1.tx cat unsignedSend.tx unsignedAssetSend.tx unsignedPriceCall.tx unsignedSend1.tx> combinedNftTransactions.tx goal clerk group -i combinedNftTransactions.tx -o groupedNftTransactions.tx goal clerk split -i groupedNftTransactions.tx -o splitNft.tx goal clerk sign -i splitNft-1.tx --program ../contracts/uncopied.teal -o signoutNft-1.tx goal clerk sign -i splitNft-0.tx -o signoutNft-0.tx goal clerk sign -i splitNft-2.tx -o signoutNft-2.tx goal clerk sign -i splitNft-3.tx -o signoutNft-3.tx cat signoutNft-0.tx signoutNft-1.tx signoutNft-2.tx signoutNft-3.tx > signoutNft.tx goal clerk rawsend -f signoutNft.tx
In the example above we sell 1 unit of our NFT at 8 algo to a new user, and this application(15976210) requires 5% of the new sale transferred to the Creator, which is 0.4 algo and equivalent to 400000 micro algos, that's why we send 400000 micro algo to the creator. After doing this, I get four successful transactions with ids:
Meaning our transactions were successful and our contracts work as expected.
Please note that this is not the final state of this program as more conditions will be added but this should be the basis of it and if this changes, these docs will be updated accordingly.