On-chain View Patch

Motivation

The on-chain view provides a synchronous way for a contract to fetch the data from another contract. This, however, doesn’t benefit legacy contracts that is originated before Protocol Hangzhou.

Ideally one can re-originate the contract with an updated contract script. This, however, is hard for contracts holding large contract storage and can add more burden to DApp developers.

To provide a solution to read necessary information from a legacy contract and thus improve the UX of Tezos contract interaction, we propose to have a new mechanism for “patching ” an on-chain view onto an existing contract.

Possible solutions

default view

Adding a default view to all existing contracts. Just like calling a pre-declared on-chain view, by calling this default view, the caller contract would get the entire storage of the callee contract. The default view should be handled on-the-fly so don’t need to store a new view definition in the storage.

This solution might be quite challenging because the caller contract needs to know the storage type of the callee contract in advance. And such storage might contain a data type, such as bigmap and ticket, which is not returnable via view.

dynamic view

This is an extended version of the previous one. We still add a default view to all the existing contracts. Yet, this default view requires a lambda function of type s -> v as its argument, where s is the storage type of the callee contract; and, v is a value that is either part of s or can be computed from s. In other words, this lambda function would define how to fetch a value from the callee storage. With the dynamic view, a contract can easily read information from another contract without requiring it to pre-define any views.

view injection

Another direction of solving this is to add a new manage operation to allow the contract owner to “inject” a new view declaration into an existing contract. Notice that, this should not allow modifying any existing views.

-

All solution has their pros/cons, please read proposal for more details. All kinds of feedback and inputs are welcomed. Please leave a comment on our proposal or on this thread.

9 Likes

This feature is very useful!
Maybe it is not only for the contracts that cannot be opened onchain-view before the hangzhou version.
For NFT marketplace, some FA2 tokens will have royalty information in other contracts, for example hicetnunc Minter (KT1Hkg5qeNhfwpKW4fXvq7HGZB9z2EnmCCA9) or akaMinter ( KT1ULea6kxqiYe1A7CZVfMuGmTx7NmDGAph1).
However, this contract is often not open the onchain-view for others to check the correctness of the royalty when selling tokens. It would be good if this mechanism could be used to verify the correctness.

3 Likes

Here is a request for a similar feature in the past: Can we add view endpoints to existing FA1.2 and FA2 contracts?

CC: @buywall

3 Likes

Thanks for pursuing this idea - I think it will be a big ergonomic improvement for contract developers.

The “default” and “dynamic” approaches seem to be good enough to effectively upgrade many existing contracts. E.g. suppose Contract A is pre-Hangzhou and thus uses callbacks when it should really use views. Then anyone can write Contract B, which wraps the API of A and also provides appropriate view endpoints by directly inspecting the storage of Contract A. New users could use Contract B but legacy users could keep using Contract A.

Another approach, not related to the proposal, is to allow view calls to call non-view calls, under the constraint that on-chain storage revert for all involved contracts at the end of the call. Then Contract B could expose a view that makes a callback call to Contract A to get the value that it returns to its caller.

I’m not a fan of the “injection” approach as I understand it, because:

  1. Many popular contracts are quite old, and it would be hard / impossible to find all the origination key holders, many of whom have left Tezos, to ask them to patch their contracts.
  2. Philosophically, giving originators special power over contracts, power which isn’t in the contract code itself, gives power to centralized actors which was not initially specified in the contract code. I.e. it breaks the trust model.

Again, thanks for putting in the work, and please push it forward!

4 Likes

@buywall thanks for your inputs. :smile:

We are indeed in favor of the default view or dynamic view. It’s more flexible to the contract developers and provide a way to keep legacy contracts vital in terms of on-chain contract interaction.

… it would be hard / impossible to find all the origination key holders, many of whom have left Tezos, to ask them to patch their contracts.

Aha, this is a very good point. I would add this to the proposal as well. Thank you! :wink:

… gives power to centralized actors which was not initially specified in the contract code. I.e. it breaks the trust model.

To this topic, since a view couldn’t do anything to the storage but reading, it’s kinda alright. But I agree unless there is a specific reason or advantage, we shouldn’t grans any special power to a specific actor.

Another approach, not related to the proposal, is to allow view calls to call non-view calls, under the constraint that on-chain storage revert for all involved contracts at the end of the call. Then Contract B could expose a view that makes a callback call to Contract A to get the value that it returns to its caller.

