diff --git a/Cargo.lock b/Cargo.lock
index 255867dbe94008..2d697533fb94a1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11966,6 +11966,7 @@ version = "0.1.0"
 dependencies = [
  "ambassador",
  "bcs 0.1.4",
+ "better_any",
  "bytes",
  "claims",
  "crossbeam",
diff --git a/api/types/src/convert.rs b/api/types/src/convert.rs
index 7a9d699c20e12a..61b9e0f97af223 100644
--- a/api/types/src/convert.rs
+++ b/api/types/src/convert.rs
@@ -892,6 +892,11 @@ impl<'a, S: StateView> MoveConverter<'a, S> {
             MoveTypeLayout::Struct(struct_layout) => {
                 self.try_into_vm_value_struct(struct_layout, val)?
             },
+            MoveTypeLayout::Function(..) => {
+                // TODO(#15664): do we actually need this? It appears the code here is dead and
+                //   nowhere used
+                bail!("unexpected move type {:?} for value {:?}", layout, val)
+            },
 
             // Some values, e.g., signer or ones with custom serialization
             // (native), are not stored to storage and so we do not expect
diff --git a/api/types/src/move_types.rs b/api/types/src/move_types.rs
index 22342761857e0a..b8c995c92fc460 100644
--- a/api/types/src/move_types.rs
+++ b/api/types/src/move_types.rs
@@ -681,6 +681,10 @@ impl From<TypeTag> for MoveType {
                 items: Box::new(MoveType::from(*v)),
             },
             TypeTag::Struct(v) => MoveType::Struct((*v).into()),
+            TypeTag::Function(..) => {
+                // TODO(#15664): determine whether functions and closures need to be supported
+                panic!("functions not supported by API types")
+            },
         }
     }
 }
@@ -701,6 +705,10 @@ impl From<&TypeTag> for MoveType {
                 items: Box::new(MoveType::from(v.as_ref())),
             },
             TypeTag::Struct(v) => MoveType::Struct((&**v).into()),
+            TypeTag::Function(..) => {
+                // TODO(#15664): determine whether functions and closures need to be supported
+                panic!("functions not supported by API types")
+            },
         }
     }
 }
diff --git a/aptos-move/aptos-gas-schedule/src/gas_schedule/misc.rs b/aptos-move/aptos-gas-schedule/src/gas_schedule/misc.rs
index b9f4a8615c02e0..755458b36c24ae 100644
--- a/aptos-move/aptos-gas-schedule/src/gas_schedule/misc.rs
+++ b/aptos-move/aptos-gas-schedule/src/gas_schedule/misc.rs
@@ -125,6 +125,11 @@ where
         self.offset = 1;
         true
     }
+
+    #[inline]
+    fn visit_closure(&mut self, depth: usize, len: usize) -> bool {
+        self.inner.visit_closure(depth, len)
+    }
 }
 
 struct AbstractValueSizeVisitor<'a> {
@@ -200,6 +205,13 @@ impl<'a> ValueVisitor for AbstractValueSizeVisitor<'a> {
         true
     }
 
+    #[inline]
+    fn visit_closure(&mut self, _depth: usize, _len: usize) -> bool {
+        // TODO(#15664): introduce a dedicated gas parameter?
+        self.size += self.params.struct_;
+        true
+    }
+
     #[inline]
     fn visit_vec(&mut self, _depth: usize, _len: usize) -> bool {
         self.size += self.params.vector;
@@ -366,6 +378,13 @@ impl AbstractValueSizeGasParameters {
                 false
             }
 
+            #[inline]
+            fn visit_closure(&mut self, _depth: usize, _len: usize) -> bool {
+                // TODO(#15664): independent gas parameter for closures?
+                self.res = Some(self.params.struct_);
+                false
+            }
+
             #[inline]
             fn visit_vec(&mut self, _depth: usize, _len: usize) -> bool {
                 self.res = Some(self.params.vector);
@@ -509,6 +528,13 @@ impl AbstractValueSizeGasParameters {
                 false
             }
 
+            #[inline]
+            fn visit_closure(&mut self, _depth: usize, _len: usize) -> bool {
+                // TODO(#15664): independent gas parameter
+                self.res = Some(self.params.struct_);
+                false
+            }
+
             #[inline]
             fn visit_vec(&mut self, _depth: usize, _len: usize) -> bool {
                 self.res = Some(self.params.vector);
diff --git a/aptos-move/aptos-sdk-builder/src/common.rs b/aptos-move/aptos-sdk-builder/src/common.rs
index 9914b5ea613cd5..1711ecaf3414d0 100644
--- a/aptos-move/aptos-sdk-builder/src/common.rs
+++ b/aptos-move/aptos-sdk-builder/src/common.rs
@@ -45,7 +45,7 @@ fn quote_type_as_format(type_tag: &TypeTag) -> Format {
             tag if &**tag == Lazy::force(&str_tag) => Format::Seq(Box::new(Format::U8)),
             _ => type_not_allowed(type_tag),
         },
-        Signer => type_not_allowed(type_tag),
+        Signer | Function(..) => type_not_allowed(type_tag),
     }
 }
 
@@ -122,7 +122,7 @@ pub(crate) fn mangle_type(type_tag: &TypeTag) -> String {
             tag if &**tag == Lazy::force(&str_tag) => "string".into(),
             _ => type_not_allowed(type_tag),
         },
-        Signer => type_not_allowed(type_tag),
+        Signer | Function(..) => type_not_allowed(type_tag),
     }
 }
 
diff --git a/aptos-move/aptos-sdk-builder/src/golang.rs b/aptos-move/aptos-sdk-builder/src/golang.rs
index dbbbc734d0661d..17a847c0a8cebe 100644
--- a/aptos-move/aptos-sdk-builder/src/golang.rs
+++ b/aptos-move/aptos-sdk-builder/src/golang.rs
@@ -672,7 +672,7 @@ func encode_{}_argument(arg {}) []byte {{
                 U8 => ("U8Vector", default_stmt),
                 _ => common::type_not_allowed(type_tag),
             },
-            Struct(_) | Signer => common::type_not_allowed(type_tag),
+            Struct(_) | Signer | Function(..) => common::type_not_allowed(type_tag),
         };
         writeln!(
             self.out,
@@ -793,7 +793,7 @@ func decode_{0}_argument(arg aptostypes.TransactionArgument) (value {1}, err err
                 tag if &**tag == Lazy::force(&str_tag) => "[]uint8".into(),
                 _ => common::type_not_allowed(type_tag),
             },
-            Signer => common::type_not_allowed(type_tag),
+            Signer | Function(..) => common::type_not_allowed(type_tag),
         }
     }
 
@@ -820,7 +820,7 @@ func decode_{0}_argument(arg aptostypes.TransactionArgument) (value {1}, err err
                 U8 => format!("(*aptostypes.TransactionArgument__U8Vector)(&{})", name),
                 _ => common::type_not_allowed(type_tag),
             },
-            Struct(_) | Signer => common::type_not_allowed(type_tag),
+            Struct(_) | Signer | Function(..) => common::type_not_allowed(type_tag),
         }
     }
 
@@ -854,7 +854,7 @@ func decode_{0}_argument(arg aptostypes.TransactionArgument) (value {1}, err err
                 tag if &**tag == Lazy::force(&str_tag) => Some("Bytes"),
                 _ => common::type_not_allowed(type_tag),
             },
-            Signer => common::type_not_allowed(type_tag),
+            Signer | Function(..) => common::type_not_allowed(type_tag),
         }
     }
 }
diff --git a/aptos-move/aptos-sdk-builder/src/rust.rs b/aptos-move/aptos-sdk-builder/src/rust.rs
index cc01736cb3748f..70980cd5d0a5e4 100644
--- a/aptos-move/aptos-sdk-builder/src/rust.rs
+++ b/aptos-move/aptos-sdk-builder/src/rust.rs
@@ -737,7 +737,7 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy<EntryFunctionDecoderMa
                 U8 => ("U8Vector", "Some(value)".to_string()),
                 _ => common::type_not_allowed(type_tag),
             },
-            Struct(_) | Signer => common::type_not_allowed(type_tag),
+            Struct(_) | Signer | Function(..) => common::type_not_allowed(type_tag),
         };
         writeln!(
             self.out,
@@ -880,7 +880,7 @@ fn decode_{}_argument(arg: TransactionArgument) -> Option<{}> {{
                 tag if &**tag == Lazy::force(&str_tag) => "Vec<u8>".into(),
                 _ => common::type_not_allowed(type_tag),
             },
-            Signer => common::type_not_allowed(type_tag),
+            Signer | Function(..) => common::type_not_allowed(type_tag),
         }
     }
 
@@ -909,7 +909,7 @@ fn decode_{}_argument(arg: TransactionArgument) -> Option<{}> {{
                 _ => common::type_not_allowed(type_tag),
             },
 
-            Struct(_) | Signer => common::type_not_allowed(type_tag),
+            Struct(_) | Signer | Function(..) => common::type_not_allowed(type_tag),
         }
     }
 }
diff --git a/aptos-move/aptos-vm/src/move_vm_ext/session/mod.rs b/aptos-move/aptos-vm/src/move_vm_ext/session/mod.rs
index f34457f6ee2c08..b07b02cf33fe35 100644
--- a/aptos-move/aptos-vm/src/move_vm_ext/session/mod.rs
+++ b/aptos-move/aptos-vm/src/move_vm_ext/session/mod.rs
@@ -127,6 +127,7 @@ impl<'r, 'l> SessionExt<'r, 'l> {
         module_storage: &impl ModuleStorage,
     ) -> VMResult<(VMChangeSet, ModuleWriteSet)> {
         let move_vm = self.inner.get_move_vm();
+
         let function_extension = module_storage.as_function_value_extension();
 
         let resource_converter = |value: Value,
diff --git a/aptos-move/aptos-vm/src/verifier/transaction_arg_validation.rs b/aptos-move/aptos-vm/src/verifier/transaction_arg_validation.rs
index d6a96952a88e77..f93d76ed899360 100644
--- a/aptos-move/aptos-vm/src/verifier/transaction_arg_validation.rs
+++ b/aptos-move/aptos-vm/src/verifier/transaction_arg_validation.rs
@@ -208,7 +208,7 @@ pub(crate) fn is_valid_txn_arg(
                 let full_name = format!("{}::{}", st.module.short_str_lossless(), st.name);
                 allowed_structs.contains_key(&full_name)
             }),
-        Signer | Reference(_) | MutableReference(_) | TyParam(_) => false,
+        Signer | Reference(_) | MutableReference(_) | TyParam(_) | Function { .. } => false,
     }
 }
 
@@ -300,7 +300,9 @@ fn construct_arg(
                 Err(invalid_signature())
             }
         },
-        Reference(_) | MutableReference(_) | TyParam(_) => Err(invalid_signature()),
+        Reference(_) | MutableReference(_) | TyParam(_) | Function { .. } => {
+            Err(invalid_signature())
+        },
     }
 }
 
@@ -369,7 +371,9 @@ pub(crate) fn recursively_construct_arg(
         U64 => read_n_bytes(8, cursor, arg)?,
         U128 => read_n_bytes(16, cursor, arg)?,
         U256 | Address => read_n_bytes(32, cursor, arg)?,
-        Signer | Reference(_) | MutableReference(_) | TyParam(_) => return Err(invalid_signature()),
+        Signer | Reference(_) | MutableReference(_) | TyParam(_) | Function { .. } => {
+            return Err(invalid_signature())
+        },
     };
     Ok(())
 }
