FA2.1 / FA3 - It's time

Hello!

There’s not much I’d really want from an FA2.1/3. Here’s a short list:

  • allowances (for fungible multi-tokens)
  • relaxing the constraints on storage layouts (to allow for some more storage efficient token contracts)
  • clearly defined onchain views, ideally compatible with the FA2 offchain views so no one who just went that route gets retconned.
2 Likes

Unfortunately there is no standard way to separate nfts from fungible tokens right now when I make api calls to get all token. The only way to distinguish them is to use some heuristics, but they won’t be 100% accurate. I think something on token standard level to clearly separate this two would help many

1 Like

Another thing I’d love to see us do better than ethereum on FA3:

On chain royalty definition from the get-go in BPS. Required to define the entrypoint/storage in some way, with a standard value for “no royalties”. Have them respected by the transfer function when XTZ amount is non-zero in the tx.

Currently royalties are stored entirely off chain, forcing web3 front ends do most of the heavy lifting of fetching metadata files, parsing the royalty config, and then feeding it into their contracts. This puts way too much pressure on the web3 developer to calculate royalties and adjust their contract code to match. Every token should be queryable for it’s royalty configuration in BPS standard. Pass in token and return a list of records containing address and BPS value. Otherwise we are certainly going to see platforms pop up which completely circumnavigate around royalties like we do on Eth since it isn’t on chain nor apart of the transfer standard (https://sudoswap.xyz).

2 Likes

Just wanted to throw my two cents in here as someone who has now dropped two collectible PFP projects on Tezos as well as written an FA2 trading contract:

  • FA3 needs to extend the FA2 standard for backwards compatibility. We should only add and modify existing implementations, not change existing method stubs. Many of our “off chain” standards should and can move to first class.
  • Balance_of is very weirdly designed. You cannot get the return value in the same entrypoint it was called in, which kind of defeats the utility of it. You have to call it in a separate pre-tx and then batch together multiple of your own contract calls after that (now that storage is set). Is there no way to solve this? Can we not make this easier for devs?
  • The is a bug in the operators standard that will cause the same type of hacks we’ve seen plague OpenSea. When you transfer the token to someone, then you get the token back, your operators are still set to what they were when you first owned the token. This is unexpected and can allow old (compromised) contracts which were allowed safe to operate years ago to still have operation now when the user is returned the token. Operators should be zerod out upon transfer. We can learn from this mistake in the ERC721 standard on Eth.
  • It’s very odd to me that the update_operators function is first class but then the boolean is_operator is optional? There is no good way to check if something has approval rights to a token right now within one entrypoint. is_operator should be moved to first class required.
  • I’ve noticed that the only way to check if someone owns a token reliably is to for the user to transfer it to the contract, and then have the contract transfer it right back in the same entrypoint. Since this workaround already works but is gassy, we may as well standardize a less gassy way of getting the same information. Moving a few of these “off-chain” optional calls to first class required would solve this. At the very least we should make self-sending a feature, so we can halve the number of calls here and avoid the ledger updating on the FA2 contract for nothing.
4 Likes

Now that we have events in Tezos (blog post), I would suggest adding standardized events to FA2.1/3, similar to ERC-777 and ERC-721. I think such events would help off-chain apps such as indexers observe state changes of the token contracts reliably.

I imagine their types would look something like this in CameLIGO (the exact definitions can change)

type balance_update_event = { owner : address; token_id : nat; diff : int; }
type operator_update_event = { owner : address; operator : address; token_id : nat; is_operator : bool }

And will be emitted like this:

EMIT %tzipXX_balance_update [{ owner =...; token_id=...; diff=...}; ...]
EMIT %tzipXX_update_operator [{ owner =...; operator=...; token_id=...; is_operator=...}; ...]

Here are the Michelson types of the above events for reference:

(list %balance_update_events
  (pair
    (address %owner)
    (pair
      (nat %token_id)
      (int %diff)
    )
  )
)

(list %operator_update_events
  (pair
    (address %owner)
    (pair
      (address %operator)
      (nat %token_id)
      (bool %is_operator)
    )
  )
)

We can also add the updated balance in the balance_update_event like this:

(list %balance_update_events
  (pair
    (address %owner)
    (pair
      (nat %token_id)
      (int %diff)
      (nat %updated_balance)
    )
  )
)

Having balance in the event would enable Merkle proofs of the current balance, and I imagine it would be convenient for other usage too.

2 Likes

I suggested the %updated_balance and the Merkle proofs, but I am very unsure about it.

It seems generally convenient, maybe, but it increases the complexity. One example: what should a consumer believe if the %diff and %updated_balance are inconsistent? Another example: a tuple of three integer values (Pair 42 42 42) is at least three times more confusing than a tuple of two integer values.

The Merkle proofs with %updated_balance seem significantly less powerful than what we already (kind of) have without events …if you let yourself look at contract storage in the context.

A third alternative would be to have only the %updated_balance without the %diff. Which of these alternatives is best?

Interesting. What should happen for tokens with balance > 1? The operator state should be cleared when the balance reaches 0? Or upon any individual transfer?

It was designed with a callback because FA2 came before onchain events were developed.
It would be better to replace this endpoint by a view in future standard.

I saw some token contracts already using a view so Indexers may already manage both

We have worked on a first draft for the specification of the FA2 update: FA2.1 - HackMD

The goal of the FA2.1 extension is to include new protocol features while remaining fully compatible with current applications that rely on the FA2 standard.

Events are a means to solve discoverability problems encountered by indexers, i.e. to know when a token is transferred especially outside the specified transfer entrypoint (mint, burn, etc…).

On-chain views are a means for smart contracts to share information easily. Callback views are kept for compatibility with already deployed contracts.

Tickets are a way to materialize tokens outside of smart contracts. They can be transferred without calling the creator contract and affecting its status. Tezos rollups rely on tickets for representing Layer 1 assets on Layer 2.

Additionally, finite allowance, enabling a user to grant permission to spend a fixed amount of tokens, is introduced (based on the FA1.2 scheme).

4 Likes

I read the draft and parts of this thread.

Some thoughts as a DeFi developer:

  • I want a simple standard that uses events and on chain views
  • I don’t want to think about FA2 style callback views
  • approve is fine, operators are probably less fine, but mainly I just don’t want to deal with both at the same time
  • I think optional backwards compatibility is way less useful (therefore no harm in dropping it completely)
  • I don’t see why the ticket related code needs to be in the token contract
  • I think the modularity mentioned by @arrijabba is a good goal, but I don’t like using FA2 as the base
1 Like

The ticket holds the address of the contract that created it so it’s easy to check if the ticket is valid or not. An alternative would be to have a ticket wrapper that everyone agrees on

Yeah, I was thinking there would be a ticket wrapper deployed by the owner of the token contract. The main difference is that you could use the token without worrying about the tickets at all, and if there is a ticket related problem it (hopefully) only affects ticket users.

Please lets dont start messing around with a ticket wrapper, the proposed solution is perfectly fine

The ticket holds the address of the contract that created it so it’s easy to check if the ticket is valid or not.

I agree. It’s not clear why the costs and risks associated with the ticket design should be inflicted on all “FA2.1” contracts, when essentially equivalent functionality can be obtained using separate ticket bridge contracts. (Right?)

Edit: I just realised that this:

is an argument for the ticket design?

(I won’t say whether I am convinced or not, but at least there is something there.)

The use of a bridge contract has the effect of significantly increasing the size of tickets and consequently increasing their size on the chain as well as the cost of storing the smart-contracts which are intended to store these tickets.
For this reason, we thought it more appropriate to implement it directly in the standard.

1 Like

I’ve added remarks on the FA2.1 proposal and originated 2 contracts for you to interact with:

The main advantage of ticket wrapping is that it does not require a standard change; we can wrap FA1.2 and FA2 tokens as tickets today and this is what Tzportal already does. You could become compatible with ticket wrapping without adding much data by changing ticket (pair nat (option bytes)) into ticket (pair (option address) nat (option bytes)) where the option address defaults to the ticketer (so it is light when the token contract does it’s own ticket wrapping because it can put None and it is heavier for generic ticket wrappers like Tzportal but they need to store the token contract address somewhere anyway).

The standard change was required anyway because of other features that can not be wrapped, such as the replacement of callback views by on-chain views (notably for reasons of gas cost reduction).

You will notice that we are already compatible with wrapping, just put the address in the serialized data in the (option bytes).
It is also possible to not need the address by doing dedicated wrapping.

Or, yet another solution avoiding the introduction of option addresses: keep a generic wrapper contract and let it manage a mapping between (token contract addresses + ids) and its own ids. Such a wrapper contract could even follow the FA2.1 standard (extended by wrapping and unwrapping entrypoints).