Title: Minimalistic_transferable_interface
Category: Magicians

Author: sullof
Date: 2023-01-10T20:35:26.879Z

There are a lot of popular discussions about non-transferable tokens, with a good proposal at EIP-4973 - Account-bound Tokens
I have been working recently on a subordinate NFT (ERC-721 Subordinate), and I realized that before everything else, we must address the problem that an exchange must know if a token is transferable or not.
Before going into so many details (like in the EIP-4973), it is necessary to solve this simple problem.
How can we avoid people spending gas trying to transfer or approve a non-transferable token?
I think we must define a minimalistic interface that tells a caller if a token is not-transferable.
This way, a marketplace can check if an NFT supports the transferability interface.
If not, the caller assumes it is a standard ERC721 token, and it is transferable.
If it supports the interface, the caller executes
function transferable(uint tokenId) external view returns (bool);

to verify if that token is transferable or not. The function is helpful because a token can be locally transferable or transferable under certain circumstances. In a game that can depend of other assets, of the status of the gamer, on other related contracts.
Since a soulbound token is a sub-case of a more general case, an account-bound token like in EIP-4973 can just implement this simple interface and start from that.
It is hard to find a short name to define it in a way that can be applied to token that are always non-transferable and tokens for which the transferability can depend on the context.
I would suggest something like
interface IERCxxxx { 
  function transferable(uint256 tokenId) external view returns (bool); 
}

Sometimes the most obvious name is the best.
ADD-ON
January 20th
As you can see in the discussion below, there are cases where the transferability of a token can be affected by the context, i.e., by its current owner or the possible recipient, but adding all those parameters in the equation over-complicates this proposal.
If a token may be non-transferable because of any internal reason, the function isTransferable should return false. This interface must be consumed by external entities (marketplaces, exchanges, pools, etc.), and for them knowing the internal logic that makes a token potentially non-transferable is irrelevant.
ADD-ON
February 9th
With @stoicdev0 and other people, we proposed a new interface at

Ethereum Improvement Proposals


ERC-6454: Minimal Transferable NFT detection interface
A minimal extension to identify the transferability of Non-Fungible Tokens.





Initially, I had doubts about calling the function isNotTransferable, but the naming makes sense because by default an ERC721 is transferrable, so, this interface focus on the case when an NFT may be not transferrable, and the function names follows that. (Still, I like simplicity and I would prefer to call the function isTransferable)

Author: stoicdev0
Date: 2023-01-10T21:21:47.615Z

This feels very similar to 5663, in draft state. Only difference I think, is naming (they use soulbound) and that the 5663 includes event.
There’s also 5192, in final state, which calls this locked and has 2 events instead of one.
Having said that. I think events are limiting, I have a use case where the token is locked or not depending on the address having another token. I’m sure there are many other cases in gaming like this. In these use cases, events don’t make sense and you cannot trust the state of a token just by indexing them.
So, I like this one better, but I’d call it something with soulbound since is what everyone is using now.

Author: xinbenlv
Date: 2023-01-10T22:05:16.787Z

@stoicdev0 I was thinking the same.
There are quite a few EIPs proposed in addressing the exact same problem. IMHO It’s ok to propose competing EIPs but I’d suggest first get familiar and maybe reach out for potential collaboration and /or discuss why propose a competing EIP and it’s merit in rationale/motivation.
That said, I do enjoy seeing this EIP of which the interface is much simpler than competetors

Author: sullof
Date: 2023-01-11T01:08:48.132Z

Thanks for pointing out to them.
I can’t find the EIP-5663.
EIP-5192: Minimal Soulbound NFTs solves the issue with trying to transfer something that is locked, but assumes that the contract implements a switch between two states and emits an event when the switch happens.
That is not always the case. It may be true with personal badges and tokens like that, but there can be a token that is observing a contract and decides if the NFT is locked or not based on that. In that case, the state of the token can not be predicted listening to the event. The only reliable way to know if it is locked or not is calling the view.
I am totally fine with the word locked, but I find it a bit risky because a token being locked can mean a lot of things. For example, in ERC721Lockable, a contract we implemented to allow people to stake their NFTs keeping the ownership of them, we use the word lock in a similar way.
I think that a more specific word would be better. Anyway, I will take a look at that discussion.

Author: sullof
Date: 2023-01-11T01:09:58.853Z

I would be happy to participate in other discussion. Sometimes, the only way to discover that there is a conversation in place, is to start a new one and get feedback

Author: sullof
Date: 2023-01-11T01:14:16.782Z

stoicdev0:

5663


I found it ERC-5633: Composable Soulbound NFT, EIP-1155 Extension
Again, this is very specific on the terminology, and has the same problem and emits an event to say to the world that that token has been bounded to a wallet. But that may not be the case.
I think that a more general naming would be better and it should not emit an event, because the state can change dynamically. In a composable environment like the EVM that should be obvious.

Author: sullof
Date: 2023-01-11T04:31:44.846Z

Maybe it makes sense to add a similar interface for approvals
interface IERCyyyy {

   function isApprovable(uint tokenId) external view returns(boolean);
}

because all the combinations can be true:

The token is approvable and transferable
The token is approvable but not transferable
The token is not approvable and not transferable
The tokens is transferable but not approvable (i.e., is transferable only by the owner)

Having two separate interfaces, one for transfers and one for approval, is the most flexible.
If the basic interfaces are minimalistic, it is easy and inexpensive to combine them and obtain whatever you like. Complex interfaces create always problems.
Anyway, the transferability is the most relevant, and a wise exchange can avoid trying to approve something that cannot be transferred.

Author: stoicdev0
Date: 2023-01-12T19:28:45.664Z

I’m with you on this. We’re creating an advanced NFT standard which includes 5773, 6059 and 6220 (to be merged) and in our repo we have a soulbound implementation in place which is basically the same you’re proposing, just a different name. We did it this way because of the same reason you mention, events would limit implementations.
So again, I like this one better. Just thought it would be good for you to have the other 2 on the radar. Let me know if we can be of any help.

Author: sullof
Date: 2023-01-13T20:18:30.084Z

I just realized that a function like
function isTransferable(uint tokenId) external view returns (bool);

is not working in all the scenarios and I would like to have your opinion about it.
I will make an example that unfortunately is a bit complex.
In Mobland a user can install a Farm over a Turf, in order to plant Seed and have rewards in Weed tokens. Farm and Turf are NFTs. Seed and Weed are ERC20.
To use, for example, a Farm in the GamePool, the user

approves the GamePool as a spender
allows the GamePool to lock the Farm

The result is that the user is still the owner of the Farm token, but they cannot transfer the token as long as it is locked in the GamePool.
In the most common case, the Farm will stay locked until all the Weed has been harvested and the user can unlock the Farm. However, we could have allowed the user to lose the Farm because an attacking team has stolen it. In that case, the GamePool could have transferred the Farm token to the attackers.
Can you see the issue with the current proposals?
That Farm is non-transferable by most wallet, but is transferable by the locker, i.e., the GamePool.
So, a simple function that expects only the tokenId as a parameter will fail.
A more general case, that would cover any possible combination is
function isTransferable(
  address from, 
  address to, 
  uint tokenId) 
external view returns(bool);

What do you think?

Author: sullof
Date: 2023-01-14T20:12:44.425Z

There are in fact more scenarios that a generic interface should cover. Let’s look at the factor that can influence the transferability of an NFT.
The token id
I think this is obvious.
The spender
As I said above, making the example of the GamePool, some spender may be able to transfer some may not.
The current owner
In a game it is possible that someone has to own other assets, or having some balance to be able to transfer an asset.
The recipient
Same like for the current owner
Then, to address all the possible scenarios, the function should be
function isTransferable(
  address sender,
  address from,
  address to,
  uint tokenId
) external view returns(bool);

I would say that in this form, it has lost its original simplicity, but a function that covers only single cases, sooner or later, must be amended. Better to have it ready for a long future.

Author: andyscraven
Date: 2023-01-15T04:08:32.645Z

I like this approach and I agree that events cannot be relied upon in such an implementation.
Although this is not as simple as the original idea it is always a balance between simplicity and thinking about what might come down the road.
I think this is a good balance.

Author: sullof
Date: 2023-01-15T20:33:14.554Z

My last iteration may have been overkill and unnecessary.
When transferring a token, there are two primary types of exchanges involved: internal marketplaces (such as those inside a game) and public marketplaces (such as OpenSea). Internal marketplaces do not need to call a view to see if an NFT is transferable or not because they already know all the rules. On the other hand, public marketplaces simply want to know if they can transfer a token or not. This means that we can remove the “spender” from the required parameters and assume that external exchanges (such as marketplaces or pools) need to know if they can transfer the token or not.
However, we still need to consider that the current owner or the recipient can influence the transferability of an NFT. Above, @stoicdev0 was pointing out
a use case where the token is locked or not depending on the address having another token
Moreover, the NFT knows who is the current owner or approved, so there is no need for it as a parameter and only the recipient is needed.
Therefore, I suggest using the function:
function transferable(
  address recipient,
  uint tokenId
) external view returns(bool);

(The simpler name “transferable” works just as well as “isTransferable” and simplicity is usually best.)

Author: toledoroy
Date: 2023-01-18T23:22:15.845Z

