diff --git a/compiler/rustc_middle/src/ty/context.rs b/compiler/rustc_middle/src/ty/context.rs
index 2ba1bf2822fbb..77a40762903e8 100644
--- a/compiler/rustc_middle/src/ty/context.rs
+++ b/compiler/rustc_middle/src/ty/context.rs
@@ -374,7 +374,11 @@ impl<'tcx> Interner for TyCtxt<'tcx> {
         self.explicit_implied_predicates_of(def_id).map_bound(|preds| preds.into_iter().copied())
     }
 
-    fn is_const_impl(self, def_id: DefId) -> bool {
+    fn impl_is_const(self, def_id: DefId) -> bool {
+        self.is_conditionally_const(def_id)
+    }
+
+    fn fn_is_const(self, def_id: DefId) -> bool {
         self.is_conditionally_const(def_id)
     }
 
diff --git a/compiler/rustc_next_trait_solver/src/solve/assembly/structural_traits.rs b/compiler/rustc_next_trait_solver/src/solve/assembly/structural_traits.rs
index 5c1a7852dc0b4..a56febec48c45 100644
--- a/compiler/rustc_next_trait_solver/src/solve/assembly/structural_traits.rs
+++ b/compiler/rustc_next_trait_solver/src/solve/assembly/structural_traits.rs
@@ -633,6 +633,76 @@ fn coroutine_closure_to_ambiguous_coroutine<I: Interner>(
     )
 }
 
+/// This duplicates `extract_tupled_inputs_and_output_from_callable` but needs
+/// to return different information (namely, the def id and args) so that we can
+/// create const conditions.
+///
+/// Doing so on all calls to `extract_tupled_inputs_and_output_from_callable`
+/// would be wasteful.
+pub(in crate::solve) fn extract_fn_def_from_const_callable<I: Interner>(
+    cx: I,
+    self_ty: I::Ty,
+) -> Result<(ty::Binder<I, (I::FnInputTys, I::Ty)>, I::DefId, I::GenericArgs), NoSolution> {
+    match self_ty.kind() {
+        ty::FnDef(def_id, args) => {
+            let sig = cx.fn_sig(def_id);
+            if sig.skip_binder().is_fn_trait_compatible()
+                && !cx.has_target_features(def_id)
+                && cx.fn_is_const(def_id)
+            {
+                Ok((
+                    sig.instantiate(cx, args).map_bound(|sig| (sig.inputs(), sig.output())),
+                    def_id,
+                    args,
+                ))
+            } else {
+                return Err(NoSolution);
+            }
+        }
+        // `FnPtr`s are not const for now.
+        ty::FnPtr(..) => {
+            return Err(NoSolution);
+        }
+        // `Closure`s are not const for now.
+        ty::Closure(..) => {
+            return Err(NoSolution);
+        }
+        // `CoroutineClosure`s are not const for now.
+        ty::CoroutineClosure(..) => {
+            return Err(NoSolution);
+        }
+
+        ty::Bool
+        | ty::Char
+        | ty::Int(_)
+        | ty::Uint(_)
+        | ty::Float(_)
+        | ty::Adt(_, _)
+        | ty::Foreign(_)
+        | ty::Str
+        | ty::Array(_, _)
+        | ty::Slice(_)
+        | ty::RawPtr(_, _)
+        | ty::Ref(_, _, _)
+        | ty::Dynamic(_, _, _)
+        | ty::Coroutine(_, _)
+        | ty::CoroutineWitness(..)
+        | ty::Never
+        | ty::Tuple(_)
+        | ty::Pat(_, _)
+        | ty::Alias(_, _)
+        | ty::Param(_)
+        | ty::Placeholder(..)
+        | ty::Infer(ty::IntVar(_) | ty::FloatVar(_))
+        | ty::Error(_) => return Err(NoSolution),
+
+        ty::Bound(..)
+        | ty::Infer(ty::TyVar(_) | ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_)) => {
+            panic!("unexpected type `{self_ty:?}`")
+        }
+    }
+}
+
 /// Assemble a list of predicates that would be present on a theoretical
 /// user impl for an object type. These predicates must be checked any time
 /// we assemble a built-in object candidate for an object type, since they
diff --git a/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs b/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs
index 0912e5effa63d..282ca2fedbc4d 100644
--- a/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs
+++ b/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs
@@ -3,15 +3,15 @@
 
 use rustc_type_ir::fast_reject::DeepRejectCtxt;
 use rustc_type_ir::inherent::*;
+use rustc_type_ir::lang_items::TraitSolverLangItem;
 use rustc_type_ir::{self as ty, Interner, elaborate};
 use tracing::instrument;
 
-use super::assembly::Candidate;
+use super::assembly::{Candidate, structural_traits};
 use crate::delegate::SolverDelegate;
