From feb5b9ea254d8927a92bac07e23353d4242c9873 Mon Sep 17 00:00:00 2001 From: Lorenzo Benetollo Date: Mon, 27 Apr 2026 12:49:17 +0200 Subject: [PATCH 1/8] Create IIP-0011.md --- iips/IIP-0011/IIP-0011.md | 383 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 iips/IIP-0011/IIP-0011.md diff --git a/iips/IIP-0011/IIP-0011.md b/iips/IIP-0011/IIP-0011.md new file mode 100644 index 0000000..2513c47 --- /dev/null +++ b/iips/IIP-0011/IIP-0011.md @@ -0,0 +1,383 @@ +--- +iip: 11 +title: Core Move View Functions +description: Move view functions - definition and specification at 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): None +replaces (*optional): None +--- + +## Abstract + +This proposal introduces _view functions_ in Move, a class of functions that are guaranteed to not modify the persistent global storage. A view function is explicitly annotated using `#[view]` and its properties are statically enforced by the compiler. + +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](https://olivine-hydrofoil-637.notion.site/Nemo-Security-Incident-Cause-Process-and-Fund-Tracing-Report-V1-1-26a6b8723d8a80e29cb8cb48fe1390f2 "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 "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 "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**. + +```move +public struct Object has key { id: UID, val: u64 } + +public fun create(addr: address, ctx: &mut TxContext) { + let o = Object { id: object::new(ctx), val: 44 }; + transfer::transfer(o, addr); +} + +public fun edit(o: &mut Object, new_val: u64) { o.val = new_val; } + +public fun delete(o: Object) { let Object { id, val } = o; object::delete(id); } +``` + +In _IOTA Move_, 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); } + ``` + +## 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 a value, which is not an IOTA object. + +### Rules + +A compliant _view function_ is declared using the `#[view]` attribute, which enforces a static validation by the compiler: + +- It MUST NOT create, update, or delete IOTA objects +- It MUST return a value. This value can be anything but an IOTA object _by value_\*. +- It MAY read from the global storage through a reference to an object +- It MAY perform pure computation + +* In this context, _by value_ refers to passing or returning a value in a way that transfers ownership of that value, rather than accessing it through a reference. + +In Move, values passed by value are consumed by the callee, meaning that ownership is moved and the original binding can no longer be used. This is in contrast to references (`&T` or `&mut T`), which provide access to a value without transferring ownership. + +For view functions, returning or accepting objects by value is disallowed, as it would imply ownership transfer or potential modification of persistent state. + +### Annotation + +A view function is declared as: + +```move +#[view] public fun foo(...): T { ... } +``` + +The `#[view]` attribute defines a **compiler-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. + +### 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 values + - MUST NOT return IOTA objects +- **Parameters** + - MUST NOT include IOTA objects passed by value + - MAY include: + - immutable references (`&T`) + - primitive types or user-defined types with `Copy` or `Drop` ability (**non IOTA objects, nor types that can contain objects**) + - **References** + - MUST NOT use mutable references (`&mut T`) to any type, including `TxContext` + - MAY use immutable references +- **Type Parameters** + - MUST be types with `Copy` or `Drop` ability (this ensures are not objects) + +### **Native Functions** + +Native functions MAY be be annotated with `#[view]`. + +## **Compiler Behavior** + +### **Errors** + +The compiler MUST reject any function annotated with `#[view]` that violates the specification. + +Example: + +> This function **does** **not** satisfy view constraints, and it can not be marked as `#[view]`. + +### **Warnings** + +The compiler SHOULD 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 S has key { id: UID, val: u64 } + public struct Dumb has copy, drop { flag: bool } + + // Valid: returns value, immutable ref, no module calls + #[view] + public fun get_value(s: &S): u64 { s.val } + + // Valid: pure computation + #[view] + public entry fun add(a: u64, b: u64): u64 { a + b } + + // Valid: returns tuple + #[view] + public fun get_pair(a: u64, b: u64): (u64, u64) { (a, b) } + + // Valid: non object parameter + #[view] + public fun deconstruct_dumb(d: Dumb): bool { + let Dumb { flag } = d; + flag + } + + // Valid: non object parameter + #[view] + public fun create_dumb(flag: bool): Dumb { Dumb { flag } } +} +``` + +### Non Valid Examples + +```move +module view_functions::invalid { + public struct S has key, store { id: UID, val: u64 } + public struct Wrapper { o: Object } + + // Error: void return type + #[view] + public fun no_return(x: u64) { let _ = x; } + + // Error: mutable reference parameter to TxContext + #[view] + public fun share_S(val: u64, ctx: &mut TxContext) { + let s = S { id: object::new(ctx), val }; + transfer::share_object(s); + } + + // Error: IOTA object by value + #[view] + public fun transfer_S(s: S, recipient: address) { + transfer::public_transfer(s, recipient); + } + + // Error: IOTA object by value + #[view] + public fun delete_object(s: S): u64 { + let S { id, val } = s; + object::delete(id); + val + } + + // Error: mutable reference to IOTA object + #[view] + public fun mutates_object_ref(s: &mut S): u64 { + s.val = s.val + 1; + s.val + } + + // Error: mutable IOTA object by value + #[view] + public fun mutates_object(mut s: S): S { + s.val = s.val + 1; + s + } + + // Error: mutable IOTA object by value + #[view] + public fun mutates_object_side_effect(mut s: S): S { + mutates_object_ref(&mut s); + s + } + + // Error: mutable reference + #[view] + public fun edit_u64_by_ref(val: &mut u64, new_val: u64): u64 { + let old_val = *val; + *val = new_val; + old_val + } + + // Error: Wrapper type contains an object type + #[view] + public fun delete_wrapped_object(wrapper: Wrapper): u64 { + let Wrapper { o } = wrapper; + let S { id, val } = o; + object::delete(id); + val + } +} +``` + +## **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 + +As an implementation detail, view functions MAY be included in `PackageMetadata`, similarly to how authenticator functions are currently exposed. This would allow 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. + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From 3e412176d9b5452fb36e7bc60fd93009e9dddeaf Mon Sep 17 00:00:00 2001 From: Lorenzo Benetollo Date: Mon, 27 Apr 2026 12:49:17 +0200 Subject: [PATCH 2/8] Create IIP-0011.md --- iips/IIP-0011/IIP-0011.md | 383 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 iips/IIP-0011/IIP-0011.md diff --git a/iips/IIP-0011/IIP-0011.md b/iips/IIP-0011/IIP-0011.md new file mode 100644 index 0000000..2513c47 --- /dev/null +++ b/iips/IIP-0011/IIP-0011.md @@ -0,0 +1,383 @@ +--- +iip: 11 +title: Core Move View Functions +description: Move view functions - definition and specification at 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): None +replaces (*optional): None +--- + +## Abstract + +This proposal introduces _view functions_ in Move, a class of functions that are guaranteed to not modify the persistent global storage. A view function is explicitly annotated using `#[view]` and its properties are statically enforced by the compiler. + +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](https://olivine-hydrofoil-637.notion.site/Nemo-Security-Incident-Cause-Process-and-Fund-Tracing-Report-V1-1-26a6b8723d8a80e29cb8cb48fe1390f2 "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 "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 "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**. + +```move +public struct Object has key { id: UID, val: u64 } + +public fun create(addr: address, ctx: &mut TxContext) { + let o = Object { id: object::new(ctx), val: 44 }; + transfer::transfer(o, addr); +} + +public fun edit(o: &mut Object, new_val: u64) { o.val = new_val; } + +public fun delete(o: Object) { let Object { id, val } = o; object::delete(id); } +``` + +In _IOTA Move_, 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); } + ``` + +## 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 a value, which is not an IOTA object. + +### Rules + +A compliant _view function_ is declared using the `#[view]` attribute, which enforces a static validation by the compiler: + +- It MUST NOT create, update, or delete IOTA objects +- It MUST return a value. This value can be anything but an IOTA object _by value_\*. +- It MAY read from the global storage through a reference to an object +- It MAY perform pure computation + +* In this context, _by value_ refers to passing or returning a value in a way that transfers ownership of that value, rather than accessing it through a reference. + +In Move, values passed by value are consumed by the callee, meaning that ownership is moved and the original binding can no longer be used. This is in contrast to references (`&T` or `&mut T`), which provide access to a value without transferring ownership. + +For view functions, returning or accepting objects by value is disallowed, as it would imply ownership transfer or potential modification of persistent state. + +### Annotation + +A view function is declared as: + +```move +#[view] public fun foo(...): T { ... } +``` + +The `#[view]` attribute defines a **compiler-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. + +### 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 values + - MUST NOT return IOTA objects +- **Parameters** + - MUST NOT include IOTA objects passed by value + - MAY include: + - immutable references (`&T`) + - primitive types or user-defined types with `Copy` or `Drop` ability (**non IOTA objects, nor types that can contain objects**) + - **References** + - MUST NOT use mutable references (`&mut T`) to any type, including `TxContext` + - MAY use immutable references +- **Type Parameters** + - MUST be types with `Copy` or `Drop` ability (this ensures are not objects) + +### **Native Functions** + +Native functions MAY be be annotated with `#[view]`. + +## **Compiler Behavior** + +### **Errors** + +The compiler MUST reject any function annotated with `#[view]` that violates the specification. + +Example: + +> This function **does** **not** satisfy view constraints, and it can not be marked as `#[view]`. + +### **Warnings** + +The compiler SHOULD 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 S has key { id: UID, val: u64 } + public struct Dumb has copy, drop { flag: bool } + + // Valid: returns value, immutable ref, no module calls + #[view] + public fun get_value(s: &S): u64 { s.val } + + // Valid: pure computation + #[view] + public entry fun add(a: u64, b: u64): u64 { a + b } + + // Valid: returns tuple + #[view] + public fun get_pair(a: u64, b: u64): (u64, u64) { (a, b) } + + // Valid: non object parameter + #[view] + public fun deconstruct_dumb(d: Dumb): bool { + let Dumb { flag } = d; + flag + } + + // Valid: non object parameter + #[view] + public fun create_dumb(flag: bool): Dumb { Dumb { flag } } +} +``` + +### Non Valid Examples + +```move +module view_functions::invalid { + public struct S has key, store { id: UID, val: u64 } + public struct Wrapper { o: Object } + + // Error: void return type + #[view] + public fun no_return(x: u64) { let _ = x; } + + // Error: mutable reference parameter to TxContext + #[view] + public fun share_S(val: u64, ctx: &mut TxContext) { + let s = S { id: object::new(ctx), val }; + transfer::share_object(s); + } + + // Error: IOTA object by value + #[view] + public fun transfer_S(s: S, recipient: address) { + transfer::public_transfer(s, recipient); + } + + // Error: IOTA object by value + #[view] + public fun delete_object(s: S): u64 { + let S { id, val } = s; + object::delete(id); + val + } + + // Error: mutable reference to IOTA object + #[view] + public fun mutates_object_ref(s: &mut S): u64 { + s.val = s.val + 1; + s.val + } + + // Error: mutable IOTA object by value + #[view] + public fun mutates_object(mut s: S): S { + s.val = s.val + 1; + s + } + + // Error: mutable IOTA object by value + #[view] + public fun mutates_object_side_effect(mut s: S): S { + mutates_object_ref(&mut s); + s + } + + // Error: mutable reference + #[view] + public fun edit_u64_by_ref(val: &mut u64, new_val: u64): u64 { + let old_val = *val; + *val = new_val; + old_val + } + + // Error: Wrapper type contains an object type + #[view] + public fun delete_wrapped_object(wrapper: Wrapper): u64 { + let Wrapper { o } = wrapper; + let S { id, val } = o; + object::delete(id); + val + } +} +``` + +## **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 + +As an implementation detail, view functions MAY be included in `PackageMetadata`, similarly to how authenticator functions are currently exposed. This would allow 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. + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From 9337b9418de4d810aef0e649b3ec0a89ea8773fc Mon Sep 17 00:00:00 2001 From: Levente Pap Date: Tue, 28 Apr 2026 12:58:37 +0200 Subject: [PATCH 3/8] chore: update readme --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2a76a1d..c7afaa3 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-04-27 + - Last updated: 2026-04-28 - 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,8 +43,9 @@ 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 | +| 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 | ## Need help? From 8f504036391d76ec45dad80a036a622d92f3e85c Mon Sep 17 00:00:00 2001 From: Lorenzo Benetollo Date: Wed, 29 Apr 2026 11:26:24 +0200 Subject: [PATCH 4/8] Review updates --- iips/IIP-0011/IIP-0011.md | 123 +++++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 56 deletions(-) diff --git a/iips/IIP-0011/IIP-0011.md b/iips/IIP-0011/IIP-0011.md index 2513c47..d819521 100644 --- a/iips/IIP-0011/IIP-0011.md +++ b/iips/IIP-0011/IIP-0011.md @@ -1,20 +1,20 @@ --- iip: 11 title: Core Move View Functions -description: Move view functions - definition and specification at language level +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): None +requires (*optional): IIP-0010 replaces (*optional): None --- ## Abstract -This proposal introduces _view functions_ in Move, a class of functions that are guaranteed to not modify the persistent global storage. A view function is explicitly annotated using `#[view]` and its properties are statically enforced by the compiler. +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. 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. @@ -22,41 +22,30 @@ These functions are suitable for reading from the ledger state and/or executing 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](https://olivine-hydrofoil-637.notion.site/Nemo-Security-Incident-Cause-Process-and-Fund-Tracing-Report-V1-1-26a6b8723d8a80e29cb8cb48fe1390f2 "https://olivine-hydrofoil-637.notion.site/Nemo-Security-Incident-Cause-Process-and-Fund-Tracing-Report-V1-1-26a6b8723d8a80e29cb8cb48fe1390f2") +- 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: + +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 "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 +- 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 "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. +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**. -```move -public struct Object has key { id: UID, val: u64 } - -public fun create(addr: address, ctx: &mut TxContext) { - let o = Object { id: object::new(ctx), val: 44 }; - transfer::transfer(o, addr); -} - -public fun edit(o: &mut Object, new_val: u64) { o.val = new_val; } - -public fun delete(o: Object) { let Object { id, val } = o; object::delete(id); } -``` - -In _IOTA Move_, object creation is not exposed as a language-level primitive. +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? +### 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. @@ -77,39 +66,60 @@ These operations include: - **Modifying an object’s fields**, which updates its internal state ```move - public fun edit(o: &mut Object, new_val: u64) { o.val = new_val; } + 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); } + 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) } + 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 }) } + 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()); } + 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); } + 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, a value is passed or returned _by value_ when ownership of that value is transferred, rather than when the value is accessed through a reference. + +In Move, passing a value 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 @@ -123,22 +133,20 @@ A _view function_ is a Move function that does not mutate any IOTA object and th A compliant _view function_ is declared using the `#[view]` attribute, which enforces a static validation by the compiler: - It MUST NOT create, update, or delete IOTA objects -- It MUST return a value. This value can be anything but an IOTA object _by value_\*. +- It MUST return a value. The returned value MUST NOT be an IOTA object by value. - 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 -* In this context, _by value_ refers to passing or returning a value in a way that transfers ownership of that value, rather than accessing it through a reference. - -In Move, values passed by value are consumed by the callee, meaning that ownership is moved and the original binding can no longer be used. This is in contrast to references (`&T` or `&mut T`), which provide access to a value without transferring ownership. - -For view functions, returning or accepting objects by value is disallowed, as it would imply ownership transfer or potential modification of persistent state. - ### Annotation A view function is declared as: ```move -#[view] public fun foo(...): T { ... } +#[view] +public fun foo(...): T { + ... +} ``` The `#[view]` attribute defines a **compiler-enforced invariant**. @@ -173,15 +181,16 @@ 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 + edit_u64_by_ref(&mut o.val, new_val); + o +} - public fun edit_u64_by_ref(val: &mut u64, new_val: u64): u64 { - let old_val = *val; - *val = new_val; - old_val - } +// 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** @@ -195,18 +204,18 @@ A function annotated with `#[view]` MUST satisfy: - MUST NOT return IOTA objects - **Parameters** - MUST NOT include IOTA objects passed by value - - MAY include: - - immutable references (`&T`) - - primitive types or user-defined types with `Copy` or `Drop` ability (**non IOTA objects, nor types that can contain objects**) + - 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** - - MUST be types with `Copy` or `Drop` ability (this ensures are not objects) + - MUST have at least one of the `Copy` or `Drop` abilities (this ensures they are not objects) ### **Native Functions** -Native functions MAY be be annotated with `#[view]`. +Native functions MAY be annotated with `#[view]`. ## **Compiler Behavior** @@ -293,13 +302,13 @@ module view_functions::invalid { transfer::share_object(s); } - // Error: IOTA object by value + // Error: IOTA object passed by value #[view] public fun transfer_S(s: S, recipient: address) { transfer::public_transfer(s, recipient); } - // Error: IOTA object by value + // Error: IOTA object passed by value #[view] public fun delete_object(s: S): u64 { let S { id, val } = s; @@ -314,14 +323,14 @@ module view_functions::invalid { s.val } - // Error: mutable IOTA object by value + // Error: mutates IOTA object passed by value #[view] public fun mutates_object(mut s: S): S { s.val = s.val + 1; s } - // Error: mutable IOTA object by value + // Error: mutates IOTA object passed by value #[view] public fun mutates_object_side_effect(mut s: S): S { mutates_object_ref(&mut s); @@ -364,7 +373,7 @@ Key design decisions: ## **Backwards Compatibility** -This proposal is backwards compatible: +This proposal is backwards-compatible: - Existing code is unaffected - The `#[view]` attribute is optional @@ -374,9 +383,11 @@ 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 -As an implementation detail, view functions MAY be included in `PackageMetadata`, similarly to how authenticator functions are currently exposed. This would allow clients and tooling to reliably discover view functions without requiring additional static analysis. +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. -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 From d67d831536e7a60697ce63819098911fbbc97372 Mon Sep 17 00:00:00 2001 From: Lorenzo Benetollo Date: Fri, 15 May 2026 13:07:40 +0200 Subject: [PATCH 5/8] Disallow returning references from #[view] functions - Warning emitting changed Tighten the #[view] definition and rules to forbid returning references (in addition to IOTA objects). Update the compiler guidance to allow emitting warnings (SHOULD -> MAY) for unannotated view-like functions. Replace and expand the examples: the valid/invalid Move modules were overhauled with many more structs and function cases (generics, vectors, options, native functions, templates, and various illegal patterns) to demonstrate allowed patterns (immutable refs, primitive and store-only returns) and disallowed ones (mutable refs, objects by value, returns of objects or references, TxContext mutable refs, etc.). --- iips/IIP-0011/IIP-0011.md | 495 +++++++++++++++++++++++++++++++++----- 1 file changed, 439 insertions(+), 56 deletions(-) diff --git a/iips/IIP-0011/IIP-0011.md b/iips/IIP-0011/IIP-0011.md index d819521..115383d 100644 --- a/iips/IIP-0011/IIP-0011.md +++ b/iips/IIP-0011/IIP-0011.md @@ -126,14 +126,14 @@ This distinction matters for view functions because accepting or returning an IO 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 a value, which is not an IOTA object. +A _view function_ is a Move function that does not mutate any IOTA object and that returns a value, which is not an IOTA object or a reference. ### Rules A compliant _view function_ is declared using the `#[view]` attribute, which enforces a static validation by the compiler: - It MUST NOT create, update, or delete IOTA objects -- It MUST return a value. The returned value MUST NOT be an IOTA object by value. +- It MUST return a value. The returned value MUST NOT be an IOTA object by value or a reference. - 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 @@ -202,6 +202,7 @@ A function annotated with `#[view]` MUST satisfy: - **Return values** - MUST return values - MUST NOT return IOTA objects + - MUST NOT return references - **Parameters** - MUST NOT include IOTA objects passed by value - MAY include immutable references (`&T`) @@ -229,7 +230,7 @@ Example: ### **Warnings** -The compiler SHOULD emit a warning when a function: +The compiler MAY emit a warning when a function: - satisfies all constraints of a view function - is not annotated with #[view] @@ -256,31 +257,203 @@ Execution MUST be deterministic: ```move module view_functions::valid { - public struct S has key { id: UID, val: u64 } - public struct Dumb has copy, drop { flag: bool } + 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, + } + + #[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 + } - // Valid: returns value, immutable ref, no module calls #[view] - public fun get_value(s: &S): u64 { s.val } + 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 + } - // Valid: pure computation #[view] - public entry fun add(a: u64, b: u64): u64 { a + b } + 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 + } - // Valid: returns tuple #[view] - public fun get_pair(a: u64, b: u64): (u64, u64) { (a, b) } + 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() + } - // Valid: non object parameter #[view] - public fun deconstruct_dumb(d: Dumb): bool { - let Dumb { flag } = d; - flag + public fun vector_non_object_by_value(value: vector): u64 { + value.length() } - // Valid: non object parameter #[view] - public fun create_dumb(flag: bool): Dumb { Dumb { flag } } + 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 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 + } } ``` @@ -288,71 +461,281 @@ module view_functions::valid { ```move module view_functions::invalid { - public struct S has key, store { id: UID, val: u64 } - public struct Wrapper { o: Object } + 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, + } - // Error: void return type #[view] - public fun no_return(x: u64) { let _ = x; } + fun private_view(): u64 { + 0 + } - // Error: mutable reference parameter to TxContext #[view] - public fun share_S(val: u64, ctx: &mut TxContext) { - let s = S { id: object::new(ctx), val }; - transfer::share_object(s); + public fun no_return() { + abort 0 } - // Error: IOTA object passed by value #[view] - public fun transfer_S(s: S, recipient: address) { - transfer::public_transfer(s, recipient); + public fun object_by_value(_object: Object): u64 { + abort 0 } - // Error: IOTA object passed by value #[view] - public fun delete_object(s: S): u64 { - let S { id, val } = s; - object::delete(id); - val + public fun object_mutable_ref(_object_ref: &mut Object): u64 { + abort 0 } - // Error: mutable reference to IOTA object #[view] - public fun mutates_object_ref(s: &mut S): u64 { - s.val = s.val + 1; - s.val + public fun concrete_multiple_object_by_value( + _generic_object2: GenericObject2, + ): u64 { + abort 0 } - // Error: mutates IOTA object passed by value #[view] - public fun mutates_object(mut s: S): S { - s.val = s.val + 1; - s + public fun generic_object_by_value(_generic_object: GenericObject): u64 { + abort 0 } - // Error: mutates IOTA object passed by value #[view] - public fun mutates_object_side_effect(mut s: S): S { - mutates_object_ref(&mut s); - s + public fun generic_object_mutable_ref(_object_ref: &mut GenericObject): u64 { + abort 0 } - // Error: mutable reference #[view] - public fun edit_u64_by_ref(val: &mut u64, new_val: u64): u64 { - let old_val = *val; - *val = new_val; - old_val + public fun template_by_value(_generic_object: GenericObject): u64 { + abort 0 } - // Error: Wrapper type contains an object type #[view] - public fun delete_wrapped_object(wrapper: Wrapper): u64 { - let Wrapper { o } = wrapper; - let S { id, val } = o; - object::delete(id); - val + 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 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 native fun store_only_type_param(x: T): u64; + + #[view] + public native fun native_mut_ref(x: &mut u64): u64; } ``` From 3ad516d3fc94681e4fac25350de15f19fc61562f Mon Sep 17 00:00:00 2001 From: Levente Pap Date: Thu, 28 May 2026 09:53:45 +0200 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Lorenzo Benetollo <38067715+lollobene@users.noreply.github.com> --- iips/IIP-0011/IIP-0011.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iips/IIP-0011/IIP-0011.md b/iips/IIP-0011/IIP-0011.md index 115383d..2a5920a 100644 --- a/iips/IIP-0011/IIP-0011.md +++ b/iips/IIP-0011/IIP-0011.md @@ -114,9 +114,9 @@ These operations include: ### By-value semantics -In this proposal, a value is passed or returned _by value_ when ownership of that value is transferred, rather than when the value is accessed through a reference. +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 a value 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. +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. From 000a00b8cd150669242c8512cd08dc152e83a6f7 Mon Sep 17 00:00:00 2001 From: Lorenzo Benetollo Date: Wed, 3 Jun 2026 14:56:08 +0200 Subject: [PATCH 7/8] Allow returning references --- iips/IIP-0011/IIP-0011.md | 65 +++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/iips/IIP-0011/IIP-0011.md b/iips/IIP-0011/IIP-0011.md index 2a5920a..6f93aae 100644 --- a/iips/IIP-0011/IIP-0011.md +++ b/iips/IIP-0011/IIP-0011.md @@ -14,7 +14,7 @@ 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. +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. @@ -126,14 +126,18 @@ This distinction matters for view functions because accepting or returning an IO 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 a value, which is not an IOTA object or a reference. +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 a static validation by the compiler: +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 a value. The returned value MUST NOT be an IOTA object by value or a reference. +- 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 @@ -149,7 +153,7 @@ public fun foo(...): T { } ``` -The `#[view]` attribute defines a **compiler-enforced invariant**. +The `#[view]` attribute defines a **compiler- and verifier-enforced invariant**. ### **Nested Function Calls** @@ -169,6 +173,8 @@ cannot be invoked from a view function, as such values cannot be constructed or 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. @@ -200,11 +206,12 @@ A function annotated with `#[view]` MUST satisfy: - **Visibility** - MUST be `public` or `public entry` - **Return values** - - MUST return values - - MUST NOT return IOTA objects - - MUST NOT return references + - 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 passed by value + - 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** @@ -212,7 +219,8 @@ A function annotated with `#[view]` MUST satisfy: - MAY use immutable references - **Type Parameters** - - MUST have at least one of the `Copy` or `Drop` abilities (this ensures they are not objects) + - 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** @@ -222,7 +230,9 @@ Native functions MAY be annotated with `#[view]`. ### **Errors** -The compiler MUST reject any function annotated with `#[view]` that violates the specification. +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: @@ -423,6 +433,23 @@ module view_functions::valid { (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 native fun native_view(v: u64): u64; @@ -715,21 +742,7 @@ module view_functions::invalid { } #[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) - } + public native fun returns_mut_reference(input: &u64): &mut u64; #[view] public native fun store_only_type_param(x: T): u64; From b5fbc57e29ddcb3875e200c8047897ec20d5628f Mon Sep 17 00:00:00 2001 From: Lorenzo Benetollo Date: Wed, 3 Jun 2026 15:18:54 +0200 Subject: [PATCH 8/8] Dynamic Field Example --- iips/IIP-0011/IIP-0011.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/iips/IIP-0011/IIP-0011.md b/iips/IIP-0011/IIP-0011.md index 6f93aae..895094a 100644 --- a/iips/IIP-0011/IIP-0011.md +++ b/iips/IIP-0011/IIP-0011.md @@ -302,6 +302,10 @@ module view_functions::valid { id: iota::object::ID, } + public struct DynamicField has copy, drop, store { + value: u64, + } + #[view] public entry fun entry_view(a: u64): u64 { a @@ -450,6 +454,11 @@ module view_functions::valid { (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;