diff --git a/aptos-move/framework/move-stdlib/src/natives/bcs.rs b/aptos-move/framework/move-stdlib/src/natives/bcs.rs
index 0028d48319cbbd..e86962d074fb94 100644
--- a/aptos-move/framework/move-stdlib/src/natives/bcs.rs
+++ b/aptos-move/framework/move-stdlib/src/natives/bcs.rs
@@ -193,10 +193,11 @@ fn constant_serialized_size(ty_layout: &MoveTypeLayout) -> (u64, PartialVMResult
         MoveTypeLayout::Signer => Ok(None),
         // vectors have no constant size
         MoveTypeLayout::Vector(_) => Ok(None),
-        // enums have no constant size
+        // enums and functions have no constant size
         MoveTypeLayout::Struct(
             MoveStructLayout::RuntimeVariants(_) | MoveStructLayout::WithVariants(_),
-        ) => Ok(None),
+        )
+        | MoveTypeLayout::Function(..) => Ok(None),
         MoveTypeLayout::Struct(MoveStructLayout::Runtime(fields)) => {
             let mut total = Some(0);
             for field in fields {
diff --git a/aptos-move/framework/src/natives/string_utils.rs b/aptos-move/framework/src/natives/string_utils.rs
index 28e9fd50b103fa..2867fef853fa01 100644
--- a/aptos-move/framework/src/natives/string_utils.rs
+++ b/aptos-move/framework/src/natives/string_utils.rs
@@ -18,7 +18,7 @@ use move_core_types::{
 use move_vm_runtime::native_functions::NativeFunction;
 use move_vm_types::{
     loaded_data::runtime_types::Type,
-    values::{Reference, Struct, Value, Vector, VectorRef},
+    values::{Closure, Reference, Struct, Value, Vector, VectorRef},
 };
 use smallvec::{smallvec, SmallVec};
 use std::{collections::VecDeque, fmt::Write, ops::Deref};
@@ -350,6 +350,23 @@ fn native_format_impl(
             )?;
             out.push('}');
         },
+        MoveTypeLayout::Function(_) => {
+            let (fun, args) = val.value_as::<Closure>()?.unpack();
+            let data = context
+                .context
+                .function_value_extension()
+                .get_serialization_data(fun.as_ref())?;
+            out.push_str(&fun.to_stable_string());
+            format_vector(
+                context,
+                data.captured_layouts.iter(),
+                args.collect(),
+                depth,
+                !context.single_line,
+                out,
+            )?;
+            out.push(')');
+        },
 
         // This is unreachable because we check layout at the start. Still, return
         // an error to be safe.
diff --git a/aptos-move/script-composer/src/helpers.rs b/aptos-move/script-composer/src/helpers.rs
index 2bbab803fcfdc5..adb27fe8bea597 100644
--- a/aptos-move/script-composer/src/helpers.rs
+++ b/aptos-move/script-composer/src/helpers.rs
@@ -29,6 +29,11 @@ pub(crate) fn import_type_tag(
     type_tag: &TypeTag,
     module_resolver: &BTreeMap<ModuleId, CompiledModule>,
 ) -> PartialVMResult<SignatureToken> {
+    let to_list = |script_builder: &mut CompiledScriptBuilder, ts: &[TypeTag]| {
+        ts.iter()
+            .map(|t| import_type_tag(script_builder, t, module_resolver))
+            .collect::<PartialVMResult<Vec<_>>>()
+    };
     Ok(match type_tag {
         TypeTag::Address => SignatureToken::Address,
         TypeTag::U8 => SignatureToken::U8,
@@ -53,13 +58,15 @@ pub(crate) fn import_type_tag(
             } else {
                 SignatureToken::StructInstantiation(
                     struct_idx,
-                    s.type_args
-                        .iter()
-                        .map(|ty| import_type_tag(script_builder, ty, module_resolver))
-                        .collect::<PartialVMResult<Vec<_>>>()?,
+                    to_list(script_builder, &s.type_args)?,
                 )
             }
         },
+        TypeTag::Function(f) => SignatureToken::Function(
+            to_list(script_builder, &f.args)?,
+            to_list(script_builder, &f.results)?,
+            f.abilities,
+        ),
     })
 }
 
diff --git a/testsuite/fuzzer/fuzz/fuzz_targets/move/utils/helpers.rs b/testsuite/fuzzer/fuzz/fuzz_targets/move/utils/helpers.rs
index 15960f5f920aab..52c3973dec322e 100644
--- a/testsuite/fuzzer/fuzz/fuzz_targets/move/utils/helpers.rs
+++ b/testsuite/fuzzer/fuzz/fuzz_targets/move/utils/helpers.rs
@@ -6,7 +6,10 @@
 use aptos_language_e2e_tests::{account::Account, executor::FakeExecutor};
 use arbitrary::Arbitrary;
 use move_binary_format::file_format::CompiledModule;
-use move_core_types::value::{MoveStructLayout, MoveTypeLayout};
+use move_core_types::{
+    function::MoveFunctionLayout,
+    value::{MoveStructLayout, MoveTypeLayout},
+};
 
 #[macro_export]
 macro_rules! tdbg {
@@ -82,6 +85,9 @@ pub(crate) fn is_valid_layout(layout: &MoveTypeLayout) -> bool {
             }
             fields.iter().all(is_valid_layout)
         },
+        L::Function(MoveFunctionLayout(args, results, _)) => {
+            args.iter().chain(results).all(is_valid_layout)
+        },
         L::Struct(_) => {
             // decorated layouts not supported
             false
diff --git a/third_party/move/move-binary-format/src/constant.rs b/third_party/move/move-binary-format/src/constant.rs
index 77d995b4323490..bd6407714db9d3 100644
--- a/third_party/move/move-binary-format/src/constant.rs
+++ b/third_party/move/move-binary-format/src/constant.rs
@@ -40,6 +40,7 @@ fn construct_ty_for_constant(layout: &MoveTypeLayout) -> Option<SignatureToken>
             construct_ty_for_constant(l.as_ref())?,
         ))),
         MoveTypeLayout::Struct(_) => None,
+        MoveTypeLayout::Function(_) => None,
         MoveTypeLayout::Bool => Some(SignatureToken::Bool),
 
         // It is not possible to have native layout for constant values.
diff --git a/third_party/move/move-binary-format/src/errors.rs b/third_party/move/move-binary-format/src/errors.rs
index 5fe9af70b5a1d2..01d4ea7072a720 100644
--- a/third_party/move/move-binary-format/src/errors.rs
+++ b/third_party/move/move-binary-format/src/errors.rs
@@ -469,6 +469,10 @@ impl PartialVMError {
         }))
     }
 
+    pub fn new_invariant_violation(msg: impl ToString) -> PartialVMError {
+        Self::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message(msg.to_string())
+    }
+
     pub fn major_status(&self) -> StatusCode {
         self.0.major_status
     }
diff --git a/third_party/move/move-binary-format/src/normalized.rs b/third_party/move/move-binary-format/src/normalized.rs
index d938f8fbcebfe7..037f6f472f8643 100644
--- a/third_party/move/move-binary-format/src/normalized.rs
+++ b/third_party/move/move-binary-format/src/normalized.rs
@@ -398,6 +398,7 @@ impl From<TypeTag> for Type {
                 name: s.name,
                 type_arguments: s.type_args.into_iter().map(|ty| ty.into()).collect(),
             },
+            TypeTag::Function(_) => panic!("function types not supported in normalized types"),
         }
     }
 }
diff --git a/third_party/move/move-compiler/src/cfgir/ast.rs b/third_party/move/move-compiler/src/cfgir/ast.rs
index 12788db3550656..e7e2de089dd855 100644
--- a/third_party/move/move-compiler/src/cfgir/ast.rs
+++ b/third_party/move/move-compiler/src/cfgir/ast.rs
@@ -323,6 +323,7 @@ impl AstDebug for MoveValue {
             },
             V::Struct(_) => panic!("ICE struct constants not supported"),
             V::Signer(_) => panic!("ICE signer constants not supported"),
+            V::Closure(_) => panic!("ICE closures not supported"),
         }
     }
 }
diff --git a/third_party/move/move-core/types/src/function.rs b/third_party/move/move-core/types/src/function.rs
index 1543c2f92396c0..bb12be5d67d022 100644
--- a/third_party/move/move-core/types/src/function.rs
+++ b/third_party/move/move-core/types/src/function.rs
@@ -1,7 +1,13 @@
 // Copyright © Aptos Foundation
 // SPDX-License-Identifier: Apache-2.0
 
-use serde::{Deserialize, Serialize};
+use crate::{
+    ability::AbilitySet,
+    identifier::Identifier,
+    language_storage::{FunctionTag, ModuleId, TypeTag},
+    value::{MoveTypeLayout, MoveValue},
+};
+use serde::{de::Error, ser::SerializeSeq, Deserialize, Serialize};
 use std::fmt;
 
 /// A `ClosureMask` is a value which determines how to distinguish those function arguments
@@ -99,6 +105,19 @@ impl ClosureMask {
         i
     }
 
+    /// Return the # of captured arguments in the mask
+    pub fn captured_count(&self) -> u16 {
+        let mut i = 0;
+        let mut mask = self.0;
+        while mask != 0 {
+            if mask & 0x1 != 0 {
+                i += 1
+            }
+            mask >>= 1;
+        }
+        i
+    }
+
     pub fn merge_placeholder_strings(
         &self,
         arity: usize,
@@ -110,3 +129,179 @@ impl ClosureMask {
         self.compose(captured, provided)
     }
 }
+
+/// Function type layout, with arguments and result types.
+#[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq)]
+#[cfg_attr(
+    any(test, feature = "fuzzing"),
+    derive(arbitrary::Arbitrary, dearbitrary::Dearbitrary)
+)]
+pub struct MoveFunctionLayout(
+    pub Vec<MoveTypeLayout>,
+    pub Vec<MoveTypeLayout>,
+    pub AbilitySet,
+);
+
+/// A closure (function value). The closure stores the name of the
+/// function and it's type instantiation, as well as the closure
+/// mask and the captured values together with their layout. The latter
+/// allows to deserialize closures context free (without needing
+/// to lookup information about the function and its dependencies).
+#[derive(Debug, PartialEq, Eq, Clone)]
+#[cfg_attr(
+    any(test, feature = "fuzzing"),
+    derive(arbitrary::Arbitrary, dearbitrary::Dearbitrary)
+)]
+pub struct MoveClosure {
+    pub module_id: ModuleId,
+    pub fun_id: Identifier,
+    pub fun_inst: Vec<TypeTag>,
+    pub mask: ClosureMask,
+    pub captured: Vec<(MoveTypeLayout, MoveValue)>,
+}
+
+#[allow(unused)] // Currently, we do not use the expected function layout
+pub(crate) struct ClosureVisitor<'a>(pub(crate) &'a MoveFunctionLayout);
+
+impl<'d, 'a> serde::de::Visitor<'d> for ClosureVisitor<'a> {
+    type Value = MoveClosure;
+
+    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+        formatter.write_str("Closure")
+    }
+
+    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+    where
+        A: serde::de::SeqAccess<'d>,
+    {
+        let module_id = read_required_value::<_, ModuleId>(&mut seq)?;
+        let fun_id = read_required_value::<_, Identifier>(&mut seq)?;
+        let fun_inst = read_required_value::<_, Vec<TypeTag>>(&mut seq)?;
+        let mask = read_required_value::<_, ClosureMask>(&mut seq)?;
+        let mut captured = vec![];
+        for _ in 0..mask.captured_count() {
+            let layout = read_required_value::<_, MoveTypeLayout>(&mut seq)?;
+            match seq.next_element_seed(&layout)? {
+                Some(v) => captured.push((layout, v)),
+                None => return Err(A::Error::invalid_length(captured.len(), &self)),
+            }
+        }
+        // If the sequence length is known, check whether there are no extra values
+        if matches!(seq.size_hint(), Some(remaining) if remaining != 0) {
+            return Err(A::Error::invalid_length(captured.len(), &self));
+        }
+        Ok(MoveClosure {
+            module_id,
+            fun_id,
+            fun_inst,
+            mask,
+            captured,
+        })
+    }
+}
+
+fn read_required_value<'a, A, T>(seq: &mut A) -> Result<T, A::Error>
+where
+    A: serde::de::SeqAccess<'a>,
+    T: serde::de::Deserialize<'a>,
+{
+    match seq.next_element::<T>()? {
+        Some(x) => Ok(x),
+        None => Err(A::Error::custom("expected more elements")),
+    }
+}
+
+impl serde::Serialize for MoveClosure {
+    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let MoveClosure {
+            module_id,
+            fun_id,
+            fun_inst,
+            mask,
+            captured,
+        } = self;
+        let mut s = serializer.serialize_seq(Some(4 + captured.len()))?;
+        s.serialize_element(module_id)?;
+        s.serialize_element(fun_id)?;
+        s.serialize_element(fun_inst)?;
+        s.serialize_element(mask)?;
+        for (l, v) in captured {
+            s.serialize_element(l)?;
+            s.serialize_element(v)?;
+        }
+        s.end()
+    }
+}
+
+impl fmt::Display for MoveFunctionLayout {
+    fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
+        let fmt_list = |l: &[MoveTypeLayout]| {
+            l.iter()
+                .map(|t| t.to_string())
+                .collect::<Vec<_>>()
+                .join(", ")
+        };
+        let MoveFunctionLayout(args, results, abilities) = self;
+        write!(
+            f,
+            "|{}|{}{}",
+            fmt_list(args),
+            fmt_list(results),
+            abilities.display_postfix()
+        )
+    }
+}
+
+impl TryInto<FunctionTag> for &MoveFunctionLayout {
+    type Error = anyhow::Error;
+
+    fn try_into(self) -> Result<FunctionTag, Self::Error> {
+        let into_list = |ts: &[MoveTypeLayout]| {
+            ts.iter()
+                .map(|t| t.try_into())
+                .collect::<Result<Vec<TypeTag>, _>>()
+        };
+        Ok(FunctionTag {
+            args: into_list(&self.0)?,
+            results: into_list(&self.1)?,
+            abilities: self.2,
+        })
+    }
+}
+
+impl fmt::Display for MoveClosure {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let MoveClosure {
+            module_id,
+            fun_id,
+            fun_inst,
+            mask,
+            captured,
+        } = self;
+        let captured_str = mask
+            .merge_placeholder_strings(
+                mask.max_captured() + 1,
+                captured.iter().map(|v| v.1.to_string()).collect(),
+            )
+            .unwrap_or_else(|| vec!["*invalid*".to_string()])
+            .join(",");
+        let inst_str = if fun_inst.is_empty() {
+            "".to_string()
+        } else {
+            format!(
+                "<{}>",
+                fun_inst
+                    .iter()
+                    .map(|t| t.to_string())
+                    .collect::<Vec<_>>()
+                    .join(",")
+            )
+        };
+        write!(
+            f,
+            // this will print `a::m::f<T>(a1,_,a2,_)`
+            "{}::{}{}({})",
+            module_id, fun_id, inst_str, captured_str
+        )
+    }
+}
diff --git a/third_party/move/move-core/types/src/language_storage.rs b/third_party/move/move-core/types/src/language_storage.rs
index 821b2e22c43d03..010417247d18d9 100644
--- a/third_party/move/move-core/types/src/language_storage.rs
+++ b/third_party/move/move-core/types/src/language_storage.rs
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use crate::{
+    ability::AbilitySet,
     account_address::AccountAddress,
     identifier::{IdentStr, Identifier},
     parser::{parse_module_id, parse_struct_tag, parse_type_tag},
@@ -67,6 +68,15 @@ pub enum TypeTag {
     U32,
     #[serde(rename = "u256", alias = "U256")]
     U256,
+
+    // NOTE: added in bytecode version v8
+    Function(
+        #[serde(
+            serialize_with = "safe_serialize::type_tag_recursive_serialize",
+            deserialize_with = "safe_serialize::type_tag_recursive_deserialize"
+        )]
+        Box<FunctionTag>,
+    ),
 }
 
 impl TypeTag {
@@ -82,6 +92,7 @@ impl TypeTag {
     /// to change and should not be used inside stable code.
     pub fn to_canonical_string(&self) -> String {
         use TypeTag::*;
+
         match self {
             Bool => "bool".to_owned(),
             U8 => "u8".to_owned(),
@@ -94,6 +105,25 @@ impl TypeTag {
             Signer => "signer".to_owned(),
             Vector(t) => format!("vector<{}>", t.to_canonical_string()),
             Struct(s) => s.to_canonical_string(),
+            Function(f) => {
+                let fmt_list = |l: &[TypeTag]| -> String {
+                    l.iter()
+                        .map(|t| t.to_canonical_string())
+                        .collect::<Vec<_>>()
+                        .join(",")
+                };
+                let FunctionTag {
+                    args,
+                    results,
+                    abilities,
+                } = f.as_ref();
+                format!(
+                    "|{}|{}{}",
+                    fmt_list(args),
+                    fmt_list(results),
+                    abilities.display_postfix()
+                )
+            },
         }
     }
 }
@@ -193,6 +223,19 @@ impl FromStr for StructTag {
     }
 }
 
+#[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Clone, PartialOrd, Ord)]
+#[cfg_attr(
+    feature = "fuzzing",
+    derive(arbitrary::Arbitrary, dearbitrary::Dearbitrary)
+)]
+#[cfg_attr(any(test, feature = "fuzzing"), derive(Arbitrary))]
+#[cfg_attr(any(test, feature = "fuzzing"), proptest(no_params))]
+pub struct FunctionTag {
+    pub args: Vec<TypeTag>,
+    pub results: Vec<TypeTag>,
+    pub abilities: AbilitySet,
+}
+
 /// Represents the initial key into global storage where we first index by the address, and then
 /// the struct tag
 #[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Clone, PartialOrd, Ord)]