-use crate::solve::assembly::{self};
 use crate::solve::{
     BuiltinImplSource, CandidateSource, Certainty, EvalCtxt, Goal, GoalSource, NoSolution,
-    QueryResult,
+    QueryResult, assembly,
 };
 
 impl<D, I> assembly::GoalKind<D> for ty::HostEffectPredicate<I>
@@ -142,7 +142,7 @@ where
             ty::ImplPolarity::Positive => {}
         };
 
-        if !cx.is_const_impl(impl_def_id) {
+        if !cx.impl_is_const(impl_def_id) {
             return Err(NoSolution);
         }
 
@@ -207,7 +207,7 @@ where
         _ecx: &mut EvalCtxt<'_, D>,
         _goal: Goal<I, Self>,
     ) -> Result<Candidate<I>, NoSolution> {
-        todo!("Copy/Clone is not yet const")
+        Err(NoSolution)
     }
 
     fn consider_builtin_pointer_like_candidate(
@@ -225,11 +225,48 @@ where
     }
 
     fn consider_builtin_fn_trait_candidates(
-        _ecx: &mut EvalCtxt<'_, D>,
-        _goal: Goal<I, Self>,
+        ecx: &mut EvalCtxt<'_, D>,
+        goal: Goal<I, Self>,
         _kind: rustc_type_ir::ClosureKind,
     ) -> Result<Candidate<I>, NoSolution> {
-        todo!("Fn* are not yet const")
+        let cx = ecx.cx();
+
+        let self_ty = goal.predicate.self_ty();
+        let (inputs_and_output, def_id, args) =
+            structural_traits::extract_fn_def_from_const_callable(cx, self_ty)?;
+
+        // A built-in `Fn` impl only holds if the output is sized.
+        // (FIXME: technically we only need to check this if the type is a fn ptr...)
+        let output_is_sized_pred = inputs_and_output.map_bound(|(_, output)| {
+            ty::TraitRef::new(cx, cx.require_lang_item(TraitSolverLangItem::Sized), [output])
+        });
+        let requirements = cx
+            .const_conditions(def_id)
+            .iter_instantiated(cx, args)
+            .map(|trait_ref| {
+                (
+                    GoalSource::ImplWhereBound,
+                    goal.with(cx, trait_ref.to_host_effect_clause(cx, goal.predicate.constness)),
+                )
+            })
+            .chain([(GoalSource::ImplWhereBound, goal.with(cx, output_is_sized_pred))]);
+
+        let pred = inputs_and_output
+            .map_bound(|(inputs, _)| {
+                ty::TraitRef::new(cx, goal.predicate.def_id(), [
+                    goal.predicate.self_ty(),
+                    Ty::new_tup(cx, inputs.as_slice()),
+                ])
+            })
+            .to_host_effect_clause(cx, goal.predicate.constness);
+
+        Self::probe_and_consider_implied_clause(
+            ecx,
+            CandidateSource::BuiltinImpl(BuiltinImplSource::Misc),
+            goal,
+            pred,
+            requirements,
+        )
     }
 
     fn consider_builtin_async_fn_trait_candidates(
@@ -314,7 +351,7 @@ where
         _ecx: &mut EvalCtxt<'_, D>,
         _goal: Goal<I, Self>,
     ) -> Result<Candidate<I>, NoSolution> {
-        unreachable!("Destruct is not const")
+        Err(NoSolution)
     }
 
     fn consider_builtin_transmute_candidate(
diff --git a/compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs b/compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs
index 129744b4db7e5..8a01659953d41 100644
--- a/compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs
+++ b/compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs
@@ -394,6 +394,9 @@ where
                     return ecx.forced_ambiguity(MaybeCause::Ambiguity);
                 }
             };
+
+        // A built-in `Fn` impl only holds if the output is sized.
+        // (FIXME: technically we only need to check this if the type is a fn ptr...)
         let output_is_sized_pred = tupled_inputs_and_output.map_bound(|(_, output)| {
             ty::TraitRef::new(cx, cx.require_lang_item(TraitSolverLangItem::Sized), [output])
         });
@@ -408,8 +411,6 @@ where
             })
             .upcast(cx);
 
-        // A built-in `Fn` impl only holds if the output is sized.
-        // (FIXME: technically we only need to check this if the type is a fn ptr...)
         Self::probe_and_consider_implied_clause(
             ecx,
             CandidateSource::BuiltinImpl(BuiltinImplSource::Misc),
@@ -438,6 +439,9 @@ where
                 goal_kind,
                 env_region,
             )?;
