NFTs and Clarity Contracts
#stacks #clarity #bitcoin #smartcontracts #thisisnumberone
The Clarity smart contract language is fast becoming the de facto standard for implementing smart contract functionality on the Bitcoin blockchain.
The standards for handling fungible and non fungible tokens have been well documented and are part of the open source community with governance care of the Stacks Foundation.
This article shares some thoughts and learning about some finer aspects of handling NFTs using Clarity in comparison with equivalent mechanisms which exist in the EIP 721 standard on Ethereum — specifically around enabling control of NFT transfer from authorised addresses.
Clarity vs Solidity NFT Standards
A comparison between the SIP-009 trait and the analogous EIP 721 standard will help as a starting point.
SIP-009 is the Clarity standard for NFT and reads as follows;
(define-trait nft-trait
(
;; Last token ID, limited to uint range
(get-last-token-id () (response uint uint))
;; URI for metadata associated with the token
(get-token-uri (uint) (response (optional (string-ascii 256)) uint))
;; Owner of a given token identifier
(get-owner (uint) (response (optional principal) uint))
;; Transfer from the sender to a new principal
(transfer (uint principal principal) (response bool uint))
)
)
EIP 721 is longer and this just shows a part missing from the Clarity trait which highlights the differences between the two with respect to who can transfer the ownership of an NFT.
interface ERC721 /* is ERC165 */ {...
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns (bool)
}
You’ll notice that EIP 721 defines the ability for the NFT owner to delegate, to an approved address, the ability to transfer the asset on the owners behalf.
In Clarity there is no need for an approval mechanism because Clarity leaves this up to the smart contract developer. While this is great for innovation and rapid development it is also a concern from a security perspective as it means the contract builder can insert backdoors which allow any NFT in in the contract to be transferred to anyone else — without the consent of the owner.
Problems with Traits
Traits in Clarity are equivalent to interfaces in object orientated languages such as Java. They say nothing about the implementation of the method — they only enforce the implementing contract declares methods with the exact same signatures — same arguments, same return values.
Taking a closer look at the transfer method;
(transfer (uint principal principal) (response bool uint))
The arguments are as follows;
- nft index (or token index)
- nft owner
- recipient
Looking at this there is nothing enforcing the second argument, the principal of the NFT owner is the actual owner. But the transfer would fail if the the addresses did not match because the implementation of this method relies on a deeper — in built Clarity mechanism to actually transfer the owning address.
(nft-transfer? asset-class asset-identifier sender recipient)
This in-built method provided by Clarity enforces the constraint that the sender address does indeed own the asset. All good - the original contract call has to provide the NFT owner address as an argument...
But this leads to the next question — what is the relationship between the NFT owner and the tx-sender?
In Clarity the tx-sender is a special variable that is assigned the principle of the issuer/signer of the transaction. There are some subtleties in that if the contract calls another contract the tx-sender changes to the principal of the calling contract but leaving this aside there is nothing so far to say that the tx-sender must be the owner of the asset.
This has all sorts of implications. One is that the contract can abuse its privilege and provide a means for say an ‘administrator’ to transfer assets literally from anyone to anyone else without the permission of the owner — the only way of knowing would be to read the source code which okay for some of us but not practical in general terms.
Plugging the Hole
Clarity NFT contract builders can vote on this issue by implementing a new trait that signals an agreed approach.
For example new trait which defines an approval process for contracts might take a very simple form;
(define-trait nft-approvable-trait (
;; Sets or unsets a user or contract principal who is allowed to call transfer
(set-approval-for (uint principal) (response bool uint))
(get-approval (uint))))
and smart contract developers can choose to implement this trait as a way to signal their concern. This in turn could create a path to a proper solution — down at the in-built Clarity method level where changes along the following lines might solve this problem;
- nft-transfer? — enforces that the ‘sender’ is either the tx-sender or the ‘approval-for’ principal
- a new in-built ‘nft-approval-for’ method would register an approved address for a given NFT.
The key question being asked here is whether the NFT handling methods should be wide open and therefore encouraging innovation but also opening the door to potential NFT ownership issues or whether, as a community we want to lock this down to at least the level that the EIP 721 standards declare?
In other words do we need a built in mechanism preventing contract developers from building back doors to NFT ownership or should we be going with the ‘can’t be evil’ motto and deliver this within the rules of the Clarity language itself?