@@ -320,6 +363,7 @@ impl Display for TypeTag {
     fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
         match self {
             TypeTag::Struct(s) => write!(f, "{}", s),
+            TypeTag::Function(_) => write!(f, "{}", self.to_canonical_string()),
             TypeTag::Vector(ty) => write!(f, "vector<{}>", ty),
             TypeTag::U8 => write!(f, "u8"),
             TypeTag::U16 => write!(f, "u16"),
@@ -334,6 +378,12 @@ impl Display for TypeTag {
     }
 }
 
+impl Display for FunctionTag {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        TypeTag::Function(Box::new(self.clone())).fmt(f)
+    }
+}
+
 impl Display for ResourceKey {
     fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
         write!(f, "0x{}/{}", self.address.short_str_lossless(), self.type_)
diff --git a/third_party/move/move-core/types/src/safe_serialize.rs b/third_party/move/move-core/types/src/safe_serialize.rs
index 42ab4b4a897089..351e8e42ddfd9f 100644
--- a/third_party/move/move-core/types/src/safe_serialize.rs
+++ b/third_party/move/move-core/types/src/safe_serialize.rs
@@ -4,9 +4,6 @@
 
 //! Custom serializers which track recursion nesting with a thread local,
 //! and otherwise delegate to the derived serializers.
-//!
-//! This is currently only implemented for type tags, but can be easily
-//! generalized, as the only type-tag specific thing is the allowed nesting.
 
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use std::cell::RefCell;
diff --git a/third_party/move/move-core/types/src/transaction_argument.rs b/third_party/move/move-core/types/src/transaction_argument.rs
index 701fbce112f484..da85bc7ccabe57 100644
--- a/third_party/move/move-core/types/src/transaction_argument.rs
+++ b/third_party/move/move-core/types/src/transaction_argument.rs
@@ -82,7 +82,7 @@ impl TryFrom<MoveValue> for TransactionArgument {
                     })
                     .collect::<Result<Vec<u8>>>()?,
             ),
