Minimalistic transferable interface

Such check could be done like this (in pseudo code):

if(supportsIERC6454 && isNonTransferrable(tokenId) {
    disable transfer and add 🔒 symbol
}
2 Likes

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.

2 Likes

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.

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.

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.

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

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

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

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

  • Change to transferable

0 voters

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:

  1. they would fail simulation anyways when doing a transferFrom/safeTransferFrom
  2. 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.

I wrote a proposal for that

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.

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.

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.

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?

1 Like

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.

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

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.

1 Like

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

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

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.

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.

1 Like

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.