diff --git a/README.md b/README.md index 363423e..9cd697d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ You may find more information about the IIP Process in [IIP-1](./iips/IIP-0001/i ## List of IIPs - - Last updated: 2026-06-08 + - Last updated: 2026-06-10 - The _Status_ of a IIP reflects its current state with respect to its progression to being supported on the IOTA Mainnet. - `Draft` IIPs are work in progress. They may or may not have a working implementation on a testnet. - `Proposed` IIPs are demonstrated to have a working implementation on the IOTA Devnet or Testnet. @@ -43,9 +43,10 @@ You may find more information about the IIP Process in [IIP-1](./iips/IIP-0001/i | 5 | [Move View Functions](iips/IIP-0005/iip-0005.md) | A standardized interface for application-specific queries to on-chain state | Standards | Interface | Draft | | 7 | [Validator Scoring Mechanism](iips/IIP-0007/IIP-0007.md) | An automated and standardized system for monitoring validator behavior and scores | Standards | Core | Draft | | 8 | [Dynamic Minimum Commission based on the Validator's Voting Power per Epoch](iips/IIP-0008/IIP-0008.md) | A dynamic minimum validator commission rate set to the validator's voting power percentage to prevent stake hoarding and promote decentralization | Standards | Core | Active | -| 9 | [Abstract IOTA Accounts](iips/IIP-0009/IIP-0009.md) | Abstract accounts on IOTA enable smart-contract-based authentication of addresses. | Standards | Core | Proposed | -| 10 | [Package Metadata](iips/IIP-0010/IIP-0010.md) | Immutable on-chain object that provides trusted metadata about Move packages during execution | Standards | Core | Proposed | -| 12 | [Starfish Speed Consensus Protocol](iips/IIP-0012/iip-0012.md) | Optimistic transaction sequencing extension for Starfish that reduces commit latency | Standards | Core | Draft | +| 9 | [Abstract IOTA Accounts](iips/IIP-0009/IIP-0009.md) | Abstract accounts on IOTA enable smart-contract-based authentication of addresses. | Standards | Core | Proposed | +| 10 | [Package Metadata](iips/IIP-0010/IIP-0010.md) | Immutable on-chain object that provides trusted metadata about Move packages during execution | Standards | Core | Proposed | +| 11 | [Core Move View Functions](iips/IIP-0011/IIP-0011.md) | Move view functions - definition and specification at language level. | Standards | Core | Proposed | +| 12 | [Starfish Speed Consensus Protocol](iips/IIP-0012/iip-0012.md) | Optimistic transaction sequencing extension for Starfish that reduces commit latency | Standards | Core | Draft | ## Need help? diff --git a/iips/IIP-0011/IIP-0011.md b/iips/IIP-0011/IIP-0011.md new file mode 100644 index 0000000..895094a --- /dev/null +++ b/iips/IIP-0011/IIP-0011.md @@ -0,0 +1,799 @@ +--- +iip: 11 +title: Core Move View Functions +description: Move view functions - definition and specification at the language level +author: Lorenzo Benetollo (@lollobene) , Mirko Zichichi (@miker83z) +discussions-to: https://github.com/iotaledger/IIPs/discussions/42 +status: Draft +type: Standards Track +layer (*only required for Standards Track): Core +created: 2026-04-27 +requires (*optional): IIP-0010 +replaces (*optional): None +--- + +## Abstract + +This proposal introduces _view functions_ in Move, a class of functions that are guaranteed not to modify the persistent global storage. A view function is explicitly annotated using `#[view]` and its properties are statically enforced by the compiler and by the IOTA bytecode verifier. + +These functions are suitable for reading from the ledger state and/or executing code in Move with no effect on the state. This proposal, then, aims to improve developer ergonomics, enable safer APIs, and allow optimized execution paths for read-only on-chain queries. + +## Motivation + +Smart contract developers frequently need to query on-chain storage without modifying it. Today, this is harder than it should be: + +- No distinction between read-only and state-modifying functions. If your dApp depends on a third-party Move package, you have no way to know whether calling a function will mutate state without manually auditing its code. This increases the risk of unintended storage mutations. See Nemo Protocol [security incident](https://olivine-hydrofoil-637.notion.site/Nemo-Security-Incident-Cause-Process-and-Fund-Tracing-Report-V1-1-26a6b8723d8a80e29cb8cb48fe1390f2) +- No clear or lightweight way to read on-chain data. If you deploy a Move package with custom object types, reading their fields requires building a custom indexer, a heavyweight solution for what should be a simple query. + +Introducing view functions provides: + +- A clear and **statically enforced** guarantee of read-only behavior +- Improved developer experience and reduced risk of unintended state mutations +- Opportunities for optimized execution and seamless [integration with RPC](https://github.com/iotaledger/IIPs/blob/main/iips/IIP-0005/iip-0005.md) interfaces and query-oriented tooling, enabling off-chain evaluation without gas costs or signature requirements + +## Background + +In Move on IOTA, the original [Move Resource model](https://developers.diem.com/papers/diem-move-a-language-with-programmable-resources/2019-06-18.pdf) has evolved into an **object-centric** model, in which structs with the `key` ability and a required `UID` (as a first field) represent on-chain objects. + +As a consequence, functions must **explicitly declare as parameters all the objects they operate on**, either by value or by reference, while the virtual machine is responsible for loading and deserializing these objects at execution time. + +This eliminates implicit global storage access and requires all storage interactions to occur via **explicitly** **passed** **objects**. + +Additionally, in Move on IOTA, object creation is not exposed as a language-level primitive. + +Instead, it is realized through framework-provided functions (e.g., `object::new`) combined with transfer operations. + +**This design implies that object creation is not performed by any language primitive but is delegated to the platform framework. More generally, as we will see in the next section, the storage can be modified either by language primitives or by framework functions.** + +### What can alter the storage? + +As previously mentioned, in Move on IOTA, objects are the only entities stored in persistent storage. Consequently, all changes to the global storage occur through operations on objects. + +These operations include: + +- **Creating an object**, which adds a new object to the storage + + ```move + public struct Object has key, store { id: UID, value: u64 } + public struct Wrapper has key { id: UID, o: Object } + + public fun create(addr: address, ctx: &mut TxContext) { + let o = Object { id: object::new(ctx), val: 44 }; + transfer::transfer(o, addr); + } + ``` + +- **Modifying an object’s fields**, which updates its internal state + + ```move + public fun edit(o: &mut Object, new_val: u64) { + o.val = new_val; + } + ``` + +- **Deleting an object**, which removes it from storage + + ```move + public fun delete(o: Object) { + let Object { id, val } = o; + object::delete(id); + } + ``` + +- **Transferring an object**, which changes its ownership + + ```move + public fun transfer(o: Object, recipient: address) { + transfer::public_transfer(o, recipient) + } + ``` + +- **Sharing an object**, which changes its accessibility model + + ```move + public fun share(value: u64, ctx: &mut TxContext) { + transfer::share_object(Object { id: object::new(ctx), value }) + } + ``` + +- **Wrapping an object** + + ```move + public fun wrap(o: Object, ctx: &mut TxContext) { + transfer::transfer(Wrapper { id: object::new(ctx), o }, ctx.sender()); + } + ``` + +- **Adding or removing dynamic fields**, which modify associated dynamic fields + + ```move + public fun add_dynamic_field(self: &mut Object, value: bool) { + dynamic_field::add(&mut self.id, b"dynamic_field", value); + } + ``` + +### By-value semantics + +In this proposal, an argument or return value, whether an object or a native type, is considered to be passed _by value_ when ownership is transferred, rather than when access is provided through a reference. + +In Move, passing an argument by value consumes it: ownership moves to the callee, and the original binding can no longer be used. References (`&T` and `&mut T`) instead provide access to a value without transferring ownership. + +This distinction matters for view functions because accepting or returning an IOTA object by value would move ownership of a persistent object, which can enable ownership changes, wrapping, deletion, or other state-modifying behavior. For this reason, view functions cannot accept or return IOTA objects by value. + +## Specification + +### Definition + +Based on the premise that global storage is solely constituted by objects in IOTA, then: + +A _view function_ is a Move function that does not mutate any IOTA object and that returns at least one value. Returned values MUST NOT be IOTA objects, or values that could contain IOTA objects, by value. Returned values MUST NOT contain mutable references. + +View functions MAY return immutable references (`&T`). Such references remain subject to the normal Move borrow checker rules, which determine whether a returned reference is well formed and whether it can outlive its source. + +### Rules + +A compliant _view function_ is declared using the `#[view]` attribute, which enforces static validation by the compiler and by IOTA bytecode verifier: + +- It MUST NOT create, update, or delete IOTA objects +- It MUST return at least one value. Returned values MUST NOT be IOTA objects, or values that could contain IOTA objects, by value. +- It MUST NOT return mutable references +- It MAY return immutable references +- It MAY read from the global storage through a reference to an object +- It MUST NOT have mutable references as parameters +- It MAY perform pure computation + +### Annotation + +A view function is declared as: + +```move +#[view] +public fun foo(...): T { + ... +} +``` + +The `#[view]` attribute defines a **compiler- and verifier-enforced invariant**. + +### **Nested Function Calls** + +A function annotated with `#[view]` MAY call other functions, including `private` and `native` ones. + +In IOTA Move, access to global storage is mediated through objects. As a result, modifications to the global storage require access to objects by value or through mutable references. + +Given the constraints imposed on view functions, it follows that a view function cannot pass the necessary inputs to call functions that perform state-modifying operations. + +In particular, any function that requires: + +- an object passed by value +- a mutable reference +- access to `&mut TxContext` + +cannot be invoked from a view function, as such values cannot be constructed or obtained within the constraints of a view execution. + +Therefore, even if a view function calls other functions, those calls are effectively restricted to functions whose parameter and return types are compatible with view constraints. This ensures that state-modifying operations cannot be reached, even transitively. + +View-specific validation is based on function signatures. It does not add a separate body-level provenance analysis for immutable reference returns; normal Move borrow checking continues to enforce reference safety. In practice, an immutable reference returned by a view function must be derived from a valid reference source, such as an immutable reference parameter, a field borrowed from such a parameter, or an immutable reference obtained by reading a dynamic object field. + +### Invocation of View Functions + +Any other function MAY invoke a function annotated with `#[view]`, so the constraints imposed on view functions MUST hold independently of the caller context. + +Although a view function may not receive objects by value or as mutable references, allowing mutable references to **non-object** types would still enable indirect state modifications. For example, a mutable reference to an object’s field could be passed to a view function, allowing modification of the object’s field through that reference. + +To **prevent** such **indirect mutations**, view functions MUST NOT accept mutable references (`&mut T`) as parameters, regardless of whether `T` is an object type. + +For example: + +```move +public fun edit(mut o: Object, new_val: u64): Object { + edit_u64_by_ref(&mut o.val, new_val); + o +} + +// NON view function because it can mutate an object field +public fun edit_u64_by_ref(val: &mut u64, new_val: u64): u64 { + let old_val = *val; + *val = new_val; + old_val +} +``` + +### **Function Signature Constraints** + +A function annotated with `#[view]` MUST satisfy: + +- **Visibility** + - MUST be `public` or `public entry` +- **Return values** + - MUST return at least one value + - MUST NOT return IOTA objects, or values that could contain IOTA objects, by value + - MUST NOT return mutable references + - MAY return immutable references (`&T`) +- **Parameters** + - MUST NOT include IOTA objects, or values that could contain IOTA objects, passed by value + - MAY include immutable references (`&T`) + - MAY include primitive types or user-defined types with at least one of the `Copy` or `Drop` abilities (**neither IOTA objects nor types that can contain objects by value**) + - **References** + - MUST NOT use mutable references (`&mut T`) to any type, including `TxContext` + - MAY use immutable references + +- **Type Parameters** + - Type parameters used by value in parameters or return values MUST have at least one of the `Copy` or `Drop` abilities + - Type parameters used only behind immutable references, or not used in the function signature, MAY be unconstrained + +### **Native Functions** + +Native functions MAY be annotated with `#[view]`. + +## **Compiler Behavior** + +### **Errors** + +The compiler MUST reject any source function annotated with `#[view]` that violates the specification. + +The publish-time bytecode verifier MUST also reject package metadata that marks a function as a view function when the compiled function signature violates these constraints. This prevents packages from bypassing source-level checks by publishing bytecode or MVIR directly. + +Example: + +> This function **does** **not** satisfy view constraints, and it can not be marked as `#[view]`. + +### **Warnings** + +The compiler MAY emit a warning when a function: + +- satisfies all constraints of a view function +- is not annotated with #[view] + +Example: + +> This function satisfies view constraints and could be marked as `#[view]`. + +## **Execution Semantics** + +View functions MAY be executed in a read-only execution mode: + +- Without submitting a transaction +- Without requiring a signature +- Without consuming gas (implementation-dependent) + +Execution MUST be deterministic: + +- Given the same state, the function MUST return the same result + +## Examples + +### Valid Examples + +```move +module view_functions::valid { + public struct Object has key { + id: iota::object::UID, + } + + public struct Wrapped has copy, drop, store { + value: u64, + } + + public struct Wrapped2 has copy, drop, store { + value: u64, + } + + public struct GenericObject2 has key { + id: iota::object::UID, + inner: T, + other: U, + } + + public struct GenericObject has key, store { + id: iota::object::UID, + inner: T, + } + + public struct NonObject has copy, drop, store { + value: u64, + } + + public struct NonObjectTemplated has copy, drop, store { + inner: T, + } + + public struct Receiving has copy, drop, store { + id: iota::object::ID, + } + + public struct DynamicField has copy, drop, store { + value: u64, + } + + #[view] + public entry fun entry_view(a: u64): u64 { + a + } + + #[view] + public fun object_immutable_ref(object: &Object): u64 { + let _ = object; + 0 + } + + #[view] + public fun primitive_by_value(object: &Object, val: u8): u64 { + let _ = object; + val as u64 + } + + #[view] + public fun multiple_generic_object_immutable_ref(generic_object: &GenericObject2): u64 { + generic_object.inner.value + generic_object.other.value + } + + #[view] + public fun wrapped_by_value(wrapped: Wrapped) : bool { + wrapped.value > 44 + } + + #[view] + public fun generic_object_immutable_ref(generic_object: &GenericObject): u64 { + generic_object.inner.value + } + + #[view] + public fun template_immutable_ref(generic_object: &GenericObject): u64 { + let _ = generic_object; + 0 + } + + #[view] + public fun template_key_store_immutable_ref(generic_object: &GenericObject): u64 { + let _ = generic_object; + 0 + } + + #[view] + public fun template_copy_drop_store_immutable_ref( + generic_object: &GenericObject, + ): u64 { + let _ = generic_object; + 0 + } + + #[view] + public fun non_object_by_value(value: NonObject): u64 { + value.value + } + + #[view] + public fun templated_non_object_by_value( + value: NonObjectTemplated, + ): u64 { + let _ = value; + 0 + } + + #[view] + public fun option_primitive_by_value(value: Option): u64 { + if (value.is_some()) { + value.destroy_some() + } else { + 0 + } + } + + #[view] + public fun option_non_object_by_value(value: Option): u64 { + if (value.is_some()) { + value.destroy_some().value + } else { + 0 + } + } + + #[view] + public fun option_generic_object_immutable_ref(value: &Option>): u64 { + let _ = value; + 0 + } + + #[view] + public fun vector_primitive_by_value(value: vector): u64 { + value.length() + } + + #[view] + public fun vector_non_object_by_value(value: vector): u64 { + value.length() + } + + #[view] + public fun vector_generic_object_immutable_ref(value: &vector>): u64 { + value.length() + } + + #[view] + public fun receiving_immutable_ref(receiving: &Receiving>): u64 { + let _ = receiving; + 0 + } + + #[view] + public fun copy_type_param(value: T): T { + value + } + + #[view] + public fun drop_type_param(value: T): u64 { + let _ = value; + 0 + } + + #[view] + public fun copy_store_type_param(value: T): T { + value + } + + #[view] + public fun primitive_tuple_return(a: u64, b: bool): (u64, bool) { + (a, b) + } + + #[view] + public fun returns_generic_obj_reference( + input: &GenericObject, + ): &GenericObject { + input + } + + #[view] + public fun returns_u64_reference(input: &u64): &u64 { + input + } + + #[view] + public fun returns_tuple_with_reference(input: &u64): (&u64, u64) { + (input, 0) + } + + #[view] + public fun returns_dynamic_field_reference(object: &Object, name: u64): &DynamicField { + iota::dynamic_field::borrow(&object.id, name) + } + + #[view] + public native fun native_view(v: u64): u64; + + #[view] + public native fun native_view_no_param(): bool; + + #[view] + public native fun native_type_param(): u64; + + #[view] + public fun unused_unconstrained_type_param(): u64 { + 0 + } + + #[view] + public fun unconstrained_type_param_by_ref(x: &T): u64 { + let _ = x; + 0 + } + + #[view] + public fun unconstrained_type_param_vector_by_ref(x: &vector): u64 { + let _ = x; + 0 + } + + #[view] + public fun unconstrained_type_param_option_by_ref(x: &Option): u64 { + let _ = x; + 0 + } +} +``` + +### Non Valid Examples + +```move +module view_functions::invalid { + se std::ascii::{String, char}; + + public struct Object has key { + id: iota::object::UID, + } + + public struct Wrapped has copy, drop, store { + value: u64, + } + + public struct StoreOnly has store { + value: u64, + } + + public struct GenericObject has key, store { + id: iota::object::UID, + inner: T, + } + + public struct GenericObject2 has key { + id: iota::object::UID, + inner: T, + other: U, + } + + public struct Wrapper has key { + id: iota::object::UID, + wrapped: vector, + } + + public struct NonObject has copy, drop, store { + value: u64, + } + + public struct NonObjectTemplated has copy, drop, store { + inner: T, + } + + public struct Receiving has copy, drop, store { + id: iota::object::ID, + } + + #[view] + fun private_view(): u64 { + 0 + } + + #[view] + public fun no_return() { + abort 0 + } + + #[view] + public fun object_by_value(_object: Object): u64 { + abort 0 + } + + #[view] + public fun object_mutable_ref(_object_ref: &mut Object): u64 { + abort 0 + } + + #[view] + public fun concrete_multiple_object_by_value( + _generic_object2: GenericObject2, + ): u64 { + abort 0 + } + + #[view] + public fun generic_object_by_value(_generic_object: GenericObject): u64 { + abort 0 + } + + #[view] + public fun generic_object_mutable_ref(_object_ref: &mut GenericObject): u64 { + abort 0 + } + + #[view] + public fun template_by_value(_generic_object: GenericObject): u64 { + abort 0 + } + + #[view] + public fun template_key_store_by_value( + _generic_object: GenericObject, + _wrapper: &Wrapper, + ): u64 { + abort 0 + } + + #[view] + public fun template_copy_drop_store_by_value( + _generic_object: GenericObject, + ): u64 { + abort 0 + } + + #[view] + public fun mutable_primitive_param(mut value: u64): u64 { + value = value + 1; + value + } + + #[view] + public fun mutable_non_object_param(mut value: NonObject): u64 { + value.value = value.value + 1; + value.value + } + + #[view] + public fun update_string_by_value(mut name: String): String { + name.push_char(char(43)); + name + } + + #[view] + public fun direct_key_store_type_param_by_value(_generic_object: T): u64 { + abort 0 + } + + #[view] + public fun unconstrained_type_param_by_value(_value: T): u64 { + abort 0 + } + + #[view] + public fun store_only_by_value(value: StoreOnly): u64 { + let StoreOnly { value } = value; + value + } + + #[view] + public fun store_only_type_param_by_value(_value: T): u64 { + abort 0 + } + + #[view] + public fun option_object_by_value(_value: Option>): u64 { + abort 0 + } + + #[view] + public fun option_template_object_by_value(_value: Option): u64 { + abort 0 + } + + #[view] + public fun option_vector_object_by_value(_value: Option>>): u64 { + abort 0 + } + + #[view] + public fun vector_option_object_by_value(_value: vector>>): u64 { + abort 0 + } + + #[view] + public fun option_primitive_mutable_ref(_value: &mut Option): u64 { + abort 0 + } + + #[view] + public fun option_non_object_mutable_ref(_value: &mut Option): u64 { + abort 0 + } + + #[view] + public fun option_object_mutable_ref(_value: &mut Option>): u64 { + abort 0 + } + + #[view] + public fun vector_object_by_value(_value: vector>): u64 { + abort 0 + } + + #[view] + public fun vector_template_object_by_value(_value: vector): u64 { + abort 0 + } + + #[view] + public fun vector_primitive_mutable_ref(_value: &mut vector): u64 { + abort 0 + } + + #[view] + public fun vector_non_object_mutable_ref(_value: &mut vector): u64 { + abort 0 + } + + #[view] + public fun vector_object_mutable_ref(_value: &mut vector>): u64 { + abort 0 + } + + #[view] + public fun receiving_by_value(_receiving: Receiving>): u64 { + abort 0 + } + + #[view] + public fun receiving_mutable_ref(_receiving: &mut Receiving>): u64 { + abort 0 + } + + #[view] + public fun tx_context_mutable_ref(_ctx: &mut iota::tx_context::TxContext): u64 { + abort 0 + } + + #[view] + public fun returns_object(): Object { + abort 0 + } + + #[view] + public fun returns_object_vector(): vector> { + abort 0 + } + + #[view] + public fun returns_option_object(): Option> { + abort 0 + } + + #[view] + public fun returns_store_only(): StoreOnly { + abort 0 + } + + #[view] + public fun returns_option_store_only(): Option { + abort 0 + } + + #[view] + public fun returns_key_store_type_param(): T { + abort 0 + } + + #[view] + public fun returns_store_only_type_param(): T { + abort 0 + } + + #[view] + public fun returns_tuple_with_object(): (u64, GenericObject) { + abort 0 + } + + #[view] + public native fun returns_mut_reference(input: &u64): &mut u64; + + #[view] + public native fun store_only_type_param(x: T): u64; + + #[view] + public native fun native_mut_ref(x: &mut u64): u64; +} +``` + +## **Rationale** + +This proposal introduces an explicit abstraction for read-only execution, similar to “view” or “pure” functions in other smart contract platforms. + +Key design decisions: + +- **Explicit annotation (**`#[view]`**)** + - Avoids ambiguity and enables static guarantees +- **Static enforcement** + - Ensures safety at compile time +- **Recursive restriction** + - Guarantees no indirect storage mutation +- **Separation of constraints** + - Improves clarity and maintainability + +## **Backwards Compatibility** + +This proposal is backwards-compatible: + +- Existing code is unaffected +- The `#[view]` attribute is optional + +However: + +- Adding `#[view]` introduces stricter constraints that are enforced at compile time +- Removing `#[view]` may affect client expectations, particularly for off-chain execution and API usage + +This proposal builds on [IIP-0010](https://github.com/iotaledger/IIPs/blob/main/iips/IIP-0010/IIP-0010.md) by treating `#[view]` as package metadata. View functions are included in `PackageMetadata`, similarly to how authenticator functions are currently exposed. This allows clients and tooling to reliably discover view functions without requiring additional static analysis. + +Such metadata exposure does not affect program semantics but improves interoperability with RPC layers and developer tooling. + +This proposal is also compatible with the RPC interface proposed in [IIP-0005](https://github.com/iotaledger/IIPs/blob/main/iips/IIP-0005/iip-0005.md). IIP-0005 defines an off-chain interface for invoking Move view functions and explicitly allows that interface to be limited to a future explicit on-chain read API. Under this proposal, that explicit API is the set of functions annotated with `#[view]` and accepted by the compiler, so implementations of IIP-0005 SHOULD use this proposal's validation rules and `PackageMetadata` exposure when determining which functions are callable as view functions. + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).