-            MoveValue::Signer(_) | MoveValue::Struct(_) => {
+            MoveValue::Signer(_) | MoveValue::Struct(_) | MoveValue::Closure(_) => {
                 return Err(anyhow!("invalid transaction argument: {:?}", val))
             },
             MoveValue::U16(i) => TransactionArgument::U16(i),
diff --git a/third_party/move/move-core/types/src/value.rs b/third_party/move/move-core/types/src/value.rs
index f90fd9e6cec2cd..28805c5315e744 100644
--- a/third_party/move/move-core/types/src/value.rs
+++ b/third_party/move/move-core/types/src/value.rs
@@ -9,6 +9,7 @@
 
 use crate::{
     account_address::AccountAddress,
+    function::{ClosureVisitor, MoveClosure, MoveFunctionLayout},
     ident_str,
     identifier::Identifier,
     language_storage::{ModuleId, StructTag, TypeTag},
@@ -135,6 +136,8 @@ pub enum MoveValue {
     U16(u16),
     U32(u32),
     U256(u256::U256),
+    // Added in bytecode version v8
+    Closure(Box<MoveClosure>),
 }
 
 /// A layout associated with a named field
@@ -263,6 +266,10 @@ pub enum MoveTypeLayout {
     // TODO[agg_v2](?): Do we need a layout here if we have custom serde
     //                  implementations available?
     Native(IdentifierMappingKind, Box<MoveTypeLayout>),
+
+    // Added in bytecode version v8
+    #[serde(rename(serialize = "fun", deserialize = "fun"))]
+    Function(MoveFunctionLayout),
 }
 
 impl MoveValue {
@@ -274,6 +281,10 @@ impl MoveValue {
         bcs::to_bytes(self).ok()
     }
 
+    pub fn closure(c: MoveClosure) -> MoveValue {
+        Self::Closure(Box::new(c))
+    }
+
     pub fn vector_u8(v: Vec<u8>) -> Self {
         MoveValue::Vector(v.into_iter().map(MoveValue::U8).collect())
     }
@@ -556,6 +567,9 @@ impl<'d> serde::de::DeserializeSeed<'d> for &MoveTypeLayout {
             },
             MoveTypeLayout::Signer => Err(D::Error::custom("cannot deserialize signer")),
             MoveTypeLayout::Struct(ty) => Ok(MoveValue::Struct(ty.deserialize(deserializer)?)),
+            MoveTypeLayout::Function(fun) => Ok(MoveValue::Closure(Box::new(
+                deserializer.deserialize_seq(ClosureVisitor(fun))?,
+            ))),
             MoveTypeLayout::Vector(layout) => Ok(MoveValue::Vector(
                 deserializer.deserialize_seq(VectorElementVisitor(layout))?,
             )),
@@ -750,6 +764,7 @@ impl serde::Serialize for MoveValue {
     fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
         match self {
             MoveValue::Struct(s) => s.serialize(serializer),
+            MoveValue::Closure(c) => c.serialize(serializer),
             MoveValue::Bool(b) => serializer.serialize_bool(*b),
             MoveValue::U8(i) => serializer.serialize_u8(*i),
             MoveValue::U16(i) => serializer.serialize_u16(*i),
@@ -877,6 +892,7 @@ impl fmt::Display for MoveTypeLayout {
             Address => write!(f, "address"),
             Vector(typ) => write!(f, "vector<{}>", typ),
             Struct(s) => fmt::Display::fmt(s, f),
+            Function(fun) => fmt::Display::fmt(fun, f),
             Signer => write!(f, "signer"),
             // TODO[agg_v2](cleanup): consider printing the tag as well.
             Native(_, typ) => write!(f, "native<{}>", typ),
@@ -944,6 +960,7 @@ impl TryInto<TypeTag> for &MoveTypeLayout {
             MoveTypeLayout::Signer => TypeTag::Signer,
             MoveTypeLayout::Vector(v) => TypeTag::Vector(Box::new(v.as_ref().try_into()?)),
             MoveTypeLayout::Struct(v) => TypeTag::Struct(Box::new(v.try_into()?)),
+            MoveTypeLayout::Function(f) => TypeTag::Function(Box::new(f.try_into()?)),
 
             // Native layout variant is only used by MoveVM, and is irrelevant
             // for type tags which are used to key resources in the global state.
@@ -981,6 +998,7 @@ impl fmt::Display for MoveValue {
             MoveValue::Signer(a) => write!(f, "signer({})", a.to_hex_literal()),
             MoveValue::Vector(v) => fmt_list(f, "vector[", v, "]"),
             MoveValue::Struct(s) => fmt::Display::fmt(s, f),
+            MoveValue::Closure(c) => fmt::Display::fmt(c, f),
         }
     }
 }
diff --git a/third_party/move/move-ir/types/src/ast.rs b/third_party/move/move-ir/types/src/ast.rs
index 40fa5695baf430..4718cf90841705 100644
--- a/third_party/move/move-ir/types/src/ast.rs
+++ b/third_party/move/move-ir/types/src/ast.rs
@@ -1807,7 +1807,7 @@ fn format_move_value(v: &MoveValue) -> String {
                 .join(", ");
             format!("vector[{}]", items)
         },
-        MoveValue::Struct(_) | MoveValue::Signer(_) => {
+        MoveValue::Struct(_) | MoveValue::Signer(_) | MoveValue::Closure(_) => {
             panic!("Should be inexpressible as a constant")
         },
         MoveValue::U16(u) => format!("{}u16", u),
diff --git a/third_party/move/move-model/src/builder/exp_builder.rs b/third_party/move/move-model/src/builder/exp_builder.rs
index b515e254d08994..3cf66a1ca2f1e7 100644
--- a/third_party/move/move-model/src/builder/exp_builder.rs
+++ b/third_party/move/move-model/src/builder/exp_builder.rs
@@ -5484,30 +5484,10 @@ impl<'env, 'translator, 'module_translator> ExpTranslator<'env, 'translator, 'mo
                     Value::Vector(b)
                 },
             },
-            (Type::Primitive(_), MoveValue::Vector(_))
-            | (Type::Primitive(_), MoveValue::Struct(_))
-            | (Type::Tuple(_), MoveValue::Vector(_))
-            | (Type::Tuple(_), MoveValue::Struct(_))
-            | (Type::Vector(_), MoveValue::Struct(_))
-            | (Type::Struct(_, _, _), MoveValue::Vector(_))
-            | (Type::Struct(_, _, _), MoveValue::Struct(_))
-            | (Type::TypeParameter(_), MoveValue::Vector(_))
-            | (Type::TypeParameter(_), MoveValue::Struct(_))
-            | (Type::Reference(_, _), MoveValue::Vector(_))
-            | (Type::Reference(_, _), MoveValue::Struct(_))
-            | (Type::Fun(..), MoveValue::Vector(_))
-            | (Type::Fun(..), MoveValue::Struct(_))
-            | (Type::TypeDomain(_), MoveValue::Vector(_))
-            | (Type::TypeDomain(_), MoveValue::Struct(_))
-            | (Type::ResourceDomain(_, _, _), MoveValue::Vector(_))
-            | (Type::ResourceDomain(_, _, _), MoveValue::Struct(_))
-            | (Type::Error, MoveValue::Vector(_))
-            | (Type::Error, MoveValue::Struct(_))
-            | (Type::Var(_), MoveValue::Vector(_))
-            | (Type::Var(_), MoveValue::Struct(_)) => {
+            _ => {
                 self.error(
                     loc,
-                    &format!("Not yet supported constant value: {:?}", value),
+                    &format!("Not supported constant value/type combination: {}", value),
                 );
                 Value::Bool(false)
             },
diff --git a/third_party/move/move-model/src/ty.rs b/third_party/move/move-model/src/ty.rs
index f3ba39c623ee05..69294a3b9f6b6c 100644
--- a/third_party/move/move-model/src/ty.rs
+++ b/third_party/move/move-model/src/ty.rs
@@ -24,7 +24,7 @@ use move_binary_format::{
 };
 use move_core_types::{
     ability::{Ability, AbilitySet},
-    language_storage::{StructTag, TypeTag},
+    language_storage::{FunctionTag, StructTag, TypeTag},
     u256::U256,
 };
 use num::BigInt;
@@ -1333,6 +1333,21 @@ impl Type {
                 Struct(qid.module_id, qid.id, type_args)
             },
             TypeTag::Vector(type_param) => Vector(Box::new(Self::from_type_tag(type_param, env))),
+            TypeTag::Function(fun) => {
+                let FunctionTag {
+                    args,
+                    results,
+                    abilities,
+                } = fun.as_ref();
+                let from_vec = |ts: &[TypeTag]| {
+                    Type::tuple(ts.iter().map(|t| Type::from_type_tag(t, env)).collect_vec())
+                };
+                Fun(
+                    Box::new(from_vec(args)),
+                    Box::new(from_vec(results)),
+                    *abilities,
+                )
+            },
         }
     }
 
diff --git a/third_party/move/move-stdlib/src/natives/debug.rs b/third_party/move/move-stdlib/src/natives/debug.rs
index 38876b7685fdb6..408b5a084e8994 100644
--- a/third_party/move/move-stdlib/src/natives/debug.rs
+++ b/third_party/move/move-stdlib/src/natives/debug.rs
@@ -460,6 +460,9 @@ mod testing {
                     )?;
                 }
             },
+            MoveValue::Closure(clos) => {
+                write!(out, "{}", clos).map_err(fmt_error_to_partial_vm_error)?;
+            },
             MoveValue::Struct(move_struct) => match move_struct {
                 MoveStruct::WithTypes {
                     _type_: type_,
diff --git a/third_party/move/move-vm/runtime/Cargo.toml b/third_party/move/move-vm/runtime/Cargo.toml
index 4b20b934b638ca..e3047e2a5ce068 100644
--- a/third_party/move/move-vm/runtime/Cargo.toml
+++ b/third_party/move/move-vm/runtime/Cargo.toml
@@ -37,7 +37,7 @@ hex = { workspace = true }
 move-binary-format = { workspace = true, features = ["fuzzing"] }
 move-compiler = { workspace = true }
 move-ir-compiler = { workspace = true }
-move-vm-test-utils ={ workspace = true }
+move-vm-test-utils = { workspace = true }
 proptest = { workspace = true }
 
 [features]
diff --git a/third_party/move/move-vm/runtime/src/interpreter.rs b/third_party/move/move-vm/runtime/src/interpreter.rs
index f20f3b3b0cd98e..30faf540459e85 100644
--- a/third_party/move/move-vm/runtime/src/interpreter.rs
+++ b/third_party/move/move-vm/runtime/src/interpreter.rs
@@ -394,11 +394,9 @@ impl InterpreterImpl {
 
                     // Charge gas
                     let module_id = function.module_id().ok_or_else(|| {
-                        let err =
-                            PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)
-                                .with_message(
-                                    "Failed to get native function module id".to_string(),
-                                );
+                        let err = PartialVMError::new_invariant_violation(
+                            "Failed to get native function module id".to_string(),
+                        );
                         set_err_info!(current_frame, err)
                     })?;
                     gas_meter
@@ -490,8 +488,9 @@ impl InterpreterImpl {
                     let module_id = function
                         .module_id()
                         .ok_or_else(|| {
-                            PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)
-                                .with_message("Failed to get native function module id".to_string())
+                            PartialVMError::new_invariant_violation(
+                                "Failed to get native function module id",
+                            )
                         })
                         .map_err(|e| set_err_info!(current_frame, e))?;
                     // Charge gas
@@ -765,13 +764,9 @@ impl InterpreterImpl {
                 // Paranoid check to protect us against incorrect native function implementations. A native function that
                 // returns a different number of values than its declared types will trigger this check.
                 if return_values.len() != function.return_tys().len() {
-                    return Err(
-                        PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)
-                            .with_message(
-                            "Arity mismatch: return value count does not match return type count"
-                                .to_string(),
-                        ),
-                    );
+                    return Err(PartialVMError::new_invariant_violation(
+                        "Arity mismatch: return value count does not match return type count",
+                    ));
                 }
                 // Put return values on the top of the operand stack, where the caller will find them.
                 // This is one of only two times the operand stack is shared across call stack frames; the other is in handling
@@ -795,13 +790,14 @@ impl InterpreterImpl {
                 Err(PartialVMError::new(StatusCode::ABORTED).with_sub_status(abort_code))
             },
             NativeResult::OutOfGas { partial_cost } => {
-                let err = match gas_meter.charge_native_function(
-                    partial_cost,
-                    Option::<std::iter::Empty<&Value>>::None,
-                ) {
+                let err = match gas_meter
+                    .charge_native_function(partial_cost, Option::<std::iter::Empty<&Value>>::None)
+                {
                     Err(err) if err.major_status() == StatusCode::OUT_OF_GAS => err,
-                    Ok(_) | Err(_) => PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message(
-                        "The partial cost returned by the native function did not cause the gas meter to trigger an OutOfGas error, at least one of them is violating the contract".to_string()
+                    Ok(_) | Err(_) => PartialVMError::new_invariant_violation(
+                        "The partial cost returned by the native function did \
+                        not cause the gas meter to trigger an OutOfGas error, at least \
+                        one of them is violating the contract",
                     ),
                 };
 
@@ -1609,6 +1605,18 @@ fn check_depth_of_type_impl(
             )?;
             check_depth!(formula.solve(&ty_arg_depths))
         },
+        Type::Function { args, results, .. } => {
+            let mut ty_max = depth;
+            for ty in args.iter().chain(results.iter()).map(|rc| rc.as_ref()) {
+                ty_max = ty_max.max(check_depth_of_type_impl(
+                    resolver,
+                    ty,
+                    max_depth,
+                    check_depth!(1),
+                )?);
+            }
+            ty_max
+        },
         Type::TyParam(_) => {
             return Err(
                 PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)
diff --git a/third_party/move/move-vm/runtime/src/loader/function.rs b/third_party/move/move-vm/runtime/src/loader/function.rs
index e65b3c2c8ca78f..8ad45277fbbb13 100644
--- a/third_party/move/move-vm/runtime/src/loader/function.rs
+++ b/third_party/move/move-vm/runtime/src/loader/function.rs
@@ -8,8 +8,10 @@ use crate::{
         Resolver, Script,
     },
     native_functions::{NativeFunction, NativeFunctions, UnboxedNativeFunction},
+    storage::ty_tag_converter::TypeTagConverter,
     ModuleStorage,
 };
+use better_any::{Tid, TidAble, TidExt};
 use move_binary_format::{
     access::ModuleAccess,
     binary_views::BinaryIndexedView,
@@ -17,13 +19,21 @@ use move_binary_format::{
     file_format::{Bytecode, CompiledModule, FunctionDefinitionIndex, Visibility},
 };
 use move_core_types::{
-    ability::AbilitySet, identifier::Identifier, language_storage::ModuleId, vm_status::StatusCode,
+    ability::{Ability, AbilitySet},
+    function::ClosureMask,
+    identifier::{IdentStr, Identifier},
+    language_storage::{ModuleId, TypeTag},
+    value::MoveTypeLayout,
+    vm_status::StatusCode,
 };
-use move_vm_types::loaded_data::{
-    runtime_access_specifier::AccessSpecifier,
-    runtime_types::{StructIdentifier, Type},
+use move_vm_types::{
+    loaded_data::{
+        runtime_access_specifier::AccessSpecifier,
+        runtime_types::{StructIdentifier, Type},
+    },
+    values::{AbstractFunction, SerializedFunctionData},
 };
-use std::{fmt::Debug, sync::Arc};
+use std::{cell::RefCell, cmp::Ordering, fmt::Debug, rc::Rc, sync::Arc};
 
 /// A runtime function definition representation.
 pub struct Function {
@@ -45,12 +55,14 @@ pub struct Function {
 }
 
 /// For loaded function representation, specifies the owner: a script or a module.
+#[derive(Clone)]
 pub(crate) enum LoadedFunctionOwner {
     Script(Arc<Script>),
     Module(Arc<Module>),
 }
 
 /// A loaded runtime function representation along with type arguments used to instantiate it.
+#[derive(Clone)]
 pub struct LoadedFunction {
     pub(crate) owner: LoadedFunctionOwner,
     // A set of verified type arguments provided for this definition. If
@@ -60,16 +72,256 @@ pub struct LoadedFunction {
     pub(crate) function: Arc<Function>,
 }
 
+/// A lazy loaded function, which can either be unresolved (as resulting
+/// from deserialization) or resolved, and then forwarding to a
+/// `LoadedFunction`. This is wrapped into a Rc so one can clone the
+/// function while sharing the loading state.
+#[derive(Clone, Tid)]
+pub(crate) struct LazyLoadedFunction(pub(crate) Rc<RefCell<LazyLoadedFunctionState>>);
+
+#[derive(Clone)]
+pub(crate) enum LazyLoadedFunctionState {
+    Unresolved {
+        data: SerializedFunctionData,
+        resolution_error: Option<PartialVMError>,
+    },
+    Resolved {
+        fun: Rc<LoadedFunction>,
+        fun_inst: Vec<TypeTag>,
+        mask: ClosureMask,
+    },
+}
+
+impl LazyLoadedFunction {
+    pub(crate) fn new_unresolved(data: SerializedFunctionData) -> Self {
+        Self(Rc::new(RefCell::new(LazyLoadedFunctionState::Unresolved {
+            data,
+            resolution_error: None,
+        })))
+    }
+
+    #[allow(unused)]
+    pub(crate) fn new_resolved(
+        converter: &TypeTagConverter,
+        fun: Rc<LoadedFunction>,
+        mask: ClosureMask,
+    ) -> PartialVMResult<Self> {
+        let fun_inst = fun
+            .ty_args
+            .iter()
+            .map(|t| converter.ty_to_ty_tag(t))
+            .collect::<PartialVMResult<Vec<_>>>()?;
+        Ok(Self(Rc::new(RefCell::new(
+            LazyLoadedFunctionState::Resolved {
+                fun,
+                fun_inst,
+                mask,
+            },
+        ))))
+    }
+
+    pub(crate) fn expect_this_impl(
+        fun: &dyn AbstractFunction,
+    ) -> PartialVMResult<&LazyLoadedFunction> {
+        fun.downcast_ref::<LazyLoadedFunction>().ok_or_else(|| {
+            PartialVMError::new_invariant_violation("unexpected abstract function implementation")
+        })
+    }
+
+    /// Access name components independent of resolution state. Since RefCell is in the play,
+    /// the accessor is passed in as a function.
+    pub(crate) fn with_name_and_inst<T>(
+        &self,
+        action: impl FnOnce(Option<&ModuleId>, &IdentStr, &[TypeTag]) -> T,
+    ) -> T {
+        match &*self.0.borrow() {
+            LazyLoadedFunctionState::Unresolved {
+                data:
+                    SerializedFunctionData {
+                        module_id,
+                        fun_id,
+                        fun_inst,
+                        ..
+                    },
+                ..
+            } => action(Some(module_id), fun_id, fun_inst),
+            LazyLoadedFunctionState::Resolved { fun, fun_inst, .. } => {
+                action(fun.module_id(), fun.name_id(), fun_inst)
+            },
+        }
+    }
+
+    /// Executed an action with the resolved loaded function. If the function hasn't been
+    /// loaded yet, it will be loaded now.
+    #[allow(unused)]
+    pub(crate) fn with_resolved_function<T>(
+        &self,
+        storage: &dyn ModuleStorage,
+        action: impl FnOnce(Rc<LoadedFunction>) -> PartialVMResult<T>,
+    ) -> PartialVMResult<T> {
+        let mut state = self.0.borrow_mut();
+        match &mut *state {
+            LazyLoadedFunctionState::Resolved { fun, .. } => action(fun.clone()),
+            LazyLoadedFunctionState::Unresolved {
+                resolution_error: Some(e),
+                ..
+            } => Err(e.clone()),
+            LazyLoadedFunctionState::Unresolved {
+                data:
+                    SerializedFunctionData {
+                        module_id,
+                        fun_id,
+                        fun_inst,
+                        mask,
+                        captured_layouts,
+                    },
+                resolution_error,
+            } => match Self::resolve(
+                storage,
+                module_id,
+                fun_id,
+                fun_inst,
+                *mask,
+                captured_layouts,
+            ) {
+                Ok(fun) => {
+                    let result = action(fun.clone());
+                    *state = LazyLoadedFunctionState::Resolved {
+                        fun,
+                        fun_inst: fun_inst.clone(),
+                        mask: *mask,
+                    };
+                    result
+                },
+                Err(e) => {
+                    *resolution_error = Some(e.clone());
+                    Err(e)
+                },
+            },
+        }
+    }
+
+    /// Resolves a function into a loaded function. This verifies existence of the named
+    /// function as well as whether it has the type used for deserializing the captured values.
+    fn resolve(
+        module_storage: &dyn ModuleStorage,
+        module_id: &ModuleId,
+        fun_id: &IdentStr,
+        fun_inst: &[TypeTag],
+        mask: ClosureMask,
+        captured_layouts: &[MoveTypeLayout],
+    ) -> PartialVMResult<Rc<LoadedFunction>> {
+        let (module, function) = module_storage
+            .fetch_function_definition(module_id.address(), module_id.name(), fun_id)
+            .map_err(|err| err.to_partial())?;
+        let ty_args = fun_inst
+            .iter()
+            .map(|t| module_storage.fetch_ty(t))
+            .collect::<PartialVMResult<Vec<_>>>()?;
+
+        // Verify that the function arguments match the types used for deserialization.
+        let captured_arg_types = mask.extract(function.param_tys(), true);
+        let converter = TypeTagConverter::new(module_storage.runtime_environment());
+        let mut ok = captured_arg_types.len() == captured_layouts.len();
+        if ok {
+            for (ty, layout) in captured_arg_types.into_iter().zip(captured_layouts) {
+                // Convert layout into TypeTag
+                let serialized_tag: TypeTag = layout.try_into().map_err(|_| {
+                    PartialVMError::new_invariant_violation(
+                        "unexpected type tag conversion failure",
+                    )
+                })?;
+                let arg_tag = converter.ty_to_ty_tag(ty)?;
+                if arg_tag != serialized_tag {
+                    ok = false;
+                    break;
+                }
+            }
+        }
+        if ok {
+            Ok(Rc::new(LoadedFunction {
+                owner: LoadedFunctionOwner::Module(module),
+                ty_args,
+                function,
+            }))
+        } else {
+            Err(
+                PartialVMError::new(StatusCode::FUNCTION_RESOLUTION_FAILURE).with_message(
+                    "inconsistency between types of serialized captured \
+                values and function parameter types"
+                        .to_string(),
+                ),
+            )
+        }
+    }
+}
+
+impl AbstractFunction for LazyLoadedFunction {
+    fn closure_mask(&self) -> ClosureMask {
+        let state = self.0.borrow();
+        match &*state {
+            LazyLoadedFunctionState::Resolved { mask, .. } => *mask,
+            LazyLoadedFunctionState::Unresolved {
+                data: SerializedFunctionData { mask, .. },
+                ..
+            } => *mask,
+        }
+    }
+
+    fn cmp_dyn(&self, other: &dyn AbstractFunction) -> PartialVMResult<Ordering> {
+        let other = LazyLoadedFunction::expect_this_impl(other)?;
+        self.with_name_and_inst(|mid1, fid1, inst1| {
+            other.with_name_and_inst(|mid2, fid2, inst2| {
+                Ok(mid1
+                    .cmp(&mid2)
+                    .then_with(|| fid1.cmp(fid2))
+                    .then_with(|| inst1.cmp(inst2)))
+            })
+        })
+    }
+
+    fn clone_dyn(&self) -> PartialVMResult<Box<dyn AbstractFunction>> {
+        Ok(Box::new(self.clone()))
+    }
+
+    fn to_stable_string(&self) -> String {
+        self.with_name_and_inst(|module_id, fun_id, fun_inst| {
+            let prefix = if let Some(m) = module_id {
+                format!("0x{}::{}::", m.address(), m.name())
+            } else {
+                "".to_string()
+            };
+            let fun_inst_str = if fun_inst.is_empty() {
+                "".to_string()
+            } else {
+                format!(
+                    "<{}>",
+                    fun_inst
+                        .iter()
+                        .map(|t| t.to_canonical_string())
+                        .collect::<Vec<_>>()
+                        .join(",")
+                )
+            };
+            format!("{}::{}:{}", prefix, fun_id, fun_inst_str)
+        })
+    }
+}
+
 impl LoadedFunction {
     /// Returns type arguments used to instantiate the loaded function.
     pub fn ty_args(&self) -> &[Type] {
         &self.ty_args
     }
 
+    pub fn abilities(&self) -> AbilitySet {
+        self.function.abilities()
+    }
+
     /// Returns the corresponding module id of this function, i.e., its address and module name.
     pub fn module_id(&self) -> Option<&ModuleId> {
         match &self.owner {
-            LoadedFunctionOwner::Module(m) => Some(m.self_id()),
+            LoadedFunctionOwner::Module(m) => Some(Module::self_id(m)),
             LoadedFunctionOwner::Script(_) => None,
         }
     }
@@ -79,6 +331,11 @@ impl LoadedFunction {
         self.function.name()
     }
 
+    /// Returns the id of this function's name.
+    pub fn name_id(&self) -> &IdentStr {
+        self.function.name_id()
+    }
+
     /// Returns true if the loaded function has friend or private visibility.
     pub fn is_friend_or_private(&self) -> bool {
         self.function.is_friend_or_private()
@@ -254,6 +511,10 @@ impl Function {
         self.name.as_str()
     }
 
+    pub(crate) fn name_id(&self) -> &IdentStr {
+        &self.name
+    }
+
     pub fn ty_param_abilities(&self) -> &[AbilitySet] {
         &self.ty_param_abilities
     }
@@ -270,6 +531,36 @@ impl Function {
         &self.param_tys
     }
 
+    /// Creates the function type instance for this function. This requires cloning
+    /// the parameter and result types.
+    pub fn create_function_type(&self) -> Type {
+        Type::Function {
+            args: self
+                .param_tys
+                .iter()
+                .map(|t| triomphe::Arc::new(t.clone()))
+                .collect(),
+            results: self
+                .return_tys
+                .iter()
+                .map(|t| triomphe::Arc::new(t.clone()))
+                .collect(),
+            abilities: self.abilities(),
+        }
+    }
+
+    /// Returns the abilities associated with this function, without consideration of any captured
+    /// closure arguments. By default, this is copy and drop, and if the function is
+    /// immutable (public), also store.
+    pub fn abilities(&self) -> AbilitySet {
+        let result = AbilitySet::singleton(Ability::Copy).add(Ability::Drop);
+        if !self.is_friend_or_private {
+            result.add(Ability::Store)
+        } else {
+            result
+        }
+    }
+
     pub fn is_native(&self) -> bool {
         self.is_native
     }
diff --git a/third_party/move/move-vm/runtime/src/loader/mod.rs b/third_party/move/move-vm/runtime/src/loader/mod.rs
index 2753b3775b8502..b611f6ae395684 100644
--- a/third_party/move/move-vm/runtime/src/loader/mod.rs
+++ b/third_party/move/move-vm/runtime/src/loader/mod.rs
@@ -57,17 +57,22 @@ use crate::{
     },
 };
 pub use function::{Function, LoadedFunction};
-pub(crate) use function::{FunctionHandle, FunctionInstantiation, LoadedFunctionOwner};
+pub(crate) use function::{
+    FunctionHandle, FunctionInstantiation, LazyLoadedFunction, LazyLoadedFunctionState,
+    LoadedFunctionOwner,
+};
 pub use modules::Module;
 pub(crate) use modules::{LegacyModuleCache, LegacyModuleStorage, LegacyModuleStorageAdapter};
 use move_binary_format::file_format::{
     StructVariantHandleIndex, StructVariantInstantiationIndex, TypeParameterIndex,
     VariantFieldHandleIndex, VariantFieldInstantiationIndex, VariantIndex,
 };
+use move_core_types::language_storage::FunctionTag;
 use move_vm_metrics::{Timer, VM_TIMER};
 use move_vm_types::{
     loaded_data::runtime_types::{DepthFormula, StructLayout, TypeBuilder},
     value_serde::FunctionValueExtension,
+    values::{AbstractFunction, SerializedFunctionData},
 };
 pub use script::Script;
 pub(crate) use script::ScriptCache;
@@ -1662,6 +1667,26 @@ impl<'a> FunctionValueExtension for Resolver<'a> {
         };
         function_value_extension.get_function_arg_tys(module_id, function_name, ty_arg_tags)
     }
+
+    fn create_from_serialization_data(
+        &self,
+        data: SerializedFunctionData,
+    ) -> PartialVMResult<Box<dyn AbstractFunction>> {
+        let function_value_extension = FunctionValueExtensionAdapter {
+            module_storage: self.module_storage,
+        };
+        function_value_extension.create_from_serialization_data(data)
+    }
+
+    fn get_serialization_data(
+        &self,
+        fun: &dyn AbstractFunction,
+    ) -> PartialVMResult<SerializedFunctionData> {
+        let function_value_extension = FunctionValueExtensionAdapter {
+            module_storage: self.module_storage,
+        };
+        function_value_extension.get_serialization_data(fun)
+    }
 }
 
 /// Maximal depth of a value in terms of type depth.
@@ -1777,6 +1802,24 @@ impl LoaderV1 {
             Type::StructInstantiation { idx, ty_args, .. } => TypeTag::Struct(Box::new(
                 self.struct_name_to_type_tag(*idx, ty_args, gas_context)?,
             )),
+            Type::Function {
+                args,
+                results,
+                abilities,
+            } => {
+                let to_vec = |ts: &[triomphe::Arc<Type>],
+                              gas_ctx: &mut PseudoGasContext|
+                 -> PartialVMResult<Vec<TypeTag>> {
+                    ts.iter()
+                        .map(|t| self.type_to_type_tag_impl(t, gas_ctx))
+                        .collect()
+                };
+                TypeTag::Function(Box::new(FunctionTag {
+                    args: to_vec(args, gas_context)?,
+                    results: to_vec(results, gas_context)?,
+                    abilities: *abilities,
+                }))
+            },
             Type::Reference(_) | Type::MutableReference(_) | Type::TyParam(_) => {
                 return Err(
                     PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)
@@ -1893,6 +1936,18 @@ impl Loader {
                 subst_struct_formula.scale(1);
                 subst_struct_formula
             },
+            Type::Function {
+                args,
+                results,
+                abilities: _,
+            } => DepthFormula::normalize(
+                args.iter()
+                    .chain(results)
+                    .map(|rc| {
+                        self.calculate_depth_of_type(rc.as_ref(), module_store, module_storage)
+                    })
+                    .collect::<PartialVMResult<Vec<_>>>()?,
+            ),
         })
     }
 }
@@ -1939,6 +1994,8 @@ impl Loader {
 // Matches the actual returned type to the expected type, binding any type args to the
 // necessary type as stored in the map. The expected type must be a concrete type (no TyParam).
 // Returns true if a successful match is made.
+// TODO: is this really needed in presence of paranoid mode? This does a deep structural
+//   comparison and is expensive.
 fn match_return_type<'a>(
     returned: &Type,
     expected: &'a Type,
@@ -1961,6 +2018,30 @@ fn match_return_type<'a>(
         (Type::Vector(ret_inner), Type::Vector(expected_inner)) => {
             match_return_type(ret_inner, expected_inner, map)
         },
+        // Function types, the expected abilities need to be a subset of the provided ones,
+        // and recursively argument and result types need to match.
+        (
+            Type::Function {
+                args,
+                results,
+                abilities,
+            },
+            Type::Function {
+                args: exp_args,
+                results: exp_results,
+                abilities: exp_abilities,
+            },
+        ) => {
+            exp_abilities.is_subset(*abilities)
+                && args
+                    .iter()
+                    .zip(exp_args)
+                    .all(|(t, e)| match_return_type(t, e, map))
+                && results
+                    .iter()
+                    .zip(exp_results)
+                    .all(|(t, e)| match_return_type(t, e, map))
+        },
         // Abilities should not contribute to the equality check as they just serve for caching computations.
         // For structs the both need to be the same struct.
         (
@@ -2013,6 +2094,7 @@ fn match_return_type<'a>(
         | (Type::Signer, _)
         | (Type::Struct { .. }, _)
         | (Type::StructInstantiation { .. }, _)
+        | (Type::Function { .. }, _)
         | (Type::Vector(_), _)
         | (Type::MutableReference(_), _)
         | (Type::Reference(_), _) => false,
diff --git a/third_party/move/move-vm/runtime/src/storage/module_storage.rs b/third_party/move/move-vm/runtime/src/storage/module_storage.rs
index c96571b046cd1c..4a3e65a57b618c 100644
--- a/third_party/move/move-vm/runtime/src/storage/module_storage.rs
+++ b/third_party/move/move-vm/runtime/src/storage/module_storage.rs
@@ -2,9 +2,9 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use crate::{
-    loader::{Function, Module},
+    loader::{Function, LazyLoadedFunction, LazyLoadedFunctionState, Module},
     logging::expect_no_verification_errors,
-    WithRuntimeEnvironment,
+    LayoutConverter, StorageLayoutConverter, WithRuntimeEnvironment,
 };
 use ambassador::delegatable_trait;
 use bytes::Bytes;
@@ -26,6 +26,7 @@ use move_vm_types::{
     loaded_data::runtime_types::{StructType, Type},
     module_cyclic_dependency_error, module_linker_error,
     value_serde::FunctionValueExtension,
+    values::{AbstractFunction, SerializedFunctionData},
 };
 use std::sync::Arc;
 
@@ -448,4 +449,58 @@ impl<'a> FunctionValueExtension for FunctionValueExtensionAdapter<'a> {
             })
             .collect::<PartialVMResult<Vec<_>>>()
     }
+
+    fn create_from_serialization_data(
+        &self,
+        data: SerializedFunctionData,
+    ) -> PartialVMResult<Box<dyn AbstractFunction>> {
+        Ok(Box::new(LazyLoadedFunction::new_unresolved(data)))
+    }
+
+    fn get_serialization_data(
+        &self,
+        fun: &dyn AbstractFunction,
+    ) -> PartialVMResult<SerializedFunctionData> {
+        match &*LazyLoadedFunction::expect_this_impl(fun)?.0.borrow() {
+            LazyLoadedFunctionState::Unresolved { data, .. } => Ok(data.clone()),
+            LazyLoadedFunctionState::Resolved {
+                fun,
+                mask,
+                fun_inst,
+            } => {
+                let ty_converter = StorageLayoutConverter::new(self.module_storage);
+                let ty_builder = &self
+                    .module_storage
+                    .runtime_environment()
+                    .vm_config()
+                    .ty_builder;
+                let instantiate = |ty: &Type| -> PartialVMResult<Type> {
+                    if fun.ty_args.is_empty() {
+                        Ok(ty.clone())
+                    } else {
+                        ty_builder.create_ty_with_subst(ty, &fun.ty_args)
+                    }
+                };
+                let captured_layouts = mask
+                    .extract(fun.param_tys(), true)
+                    .into_iter()
+                    .map(|t| ty_converter.type_to_type_layout(&instantiate(t)?))
+                    .collect::<PartialVMResult<Vec<_>>>()?;
+                Ok(SerializedFunctionData {
+                    module_id: fun
+                        .module_id()
+                        .ok_or_else(|| {
+                            PartialVMError::new_invariant_violation(
+                                "attempt to serialize a script function",
+                            )
+                        })?
+                        .clone(),
+                    fun_id: fun.function.name.clone(),
+                    fun_inst: fun_inst.clone(),
+                    mask: *mask,
+                    captured_layouts,
+                })
+            },
+        }
+    }
 }
diff --git a/third_party/move/move-vm/runtime/src/storage/ty_layout_converter.rs b/third_party/move/move-vm/runtime/src/storage/ty_layout_converter.rs
index 22bdb610c005bd..270368d7fee7ad 100644
--- a/third_party/move/move-vm/runtime/src/storage/ty_layout_converter.rs
+++ b/third_party/move/move-vm/runtime/src/storage/ty_layout_converter.rs
@@ -12,6 +12,7 @@ use crate::{
 };
 use move_binary_format::errors::{PartialVMError, PartialVMResult};
 use move_core_types::{
+    function::MoveFunctionLayout,
     language_storage::StructTag,
     value::{IdentifierMappingKind, MoveFieldLayout, MoveStructLayout, MoveTypeLayout},
     vm_status::StatusCode,
@@ -151,6 +152,32 @@ pub(crate) trait LayoutConverterBase {
                 *count += 1;
                 self.struct_name_to_type_layout(*idx, ty_args, count, depth + 1)?
             },
+            Type::Function {
+                args,
+                results,
+                abilities,
+            } => {
+                let mut identifier_mapping = false;
+                let mut to_list = |rcs: &[triomphe::Arc<Type>]| {
+                    rcs.iter()
+                        .map(|rc| {
+                            self.type_to_type_layout_impl(rc.as_ref(), count, depth + 1)
+                                .map(|(l, has)| {
+                                    identifier_mapping |= has;
+                                    l
+                                })
+                        })
+                        .collect::<PartialVMResult<Vec<_>>>()
+                };
+                (
+                    MoveTypeLayout::Function(MoveFunctionLayout(
+                        to_list(args)?,
+                        to_list(results)?,
+                        *abilities,
+                    )),
+                    identifier_mapping,
+                )
+            },
             Type::Reference(_) | Type::MutableReference(_) | Type::TyParam(_) => {
                 return Err(
                     PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)
@@ -318,6 +345,24 @@ pub(crate) trait LayoutConverterBase {
             Type::StructInstantiation { idx, ty_args, .. } => {
                 self.struct_name_to_fully_annotated_layout(*idx, ty_args, count, depth + 1)?
             },
+            Type::Function {
+                args,
+                results,
+                abilities,
+            } => {
+                let mut to_list = |rcs: &[triomphe::Arc<Type>]| {
+                    rcs.iter()
+                        .map(|rc| {
+                            self.type_to_fully_annotated_layout_impl(rc.as_ref(), count, depth + 1)
+                        })
+                        .collect::<PartialVMResult<Vec<_>>>()
+                };
+                MoveTypeLayout::Function(MoveFunctionLayout(
+                    to_list(args)?,
+                    to_list(results)?,
+                    *abilities,
+                ))
+            },
             Type::Reference(_) | Type::MutableReference(_) | Type::TyParam(_) => {
                 return Err(
                     PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)
diff --git a/third_party/move/move-vm/runtime/src/storage/ty_tag_converter.rs b/third_party/move/move-vm/runtime/src/storage/ty_tag_converter.rs
index b082ccaa63d194..ca0e83fbcc8727 100644
--- a/third_party/move/move-vm/runtime/src/storage/ty_tag_converter.rs
+++ b/third_party/move/move-vm/runtime/src/storage/ty_tag_converter.rs
@@ -4,7 +4,7 @@
 use crate::{loader::PseudoGasContext, RuntimeEnvironment};
 use move_binary_format::errors::{PartialVMError, PartialVMResult};
 use move_core_types::{
-    language_storage::{StructTag, TypeTag},
+    language_storage::{FunctionTag, StructTag, TypeTag},
     vm_status::StatusCode,
 };
 use move_vm_types::loaded_data::runtime_types::{StructNameIndex, Type};
@@ -172,6 +172,26 @@ impl<'a> TypeTagConverter<'a> {
                 TypeTag::Struct(Box::new(struct_tag))
             },
 
+            // Functions: recurse
+            Type::Function {
+                args,
+                results,
+                abilities,
+            } => {
+                let to_vec = |ts: &[triomphe::Arc<Type>],
+                              gas_ctx: &mut PseudoGasContext|
+                 -> PartialVMResult<Vec<TypeTag>> {
+                    ts.iter()
+                        .map(|t| self.ty_to_ty_tag_impl(t, gas_ctx))
+                        .collect()
+                };
+                TypeTag::Function(Box::new(FunctionTag {
+                    args: to_vec(args, gas_context)?,
+                    results: to_vec(results, gas_context)?,
+                    abilities: *abilities,
+                }))
+            },
+
             // References and type parameters cannot be converted to tags.
             Type::Reference(_) | Type::MutableReference(_) | Type::TyParam(_) => {
                 return Err(
diff --git a/third_party/move/move-vm/types/Cargo.toml b/third_party/move/move-vm/types/Cargo.toml
index 858a924fc16412..cab1e94eaa89f0 100644
--- a/third_party/move/move-vm/types/Cargo.toml
+++ b/third_party/move/move-vm/types/Cargo.toml
@@ -12,6 +12,7 @@ edition = "2021"
 [dependencies]
 ambassador = { workspace = true }
 bcs = { workspace = true }
+better_any = { workspace = true }
 bytes = { workspace = true }
 crossbeam = { workspace = true }
 dashmap = { workspace = true }
diff --git a/third_party/move/move-vm/types/src/loaded_data/runtime_types.rs b/third_party/move/move-vm/types/src/loaded_data/runtime_types.rs
index 60a648056d999e..2cb4ae958d06f5 100644
--- a/third_party/move/move-vm/types/src/loaded_data/runtime_types.rs
+++ b/third_party/move/move-vm/types/src/loaded_data/runtime_types.rs
@@ -17,7 +17,7 @@ use move_core_types::account_address::AccountAddress;
 use move_core_types::{
     ability::{Ability, AbilitySet},
     identifier::Identifier,
-    language_storage::{ModuleId, StructTag, TypeTag},
+    language_storage::{FunctionTag, ModuleId, StructTag, TypeTag},
     vm_status::{sub_status::unknown_invariant_violation::EPARANOID_FAILURE, StatusCode},
 };
 use serde::Serialize;
@@ -26,7 +26,7 @@ use smallvec::{smallvec, SmallVec};
 use std::{
     cell::RefCell,
     cmp::max,
-    collections::{btree_map, BTreeMap},
+    collections::{btree_map, BTreeMap, BTreeSet},
     fmt,
     fmt::Debug,
     sync::Arc,
@@ -287,6 +287,11 @@ pub enum Type {
         ty_args: TriompheArc<Vec<Type>>,
         ability: AbilityInfo,
     },
+    Function {
+        args: Vec<TriompheArc<Type>>,
+        results: Vec<TriompheArc<Type>>,
+        abilities: AbilitySet,
+    },
     Reference(Box<Type>),
     MutableReference(Box<Type>),
     TyParam(u16),
@@ -329,6 +334,11 @@ impl<'a> Iterator for TypePreorderTraversalIter<'a> {
                     },
 
                     StructInstantiation { ty_args, .. } => self.stack.extend(ty_args.iter().rev()),
+
+                    Function { args, results, .. } => {
+                        self.stack.extend(args.iter().map(|rc| rc.as_ref()));
+                        self.stack.extend(results.iter().map(|rc| rc.as_ref()))
+                    },
                 }
                 Some(ty)
             },
@@ -651,6 +661,7 @@ impl Type {
                         .with_message(e.to_string())
                 })
             },
+            Type::Function { abilities, .. } => Ok(*abilities),
         }
     }
 
@@ -717,13 +728,109 @@ impl Type {
                     | Struct { .. }
                     | Reference(..)
                     | MutableReference(..)
-                    | StructInstantiation { .. } => n += 1,
+                    | StructInstantiation { .. }
+                    | Function { .. } => n += 1,
                 }
             }
 
             Ok(n)
         })
     }
+
+    /// Term unification of two types where the type parameters are considered
+    /// as variables. Uses and extends as needed the passed type parameter
+    /// substitution. Returns true if unification succeeds.
+    pub fn unify(&self, other: &Type, subs: &mut BTreeMap<u16, Type>) -> bool {
+        self.unify_impl(other, subs, &mut BTreeSet::new()).is_some()
+    }
+
+    fn unify_impl(
+        &self,
+        other: &Type,
+        subs: &mut BTreeMap<u16, Type>,
+        visiting: &mut BTreeSet<u16>,
+    ) -> Option<()> {
+        use Type::*;
+        match (self, other) {
+            (TyParam(idx), ty) | (ty, TyParam(idx)) => {
+                if let Some(s) = subs.get(idx) {
+                    if !visiting.insert(*idx) {
+                        // Cyclic substitution
+                        None
+                    } else {
+                        s.clone().unify_impl(ty, subs, visiting)?;
+                        visiting.remove(idx);
+                        Some(())
+                    }
+                } else {
+                    subs.insert(*idx, ty.clone());
+                    Some(())
+                }
+            },
+            (Reference(ty1), Reference(ty2)) | (MutableReference(ty1), MutableReference(ty2)) => {
+                ty1.unify_impl(ty2.as_ref(), subs, visiting)
+            },
+            (
+                StructInstantiation { idx, ty_args, .. },
+                StructInstantiation {
+                    idx: idx2,
+                    ty_args: ty_args2,
+                    ..
+                },
+            ) if idx == idx2 && ty_args.len() == ty_args2.len() => {
+                for (ty1, ty2) in ty_args.iter().zip(ty_args2.as_ref()) {
+                    ty1.unify_impl(ty2, subs, visiting)?
+                }
+                Some(())
+            },
+            (Vector(ty1), Vector(ty2)) => ty1.unify_impl(ty2.as_ref(), subs, visiting),
+            (
+                Function {
+                    args,
+                    results,
+                    abilities,
+                },
+                Function {
+                    args: args2,
+                    results: results2,
+                    abilities: abilities2,
+                },
+            ) if abilities == abilities2
+                && args.len() == args2.len()
+                && results.len() == results2.len() =>
+            {
+                for (ty1, ty2) in args.iter().zip(args2).chain(results.iter().zip(results2)) {
+                    ty1.unify_impl(ty2.as_ref(), subs, visiting)?
+                }
+                Some(())
+            },
+            // The remaining combinations with recursive types can't match
+            (
+                Vector(..)
+                | StructInstantiation { .. }
+                | Function { .. }
+                | Reference(..)
+                | MutableReference(..),
+                _,
+            )
+            | (
+                _,
+                Vector(..)
+                | StructInstantiation { .. }
+                | Function { .. }
+                | Reference(..)
+                | MutableReference(..),
+            ) => None,
+            // Everything else matched by non-recursive term equality
+            (ty1, ty2) => {
+                if ty1 == ty2 {
+                    Some(())
+                } else {
+                    None
+                }
+            },
+        }
+    }
 }
 
 impl fmt::Display for StructIdentifier {
@@ -762,6 +869,17 @@ impl fmt::Display for Type {
                 idx.0,
                 ty_args.iter().map(|t| t.to_string()).join(",")
             ),
+            Function {
+                args,
+                results,
+                abilities,
+            } => write!(
+                f,
+                "|{}|{}{}",
+                args.iter().map(|t| t.to_string()).join(","),
+                results.iter().map(|t| t.to_string()).join(","),
+                abilities.display_postfix()
+            ),
             Reference(t) => write!(f, "&{}", t),
             MutableReference(t) => write!(f, "&mut {}", t),
             TyParam(no) => write!(f, "_{}", no),
@@ -1141,6 +1259,36 @@ impl TypeBuilder {
                     ability: ability.clone(),
                 }
             },
+            Function {
+                args,
+                results,
+                abilities,
+            } => {
+                let subs_elem = |count: &mut u64,
+                                 ty: &TriompheArc<Type>|
+                 -> PartialVMResult<TriompheArc<Type>> {
+                    Ok(TriompheArc::new(Self::apply_subst(
+                        ty.as_ref(),
+                        subst,
+                        count,
+                        depth + 1,
+                        check,
+                    )?))
+                };
+                let args = args
+                    .iter()
+                    .map(|ty| subs_elem(count, ty))
+                    .collect::<PartialVMResult<Vec<_>>>()?;
+                let results = results
+                    .iter()
+                    .map(|ty| subs_elem(count, ty))
+                    .collect::<PartialVMResult<Vec<_>>>()?;
+                Function {
+                    args,
+                    results,
+                    abilities: *abilities,
+                }
+            },
         })
     }
 
