Minimalistic transferable interface

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

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.

@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?

1 Like

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.

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)
2 Likes

@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.

2 Likes

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. :thinking:

Congratulations! I will make sure to check it out! :smile:

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. :smile:

1 Like

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.

1 Like

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:

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

  2. 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?

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.

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.

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.

1 Like

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.

1 Like

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

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.

1 Like

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.

We are actually doing one of the things you proposed.

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.

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

  1. returns whether a sender can transfer tokenId to address(0) ==> can I burn?
  2. 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 ;))

1 Like

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.