Just brainstorming this a little bit further.
What do you think about instead of having every contract implement all these new functionality and add that little piece of data to its own state, to just have a central contract on each chain to manage all that sort of data (on-chain metadata, so to speak) in one place. Contract as a service sort of thing that would hold arbitrary information for all other contracts on the chain. 
That could have a huge DRY impact

Author: stoicdev0
Date: 2023-01-19T13:52:10.191Z

However, we still need to consider that the current owner or the recipient can influence the transferability of an NFT. Above, @stoicdev0 was pointing out
a use case where the token is locked or not depending on the address having another token

No special need for this beyond not having events since they don’t make sense in this scenario. You can implement whatever logic you want to return the result, that includes calling other contracts.
I don’t like this depending on recipient or anything besides the tokenId. It’s too specific IMO for a minimalistic interface.

Author: sullof
Date: 2023-01-19T20:03:17.085Z

stoicdev0:

I don’t like this depending on recipient or anything besides the tokenId. It’s too specific IMO for a minimalistic interface.


I prefer the simplest solution too, but in your specific case, if a token can be transferred depending on the address having another token, I would expect that a similar rule can be applied also to the receiver. So, if you do not specify the receiver, how can you establish if that token is transferable or not?
The tradeoff is between the simplest possible interface and a bit more complex interface that covers all the possible scenarios. If we go with the first case, maybe a partial implementation of ERC5192 is enough.

Author: sullof
Date: 2023-01-19T20:06:09.984Z

toledoroy:

What do you think about instead of having every contract implement all these new functionality and add that little piece of data to its own state, to just have a central contract on each chain to manage all that sort of data (on-chain metadata, so to speak) in one place


That is an interesting approach, but how can that central contract know all the rules applicable to the NFT in different games, defi, etc.? It would require that the game, somehow, tells the registry how to manage the single case. It seems not feasible. Much easier that everyone manages its own stuff and there is a simple way to tell others what is going on.

Author: toledoroy
Date: 2023-01-19T21:06:28.541Z

Yeah, well, the contract has to explicitly declare that either way. And either way, we’d need to add some code to write that data. It would make it simpler to read, find and index. Well, I guess you could call it an index contract.
It just seems that looking at the crypto space as a community of microservices, and especially when limited to 24k, it might make more sense to have a central service that handles all similar things everyone needs. Like arbitrary identifying data, such as this, contractURI, etc’
Maybe some kind of an arbitrary data protocol for taking out all of these birds and future birds with one stone.
It would also maybe offer some relief to the spaghetti and lack of backward compatibility issue that each of these standard changes introduces.
… just a thought

Author: yuki-js
Date: 2023-01-20T01:36:41.132Z

I recently submitted the “Untransferability Indicator” EIP. Currently it is for EIP-1155 tokens, but eventually I want to implement untransferability for any token specification.



EIP-6268: UNTransferability Indicator for EIP-1155 EIPs


    I submitted an EIP, that is inspired by EIP-5172 and have a similar simple interface. 
This is something like EIP-5172 for EIP-1155.
  

I support your idea if it can be applied to ERC-20 token.
IMO, any tokens have transferability by default. Untransferable tokens is the specific case of them. So, I think it’s better “Untransferability Indicator” or “Untransferability Interface” than “Transferable Interface”

Author: sullof
Date: 2023-01-20T17:49:57.242Z

toledoroy:

It would also maybe offer some relief to the spaghetti and lack of backward compatibility issue that each of these standard changes introduces.


That is a big problem. For Everdragons2 we are building a contract-as-a-service system to generate subordinate contracts and I am very sensitive to that issue. The problem is that a service like that, to be reliable, should index the entire blockchain. Some kind of Google for EVM. It would be fantastic, if we had something like that. I am sure that can be done, but composability, for now, is what we have and we take advantage of it.

Author: sullof
Date: 2023-01-20T20:23:15.916Z

stoicdev0:

No special need for this beyond not having events since they don’t make sense in this scenario. You can implement whatever logic you want to return the result, that includes calling other contracts.


I think you are right. Let’s go back to the initial formulation. A minimalistic case where it just says if a tokenId is transferable or not. In the end, that is needed by external entities, like marketplaces, the internal marketplace does not need to call the view because it knows what to do.
I think the discussion has been very productive.
A question for the moderators
In general, is it better to update the initial post or is it better to link an updated document? I may create a simple repo for it.

Author: sullof
Date: 2023-01-20T23:51:36.405Z

yuki-js:

I support your idea if it can be applied to ERC-20 token.


Since an ERC20 is fungible, it can be either transferable or not transferable, if this interface would be applied.
If that is not the case, for example a token can be vested and there can be cases where the owner cannot transfer it or the recipient cannot receive it, this interface doesn’t work.
We need a more complex function, like the one I proposed a few comments above:
function isTransferable(
  address from, 
  address to, 
  uint tokenIdOrAmount) 
external view returns(bool);

This would support also ERC20 and any other asset that is transferable between two addresses. The problem here is if to keep the interface minimalistic and simple, or make it capable of managing any possible scenario. It looks like the tendency is towards the first case.

Author: toledoroy
Date: 2023-01-21T05:16:53.071Z

Cool. Thanks for the feedback. That’s what I use that in my protocols, I’ll try to push it forwards a bit.
as for a simple solution for finding out if a token is transferable, I think it should be enough for us all to agree on an error message (like ‘Token-Not-transferable’) in case it’s soulbound. Then if a contract doesn’t have a transfer function, or returns such an error, we can deduce it’s soulbound. Seems simpler and more lightweight.
Honestly, NFT standards only leave you about 18kb to work with. I would have a hard time supporting any additions to them. It seems to make much more sense to just remove the transfer functions. Wouldn’t that serve the same purpose with less code?

Author: sullof
Date: 2023-01-21T21:10:03.949Z

toledoroy:

as for a simple solution for finding out if a token is transferable, I think it should be enough for us all to agree on an error message (like ‘Token-Not-transferable’) in case it’s soulbound


If the token is non-transferable, despite if we agree or not on a specific error, that transaction will revert consuming at least 40000 gas. It is better to allow a marketplace to check that before calling it. If not, we don’t really solve the issue.

Author: toledoroy
Date: 2023-01-22T16:25:25.336Z

From my experience if transactions are going to fail metamask will tell you beforehand. You can also simulate the transaction before sending it to the chain to check if it might fail and that’s free.
Maybe you can expand more about the specific situation where a revert incur a cost?

Author: sullof
Date: 2023-01-22T20:27:01.712Z

toledoroy:

You can also simulate the transaction before sending it to the chain to check if it might fail and that’s free.


I think that that is exactly what MetaMask does. But it is a work around in absence of a clear solution that is valid for everyone and everyone can verify. Anyway, I got your point and it is valid.

Author: toledoroy
Date: 2023-01-22T23:59:53.259Z

Gotcha. Thanks, and good luck!

Author: sullof
Date: 2023-02-12T01:33:58.899Z

I am working on a new project where, for security reasons, the tokens are transferable only by the owner and cannot be approved without the owner make the contract approvable.
So, I am thinking that it could make sense to have a just-a-bit-less-minimalistic interface like
interface IERCxxx {
  function isTransferable(uint tokenId) external view returns (bool);
  function isApprovable(uint tokenId) external view returns (bool);
}

Of course, we may suggest a new interface only for the approvability, but any token who would implement both, would spend more gas because it has to check the two interfaceId.
What do you think?

Author: stoicdev0
Date: 2023-02-15T13:28:34.671Z

I’m strongly against it, it defeats the purpose of minimalism. I have created 4-5 use cases using the minimal version and in non of them there was approvable in any way.
I would do a different EIP for that. It’s true implementers of both will spend a bit more gas on the check (unless you use a mapping, in which case the extra gas is only paid once, and by the issuer). The alternative is to force all implementers of only soulbound to have an extra unused method, it’s a clear choice IMO.

Author: sullof
Date: 2023-02-18T01:11:54.402Z

I’m feeling conflicted. While I agree with the principle, I’m currently working on several projects related to escrows and secure vaults, and in none of them can I use the IERC6454 function. By default, the tokens in these projects are non-tradeable for security reasons, which means they can’t be approved. The scenario where you only need to check the transferability of a token and don’t have to worry about its approvability seems rare.
A token that is non-transferable can either be approved for future transferability or cannot be approved at all. As an exchange, it’s challenging to decide whether to try to approve the spending or not. The easy solution is to have a second interface that checks only for approvability, but I don’t find it exciting.
I believe that whoever wrote the ERC721 proposal overlooked these two views. They should have been there from the beginning.

Author: sullof
Date: 2023-02-18T01:52:07.886Z

Anyway, I have implemented an IERC721Approvable interface inside the protector-protected-protocol. I wonder if it would make sense to make a standard proposal of it.
I think I will start a new discussion about it.

Author: 5cent-AI
Date: 2023-02-18T06:27:49.334Z

In eip6147, by querying the guard information, it is already possible to know whether the token can be transferred.

Author: sullof
Date: 2023-02-18T22:37:44.060Z

Nice proposal. But it would make no sense for someone to implement it without implementing the entire protocol. Here (as in ERC6454) we are talking about the minimal effort necessary to let an exchange know it the token is transferable or not.