@@ -1200,6 +1348,26 @@ impl TypeBuilder {
                     }
                 }
             },
+            T::Function(fun) => {
+                let FunctionTag {
+                    args,
+                    results,
+                    abilities,
+                } = fun.as_ref();
+                let mut to_list = |ts: &[TypeTag]| {
+                    ts.iter()
+                        .map(|t| {
+                            self.create_ty_impl(t, resolver, count, depth + 1)
+                                .map(TriompheArc::new)
+                        })
+                        .collect::<VMResult<Vec<_>>>()
+                };
+                Function {
+                    args: to_list(args)?,
+                    results: to_list(results)?,
+                    abilities: *abilities,
+                }
+            },
         })
     }
 
diff --git a/third_party/move/move-vm/types/src/value_serde.rs b/third_party/move/move-vm/types/src/value_serde.rs
index dd9c22140ad26d..ac01bf69b79cd2 100644
--- a/third_party/move/move-vm/types/src/value_serde.rs
+++ b/third_party/move/move-vm/types/src/value_serde.rs
@@ -4,7 +4,10 @@
 use crate::{
     delayed_values::delayed_field_id::DelayedFieldID,
     loaded_data::runtime_types::Type,
-    values::{DeserializationSeed, SerializationReadyValue, Value},
+    values::{
+        AbstractFunction, DeserializationSeed, SerializationReadyValue, SerializedFunctionData,
+        Value,
+    },
 };
 use move_binary_format::errors::{PartialVMError, PartialVMResult};
 use move_core_types::{
@@ -15,7 +18,7 @@ use move_core_types::{
 };
 use std::cell::RefCell;
 
-/// An extension to (de)serializer to lookup information about function values.
+/// An extension to (de)serialize information about function values.
 pub trait FunctionValueExtension {
     /// Given the module's id and the function name, returns the parameter types of the
     /// corresponding function, instantiated with the provided set of type tags.
@@ -25,6 +28,18 @@ pub trait FunctionValueExtension {
         function_name: &IdentStr,
         ty_arg_tags: Vec<TypeTag>,
     ) -> PartialVMResult<Vec<Type>>;
+
+    /// Create an implementation of an `AbstractFunction` from the serialization data.
+    fn create_from_serialization_data(
+        &self,
+        data: SerializedFunctionData,
+    ) -> PartialVMResult<Box<dyn AbstractFunction>>;
+
+    /// Get serialization data from an `AbstractFunction`.
+    fn get_serialization_data(
+        &self,
+        fun: &dyn AbstractFunction,
+    ) -> PartialVMResult<SerializedFunctionData>;
 }
 
 /// An extension to (de)serializer to lookup information about delayed fields.
@@ -92,6 +107,14 @@ impl<'a> ValueSerDeContext<'a> {
         self
     }
 
+    pub fn required_function_extension(&self) -> PartialVMResult<&dyn FunctionValueExtension> {
+        self.function_extension.ok_or_else(|| {
+            PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message(
+                "require function extension context for serialization of closures".to_string(),
+            )
+        })
+    }
+
     /// Returns the same extension but without allowing the delayed fields.
     pub(crate) fn clone_without_delayed_fields(&self) -> Self {
         Self {
diff --git a/third_party/move/move-vm/types/src/value_traversal.rs b/third_party/move/move-vm/types/src/value_traversal.rs
index 54cfac487e5bc7..2fd6edc01f5280 100644
--- a/third_party/move/move-vm/types/src/value_traversal.rs
+++ b/third_party/move/move-vm/types/src/value_traversal.rs
@@ -3,7 +3,7 @@
 
 use crate::{
     delayed_values::error::code_invariant_error,
-    values::{Container, Value, ValueImpl},
+    values::{Closure, Container, Value, ValueImpl},
 };
 use move_binary_format::errors::{PartialVMError, PartialVMResult};
 use move_core_types::vm_status::StatusCode;
@@ -56,6 +56,12 @@ fn find_identifiers_in_value_impl(
             },
         },
 
+        ValueImpl::ClosureValue(Closure(_, captured)) => {
+            for val in captured.iter() {
+                find_identifiers_in_value_impl(val, identifiers)?;
+            }
+        },
+
         ValueImpl::Invalid | ValueImpl::ContainerRef(_) | ValueImpl::IndexedRef(_) => {
             return Err(PartialVMError::new(
                 StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
diff --git a/third_party/move/move-vm/types/src/values/function_values_impl.rs b/third_party/move/move-vm/types/src/values/function_values_impl.rs
new file mode 100644
index 00000000000000..6f7b9214693479
--- /dev/null
+++ b/third_party/move/move-vm/types/src/values/function_values_impl.rs
@@ -0,0 +1,208 @@
+// Copyright © Aptos Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::values::{DeserializationSeed, SerializationReadyValue, VMValueCast, Value, ValueImpl};
+use better_any::Tid;
+use move_binary_format::errors::{PartialVMError, PartialVMResult};
+use move_core_types::{
+    function::{ClosureMask, MoveFunctionLayout},
+    identifier::Identifier,
+    language_storage::{ModuleId, TypeTag},
+    value::MoveTypeLayout,
+    vm_status::StatusCode,
+};
+use serde::{
+    de::Error as DeError,
+    ser::{Error, SerializeSeq},
+    Deserialize, Serialize,
+};
+use std::{
+    cmp::Ordering,
+    fmt,
+    fmt::{Debug, Display, Formatter},
+};
+
+/// A trait describing a function which can be executed. If this is a generic
+/// function, the type instantiation is part of this.
+/// The value system is agnostic about how this is implemented in the runtime.
+/// The `FunctionValueExtension` trait describes how to construct and
+/// deconstruct instances for serialization.
+pub trait AbstractFunction: for<'a> Tid<'a> {
+    fn closure_mask(&self) -> ClosureMask;
+    fn cmp_dyn(&self, other: &dyn AbstractFunction) -> PartialVMResult<Ordering>;
+    fn clone_dyn(&self) -> PartialVMResult<Box<dyn AbstractFunction>>;
+    fn to_stable_string(&self) -> String;
+}
+
+/// A closure, consisting of an abstract function descriptor and the captured arguments.
+pub struct Closure(
+    pub(crate) Box<dyn AbstractFunction>,
+    pub(crate) Vec<ValueImpl>,
+);
+
+/// The representation of a function in storage.
+#[derive(Serialize, Deserialize, Clone)]
+pub struct SerializedFunctionData {
+    pub module_id: ModuleId,
+    pub fun_id: Identifier,
+    pub fun_inst: Vec<TypeTag>,
+    pub mask: ClosureMask,
+    /// The layouts used for deserialization of the captured arguments
+    /// are stored so one can verify type consistency at
+    /// resolution time. It also allows to serialize an unresolved
+    /// closure, making unused closure data cheap in round trips.
+    pub captured_layouts: Vec<MoveTypeLayout>,
+}
+
+impl Closure {
+    pub fn pack(fun: Box<dyn AbstractFunction>, captured: impl IntoIterator<Item = Value>) -> Self {
+        Self(fun, captured.into_iter().map(|v| v.0).collect())
+    }
+
+    pub fn unpack(self) -> (Box<dyn AbstractFunction>, impl Iterator<Item = Value>) {
+        let Self(fun, captured) = self;
+        (fun, captured.into_iter().map(Value))
+    }
+
+    pub fn into_call_data(
+        self,
+        args: Vec<Value>,
+    ) -> PartialVMResult<(Box<dyn AbstractFunction>, Vec<Value>)> {
+        let (fun, captured) = self.unpack();
+        if let Some(all_args) = fun.closure_mask().compose(captured, args) {
+            Ok((fun, all_args))
+        } else {
+            Err(
+                PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)
+                    .with_message("invalid closure mask".to_string()),
+            )
+        }
+    }
+}
+
+impl Debug for Closure {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        let Self(fun, captured) = self;
+        write!(f, "Closure({}, {:?})", fun.to_stable_string(), captured)
+    }
+}
+
+impl Display for Closure {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        let Self(fun, captured) = self;
+        let captured = fun
+            .closure_mask()
+            .merge_placeholder_strings(
+                captured.len(),
+                captured.iter().map(|v| v.to_string()).collect(),
+            )
+            .unwrap_or_else(|| vec!["*invalid*".to_string()]);
+        write!(f, "{}({})", fun.to_stable_string(), captured.join(","))
+    }
+}
+
+impl VMValueCast<Closure> for Value {
+    fn cast(self) -> PartialVMResult<Closure> {
+        match self.0 {
+            ValueImpl::ClosureValue(c) => Ok(c),
+            v => Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR)
+                .with_message(format!("cannot cast {:?} to closure", v))),
+        }
+    }
+}
+
+impl<'c, 'l, 'v> serde::Serialize
+    for SerializationReadyValue<'c, 'l, 'v, MoveFunctionLayout, Closure>
+{
+    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let Closure(fun, captured) = self.value;
+        let fun_ext = self
+            .ctx
+            .required_function_extension()
+            .map_err(S::Error::custom)?;
+        let data = fun_ext
+            .get_serialization_data(fun.as_ref())
+            .map_err(S::Error::custom)?;
+        let mut seq = serializer.serialize_seq(Some(4 + captured.len()))?;
+        seq.serialize_element(&data.module_id)?;
+        seq.serialize_element(&data.fun_id)?;
+        seq.serialize_element(&data.fun_inst)?;
+        seq.serialize_element(&data.mask)?;
+        for (layout, value) in data.captured_layouts.into_iter().zip(captured) {
+            seq.serialize_element(&layout)?;
+            seq.serialize_element(&SerializationReadyValue {
+                ctx: self.ctx,
+                layout: &layout,
+                value,
+            })?
+        }
+        seq.end()
+    }
+}
+
+pub(crate) struct ClosureVisitor<'c, 'l>(
+    pub(crate) DeserializationSeed<'c, &'l MoveFunctionLayout>,
+);
+
+impl<'d, 'c, 'l> serde::de::Visitor<'d> for ClosureVisitor<'c, 'l> {
+    type Value = Closure;
+
+    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+        formatter.write_str("Closure")
+    }
+
+    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+    where
+        A: serde::de::SeqAccess<'d>,
+    {
+        let fun_ext = self
+            .0
+            .ctx
+            .required_function_extension()
+            .map_err(A::Error::custom)?;
+        let module_id = read_required_value::<_, ModuleId>(&mut seq)?;
+        let fun_id = read_required_value::<_, Identifier>(&mut seq)?;
+        let fun_inst = read_required_value::<_, Vec<TypeTag>>(&mut seq)?;
+        let mask = read_required_value::<_, ClosureMask>(&mut seq)?;
+        let mut captured_layouts = vec![];
+        let mut captured = vec![];
+        for _ in 0..mask.captured_count() {
+            let layout = read_required_value::<_, MoveTypeLayout>(&mut seq)?;
+            match seq.next_element_seed(DeserializationSeed {
+                ctx: self.0.ctx,
+                layout: &layout,
+            })? {
+                Some(v) => {
+                    captured_layouts.push(layout);
+                    captured.push(v.0)
+                },
+                None => return Err(A::Error::invalid_length(captured.len(), &self)),
+            }
+        }
+        // If the sequence length is known, check whether there are no extra values
+        if matches!(seq.size_hint(), Some(remaining) if remaining != 0) {
+            return Err(A::Error::invalid_length(captured.len(), &self));
+        }
+        let fun = fun_ext
+            .create_from_serialization_data(SerializedFunctionData {
+                module_id,
+                fun_id,
+                fun_inst,
+                mask,
+                captured_layouts,
+            })
+            .map_err(A::Error::custom)?;
+        Ok(Closure(fun, captured))
+    }
+}
+
+fn read_required_value<'a, A, T>(seq: &mut A) -> Result<T, A::Error>
+where
+    A: serde::de::SeqAccess<'a>,
+    T: serde::de::Deserialize<'a>,
+{
+    match seq.next_element::<T>()? {
+        Some(x) => Ok(x),
+        None => Err(A::Error::custom("expected more elements")),
+    }
+}
diff --git a/third_party/move/move-vm/types/src/values/mod.rs b/third_party/move/move-vm/types/src/values/mod.rs
index 4796eb7fbd9472..31b773cffd3da9 100644
--- a/third_party/move/move-vm/types/src/values/mod.rs
+++ b/third_party/move/move-vm/types/src/values/mod.rs
@@ -2,6 +2,7 @@
 // Copyright (c) The Move Contributors
 // SPDX-License-Identifier: Apache-2.0
 
