Minimalistic transferable interface

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.

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… :confused:

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.

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… :confused:

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.

1 Like

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…

2 Likes

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:

)

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.

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.

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

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.

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.

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.

1 Like

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

2 Likes

A few points here:

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

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

2 Likes