+
+        // A built-in `AsyncFn` impl only holds if the output is sized.
+        // (FIXME: technically we only need to check this if the type is a fn ptr...)
         let output_is_sized_pred = tupled_inputs_and_output_and_coroutine.map_bound(
             |AsyncCallableRelevantTypes { output_coroutine_ty: output_ty, .. }| {
                 ty::TraitRef::new(cx, cx.require_lang_item(TraitSolverLangItem::Sized), [output_ty])
@@ -494,8 +498,6 @@ where
             )
             .upcast(cx);
 
-        // A built-in `AsyncFn` impl only holds if the output is sized.
-        // (FIXME: technically we only need to check this if the type is a fn ptr...)
         Self::probe_and_consider_implied_clause(
             ecx,
             CandidateSource::BuiltinImpl(BuiltinImplSource::Misc),
diff --git a/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs b/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs
index 5f7405907127d..e64d4eed9d862 100644
--- a/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs
+++ b/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs
@@ -326,6 +326,9 @@ where
                     return ecx.forced_ambiguity(MaybeCause::Ambiguity);
                 }
             };
+
+        // A built-in `Fn` impl only holds if the output is sized.
+        // (FIXME: technically we only need to check this if the type is a fn ptr...)
         let output_is_sized_pred = tupled_inputs_and_output.map_bound(|(_, output)| {
             ty::TraitRef::new(cx, cx.require_lang_item(TraitSolverLangItem::Sized), [output])
         });
@@ -335,8 +338,6 @@ where
                 ty::TraitRef::new(cx, goal.predicate.def_id(), [goal.predicate.self_ty(), inputs])
             })
             .upcast(cx);
-        // A built-in `Fn` impl only holds if the output is sized.
-        // (FIXME: technically we only need to check this if the type is a fn ptr...)
         Self::probe_and_consider_implied_clause(
             ecx,
             CandidateSource::BuiltinImpl(BuiltinImplSource::Misc),
@@ -364,6 +365,9 @@ where
                 // This region doesn't matter because we're throwing away the coroutine type
                 Region::new_static(cx),
             )?;
+
+        // A built-in `AsyncFn` impl only holds if the output is sized.
+        // (FIXME: technically we only need to check this if the type is a fn ptr...)
         let output_is_sized_pred = tupled_inputs_and_output_and_coroutine.map_bound(
             |AsyncCallableRelevantTypes { output_coroutine_ty, .. }| {
                 ty::TraitRef::new(cx, cx.require_lang_item(TraitSolverLangItem::Sized), [
@@ -380,8 +384,6 @@ where
                 ])
             })
             .upcast(cx);
-        // A built-in `AsyncFn` impl only holds if the output is sized.
-        // (FIXME: technically we only need to check this if the type is a fn ptr...)
         Self::probe_and_consider_implied_clause(
             ecx,
             CandidateSource::BuiltinImpl(BuiltinImplSource::Misc),
diff --git a/compiler/rustc_type_ir/src/interner.rs b/compiler/rustc_type_ir/src/interner.rs
index f988f003c0f95..6e6cf91d85524 100644
--- a/compiler/rustc_type_ir/src/interner.rs
+++ b/compiler/rustc_type_ir/src/interner.rs
@@ -223,7 +223,8 @@ pub trait Interner:
         def_id: Self::DefId,
     ) -> ty::EarlyBinder<Self, impl IntoIterator<Item = (Self::Clause, Self::Span)>>;
 