+pub mod function_values_impl;
 pub mod values_impl;
 
 #[cfg(test)]
@@ -12,4 +13,5 @@ mod serialization_tests;
 #[cfg(all(test, feature = "fuzzing"))]
 mod value_prop_tests;
 
+pub use function_values_impl::*;
 pub use values_impl::*;
diff --git a/third_party/move/move-vm/types/src/values/values_impl.rs b/third_party/move/move-vm/types/src/values/values_impl.rs
index 544ca10d2ac43a..7847c08efb6c5c 100644
--- a/third_party/move/move-vm/types/src/values/values_impl.rs
+++ b/third_party/move/move-vm/types/src/values/values_impl.rs
@@ -8,6 +8,7 @@ use crate::{
     delayed_values::delayed_field_id::{DelayedFieldID, TryFromMoveValue, TryIntoMoveValue},
     loaded_data::runtime_types::Type,
     value_serde::ValueSerDeContext,
+    values::function_values_impl::{AbstractFunction, Closure, ClosureVisitor},
     views::{ValueView, ValueVisitor},
 };
 use itertools::Itertools;
@@ -93,6 +94,11 @@ pub(crate) enum ValueImpl {
     DelayedFieldID {
         id: DelayedFieldID,
     },
+
+    /// A closure, consisting of a function reference and captured arguments.
+    /// Notice that captured arguments cannot be referenced, hence a closure is
+    /// not a container.
+    ClosureValue(Closure),
 }
 
 /// A container is a collection of values. It is used to represent data structures like a