Allowing a view to call an entrypoint might be to costy at runtime. We need to not just revert the storage but also remove its internal operations. Not mention that executing an entrypoint requires gas, which may be a lot. Technically I think it’s possible to apply a strong constraint that this view-to-entrypoint call works only for certain specific cases. Could be wrong, but I suspect it will cause more troubles than benefits.

1 Like

One problem with default and dynamic is that usually entrypoints are standardised whereas storage isn’t. For example, in FA2 balance_of is part of the standard but the type of the internal token storage representation (like the ledger) isn’t. So there is no guarantee you can write more generic code, for example, one that can handle arbitrary FA2 contracts because they might have a different internal representation of the ledger will still conforming to the FA2 standard.

2 Likes

@Ivan-Tigan Thanks for the input :slight_smile:

At least at this point, I don’t think we are looking for a universal way to read arbitrary storage regardless of its structure. In the current setting, it should be the caller contract who is responsible to have the domain knowledge of the callee contract. In other words, caller contract need to understand the shape of the targeted storage and thus know how to get the needed information from the storage.

If, let say we have a NFT market contract who wants to interact with NFTs defined by two different FA2 contracts. Then this market contract must realize how to read, say, token metadata, from each of these FA2 contracts no matter how those information is stored inside.

Moreover, in the case of dynamic view, such lambda of indicating-how-to-read-data can be stored in the caller’s storage so that it’s possible to to be amended later on.

1 Like

To me the default or dynamic view seems like a kludge. Why not add a new feature – separate from VIEW – and call it VIEW_STORAGE or something?

2 Likes

Hi @tom,

Thanks for putting inputs!

That is an great idea! Adding a new Michelson instruction can avoid putting "default" into the contract code and thus don’t need to care about “what if that name has been used” problem. And simply adding a new instruction indeed decreases the tech challenge level (well, at least, not making it harder to implement). I would add this and adjust proposal accordingly. One extra minor thing, VIEW_STORAGE seems a little bit too long to me, how about SKIM? :stuck_out_tongue:

What do you think about the cost in terms of gas consumption? To me, this should be charged as much as normal VIEW.

2 Likes

Hello all,
I would like to update the progress of this proposal. We have been working on the specification and the implementation of the approach that we chose namely: “dynamic view”. We also have the first draft of this implementation.

For more information please read:

Below is the short specification of this choice.

To this aim, an extend Michelson primitive is introduced: the VIEW_EXEC instruction.

The type of VIEW_EXEC is:

:: lambda 'storage_ty 'return : address : 'S -> option 'return : 'S
  • lambda 'storage_ty 'return: The lambda that we apply to the target contract’s storage 'storage_ty to retreive a value 'return.
  • address: The target contract address. If a contract address containing an entrypoint address%entrypoint, only the address part will be taken.
  • option 'return: The value retrieved from the target contract. Returns None if failed.

The semantics of VIEW_EXEC is:

> VIEW_EXEC / code: addr : S => Some v : S
    iff 
        code / s : []  =>  v : [] 
    where
        addr is the address of the target contract with storage s  
       
> VIEW_EXEC / _ : _ : S => None : S 
       otherwise

VIEW_EXEC will return None if:

  • Contract with address addr:
    • is nonexistent.
    • the storage type does not match the argument type of the lambda ('storage_ty).
  • Lambda function:
    • code results in runtime failure

Example

Suppose we have a contract that looks like below:

# contract_a
parameter unit ;
storage int ; # int 2
code { CDR;
       NIL operation;
       PAIR
}

Suppose we want to write contract_b that fetches the int in the storage of contract_a and increment it.

Using the new VIEW_EXEC instruction, we can write:

# contract_b
parameter address ;
storage int ;
code { 
      CAR; # address
      LAMBDA int int { PUSH int 1; ADD } ;
      VIEW_EXEC ; # result: Some (int 3)
      ASSERT_SOME ;
      NIL operation ;
      PAIR ;
     }

Properties

VIEW_EXEC has the same properties as VIEW, namely they are:

  • read-only: they may depend on the storage of the contract but cannot modify it nor emit operations,
  • synchronous: the result is immediately available on the stack of the contract

UnlikeVIEW,VIEW_EXEC has:

  • no name,
  • no input and output arguments,
  • lambda function in a stack. In lambda output (type 'return), the following types are forbidden: ticket, operation, big_map and sapling_state.
1 Like

Just hopping in to say, this is a great idea and will add tons of utility across existing contracts. Plus future contracts can be simpler in terms of pre-made views. I’m excited to see this implemented! :clap:

2 Likes

Big fan of this and hope it gets implemented! Even contracts with views do not always have the needed storage available to view

3 Likes

Whats the progress with the proposal? @yychi @lykimq