-    fn is_const_impl(self, def_id: Self::DefId) -> bool;
+    fn impl_is_const(self, def_id: Self::DefId) -> bool;
+    fn fn_is_const(self, def_id: Self::DefId) -> bool;
     fn const_conditions(
         self,
         def_id: Self::DefId,
diff --git a/tests/ui/traits/const-traits/const-fns-are-early-bound.rs b/tests/ui/traits/const-traits/const-fns-are-early-bound.rs
deleted file mode 100644
index c26eaf674546a..0000000000000
--- a/tests/ui/traits/const-traits/const-fns-are-early-bound.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-//@ known-bug: #110395
-//@ failure-status: 101
-//@ dont-check-compiler-stderr
-// FIXME(const_trait_impl) check-pass
-//@ compile-flags: -Znext-solver
-
-#![crate_type = "lib"]
-#![allow(internal_features, incomplete_features)]
-#![no_std]
-#![no_core]
-#![feature(
-    auto_traits,
-    const_trait_impl,
-    effects,
-    lang_items,
-    no_core,
-    staged_api,
-    unboxed_closures,
-    rustc_attrs,
-    marker_trait_attr,
-)]
-#![stable(feature = "minicore", since = "1.0.0")]
-
-fn test() {
-    fn is_const_fn<F>(_: F)
-    where
-        F: const FnOnce<()>,
-    {
-    }
-
-    const fn foo() {}
-
-    is_const_fn(foo);
-}
-
-/// ---------------------------------------------------------------------- ///
-/// Const fn trait definitions
-
-#[const_trait]
-#[lang = "fn"]
-#[rustc_paren_sugar]
-trait Fn<Args: Tuple>: ~const FnMut<Args> {
-    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
-}
-
-#[const_trait]
-#[lang = "fn_mut"]
-#[rustc_paren_sugar]
-trait FnMut<Args: Tuple>: ~const FnOnce<Args> {
-    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
-}
-
-#[const_trait]
-#[lang = "fn_once"]
-#[rustc_paren_sugar]
-trait FnOnce<Args: Tuple> {
-    #[lang = "fn_once_output"]
-    type Output;
-
-    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
-}
-
-/// ---------------------------------------------------------------------- ///
-/// All this other stuff needed for core. Unrelated to test.
-
-#[lang = "destruct"]
-#[const_trait]
-trait Destruct {}
-
-#[lang = "freeze"]
-unsafe auto trait Freeze {}
-
-#[lang = "drop"]
-#[const_trait]
-trait Drop {
-    fn drop(&mut self);
-}
-
-#[lang = "sized"]
-trait Sized {}
-#[lang = "copy"]
-trait Copy {}
-
-#[lang = "tuple_trait"]
-trait Tuple {}
-
-#[lang = "legacy_receiver"]
-trait LegacyReceiver {}
-
-impl<T: ?Sized> LegacyReceiver for &T {}
diff --git a/tests/ui/traits/const-traits/effects/minicore-const-fn-early-bound.rs b/tests/ui/traits/const-traits/effects/minicore-const-fn-early-bound.rs
new file mode 100644
index 0000000000000..ee47f92a0bcfc
--- /dev/null
+++ b/tests/ui/traits/const-traits/effects/minicore-const-fn-early-bound.rs
@@ -0,0 +1,22 @@
+//@ aux-build:minicore.rs
+//@ compile-flags: --crate-type=lib -Znext-solver -Cpanic=abort
+//@ check-pass
+
+#![feature(no_core, const_trait_impl)]
+#![no_std]
+#![no_core]
+
+extern crate minicore;
+use minicore::*;
+
+fn is_const_fn<F>(_: F)
+where
+    F: const FnOnce(),
+{
+}
+
+const fn foo() {}
+
+fn test() {
+    is_const_fn(foo);
+}
diff --git a/tests/ui/traits/const-traits/effects/minicore-fn-fail.rs b/tests/ui/traits/const-traits/effects/minicore-fn-fail.rs
new file mode 100644
index 0000000000000..ae1cbc6ca5885
--- /dev/null
+++ b/tests/ui/traits/const-traits/effects/minicore-fn-fail.rs
@@ -0,0 +1,21 @@
+//@ aux-build:minicore.rs
+//@ compile-flags: --crate-type=lib -Znext-solver
+
+#![feature(no_core, const_trait_impl)]
+#![no_std]
+#![no_core]
+
+extern crate minicore;
+use minicore::*;
+
+const fn call_indirect<T: ~const Fn()>(t: &T) { t() }
+
+#[const_trait]
+trait Foo {}
+impl Foo for () {}
+const fn foo<T: ~const Foo>() {}
+
+const fn test() {
+    call_indirect(&foo::<()>);
+    //~^ ERROR the trait bound `(): ~const Foo` is not satisfied
+}
diff --git a/tests/ui/traits/const-traits/effects/minicore-fn-fail.stderr b/tests/ui/traits/const-traits/effects/minicore-fn-fail.stderr
new file mode 100644
index 0000000000000..cf158643b3472
--- /dev/null
+++ b/tests/ui/traits/const-traits/effects/minicore-fn-fail.stderr
@@ -0,0 +1,9 @@
+error[E0277]: the trait bound `(): ~const Foo` is not satisfied
+  --> $DIR/minicore-fn-fail.rs:19:5
+   |
+LL |     call_indirect(&foo::<()>);
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: aborting due to 1 previous error
+
+For more information about this error, try `rustc --explain E0277`.
diff --git a/tests/ui/traits/const-traits/effects/minicore-works.rs b/tests/ui/traits/const-traits/effects/minicore-works.rs
index bfbfa8b2d056c..c79b4fc07dfd6 100644
--- a/tests/ui/traits/const-traits/effects/minicore-works.rs
+++ b/tests/ui/traits/const-traits/effects/minicore-works.rs
@@ -20,3 +20,9 @@ const fn test_op() {
     let _x = Add::add(1, 2);
     let _y = Custom + Custom;
 }
+
+const fn call_indirect<T: ~const Fn()>(t: &T) { t() }
+
+const fn call() {
+    call_indirect(&call);
+}