@@ -414,6 +420,14 @@ impl ValueImpl {
             // Native values can be copied because this is how read_ref operates,
             // and copying is an internal API.
             DelayedFieldID { id } => DelayedFieldID { id: *id },
+
+            ClosureValue(Closure(fun, captured)) => {
+                let captured = captured
+                    .iter()
+                    .map(|v| v.copy_value())
+                    .collect::<PartialVMResult<_>>()?;
+                ClosureValue(Closure(fun.clone_dyn()?, captured))
+            },
         })
     }
 }
@@ -537,6 +551,21 @@ impl ValueImpl {
                     .with_message("cannot compare delayed values".to_string()))
             },
 
+            (ClosureValue(Closure(fun1, captured1)), ClosureValue(Closure(fun2, captured2))) => {
+                if fun1.cmp_dyn(fun2.as_ref())? == Ordering::Equal
+                    && captured1.len() == captured2.len()
+                {
+                    for (v1, v2) in captured1.iter().zip(captured2.iter()) {
+                        if !v1.equals(v2)? {
+                            return Ok(false);
+                        }
+                    }
+                    true
+                } else {
+                    false
+                }
+            },
+
             (Invalid, _)
             | (U8(_), _)
             | (U16(_), _)
@@ -549,6 +578,7 @@ impl ValueImpl {
             | (Container(_), _)
             | (ContainerRef(_), _)
             | (IndexedRef(_), _)
+            | (ClosureValue(_), _)
             | (DelayedFieldID { .. }, _) => {
                 return Err(
                     PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(format!(
@@ -587,6 +617,21 @@ impl ValueImpl {
                     .with_message("cannot compare delayed values".to_string()))
             },
 
+            (ClosureValue(Closure(fun1, captured1)), ClosureValue(Closure(fun2, captured2))) => {
+                let o = fun1.cmp_dyn(fun2.as_ref())?;
+                if o == Ordering::Equal {
+                    for (v1, v2) in captured1.iter().zip(captured2.iter()) {
+                        let o = v1.compare(v2)?;
+                        if o != Ordering::Equal {
+                            return Ok(o);
+                        }
+                    }
+                    captured1.iter().len().cmp(&captured2.len())
+                } else {
+                    o
+                }
+            },
+
             (Invalid, _)
             | (U8(_), _)
             | (U16(_), _)
@@ -599,6 +644,7 @@ impl ValueImpl {
             | (Container(_), _)
             | (ContainerRef(_), _)
             | (IndexedRef(_), _)
+            | (ClosureValue(_), _)
             | (DelayedFieldID { .. }, _) => {
                 return Err(
                     PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR).with_message(format!(
@@ -1410,6 +1456,7 @@ impl ContainerRef {
                     | ValueImpl::U256(_)
                     | ValueImpl::Bool(_)
                     | ValueImpl::Address(_)
+                    | ValueImpl::ClosureValue(_)
                     | ValueImpl::DelayedFieldID { .. } => ValueImpl::IndexedRef(IndexedRef {
                         idx,
                         container_ref: self.copy_value(),
@@ -1504,6 +1551,7 @@ impl Locals {
             | ValueImpl::U256(_)
             | ValueImpl::Bool(_)
             | ValueImpl::Address(_)
+            | ValueImpl::ClosureValue(_)
             | ValueImpl::DelayedFieldID { .. } => Ok(Value(ValueImpl::IndexedRef(IndexedRef {
                 idx,
                 container_ref: ContainerRef::Local(Container::Locals(Rc::clone(&self.0))),
@@ -1595,8 +1643,8 @@ impl Locals {
                             return Err(PartialVMError::new(
                                 StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
                             )
-                            .with_message("moving container with dangling references".to_string())
-                            .with_sub_status(move_core_types::vm_status::sub_status::unknown_invariant_violation::EREFERENCE_COUNTING_FAILURE));
+                                .with_message("moving container with dangling references".to_string())
+                                .with_sub_status(move_core_types::vm_status::sub_status::unknown_invariant_violation::EREFERENCE_COUNTING_FAILURE));
                         }
                     }
                 }
@@ -1789,6 +1837,13 @@ impl Value {
             it.into_iter().map(|v| v.0).collect(),
         )))))
     }
+
+    pub fn closure(
+        fun: Box<dyn AbstractFunction>,
+        captured: impl IntoIterator<Item = Value>,
+    ) -> Self {
+        Self(ValueImpl::ClosureValue(Closure::pack(fun, captured)))
+    }
 }
 
 /***************************************************************************************
@@ -2537,6 +2592,8 @@ pub const INDEX_OUT_OF_BOUNDS: u64 = NFE_VECTOR_ERROR_BASE + 1;
 pub const POP_EMPTY_VEC: u64 = NFE_VECTOR_ERROR_BASE + 2;
 pub const VEC_UNPACK_PARITY_MISMATCH: u64 = NFE_VECTOR_ERROR_BASE + 3;
 
+// TODO: this check seems to be obsolete if paranoid mode is on,
+//   and should either be removed or move over to runtime_type_checks?
 fn check_elem_layout(ty: &Type, v: &Container) -> PartialVMResult<()> {
     match (ty, v) {
         (Type::U8, Container::VecU8(_))
@@ -2553,7 +2610,8 @@ fn check_elem_layout(ty: &Type, v: &Container) -> PartialVMResult<()> {
 
         (Type::Struct { .. }, Container::Vec(_))
         | (Type::Signer, Container::Vec(_))
-        | (Type::StructInstantiation { .. }, Container::Vec(_)) => Ok(()),
+        | (Type::StructInstantiation { .. }, Container::Vec(_))
+        | (Type::Function { .. }, Container::Vec(_)) => Ok(()),
 
         (Type::Reference(_), _) | (Type::MutableReference(_), _) | (Type::TyParam(_), _) => Err(
             PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)
@@ -2571,7 +2629,8 @@ fn check_elem_layout(ty: &Type, v: &Container) -> PartialVMResult<()> {
         | (Type::Signer, _)
         | (Type::Vector(_), _)
         | (Type::Struct { .. }, _)
-        | (Type::StructInstantiation { .. }, _) => Err(PartialVMError::new(
+        | (Type::StructInstantiation { .. }, _)
+        | (Type::Function { .. }, _) => Err(PartialVMError::new(
             StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
         )
         .with_message(format!(
@@ -2860,11 +2919,10 @@ impl Vector {
             Type::Signer
             | Type::Vector(_)
             | Type::Struct { .. }
-            | Type::StructInstantiation {
-                idx: _, ty_args: _, ..
-            } => Value(ValueImpl::Container(Container::Vec(Rc::new(RefCell::new(
-                elements.into_iter().map(|v| v.0).collect(),
-            ))))),
+            | Type::StructInstantiation { .. }
+            | Type::Function { .. } => Value(ValueImpl::Container(Container::Vec(Rc::new(
+                RefCell::new(elements.into_iter().map(|v| v.0).collect()),
+            )))),
 
             Type::Reference(_) | Type::MutableReference(_) | Type::TyParam(_) => {
                 return Err(
@@ -2971,6 +3029,9 @@ pub(crate) const LEGACY_REFERENCE_SIZE: AbstractMemorySize = AbstractMemorySize:
 /// The size of a struct in bytes
 pub(crate) const LEGACY_STRUCT_SIZE: AbstractMemorySize = AbstractMemorySize::new(2);
 
+/// The size of a closure in bytes
+pub(crate) const LEGACY_CLOSURE_SIZE: AbstractMemorySize = AbstractMemorySize::new(6);
+
 impl Container {
     #[cfg(test)]
     fn legacy_size(&self) -> AbstractMemorySize {
@@ -3038,6 +3099,12 @@ impl ValueImpl {
             // Legacy size is only used by event native functions (which should not even
             // be part of move-stdlib), so we should never see any delayed values here.
             DelayedFieldID { .. } => unreachable!("Delayed values do not have legacy size!"),
+
+            ClosureValue(..) => {
+                // TODO(#15664): similarly as with delayed values, closures should not appear here,
+                //   but this needs to be verified
+                unreachable!("Closures do not have legacy size!")
+            },
         }
     }
 }
@@ -3341,6 +3408,8 @@ impl Debug for ValueImpl {
             Self::ContainerRef(r) => write!(f, "ContainerRef({:?})", r),
             Self::IndexedRef(r) => write!(f, "IndexedRef({:?})", r),
 
+            Self::ClosureValue(c) => write!(f, "Function({:?})", c),
+
             // Debug information must be deterministic, so we cannot print
             // inner fields.
             Self::DelayedFieldID { .. } => write!(f, "Delayed(?)"),
@@ -3376,6 +3445,8 @@ impl Display for ValueImpl {
             Self::ContainerRef(r) => write!(f, "{}", r),
             Self::IndexedRef(r) => write!(f, "{}", r),
 
+            Self::ClosureValue(c) => write!(f, "{}", c),
+
             // Display information must be deterministic, so we cannot print
             // inner fields.
             Self::DelayedFieldID { .. } => write!(f, "Delayed(?)"),
@@ -3534,6 +3605,10 @@ pub mod debug {
         debug_write!(buf, "{}", x.to_hex())
     }
 
+    fn print_closure<B: Write>(buf: &mut B, c: &Closure) -> PartialVMResult<()> {
+        debug_write!(buf, "{}", c)
+    }
+
     fn print_value_impl<B: Write>(buf: &mut B, val: &ValueImpl) -> PartialVMResult<()> {
         match val {
             ValueImpl::Invalid => print_invalid(buf),
@@ -3552,6 +3627,8 @@ pub mod debug {
             ValueImpl::ContainerRef(r) => print_container_ref(buf, r),
             ValueImpl::IndexedRef(r) => print_indexed_ref(buf, r),
 
+            ValueImpl::ClosureValue(c) => print_closure(buf, c),
+
             ValueImpl::DelayedFieldID { .. } => print_delayed_value(buf),
         }
     }
@@ -3722,6 +3799,14 @@ impl<'c, 'l, 'v> serde::Serialize
                 .serialize(serializer)
             },
 
+            // Functions.
+            (L::Function(fun_layout), ValueImpl::ClosureValue(clos)) => SerializationReadyValue {
+                ctx: self.ctx,
+                layout: fun_layout,
+                value: clos,
+            }
+            .serialize(serializer),
+
             // Vectors.
             (L::Vector(layout), ValueImpl::Container(c)) => {
                 let layout = layout.as_ref();
@@ -3986,6 +4071,16 @@ impl<'d, 'c> serde::de::DeserializeSeed<'d> for DeserializationSeed<'c, &MoveTyp
                 },
             }),
 
+            // Functions
+            L::Function(fun_layout) => {
+                let seed = DeserializationSeed {
+                    ctx: self.ctx,
+                    layout: fun_layout,
+                };
+                let closure = deserializer.deserialize_seq(ClosureVisitor(seed))?;
+                Ok(Value(ValueImpl::ClosureValue(closure)))
+            },
+
             // Delayed values should always use custom deserialization.
             L::Native(kind, layout) => {
                 match &self.ctx.delayed_fields_extension {
@@ -4008,9 +4103,9 @@ impl<'d, 'c> serde::de::DeserializeSeed<'d> for DeserializationSeed<'c, &MoveTyp
                                     DelayedFieldID::try_from_move_value(layout, value, &())
                                         .map_err(|_| {
                                             D::Error::custom(format!(
-                                            "Custom deserialization failed for {:?} with layout {}",
-                                            kind, layout
-                                        ))
+                                        "Custom deserialization failed for {:?} with layout {}",
+                                        kind, layout
+                                    ))
                                         })?;
                                 id
                             },
@@ -4318,6 +4413,17 @@ impl Container {
     }
 }
 
+impl Closure {
+    fn visit_impl(&self, visitor: &mut impl ValueVisitor, depth: usize) {
+        let Self(_, captured) = self;
+        if visitor.visit_closure(depth, captured.len()) {
+            for val in captured {
+                val.visit_impl(visitor, depth + 1);
+            }
+        }
+    }
+}
+
 impl ContainerRef {
     fn visit_impl(&self, visitor: &mut impl ValueVisitor, depth: usize) {
         use ContainerRef::*;
@@ -4369,6 +4475,8 @@ impl ValueImpl {
             ContainerRef(r) => r.visit_impl(visitor, depth),
             IndexedRef(r) => r.visit_impl(visitor, depth),
 
+            ClosureValue(c) => c.visit_impl(visitor, depth),
+
             DelayedFieldID { id } => visitor.visit_delayed(depth, *id),
         }
     }
@@ -4631,6 +4739,14 @@ pub mod prop {
                 .prop_map(move |vals| Value::struct_(Struct::pack(vals)))
                 .boxed(),
 
+            L::Function(_function_layout) => {
+                // TODO(#15664): not clear how to generate closure values, we'd need
+                //   some test functions for this, and generate `AbstractFunction` impls.
+                //   As we do not generate function layouts in the first place, we can bail
+                //   out here
+                unreachable!("unexpected function layout")
+            },
+
             // TODO[agg_v2](cleanup): double check what we should do here (i.e. if we should
             //  even skip these kinds of layouts, or if need to construct a delayed value)?
             L::Native(_, layout) => value_strategy_with_layout(layout.as_ref()),
diff --git a/third_party/move/move-vm/types/src/views.rs b/third_party/move/move-vm/types/src/views.rs
index 3c11766c55841a..120e712adc89fc 100644
--- a/third_party/move/move-vm/types/src/views.rs
+++ b/third_party/move/move-vm/types/src/views.rs
@@ -1,7 +1,7 @@
 // Copyright (c) The Move Contributors
 // SPDX-License-Identifier: Apache-2.0
 
-use crate::delayed_values::delayed_field_id::DelayedFieldID;
+use crate::{delayed_values::delayed_field_id::DelayedFieldID, values::LEGACY_CLOSURE_SIZE};
 use move_core_types::{
     account_address::AccountAddress, gas_algebra::AbstractMemorySize, language_storage::TypeTag,
 };
@@ -79,6 +79,11 @@ pub trait ValueView {
                 true
             }
 
+            fn visit_closure(&mut self, _depth: usize, _len: usize) -> bool {
+                self.0 += LEGACY_CLOSURE_SIZE;
+                true
+            }
+
             fn visit_vec(&mut self, _depth: usize, _len: usize) -> bool {
                 self.0 += LEGACY_STRUCT_SIZE;
                 true
@@ -142,6 +147,7 @@ pub trait ValueVisitor {
     fn visit_address(&mut self, depth: usize, val: AccountAddress);
 
     fn visit_struct(&mut self, depth: usize, len: usize) -> bool;
+    fn visit_closure(&mut self, depth: usize, len: usize) -> bool;
     fn visit_vec(&mut self, depth: usize, len: usize) -> bool;
 
     fn visit_ref(&mut self, depth: usize, is_global: bool) -> bool;
diff --git a/third_party/move/tools/move-bytecode-utils/src/layout.rs b/third_party/move/tools/move-bytecode-utils/src/layout.rs
index 46863644cdc55b..00235b3b32b32a 100644
--- a/third_party/move/tools/move-bytecode-utils/src/layout.rs
+++ b/third_party/move/tools/move-bytecode-utils/src/layout.rs
@@ -14,6 +14,7 @@ use move_binary_format::{
 };
 use move_core_types::{
     account_address::AccountAddress,
+    function::MoveFunctionLayout,
     identifier::{IdentStr, Identifier},
     language_storage::{ModuleId, StructTag, TypeTag},
     value::{MoveFieldLayout, MoveStructLayout, MoveTypeLayout},
@@ -374,6 +375,18 @@ impl TypeLayoutBuilder {
                 compiled_module_view,
                 layout_type,
             )?),
+            Function(f) => {
+                let build_list = |ts: &[TypeTag]| {
+                    ts.iter()
+                        .map(|t| Self::build(t, compiled_module_view, layout_type))
+                        .collect::<anyhow::Result<Vec<_>>>()
+                };
+                MoveTypeLayout::Function(MoveFunctionLayout(
+                    build_list(&f.args)?,
+                    build_list(&f.results)?,
+                    f.abilities,
+                ))
+            },
         })
     }
 
diff --git a/third_party/move/tools/move-resource-viewer/src/fat_type.rs b/third_party/move/tools/move-resource-viewer/src/fat_type.rs
index d4f8097a4f1789..0d9f75889d44b1 100644
--- a/third_party/move/tools/move-resource-viewer/src/fat_type.rs
+++ b/third_party/move/tools/move-resource-viewer/src/fat_type.rs
@@ -275,6 +275,10 @@ impl From<&TypeTag> for FatType {
             TypeTag::Vector(inner) => Vector(Box::new(inner.as_ref().into())),
             TypeTag::Struct(inner) => Struct(Box::new(inner.as_ref().into())),
             TypeTag::U256 => U256,
+            TypeTag::Function(..) => {
+                // TODO(#15664): implement functions for fat types
+                todo!("functions not supported by fat types")
+            },
         }
     }
 }
diff --git a/third_party/move/tools/move-resource-viewer/src/lib.rs b/third_party/move/tools/move-resource-viewer/src/lib.rs
index 9142608d9ef68e..4eb0d33ec4a139 100644
--- a/third_party/move/tools/move-resource-viewer/src/lib.rs
+++ b/third_party/move/tools/move-resource-viewer/src/lib.rs
@@ -442,6 +442,10 @@ impl<V: CompiledModuleView> MoveValueAnnotator<V> {
             TypeTag::U256 => FatType::U256,
             TypeTag::U128 => FatType::U128,
             TypeTag::Vector(ty) => FatType::Vector(Box::new(self.resolve_type_impl(ty, limit)?)),
+            TypeTag::Function(..) => {
+                // TODO(#15664) implement functions for fat types"
+                todo!("functions for fat types")
+            },
         })
     }
 
@@ -583,6 +587,10 @@ impl<V: CompiledModuleView> MoveValueAnnotator<V> {
             (MoveValue::Struct(s), FatType::Struct(ty)) => {
                 AnnotatedMoveValue::Struct(self.annotate_struct(s, ty.as_ref(), limit)?)
             },
+            (MoveValue::Closure(..), _) => {
+                // TODO(#15664) implement functions for annotated move values
+                todo!("functions not implemented")
+            },
             (MoveValue::U8(_), _)
             | (MoveValue::U64(_), _)
             | (MoveValue::U128(_), _)