Author: stoicdev0
Date: 2023-02-20T17:52:16.228Z

I guess you’re biased here by your projects. Let me give you few simple cases where this makes sense on it’s own:

Reputational NFTs: They’re bound to an address, no need for approval. We have a version with Social Recovery where holders can vote to help someone recover their reputation NFT after lost keys. We could force approvals there, but it would be really forced as the approval is not by one party but by the community together.
Nested Non-transferable NFTs: We introduced Nestable tokens in ERC-6059. We have quite a few collections where once you mint and NFT into another, it becomes attached to it for good. No approval needed at all. This has many use cases in gaming.
Non-transferable depending on context: I have a project where you can transfer an NFT out depending on the balance of an specific ERC20 token. An approve scheme is not practical there, it depends on the balance at the time of execution.

This can of course be any other condition, like number of certain emojis on token (introduced in ERC-6381), non-transferable after certain number of blocks or after certain number of transfers, and I’m sure many others we’re not seeing just yet. So I insist we should not limit them.

Author: sullof
Date: 2023-02-20T19:23:36.681Z

I agree with you on the points you list. What I realized is that we are restricting the field of application, while, ideally, we want to have an interface that is applicable in most cases, i.e., that covers what is missed in the original ERC721 proposal.
Thus said, I will stick with the minimalistic approach, because as you say makes sense in many scenarios, while I will propose an interface for the approvability. Whoever needs both, can implement both.

Author: SamWilsn
Date: 2023-03-21T15:50:02.066Z

This is rather late to the game, but negated booleans are really annoying for programmers. I’d recommend using isTransferable instead of isNonTransferable.

Author: ThunderDeliverer
Date: 2023-04-04T19:16:35.845Z

Thank you for the note @SamWilsn! While we agree that negated booleans are annoying, we feel that, in this case, you are actually checking if the token is non-transferrable (as this is the focus of the proposal), so we feel that this better represents the state of the token. Much like we explained in the rationale of the proposal:

ERC-721 tokens are usually transferable, but this interface focuses on the use case where NFTs may not be transferable. The method name was chosen to reflect this.

Author: SamWilsn
Date: 2023-04-04T19:32:12.573Z

I’m just thinking usability here, and trying to avoid philosophic discussion. For example, I find this much more non-understandable:
if (!someContract.isNonTransferable()) {
    // <code that renders the transfer UI elements>
}

Author: sullof
Date: 2023-04-04T19:56:02.620Z

Personally, I agree that this would be better.

Author: ThunderDeliverer
Date: 2023-04-04T20:13:13.576Z

From what we’ve experienced, the most common use case is to check if the token is non-transferrable and revert if it is, so something like:
if (someContract.isNonTransferrable(tokenId)) {
    revert TokenIsNonTransferrable();
}

Author: SamWilsn
Date: 2023-04-04T20:30:26.063Z

Won’t every wallet ever have to check if the token is transferable to know whether or not to display the UI for doing so?

Author: ThunderDeliverer
Date: 2023-04-04T21:07:39.770Z

Such check could be done like this (in pseudo code):
if(supportsIERC6454 && isNonTransferrable(tokenId) {
    disable transfer and add 🔒 symbol
}

Author: stoicdev0
Date: 2023-04-05T12:55:41.470Z

This and the previous example show perfectly our reasoning. You are not expected to check if a token is transferable, that’s the norm.
Forbidding/reverting the transfer is done only if isNonTransferable, showing some special icon or giving special treatment again is done only if isNonTransferable.
You’re very unlikely to get a double negation, on the contrary if we went with isTransferable you will probably be adding the negation always.

Author: sullof
Date: 2023-04-05T16:15:50.638Z

That is one way to implement it.
You chose to do:
if isNotTransferable it is locked, then it cannot be transferred
But others can prefer to do:
if !isNotTransferable transfer it
because it consumes less gas.
This to say that the problem addressed by @SamWilsn is real.

Author: stoicdev0
Date: 2023-04-05T16:20:26.327Z

sullof:

if !isNotTransferable transfer it


I’m trying to think, in what case would you do that?  And why would it consume less gas?
I really have no strong opinion for either version, but everywhere we’ve used it we just check for non transferability.

Author: sullof
Date: 2023-04-05T17:16:17.523Z

It depends of what you are building. If I build a marketplace, I will most likely follow your suggested approach and I will show a token as not tradeable. But if I build a fully on-chain pool, to avoid adding computation I can have a command like
function deposit(…) {
if (!isNotTransferable) {
do something
} // else do nothing
}
In this case, the alternative would be
function deposit() {
if (isNotTransferable) revert NotTransferrable();
…
}
which would cause a revert, consume more gas, etc.

Author: sullof
Date: 2023-04-05T17:18:08.225Z

Anyway, to me it looks almost the same. In general, I prefer the simplest shortest solution. That is what make me liking transferable over isTransferable and isNotTransferable

Author: ThunderDeliverer
Date: 2023-04-06T23:38:10.819Z

As today is the Last call deadline of the proposal, I suggest we vote on whether to keep the current implementation or to change it. This way we don’t block the progression of the proposal to the Final stage.
@stoicdev0 @SamWilsn @sullof and anyone that has an opinion, please cast your votes.




Keep isNonTransferrable
Change to isTransferrable




0
voters

Author: sullof
Date: 2023-04-07T17:29:36.890Z

Can you add also just transferable which is more in line with functions like exists ownerOf balanceOf?

Author: ThunderDeliverer
Date: 2023-04-07T20:07:35.989Z

Unfortunately I can’t but I can add another poll just for this and count it as a part of the original poll… 




Change to transferable




0
voters

Author: wwhchung
Date: 2023-04-08T01:09:16.976Z

Not sure I see the value in this or other EIP’s on the soulbound front. There’s no need for an explicit function (at least as written) because:

they would fail simulation anyways when doing a transferFrom/safeTransferFrom
no marketplaces would implement a pre-transfer check anyways as it would waste gas and would be unnecessary.

The thing that ‘could’ be useful is an event that marketplaces/display layers could observe to re-simulate, but additional function to do the check seems unnecessary.

Author: sullof
Date: 2023-04-08T20:16:55.597Z

I wrote a proposal for that



ERC-6982: Default Lockable Proposal Tokens


    Many proposals for lockable ERC721 contracts exist in different phases of development: 






and many others. 
Unfortunately, any of them misses something or is too complicated and add extra functions that do not need to be part of a standard. 
I tried to influence ERC-5192 making many comments and a PR that was closed by @Pandapip1 who suggested I make a new proposal. So, here we are. 
The updated Interface (based on comment and discussions): 
pragma solidity ^0.8.9;

// ERC165 interfaceId 0x6…
  

The rationale here is that there are projects where the transferability is affected by dynamic reasons and emitting events does not make sense. This proposal tries to address that case.
I agree that Marketplaces use events, and without events won’t probably see anything and will just try to execute it anyway and fail.

Author: wwhchung
Date: 2023-04-08T20:53:20.342Z

Not following here. Why wouldn’t these projects and games be able to use events and simulate as is to determine transferability?
We’ve built a few products that use the existing spec and haven’t run into any issues. The only thing we observed was that marketplaces don’t indicate state appropriately, which, if that was the thing to be solved, event emission would make more sense.

Author: sullof
Date: 2023-04-13T18:59:13.137Z

I agree that marketplaces won’t look at it, because they looks only to events. But Ethereum smart contracts are composable, and other contracts may want to interact. Also, marketplaces evolve. Now they look only at indexing the events, later they will query a smart contract before spending gas uselessly to approve a token or trying to transfer it. This is an evolving world and we think to the next step.

Author: wwhchung
Date: 2023-04-13T20:36:05.247Z

I don’t follow that statement “spending gas uselessly”.  The simulation would fail, the frontend would identify it.  The transaction would never initiate.
Even IF you were going to do a pre-check because, for some reason, the transferability can change dynamically from one block to the next, it would be cheaper to attempt and fail it in the general case, as a failure would be detected in the simulation and prevent the transaction from even being attempted.
Can you highlight a case where this check would be more gas efficient on chain?

Author: sullof
Date: 2023-04-13T23:34:56.681Z

If tokens’ transferability changes continuously, minute by minute, I don’t think that there is any other solution than attempting the transaction and if it fails, it fails. But, in games, a token may be temporarily locked because

I own a conflicting asset
I do not own a needed asset
my balance is either too low or too high
etc.

None of this is changing continuously. If you query the smart contract to check the transferability (which is a view and is free), if the token is transferrable, it is unlikely that the transaction will fail.

Author: wwhchung
Date: 2023-04-13T23:49:09.116Z

(Side note: Had a mobile gaming company so pretty familiar with game mechanics).
Even the case you’re describing (balance too low, conflicting asset, etc.) are things you would want to enforce on chain, and you would want to modify the safeTransferFunction for to reject those transfers anyways.  The same ‘check’ you’re doing with the new method would presumably apply the same logic as the transfer function, so I don’t understand how this helps at all from a contract perspective.
Say, your game has a mechanic where an item cannot transfer if your balance is too low.
Your transferFrom function AND this function would use the same internal balance check, and they would both fail.  The new method doesn’t really add anything as the simulation would fail in the same manner.
Can you walk me through the exact case and perhaps an implementation example of where this would actually prevent an issue that a safeTransferFrom (or transferFrom) simulation wouldn’t fail on?

Author: wwhchung
Date: 2023-04-14T00:02:09.663Z

example:
You would probably code the 721 smart contract as such (if you’re overriding the OZ implementation or similar)
contract GameAsset is ERC721 {

    function _safeTransfer(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) internal virtual override {
        require(_transferLockCheck(from, to, tokenId), "TransferLocked");
        super._transfer(from, to, tokenId);
    }

   function _transferLockCheck(address from, address to, uint256 tokenId) private view returns(bool) {
      // DO YOUR GAME CHECKS HERE
   }
}

If you were going to implement the function isTransferable(uint tokenId) function, it would point to the same private _transferLockCheck (because, why wouldn’t you)?
And if so, then the token transfer simulation would do exactly the same thing as the transferability check.

Author: sullof
Date: 2023-04-16T01:36:00.906Z

I got your point. Your example makes sense and I would probably apply it that way.

Author: ThunderDeliverer
Date: 2023-04-18T20:15:54.138Z

wwhchung:


no marketplaces would implement a pre-transfer check anyways as it would waste gas and would be unnecessary.



We actually have a marketplace that implements a pre-transfer check, so it makes sense to have an explicit function enabling it.



 wwhchung:

I don’t follow that statement “spending gas uselessly”. The simulation would fail, the frontend would identify it. The transaction would never initiate.


I agree that the transaction would fail on the pre-check and that it would effectively assert that the token is non-transferable, but this is terrible UX. No one wants to initiate the transaction, and expect it to be valid, only to realize it is invalid when the pre-check is run.
By this logic, we could say that ERC-165 makes no sense as we could just rely on transactions failing if the target smart contract doesn’t implement the expected interface, and I feel we can all agree that it is beneficial.



 wwhchung:

Can you walk me through the exact case and perhaps an implementation example of where this would actually prevent an issue that a safeTransferFrom (or transferFrom) simulation wouldn’t fail o


Well, tokens that implement ERC-6059 work really nicely with this spec when nested. The non-transferability can be conditional or final and having the method to actively check it, allow for smooth operation of nesting.

Author: wwhchung
Date: 2023-04-18T20:37:07.433Z

We actually have a marketplace that implements a pre-transfer check, so it makes sense to have an explicit function enabling it.
Who?  Would like to see the code, as a pre-check at the contract level is completely unecessary and is a waste of gas, and the view function itself isn’t even needed as you can do the pre-check in real time, as described in my next point.  You can accomplish everything that this read function does via javascript with existing smart contract functionality:
pseudo-code:

function isTransferrable(contractAddress: string, tokenId: string) {
  const contract = new ethers.Contract(contractAddress, ERC721ABI, provider)
  const owner = await contract.ownerOf(tokenId)
  let transferrable = true;
  try {
    await contract.estimateGas.transferFrom(owner, owner, tokenId, {from:owner})
  } except {
    transferrable = false
  }
  return transferrable;
}

You can get a transferrable indicator without any new interface.

I agree that the transaction would fail on the pre-check and that it would effectively assert that the token is non-transferable, but this is terrible UX
That’s not what I’m saying at all. A javascript library could call estimateGas on transferFrom to any address (or even yourself) and determine this to have a proactive UX indicator.

On 6059, interesting spec but quickly runs into cycle issues and introducing another non-standards paradigm.  ERC-6551: Non-fungible Token Bound Accounts is more interesting to me wrt NFT’s owning NFT’s.

Author: ThunderDeliverer
Date: 2023-04-18T21:13:23.113Z

wwhchung:

Who? Would like to see the code, as a pre-check at the contract level is completely unecessary and is a waste of gas, and the view function itself isn’t even needed as you can do the pre-check in real time, as described in my next point.


I’m not sure that we are talking about the same thing… Why would you waste gas on a view function being called directly? View functions are free… 



 wwhchung:

On 6059, interesting spec but quickly runs into cycle issues and introducing another non-standards paradigm. ERC-6551: Non-fungible Token Bound Accounts  is more interesting to me wrt NFT’s owning NFT’s.


Thank you, but the discussion here is focused on 6454 and 6059 is finalized anyway. The point is, that 6059 is a great example of this implementation being useful.

Author: wwhchung
Date: 2023-04-18T21:20:36.070Z

I’m not sure that we are talking about the same thing… Why would you waste gas on a view function being called directly? View functions are free… 

If they’re doing the check on contract, then it wastes gas.  View functions used within a contract write are not free.
If they’re only using it for the frontend, it’s not necessary at all because you can do the same without that method.  And in fact, you’d probably just end up using the function I pseudo-coded above for maximum compatability.

Author: ThunderDeliverer
Date: 2023-04-18T21:35:29.081Z

wwhchung:

If they’re doing the check on contract, then it wastes gas. View functions used within a contract write are not free.


True, but it is used for both, and if you have conditional mechanics attached to it, you need to be able to call this within a contract.
Let’s say you have a game token that gets a buff if they have a non-transferable token equipped into it (like ERC-6220 allows). You have to be able to validate non-transferability from within the smart contract.
You might get special conditions when selling a token that has a non-transferable token nested. This also requires the function to validate non-transferability when calculating a discount or special fee…

Author: wwhchung
Date: 2023-04-18T21:57:26.697Z

As I was explaining, you don’t.  You are simply wasting gas by attempting to do on contract validation, as simply doing the transfer would fail, and your frontend should be simulating the tx before even submitting.
I’ve provided a few code examples above which would be functionally equivalent on both the UX and smart contract side without doing the additional check for the case you’re describing.  In fact, that’s how most smart contracts try to save on gas efficiency, by removing unnecessary checks that would cause reversions anyways.  This would fall under this category.
Do you have a sample piece of smart contract code that illustrates where the standard transferFrom revert wouldn’t work and this new view method is absolutely needed?
example of exactly what I mean:
smart contract pseudocode
function buyNFT(address owner, address contractAddress, uint256 tokenId) {
  require(!ERC6454(contractAddress).isNonTransferable(tokenId), "Cannot be transferred");
  ERC721(contractAddress).transferFrom(owner, msg.sender, tokenId);
}

vs:
function buyNFT(address owner, address contractAddress, uint256 tokenId) {
  ERC721(contractAddress).transferFrom(owner, msg.sender, tokenId);
}

Would both cause a revert if it’s not transferrable, except the second one uses less gas.  There’s no reason to call the view method beforehand, because, even if you implemented it, you would use the same underlying check within transferFrom (see:



Minimalistic transferable interface Tokens


    example: 
You would probably code the 721 smart contract as such (if you’re overriding the OZ implementation or similar) 
contract GameAsset is ERC721 {

    function _safeTransfer(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) internal virtual override {
        require(_transferLockCheck(from, to, tokenId), "TransferLocked");
        super._transfer(from, to, tokenId);
    }

   function _transferLockCheck(address from, address to, uint256 t…
  


)
I have yet to see an example where ERC6454 helps.  This example shows why it’s unecessary at the smart contract level (both cases will revert anyways), and the post right before shows how you can accomplish the same check already, without ERC6454, on a frontend UX.

Author: wwhchung
Date: 2023-04-18T22:09:21.442Z

Btw, if we’re talking about ‘conditional’ checking to change behavior, I get that case.  But I would also say that, in that case, you want a general ‘attribute’ EIP for NFT’s, not something specific to ‘soulbound’, since you’re going to end up with a billion EIP’s for all these features and properties.

Author: ThunderDeliverer
Date: 2023-04-18T22:12:44.415Z

I think you missed the point of the last reply. You do. Because tokens being non-transferrable could influence a game or a fee.
Some pseudo-code to illustrate it:
function calculateGemDamageBuff(address gemCollectionSmartContract, uint256 gemTokenId) returns (uint256) {
    uint256 baseDamage =  gemCollectionSmartContract.baseDamage(gemTokenId);
    uint256 damageMultiplier;
    gemCollectionSmartContract.isNonTransferable(gemTokenId) ? damageMultiplier = 2 : damageMultiplier = 1;

    return baseDamage * damageMultiplier;
}

If you want to run a game on-chain, you need ifNonTransferable if you don’t you can ignore the proposal and implement the game mechanic the web2 way.
There is more benefit to be gained by this proposal than just simple blocking of transfers (which is an integral part of it, just to be clear).

Author: ThunderDeliverer
Date: 2023-04-18T22:14:55.340Z

Just saw this reply after sending the previous one…
You could, but this is minimal in order to reliably serve as many use cases as it can.
Soulbound or non-transferable is quite universal and useful for many use cases. It is quite beneficial to have it standardized.
Not everyone has to agree with an EIP, if it is useful for enough use cases, it benefits from becoming a standard. You are not forced to use it.
Also, these same arguments can be applied to disqualify the usefulness of ERC165, which is obviously useful at this point.

Author: wwhchung
Date: 2023-04-18T22:16:31.240Z

Ya, just responded to this as well.  There’s many on-chain attribute props that can trigger different behavior/computation.
I would say that an EIP that allows for storage of a prop is more effective than one specifically around transferability.  Where do you draw the line between what is/isn’t generalizable enough to be a completely separate method and EIP?
This doesn’t seem like one of them.

Author: wwhchung
Date: 2023-04-18T22:18:05.572Z

Btw, I can see this use case, but the ones from before around it being useful for marketplaces/etc. aren’t really for the reasons I mentioned.
The use case that this EIP specifically enables is dynamic non-revert behavior changes based on transferability.  Just want to be clear as to what this actually enables.

Author: YuriNondual
Date: 2023-04-18T23:24:58.254Z

Looking at alternative solution you proposed.
The front end part: Yes sure the pre-check would work, but it is important to take FE developer’s UX in mind, and not just end-user’s UX. As a client-side developer, I would rather have a ready made method on the contract that tells me if token is transferable with a simple function call that is descriptive and clear, and I am fine that this comes at a slightly increased gas cost for the sake of clearer integration (both for me and other people who come across this ERC) VS doing this check with gasEstimate, which is very un-intuitive.
From the contract-to-contract checks:
Just the revert is not enough for so many possible use cases, I agree that the immediate and most obvious benefits here VS just expecting a revert, is relevant on custom implementations and combining this EIP with other EIPs and gaming/trading or marketplace contracts

Marketplace contract skipping royatlies distribution for nested (ERC-6059) non transferable children when parent is sold.
Applying discounts and buffs if the token or it’s children (ERC-6059) are non transferable.
etc

Using on-chain attributes as you suggested can solve some cases where a new behaviour/computation should be triggered. But we can’t deny that there are plethora of such ideas and use-cases out there specifically that require non-transferability and conditional logic based on transferability, so having people implement their own solutions for both instead of providing them with a clear method to do it is exactly why the ERC is a good idea. At the end of the day if this was just blocking transferability then this would be pointless, this EIP is specifically for those who want to prevent transferability and react to it from other contracts (games, reputation systems, achievement registries etc) while having a clear way to check transferability status from a UI.
Right now the most easy to imagine use cases are very well paired with Nestable tokens (ERC-6059) and other custom contracts (gaming, reputation), but the bonus that the same standard works with just simple ERC-721 and thus will automatically just work on existing dapps, and these more advanced ERCs and custom implementation, outweighs the minimal gas saving of just having everyone to modify safeTransfer in their own way and apply custom solutions to react to it from other contracts

Author: wwhchung
Date: 2023-04-18T23:56:25.485Z

A few points here:


This EIP does not do anything to prevent actual token transferability or provide additional UX support that can’t already be done.  (your FE UX statement isn’t sufficient for me, as you can make it ‘easy’ by wrapping it in a lib.  And tbh, all marketplaces should be checking transferability anyways (as ppl already have a variety of mechanics to block transfers → see Operator Filters, that are not token specific, but context specific).  So, even if someone was trying to adjust their UX for general tokens to handle non-transferability, this EIP isn’t sufficient.


On the examples provided point, those cases all still fall under the “non-revert behavior changes based on transferability” cases I mentioned.  You could call it “mechanicModifier” if you wanted to, and for any game system that actually wants to do complex on chain dynamic behavior similar to what you describe, it’s likely that this one attribute isn’t sufficient.  So I still see this as fairly limited/not generalizable enough for the purposes of those examples.


In general, this EIP, in my opinion, is far too narrow.  It seems to be designed for ERC-6059 (which I have critiques of as well), but if the reason is around dynamic on-chain behavior, it’s still severely limiting as it doesn’t take into account other transaction context.  Example, the whole Opensea Operator Filter (which I don’t agree with but is a good example of what I mean) does not work here.
Anyways, I can see what you’re trying to put forth, but I feel that there are fundamental flaws with this approach.  There’s not much else I can add, so I’ll leave it at that.
Last note:
If the proposal was for a view function equivalent of transferFrom (like checkTransferFrom/checkSafeTransferFrom) with the exact same signature as their counterparts, I could buy that, as it would address the context issues I mentioned.

Author: sullof
Date: 2023-04-19T00:28:53.440Z

Great discussion above. I started this thread with a specific use case in mind and did not expect a long conversation on it.
Thus said, I actually like the checkTransferFrom/checkSafeTransferFrom using the same signature, and I suggested something similar above in the discussion because it covers most scenarios — since (in our specific use case) the transferability does not depend on the tokenId but on who you are trying to transfer it to.

Author: tbergmueller
Date: 2023-04-19T09:55:20.619Z

I would make a point, that the interface function isNonTransferable(uint256 tokenId) like suggested (ERC-6454: Minimalistic Non-Transferable NFTs) is elegant due to its simplicity (hence flexibility).
IMO it should stay an interface only, NOT a guide on how it shall be implemented.
isNonTransferable(tokenId) is - besides NFT marketplaces - particularily interesting for DeFi, when an NFT shall be used as security e.g. for loans, lending etc. It can only serve as a security, as long as it cannot be transferred (unconditionally).
A side benefit is imho that the implementing contract becomes easy to read/review, as it is easy to grasp the contracts behavior, when isNonTransferable(tokenId) can  trivially be used in ERC721’s _beforeTokenTransfer(tokenId), enforcing compliance with the “external” view on the transferability.
For disclosure, we drafted an EIP, where we need a similar mechanism and would rather prefer referencing an already existing standard interface rather than proposing another competitor method. In our case for example the required events of EIP-5192

Author: ThunderDeliverer
Date: 2023-04-19T10:23:07.875Z

Well said!
We are also in contact with several projects that have this as the only blocker before launch. They want to have ERC-6454 standardized before launching, so they don’t run into any incompatibility issues if the interface changes.
I suggest we proceed with the EIP process so as to not block the interested parties and finalize the proposal.



 sullof:

Thus said, I actually like the checkTransferFrom/checkSafeTransferFrom using the same signature, and I suggested something similar above in the discussion because it covers most scenarios — since (in our specific use case) the transferability does not depend on the tokenId but on who you are trying to transfer it to.


We see the current implementation as universal and as minimal as it gets. This seems beneficial as it can be the core building block of many solutions and implementations.
I think that if you have a collection-wide use case based on the receiver, you can add custom logic on top of this to extend the interface specified in the proposal

Author: SamWilsn
Date: 2023-04-21T15:36:58.205Z

As I mentioned on the Final status change pull request, I think there are a couple active questions being discussed here, so I am hesitant to move this proposal to final.
My understanding of the open questions:





 wwhchung:

This EIP does not do anything to prevent actual token transferability


I think this deserves an explicit mention in the Security Considerations section. Are there any potential problems that could arise if isNonTransferable doesn’t accurately report? Returning false but preventing transfers just seems like a minor annoyance, but what about returning true and allowing transfers?




 wwhchung:

This EIP does not […] provide additional UX support that can’t already be done.


I disagree on this point. The existing API can only tell you if a particular transfer (with an exact source and destination) will fail, not if all transfers will fail. That makes a significant difference in UI design.




 wwhchung:

a view function equivalent of transferFrom (like checkTransferFrom/checkSafeTransferFrom) with the exact same signature


This is an interesting approach, and I think it merits exploration. With this interface, is it possible to also check more general questions like: is token 0x3 transferable; can address 0x1234...5555 transfer any tokens; can address 0x5555...1111 receive token 0x5 regardless of who would’ve sent it?

Author: SamWilsn
Date: 2023-04-21T15:40:47.007Z

ThunderDeliverer:

We are also in contact with several projects that have this as the only blocker before launch. They want to have ERC-6454 standardized before launching, so they don’t run into any incompatibility issues if the interface changes.


Do you also have contact with projects on the flip-side? For example wallets, or NFT Marketplaces?
I ask because the EIP process doesn’t enforce any exclusivity. Even if we finalize ERC-6454 as it stands today, if wallets/NFT marketplaces implement a different standard, your contracts will still be incompatible.

Author: wwhchung
Date: 2023-04-21T16:14:55.745Z

SamWilsn:

I disagree on this point. The existing API can only tell you if a particular transfer (with an exact source and destination) will fail, not if all transfers will fail. That makes a significant difference in UI design.


I think that’s the flaw with this spec: assuming transferability is solely bound to the token level, which is really limiting.
example: incorrectly determines transferability capability for tokens using marketplace blockers.  The proposed spec should take into account receiver/sender addresses.  Token level is insufficient in a general sense.  Note that transferability constraints outside of tokenId only are extremely desirable (e.g. restricting token transfers outside a set of specific authorized engagers for those trying to create a more controlled token transfer ecosystem).
Tokens that would fail for ALL would fail for a dummy recipient address anyways, so you could pass in a dummy 0xdead address as the ‘fail for all’ check (or define some standard checker address).
IMO, the spec would be ok if:

It specified an event based system (rather than the current view method)
It specified a specific ‘to’ address as a way to check if a transfer can occur at all at a token level
‘ok’ if we create view methods which mirror the transfer methods.

Author: SamWilsn
Date: 2023-04-21T16:30:32.717Z

wwhchung:

Tokens that would fail for ALL would fail for a dummy recipient address anyways, so you could pass in a dummy 0xdead address as the ‘fail for all’ check (or define some standard checker address).


A dummy address wouldn’t work for tokens limited to a certain owner set. Imagine a token which can only be owned by one of seven addresses, but can be transferred freely between those seven. isNonTransferable would correctly return false, but testing random addresses with transfer would nearly always claim the token is not transferable.

Author: stoicdev0
Date: 2023-04-21T16:44:52.839Z

Thanks for your time @SamWilsn!

I think this deserves an explicit mention in the Security Considerations section. Are there any potential problems that could arise if isNonTransferable doesn’t accurately report? Returning false but preventing transfers just seems like a minor annoyance, but what about returning true and allowing transfers?

I feel you’re taking maybe about malicious implementations. If that’s the case, it is as much of a problem here as it is on any ERC. I could easily produce and ERC721 compliant collection with a behind door that allows me to transfer NFTs to my self. If you’re using this for instance in Defi to secure an NFT as collateral, you must trust the implementation of the ERC6454 on that specific contract as much as you must trust its implementation of ERC721. We could add a note on that, but to me it feels obvious, unless I missed your point.

The existing API can only tell you if a particular transfer (with an exact source and destination) will fail, not if all transfers will fail. That makes a significant difference in UI design.

I think this has been his stronger argument, but we are on your same position as you,  we see clear value to this EIP from UI/UX point of view.

This is an interesting approach, and I think it merits exploration. With this interface, is it possible to also check more general questions like: is token 0x3 transferable; can address 0x1234...5555 transfer any tokens; can address 0x5555...1111 receive token 0x5 regardless of who would’ve sent it?

I agree it would be an interesting approach. It would require at least the from and to arguments on the view, but this only applies for a specific use case and it would make this proposal no longer minimal. It could be work for a separate ERC.

Do you also have contact with projects on the flip-side? For example wallets, or NFT Marketplaces?
I ask because the EIP process doesn’t enforce any exclusivity. Even if we finalize ERC-6454 as it stands today, if wallets/NFT marketplaces implement a different standard, your contracts will still be incompatible.

We have a marketplace which uses this interface to display non transferability, and few gaming projects working with it already. This implementation comes from a non EVM blockhain where this simple approach was widely used for over a year.  There are also a few projects using this ERC on EVM for reputational avatars, in some case together with ERC6059. @tbergmueller also mentioned they’re working on a use case on Defi. We’re also in talks with some wallet teams about supporting these and other ERCs we have proposed.
We’re aware that other alternatives may come in the future, there is even an ERC now which is similar but forces you to emit event on change of state, we already explained how this is limiting for many use cases. However we will be vigilant to point new comers to this ERC if it solves their use cases. Making an ERC go mainstream is no easy task but we think a minimal interface for this use case has the highest chances.

Author: wwhchung
Date: 2023-04-21T16:56:34.417Z

But, isNonTransferrable is incorrect here, since it would still return false which is technically incorrect since it’s not actually transferrable for anything outside those 7.  I’m not following how the proposal even addresses that case or what the desired UX behavior is.
In your specific example, the UX would show it’s transferrable, even though it’s not (say it’s a marketplace UX, it would think you could buy the token when in fact, you can’t).

Author: SamWilsn
Date: 2023-04-21T17:18:54.689Z

stoicdev0:

I feel you’re taking maybe about malicious implementations. If that’s the case, it is as much of a problem here as it is on any ERC. I could easily produce and ERC721 compliant collection with a behind door that allows me to transfer NFTs to my self. If you’re using this for instance in Defi to secure an NFT as collateral, you must trust the implementation of the ERC6454 on that specific contract as much as you must trust its implementation of ERC721. We could add a note on that, but to me it feels obvious, unless I missed your point.


Yep, I’m talking about malicious implementations. I haven’t thought much about it, so I’m a perfect canary for obviousness: I have no idea what would happen if an NFT marketplace displayed a token with a lying isNonTransferable implementation. Is there anything special that apps/wallets displaying these tokens need to consider?

Author: stoicdev0
Date: 2023-04-21T17:30:16.681Z

SamWilsn:

Yep, I’m talking about malicious implementations. I haven’t thought much about it, so I’m a perfect canary for obviousness: I have no idea what would happen if an NFT marketplace displayed a token with a lying isNonTransferable implementation. Is there anything special that apps/wallets displaying these tokens need to consider?


I don’t think it’s much different from a lying 20 or 721. You can’t enforce a honest implementation of those either, and if a backdoor is discovered they would quickly loose all value.
One thing to consider is that this value may need to be refreshed often, but it can vary a lot from case to case. So I’m thinking the most reasonable request to marketplaces/wallets is to have an option to refresh the value any time, seems worth adding this note.

Author: SamWilsn
Date: 2023-04-21T17:43:07.946Z

stoicdev0:

You can’t enforce a honest implementation of those either, and if a backdoor is discovered they would quickly loose all value.


Right. I’d just like to see a note in security considerations about two things: what might happen if isNonTransferable lies, and how apps can mitigate that risk.

Author: ThunderDeliverer
Date: 2023-04-21T22:34:06.950Z

Thank you for the suggestion; we updated the section accordingly.

Author: tbergmueller
Date: 2023-04-22T13:46:20.251Z

@tbergmueller also mentioned they’re working on a use case on Defi. We’re also in talks with some wallet teams about supporting these and other ERCs we have proposed.

To clarify, we do not intend to use the isNonTransferable() to implement our EIP. But in our protocol, when an NFT is e.g. collateral/security for a loan, transfers are typically blocked for a specific period of time or until some contractual obligations / conditions are met. We saw/see issues with tokens according to our protocol, when people try to sell on market places.
The following has been tried with ERC-5484 , which essentially always throws in _beforeTokenTransfer() on OpenSea and Rarible (testnet implementations);
If transfer of an ERC721 is blocked temporarily or permanently, the owner can still go ahead and list the token. Often, a owner of a token (which is currently non-transferable) would even pay gas to approve e.g. OpenSea, then list the token for selling. When a buyer comes along and completes signing the purchase, only then would OpenSea says “whoops, I cannot transfer for unknown reasons”.
This is not only annoying, but both, the seller (who pays gas) and often likely also the buyer (who may have sold some other assets to have liquidity (and paid gas+fees+ royalty, … )) spent money on a transaction, that cannot be carried out. Not through the failed transaction, but through other transactions working towards the failing transaction.
To avoid those situations, it would be great if we can propose a standard . Naturally, marketplaces would then need to implement that standard. This is in general likely, as Rarible evidentially already recognizes throwing behavior, as the error says “Soulbound tokens cannot be transfered”.
Towards the question, of how the interface should look like, I can also see a isNonTransferable(from, to, tokenId), similar to safeTransfer(from, to, tokenId). If tokens cannot be transferred in general, those implementations should simply ignore then the from and to and implement logic just on a tokenId-level, essentially achieving the same as isNonTransferable(tokenId)

Author: SamWilsn
Date: 2023-04-24T15:13:13.124Z

tbergmueller:

Towards the question, of how the interface should look like, I can also see a isNonTransferable(from, to, tokenId), similar to safeTransfer(from, to, tokenId). If tokens cannot be transferred in general, those implementations should simply ignore then the from and to and implement logic just on a tokenId-level, essentially achieving the same as isNonTransferable(tokenId)


That interface only answers the question: “can I make a specific transfer?”. ERC-6454’s interface answers a slightly different question: “can any tokens be transferred?”.
Ideally we’d want an interface that can say if you can transfer any combination of: fixed from / any from, fixed to / any to, fixed tokenId / any tokenId.

Author: stoicdev0
Date: 2023-04-24T15:36:26.897Z

Yep, it’s 2 different questions. We see the value of both and are actually thinking on creating a draft for the from/to version. This one is meant to be minimal.

Author: tbergmueller
Date: 2023-04-26T07:08:43.832Z

ERC-6454’s interface answers a slightly different question: “can any tokens be transferred?”

We may have different interpretations of the word “any”, but just to be clear, the current EIP-6454 suggests isNonTransferable(uint256 tokenId). So it is an interface to determine whether a specific token can be transferred, i.e. the same contract may have isNonTransferable(123) == true and isNonTransferable(456)==false.
~~
@stoicdev0 Instead of another draft doing a similar thing, it would be great if the community can agree on one interface? The more similar interfaces we have, the less likely it will become that any of those will get mass-adaption. So maybe @stoicdev0 and @sullof , you could find some common ground and e.g. adapt this EIP?

Author: YuriNondual
Date: 2023-04-26T11:44:22.379Z

The ability to pass from to to check if token is transferable is great for a particular use cases, but in the case of something like ERC-6059 where a token is nested in another token, it doesn’t always make sense. I want to be able to display token’s transferability/soulbound state regardless of what account owns it or who it is being transferred to. Let’s say there is an in-game marketplace which sells a “gauntlet” item NFT that has a cursed soulbound “gem” NFT nested into it, I want to be able to clearly display that it’s soulbound into the gauntlet so when the user browses the marketplace (even without a connected account), he will know that he won’t be able to get rid of the cursed gem. Passing from and to fields every time to check this is impractical, especially before we know the to address. So while there are use cases where from/to checks are useful, I would prefer it to be a separate method or a separate EIP

Author: stoicdev0
Date: 2023-04-26T11:53:50.982Z

Even without Nestability, I may hold reputational NFTs that I’d like to see either on the marketplace or some other dApp.  On a marketplace these shouldn’t even have the option to be sold as they will fail. All this is again independent from the from and to.

Author: stoicdev0
Date: 2023-04-26T12:27:18.928Z

@SamWilsn would it make sense to have both versions in the same EIP?
Something like ERC6454a for the minimal version with tokenId only, and ERC6454b for the version with from, to and tokenId?
If possible, what do the rest of you think about it?

Author: SamWilsn
Date: 2023-04-26T15:03:57.185Z

There isn’t a clear-cut answer unfortunately. I tend to recommend splitting into separate proposals if:

For independent interfaces/ideas: each idea can stand on its own; or
For dependant interfaces/ideas: it’s more likely that the dependant proposal won’t be implemented.

A good example of the latter point is the ERC721Metadata interface. It’s more likely that tokens will implement it than not implement it, so I think it was the correct choice to include it in ERC-721.
If ERC6454a and ERC6454b can be useful separately, then that’s a great indicator that they belong in their own proposal(s).
Here, however, it sounds like if you’re going to implement one interface, you’re likely going to implement both, so one proposal is probably good.

Author: ThunderDeliverer
Date: 2023-04-27T23:30:50.962Z

Based on the discussion, we are considering adding another method to our proposal. So the updated proposal would have a method that accepts tokenId and a method that accepts tokenId, from, and to parameters. With the addition of the new method, it also makes sense to rename both to isTransferable.
Our current suggestion is to use the following:
function isTransferable (uint256 tokenId) external view returns (bool)

function isTransferable (uint256 tokenId, address from, address to) external view returns (bool)

Author: tbergmueller
Date: 2023-04-29T13:26:37.611Z

@ThunderDeliverer given your two overloaded methods, would it make sense to either require or recommend the following implementation:
isTransferable (uint256 tokenId, address from, address to) { 
      return isTransferable(tokenId) && fromToBasedConditions;
} 

By the way, we just did the PR for the EIP I mentioned  earlier (discussion in Draft ERC xxxx: Asset-Bound Non-Fungible Tokens), I already referenced this ERC there and will adapt our draft accordingly, as soon as this is becomes a final EIP.
Of course, happy to receive (critical) comments on our proposal.

Author: ThunderDeliverer
Date: 2023-04-29T20:25:56.337Z

This recommendation makes sense, but I’m unsure whether we can specify it within the specification. This might be implementation instructions,  so I think it’s best to ask @SamWilsn if it’s allowed. 



 tbergmueller:

By the way, we just did the PR for the EIP I mentioned earlier (discussion in Draft ERC xxxx: Asset-Bound Non-Fungible Tokens), I already referenced this ERC there and will adapt our draft accordingly, as soon as this is becomes a final EIP.
Of course, happy to receive (critical) comments on our proposal.


Congratulations! I will make sure to check it out!

Author: ThunderDeliverer
Date: 2023-05-01T18:18:13.467Z

We have updated the specification of the proposal to include two methods now. Please review it and let us know how you feel about the revised proposal. 


Ethereum Improvement Proposals


ERC-6454: Minimal Transferable NFT detection interface
A minimal extension to identify the transferability of Non-Fungible Tokens.

Author: SamWilsn
Date: 2023-05-01T20:31:57.027Z

ThunderDeliverer:

I’m unsure whether we can specify it within the specification. This might be implementation instructions, so I think it’s best to ask @SamWilsn if it’s allowed. 


The “proper” way to encode that in the spec would be something like:

The three argument isTransferable MUST return false if the one argument isTransferable would return false for the same tokenId.

Author: sullof
Date: 2023-05-01T21:16:20.757Z

Your comment exposes a possible problem with the current specification with two functions.
In fact, if isTransferable(uint tokenId) has priority, why should we have the second function?
You would assume that the two are consistent, which may be impossible or would remove sense.
For example:
isTransferable(1) => false
isTransferable(from, to, 1) => false because the above returns false, right?
isTransferable(1) => true
isTransferable(from, to, 1) => true because the above returns true, right?
So, why should we have the isTransferable(from, to, 1) function? This seems to confirm the initial proposal and make isTransferable(from, to, 1) useless.
I see two reasonable cases:


There is only one Transferable() function and the second one is abolished.


There are two function, one returning a state, and the other a boolean, like


enum Transferable {
  YES,
  NO,
  MAYBE
}

function isTransferable(uint tokenId) external returns (Transferable);

function isTransferable(address from, address to, uint tokenId) external returns (bool);


In this case, if isTransferable(1) returns MAYBE it means that the result depend on from and/or to and the caller can make a second request.
What do you think?

Author: SamWilsn
Date: 2023-05-01T21:50:10.904Z

sullof:

true because the above returns true, right?


The way I worded it didn’t require true if true, just false if false. If isTransferable(tokenId) returns true then isTransferable(address from, address to, uint tokenId) can return either.

Author: sullof
Date: 2023-05-01T21:51:46.799Z

I got your point, but why someone who get true as first response should make the second call? In this case, it would make more sense to make directly the second call.

Author: SamWilsn
Date: 2023-05-02T14:11:31.885Z

Agreed! You’d call the one-argument overload when deciding if you should show the transfer UI at all, and the three-argument overload for a particular transfer. I guess.

Author: stoicdev0
Date: 2023-05-02T14:45:44.471Z

SamWilsn:

Agreed! You’d call the one-argument overload when deciding if you should show the transfer UI at all, and the three-argument overload for a particular transfer. I guess.


I think this explains it clearly. Some NTFs are not meant to be transferred at all, others might be only transferred with certain from-to combination. This is our attempt to address both cases since it will be easier for dApps to adopt a single EIP instead of 2.
We agree on this implication:
isTransferable(uint256):false -> isTransferable(address,address,uint256):false
But all these are incorrect.
isTransferable(uint256):true -> isTransferable(address,address,uint256):true
isTransferable(address,address,uint256):false -> isTransferable(uint256):false
isTransferable(address,address,uint256):true -> isTransferable(uint256):true
So it’s not really redundant.

Author: stoicdev0
Date: 2023-05-04T18:47:45.127Z

@wwhchung @SamWilsn @tbergmueller what do you think of the latest version? We think we addressed all concerns.

Author: wwhchung
Date: 2023-05-04T19:01:03.147Z

Not sure why you don’t just clone the onERC721Received interface then.
The updated proposal doesn’t seem to take into account the operator, which is also used in certain mechanics.
If you’re going with multiple signatures then I think the spec needs to specify what they actually mean.
Eg if you give it a token only, if true it means it’s transferable for at least one sender/receiver. Etc.
I don’t know if you need a yes/no/maybe. Maybe yes/no/conditional is better. And if that’s the convention adopted, conditional means you have to check the complete interface.

Author: stoicdev0
Date: 2023-05-04T19:38:18.691Z

wwhchung:

Eg if you give it a token only, if true it means it’s transferable for at least one sender/receiver. Etc.


That’s exactly the idea. We’ll make it clearer on the proposal.
If the method with only tokenId returns false, it means it cannot be transferred under any combination.
If it returns true, then you need to check for the from/to combination.



 wwhchung:

IMO, the spec would be ok if:
…
2. It specified a specific ‘to’ address as a way to check if a transfer can occur at all at a token level


We are actually doing one of the things you proposed.

Author: sullof
Date: 2023-05-04T22:12:40.653Z

In some old comments I suggested a 4 parameters function to cover all scenarios.
Sometimes the token is transferable by the owner but not by a marketplace, or, at the opposite, it is transferable by a lending pool but not by the owner.
I am for the single function approach, but since we are going with the two-functions approach as said above, I think that it would be better to have
isTransferable (
  uint tokenId
) external returns (bool);

isTransferable (
  uint256 tokenId, 
  address from, 
  address to, 
  address operator
) external returns (bool);

When there is no operator in the equation, we may pass address(0).
If we just had the second function, we may call it like
isTransferable(tokenId, address(0), address(0), address(0))

to obtain what the first function expects. The problem that I see is that the implementer must implement both functions, despite which one makes sense. Having a single one able to manage all the scenario seems more powerful to me, and potentially consumes less gas.
Similarly, if I want to check if a token is transferable to someone, despite the from, I could call it like
isTransferable(tokenId, address(0), recipient, address(0))

In other words, a similar function would be very versatile and efficient.

Author: tbergmueller
Date: 2023-05-05T08:21:45.159Z

sullof:

When there is no operator in the equation, we may pass address(0).


If we go with that, I think in the EIP it would need to say say “any parameter that should be ignored is should be address(0)”.
With this definition we run into severe problems, because burning and minting is a transfer to or from address(0).
So going by your suggestion isTransferable(tokenId, address(0), recipient, address(0)), this could mean that mints are on the one hand or that the from address is ignored on the other hand. Minting may not be our biggest issue though, you could add a “must throw when tokenId does not exist”.
Burning is a real issue. isTransferable(tokenId, sender, address(0), address(0)) can have multiple meanings

returns whether a sender can transfer tokenId to address(0) ==> can I burn?
returns whether a sender can transfer tokenId to ANY address ==> can sender transfer the token w/o restrictions?

So I would argue using address(0) for from and to is not a good idea, for the operator it may work (as long as nobody finds address(0) private key ;))

Author: sullof
Date: 2023-05-05T17:32:09.799Z

You are right.
Let me clarify that my goal here is to discuss any possible development so we end up with a strong, robust proposal that does not need to be upgraded two months later because some issue comes out.
That said, we may define a magic address to be used as a zero address. Something like
0xF0F0F0f0f0F0F0f0f0F0f0f0f0f0F0F0f0f0f0F0. But it would make it too complex and gas-consuming. So, forgot about it.
The big doubt is about keeping it minimalistic, with a single function with just the tokenId as a parameter, or to cover most scenarios. I liked the minimalistic version, but I see the need for a broader coverage.
I think that one way to go is to keep ERC6454 as simple as possible, with just isTransferable(tokenId) and add a second interface, that we may call ERC6454Extended that extends ERC6454 and adds
isTransferable(uint, address, address)
isTransferable(uint, address, address, address)

Whoever wants/needs to give more details can implement the extended version.

Author: stoicdev0
Date: 2023-05-08T20:04:36.941Z

After discussing with @ThunderDeliverer, @YuriNondual and @sullof I think we have a final version.
We will only have a single method, receiving tokenId, from and to. The behavior should be as follows:


from and to are zero → Is transferable at all? (This would replace the single argument version)
Only from is zero → Can mint to to? (Can be used for allow lists)
Only to is zero → Can from burn? (Up to the implementation to allow or not).
If the call with address 0 for both from and to is false, any other combination where from and to are not zero, MUST return false as well.

Optionally, implementers may add the single argument version which SHOULD call the 3 arguments one with both addresses as 0. This is similar to what is done in many 721 implementations where they include a transfer method using the message sender as the from.
We opted for not including the operator as it would make behavior more complex and no longer be minimalistic.
@tbergmueller I hope this approach is useful to you.

Author: stoicdev0
Date: 2023-05-09T23:55:44.561Z

The proposal has been updated: ERC-6454: Minimal Transferable NFT detection interface

Author: tbergmueller
Date: 2023-05-10T14:51:39.038Z

Hi,
appreciate the changes and wording in EIP is clearer than in @stoicdev0 last post; In your post above I see potential for misunderstanding

Only from is zero → Can mint to to? (Can be used for allow lists)

Not so sure for allow-lists; often you put the minting wallet on the allowlist, not the receiving wallet. For security reasons, people often like to have a minting wallet but then mint the tokens e.g. to their ledger.
If minting wallet == to, no issue of course.

Only to is zero → Can from burn? (Up to the implementation to allow or not).

I think here we have problematic edge cases. Assume token 3 is owned by 0xaaa. 0xaaa approved 0xbbb to burn. In that case; should isTransferable(3, 0xaaa, address(0)) naturally returns true, but must isTransferable(3, 0xbbb, address(0)) also return true? In this case, 0xbbb is approved and can actually burn, but on the other hand your goal was to reflect with the parameters the parameters of the ERC721.TransferFrom I believe?
If we want to be compliant ERC721 parameter, I would maybe mention this example in rationale and suggest that - if we want to know whether 0xbbb can burn a token owned by 0xaaa the correct check would be isTransferable(3, 0xaaa, address(0)) && isApproved(0xbbb, 3)
Looking for clarification here, thanks!
Minor comment on wording of the EIP

If the general transferability of token is false, any kind of transfer of the token, save minting and burning, MUST revert execution.

instead of save minting and burning I would use except minting and burning, otherwise it’s too close to safeMint function naming.
With a bit more of clarification / examples of those edge-cases, I’d recommend to move this forward to final  Would or will implement it in my contracts

Author: stoicdev0
Date: 2023-05-10T15:01:43.774Z

tbergmueller:

appreciate the changes and wording in EIP is clearer than in @stoicdev0 last post; In your post above I see potential for misunderstanding


@ThunderDeliverer is better with words 
Regarding the burn, it is a great point. I was thinking that the burn check should also check for permissions and return false if the from is not owner or approved. But I like your suggestion better: isTransferable(3, 0xaaa, address(0)) && isApproved(0xbbb, 3). We should add that to the EIP.



 tbergmueller:

instead of save minting and burning I would use except minting and burning, otherwise it’s too close to safeMint function naming.


Also agree with this suggestion. We will apply them ASAP and request move to final now that we all agree. Thanks for your suggestions!

Author: sullof
Date: 2023-05-10T23:36:22.806Z

tbergmueller:

If we want to be compliant ERC721 parameter, I would maybe mention this example in rationale and suggest that - if we want to know whether 0xbbb can burn a token owned by 0xaaa the correct check would be isTransferable(3, 0xaaa, address(0)) && isApproved(0xbbb, 3)


I think that it is out of the scope of this EIP to tell the dev how to manage that scenario.
Let’s say that 0xbbb has not been approved yet but it wants to know if it can transfer the token if it gets approved. That suggestion would return false all the time, but as soon as 0xbbb gets approved may turn true. As you can see it can become very complicated — which is why we talked of not including spenders in the proposal.

Author: tbergmueller
Date: 2023-05-11T13:06:15.302Z

Thank you for clarification - as I said, I completely agree with the wording of the EIP and I also see (now) that mentioning those special cases may be out of scope.
Sorry for being picky, but I think there may be more potential in the interface through very simple rewording and you cannot change once you move to final;
IRC6454 suggests the following:

@param from Address from which the token is being transferred

I would argue, that in this case you cannot pass an approved address, as this is strictly speaking not the address, the token is transferred from (it is always transferred from the owner address). I would suggest to consider rewriting this as

@param from Address from which the token transfer is being initiated

By specifiying the iniating address, this would allow a very simple implementation of
isTransferable(tokenId, from, to) {
  // May add a throw here when tokenId does not exist.
   if (from == address(0) && to == address(0))
         // Indicate the token can be transferred in general
        return true;

   return ERC721.isApprovedOrOwner(from);  
}

Also, I want to empathize that I think it’s brilliant that you do not specify when isTransferable()==false. This provides enough wiggle-room to implement also more exotic use cases, like we have for example a.t.m with our ERC-6959 (oracle-based transfers, where neither approved nor owner can transfer per default, yet there is an extension, where certain tokens that are marked floatable can also be transferred by owners or approved - easy right?).
Just as an example on how I would / will implement this for ERC6956 extending IERC6956, to demonstrate the flexibility your proposed  interface has imho:
isTransferable(tokenId, from, to) {
  // May add a throw here when tokenId does not exist.
   if (from == address(0) && to == address(0))
         // Indicate the token can be transferred in general, as tokens can be oracle-transferred anytime

        return true;

   return IERC6956.isFloating(anchorByToken(tokenId)) && ERC721.isApprovedOrOwner(from);  
}

Author: sullof
Date: 2023-05-11T21:52:06.204Z

Clever. i like it.
@stoicdev0 What do you think?

Author: ThunderDeliverer
Date: 2023-05-12T15:43:29.173Z

I took some time and talked to @stoicdev0 about this suggestion.
We see the value of it, but it might also limit the usability of the proposal.
If we were to modify it to enforce checking for approvals, this would force implementers to add additional code that would in turn require more gas when executed in the before-transfer hook.
We are trying to make this proposal as minimal and unopinionated as possible and this could be considered an opinionated implementation in the sense that it differs from the common transferable interfaces (that usually include the tokenId, from and to values).
Our suggestion would be to make this behavior optional. So not enforce it, but allow it. The smart contracts and UIs interacting with the smart contract should not expect it in this case, but custom UIs could make use of it.
An additional argument for this is that approvals are already standardized and can be combined with the check that this proposal brings to the table in some form of the isConditionallyTransferable custom method. 
Would this suffice for your implementation?

Author: tbergmueller
Date: 2023-05-12T20:26:52.629Z

ThunderDeliverer:

Would this suffice for your implementation?


Yes. And optional sounds like a good compromise!
Note I’m mainly sharing my opinions in the general case, not in particular tailored for my projects.
In most of my current projects the isTransferable() interface is of no central functional importance. I’m rather looking for the current or future mass-adapted way on how to tell an external application (mostly marketplaces) that it will not be able to transfer some tokens by “standard ERC-721 ways”.
Motivation for this is I just find it very frustrating for users (see earlier post of how market places react a.t.m.) if they think they can sell something and others that they can buy it and only after all decisions have been made, the transfer reverts. I want to spare our users from this frustration and spending unneccessary gas on approvals etc  If we’re very realistic, the typical NFT holder doesn’t know what a smart contract even looks like let alone how to read them. So we market-places will need provide better usability here in the long term, and I see that this EIP can help them to check whether a token can or shall be listed or not.

Author: ThunderDeliverer
Date: 2023-05-12T23:26:27.883Z

Thank you for the feedback and all of the constructive comments! I think the note about approvals we suggested might help with the transferability issue you described.
I’m really happy as I think this is the proposal’s last amendment!

Author: ThunderDeliverer
Date: 2023-05-13T21:27:36.045Z

We published the amended proposal with the optional check for approvals. Please take a look and let us know if you agree with finalizing the proposal @sullof @tbergmueller . We want to bring it up in the next EIP editors’ call.


Ethereum Improvement Proposals


ERC-6454: Minimal Transferable NFT detection interface
A minimal extension to identify the transferability of Non-Fungible Tokens.

Author: tbergmueller
Date: 2023-05-14T03:52:02.937Z

Looks very good to me - appreciate moving it to final!