diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt
index 3a23bdecc..2b28ffc48 100644
--- a/src/test/fuzz/CMakeLists.txt
+++ b/src/test/fuzz/CMakeLists.txt
@@ -1,209 +1,210 @@
 # Fuzzer test harness
 add_custom_target(bitcoin-fuzzers)
 
 define_property(GLOBAL
 	PROPERTY FUZZ_TARGETS
 	BRIEF_DOCS "List of fuzz targets"
 	FULL_DOCS "A list of the fuzz targets"
 )
 set_property(GLOBAL APPEND PROPERTY FUZZ_TARGETS bitcoin-fuzzers)
 
 include(InstallationHelper)
 macro(add_fuzz_target TARGET EXE_NAME)
 	add_executable(${TARGET} EXCLUDE_FROM_ALL
 		fuzz.cpp
 		${ARGN}
 	)
 	set_target_properties(${TARGET} PROPERTIES OUTPUT_NAME ${EXE_NAME})
 
 	target_link_libraries(${TARGET} server testutil rpcclient)
 
 	add_dependencies(bitcoin-fuzzers ${TARGET})
 
 	set_property(GLOBAL APPEND PROPERTY FUZZ_TARGETS ${TARGET})
 
 	install_target(${TARGET}
 		COMPONENT fuzzer
 		EXCLUDE_FROM_ALL
 	)
 endmacro()
 
 function(add_regular_fuzz_targets)
 	foreach(_fuzz_test_name ${ARGN})
 		sanitize_target_name("fuzz-" ${_fuzz_test_name} _fuzz_target_name)
 		add_fuzz_target(
 			${_fuzz_target_name}
 			${_fuzz_test_name}
 
 			# Sources
 			"${_fuzz_test_name}.cpp"
 		)
 	endforeach()
 endfunction()
 
 include(SanitizeHelper)
 function(add_deserialize_fuzz_targets)
 	foreach(_fuzz_test_name ${ARGN})
 		sanitize_target_name("fuzz-" ${_fuzz_test_name} _fuzz_target_name)
 		add_fuzz_target(
 			${_fuzz_target_name}
 			${_fuzz_test_name}
 
 			# Sources
 			deserialize.cpp
 		)
 
 		sanitize_c_cxx_definition("" ${_fuzz_test_name} _target_definition)
 		string(TOUPPER ${_target_definition} _target_definition)
 		target_compile_definitions(${_fuzz_target_name} PRIVATE ${_target_definition})
 	endforeach()
 endfunction()
 
 function(add_process_message_fuzz_targets)
 	foreach(_fuzz_test_name ${ARGN})
 		sanitize_target_name("fuzz-process_message_" ${_fuzz_test_name} _fuzz_target_name)
 		add_fuzz_target(
 			${_fuzz_target_name}
 			process_message_${_fuzz_test_name}
 
 			# Sources
 			process_message.cpp
 		)
 
 		target_compile_definitions(${_fuzz_target_name} PRIVATE MESSAGE_TYPE=${_fuzz_test_name})
 	endforeach()
 endfunction()
 
 add_regular_fuzz_targets(
 	addition_overflow
 	addrdb
 	asmap
 	asmap_direct
 	base_encode_decode
 	block
 	block_header
 	blockfilter
 	bloom_filter
 	cashaddr
 	chain
 	checkqueue
+	coins_view
 	cuckoocache
 	descriptor_parse
 	eval_script
 	fee_rate
 	fees
 	flatfile
 	float
 	golomb_rice
 	hex
 	http_request
 	integer
 	key
 	key_io
 	kitchen_sink
 	locale
 	merkleblock
 	message
 	multiplication_overflow
 	net_permissions
 	netaddress
 	p2p_transport_deserializer
 	parse_hd_keypath
 	parse_iso8601
 	parse_numbers
 	parse_script
 	parse_univalue
 	prevector
 	pow
 	primitives_transaction
 	process_message
 	process_messages
 	protocol
 	psbt
 	random
 	rolling_bloom_filter
 	script
 	script_flags
 	script_ops
 	scriptnum_ops
 	signature_checker
 	span
 	spanparsing
 	string
 	strprintf
 	system
 	timedata
 	transaction
 	tx_in
 	tx_out
 )
 
 add_deserialize_fuzz_targets(
 	addr_info_deserialize
 	address_deserialize
 	addrman_deserialize
 	banentry_deserialize
 	block_deserialize
 	block_file_info_deserialize
 	block_filter_deserialize
 	block_header_and_short_txids_deserialize
 	blockheader_deserialize
 	blocklocator_deserialize
 	blockmerkleroot
 	blocktransactions_deserialize
 	blocktransactionsrequest_deserialize
 	blockundo_deserialize
 	bloomfilter_deserialize
 	coins_deserialize
 	diskblockindex_deserialize
 	fee_rate_deserialize
 	flat_file_pos_deserialize
 	inv_deserialize
 	key_origin_info_deserialize
 	merkle_block_deserialize
 	messageheader_deserialize
 	netaddr_deserialize
 	out_point_deserialize
 	partial_merkle_tree_deserialize
 	partially_signed_transaction_deserialize
 	prefilled_transaction_deserialize
 	psbt_input_deserialize
 	psbt_output_deserialize
 	pub_key_deserialize
 	script_deserialize
 	service_deserialize
 	snapshotmetadata_deserialize
 	sub_net_deserialize
 	tx_in_deserialize
 	txoutcompressor_deserialize
 	txundo_deserialize
 	uint160_deserialize
 	uint256_deserialize
 )
 
 add_process_message_fuzz_targets(
 	addr
 	block
 	blocktxn
 	cmpctblock
 	feefilter
 	filteradd
 	filterclear
 	filterload
 	getaddr
 	getblocks
 	getblocktxn
 	getdata
 	getheaders
 	headers
 	inv
 	mempool
 	notfound
 	ping
 	pong
 	sendcmpct
 	sendheaders
 	tx
 	verack
 	version
 )
diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp
new file mode 100644
index 000000000..d91449de6
--- /dev/null
+++ b/src/test/fuzz/coins_view.cpp
@@ -0,0 +1,314 @@
+// Copyright (c) 2020 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <amount.h>
+#include <chainparams.h>
+#include <chainparamsbase.h>
+#include <coins.h>
+#include <consensus/tx_verify.h>
+#include <consensus/validation.h>
+#include <key.h>
+#include <node/coinstats.h>
+#include <policy/policy.h>
+#include <primitives/blockhash.h>
+#include <primitives/transaction.h>
+#include <pubkey.h>
+#include <validation.h>
+
+#include <test/fuzz/FuzzedDataProvider.h>
+#include <test/fuzz/fuzz.h>
+#include <test/fuzz/util.h>
+
+#include <cstdint>
+#include <limits>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace {
+const Coin EMPTY_COIN{};
+
+bool operator==(const Coin &a, const Coin &b) {
+    if (a.IsSpent() && b.IsSpent()) {
+        return true;
+    }
+    return a.IsCoinBase() == b.IsCoinBase() && a.GetHeight() == b.GetHeight() &&
+           a.GetTxOut() == b.GetTxOut();
+}
+} // namespace
+
+void initialize() {
+    static const ECCVerifyHandle ecc_verify_handle;
+    ECC_Start();
+    SelectParams(CBaseChainParams::REGTEST);
+}
+
+void test_one_input(const std::vector<uint8_t> &buffer) {
+    FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
+    CCoinsView backend_coins_view;
+    CCoinsViewCache coins_view_cache{&backend_coins_view};
+    COutPoint random_out_point;
+    Coin random_coin;
+    CMutableTransaction random_mutable_transaction;
+    while (fuzzed_data_provider.ConsumeBool()) {
+        switch (fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 9)) {
+            case 0: {
+                if (random_coin.IsSpent()) {
+                    break;
+                }
+                Coin coin = random_coin;
+                bool expected_code_path = false;
+                const bool possible_overwrite =
+                    fuzzed_data_provider.ConsumeBool();
+                try {
+                    coins_view_cache.AddCoin(random_out_point, std::move(coin),
+                                             possible_overwrite);
+                    expected_code_path = true;
+                } catch (const std::logic_error &e) {
+                    if (e.what() ==
+                        std::string{"Attempted to overwrite an unspent coin "
+                                    "(when possible_overwrite is false)"}) {
+                        assert(!possible_overwrite);
+                        expected_code_path = true;
+                    }
+                }
+                assert(expected_code_path);
+                break;
+            }
+            case 1: {
+                (void)coins_view_cache.Flush();
+                break;
+            }
+            case 2: {
+                coins_view_cache.SetBestBlock(
+                    BlockHash(ConsumeUInt256(fuzzed_data_provider)));
+                break;
+            }
+            case 3: {
+                Coin move_to;
+                (void)coins_view_cache.SpendCoin(
+                    random_out_point,
+                    fuzzed_data_provider.ConsumeBool() ? &move_to : nullptr);
+                break;
+            }
+            case 4: {
+                coins_view_cache.Uncache(random_out_point);
+                break;
+            }
+            case 5: {
+                if (fuzzed_data_provider.ConsumeBool()) {
+                    backend_coins_view = CCoinsView{};
+                }
+                coins_view_cache.SetBackend(backend_coins_view);
+                break;
+            }
+            case 6: {
+                const std::optional<COutPoint> opt_out_point =
+                    ConsumeDeserializable<COutPoint>(fuzzed_data_provider);
+                if (!opt_out_point) {
+                    break;
+                }
+                random_out_point = *opt_out_point;
+                break;
+            }
+            case 7: {
+                const std::optional<Coin> opt_coin =
+                    ConsumeDeserializable<Coin>(fuzzed_data_provider);
+                if (!opt_coin) {
+                    break;
+                }
+                random_coin = *opt_coin;
+                break;
+            }
+            case 8: {
+                const std::optional<CMutableTransaction>
+                    opt_mutable_transaction =
+                        ConsumeDeserializable<CMutableTransaction>(
+                            fuzzed_data_provider);
+                if (!opt_mutable_transaction) {
+                    break;
+                }
+                random_mutable_transaction = *opt_mutable_transaction;
+                break;
+            }
+            case 9: {
+                CCoinsMap coins_map;
+                while (fuzzed_data_provider.ConsumeBool()) {
+                    CCoinsCacheEntry coins_cache_entry;
+                    coins_cache_entry.flags =
+                        fuzzed_data_provider.ConsumeIntegral<uint8_t>();
+                    if (fuzzed_data_provider.ConsumeBool()) {
+                        coins_cache_entry.coin = random_coin;
+                    } else {
+                        const std::optional<Coin> opt_coin =
+                            ConsumeDeserializable<Coin>(fuzzed_data_provider);
+                        if (!opt_coin) {
+                            break;
+                        }
+                        coins_cache_entry.coin = *opt_coin;
+                    }
+                    coins_map.emplace(random_out_point,
+                                      std::move(coins_cache_entry));
+                }
+                bool expected_code_path = false;
+                try {
+                    coins_view_cache.BatchWrite(
+                        coins_map,
+                        fuzzed_data_provider.ConsumeBool()
+                            ? BlockHash(ConsumeUInt256(fuzzed_data_provider))
+                            : coins_view_cache.GetBestBlock());
+                    expected_code_path = true;
+                } catch (const std::logic_error &e) {
+                    if (e.what() ==
+                        std::string{"FRESH flag misapplied to coin that exists "
+                                    "in parent cache"}) {
+                        expected_code_path = true;
+                    }
+                }
+                assert(expected_code_path);
+                break;
+            }
+        }
+    }
+
+    {
+        const Coin &coin_using_access_coin =
+            coins_view_cache.AccessCoin(random_out_point);
+        const bool exists_using_access_coin =
+            !(coin_using_access_coin == EMPTY_COIN);
+        const bool exists_using_have_coin =
+            coins_view_cache.HaveCoin(random_out_point);
+        const bool exists_using_have_coin_in_cache =
+            coins_view_cache.HaveCoinInCache(random_out_point);
+        Coin coin_using_get_coin;
+        const bool exists_using_get_coin =
+            coins_view_cache.GetCoin(random_out_point, coin_using_get_coin);
+        if (exists_using_get_coin) {
+            assert(coin_using_get_coin == coin_using_access_coin);
+        }
+        assert((exists_using_access_coin && exists_using_have_coin_in_cache &&
+                exists_using_have_coin && exists_using_get_coin) ||
+               (!exists_using_access_coin && !exists_using_have_coin_in_cache &&
+                !exists_using_have_coin && !exists_using_get_coin));
+        const bool exists_using_have_coin_in_backend =
+            backend_coins_view.HaveCoin(random_out_point);
+        if (exists_using_have_coin_in_backend) {
+            assert(exists_using_have_coin);
+        }
+        Coin coin_using_backend_get_coin;
+        if (backend_coins_view.GetCoin(random_out_point,
+                                       coin_using_backend_get_coin)) {
+            assert(exists_using_have_coin_in_backend);
+            assert(coin_using_get_coin == coin_using_backend_get_coin);
+        } else {
+            assert(!exists_using_have_coin_in_backend);
+        }
+    }
+
+    {
+        bool expected_code_path = false;
+        try {
+            (void)coins_view_cache.Cursor();
+        } catch (const std::logic_error &) {
+            expected_code_path = true;
+        }
+        assert(expected_code_path);
+        (void)coins_view_cache.DynamicMemoryUsage();
+        (void)coins_view_cache.EstimateSize();
+        (void)coins_view_cache.GetBestBlock();
+        (void)coins_view_cache.GetCacheSize();
+        (void)coins_view_cache.GetHeadBlocks();
+        (void)coins_view_cache.HaveInputs(
+            CTransaction{random_mutable_transaction});
+    }
+
+    {
+        const CCoinsViewCursor *coins_view_cursor = backend_coins_view.Cursor();
+        assert(coins_view_cursor == nullptr);
+        (void)backend_coins_view.EstimateSize();
+        (void)backend_coins_view.GetBestBlock();
+        (void)backend_coins_view.GetHeadBlocks();
+    }
+
+    if (fuzzed_data_provider.ConsumeBool()) {
+        switch (fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 3)) {
+            case 0: {
+                const CTransaction transaction{random_mutable_transaction};
+                bool is_spent = false;
+                for (const CTxOut &tx_out : transaction.vout) {
+                    if (Coin{tx_out, 0, transaction.IsCoinBase()}.IsSpent()) {
+                        is_spent = true;
+                    }
+                }
+                if (is_spent) {
+                    // Avoid:
+                    // coins.cpp:69: void CCoinsViewCache::AddCoin(const
+                    // COutPoint &, Coin &&, bool): Assertion `!coin.IsSpent()'
+                    // failed.
+                    break;
+                }
+                bool expected_code_path = false;
+                const int height = fuzzed_data_provider.ConsumeIntegral<int>();
+                const bool possible_overwrite =
+                    fuzzed_data_provider.ConsumeBool();
+                try {
+                    AddCoins(coins_view_cache, transaction, height,
+                             possible_overwrite);
+                    expected_code_path = true;
+                } catch (const std::logic_error &e) {
+                    if (e.what() ==
+                        std::string{"Attempted to overwrite an unspent coin "
+                                    "(when possible_overwrite is false)"}) {
+                        assert(!possible_overwrite);
+                        expected_code_path = true;
+                    }
+                }
+                assert(expected_code_path);
+                break;
+            }
+            case 1: {
+                uint32_t flags =
+                    fuzzed_data_provider.ConsumeIntegral<uint32_t>();
+                (void)AreInputsStandard(
+                    CTransaction{random_mutable_transaction}, coins_view_cache,
+                    flags);
+                break;
+            }
+            case 2: {
+                TxValidationState state;
+                Amount tx_fee_out;
+                const CTransaction transaction{random_mutable_transaction};
+                if (ContainsSpentInput(transaction, coins_view_cache)) {
+                    // Avoid:
+                    // consensus/tx_verify.cpp:171: bool
+                    // Consensus::CheckTxInputs(const CTransaction &,
+                    // TxValidationState &, const CCoinsViewCache &, int,
+                    // CAmount &): Assertion `!coin.IsSpent()' failed.
+                    break;
+                }
+                try {
+                    (void)Consensus::CheckTxInputs(
+                        transaction, state, coins_view_cache,
+                        fuzzed_data_provider.ConsumeIntegralInRange<int>(
+                            0, std::numeric_limits<int>::max()),
+                        tx_fee_out);
+                    assert(MoneyRange(tx_fee_out));
+                } catch (const std::runtime_error &) {
+                }
+                break;
+            }
+            case 3: {
+                CCoinsStats stats;
+                bool expected_code_path = false;
+                try {
+                    (void)GetUTXOStats(&coins_view_cache, stats);
+                } catch (const std::logic_error &) {
+                    expected_code_path = true;
+                }
+                assert(expected_code_path);
+                break;
+            }
+        }
+    }
+}
diff --git a/src/test/fuzz/util.h b/src/test/fuzz/util.h
index 36ffd0284..cb680131a 100644
--- a/src/test/fuzz/util.h
+++ b/src/test/fuzz/util.h
@@ -1,149 +1,162 @@
 // Copyright (c) 2009-2019 The Bitcoin Core developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
 #ifndef BITCOIN_TEST_FUZZ_UTIL_H
 #define BITCOIN_TEST_FUZZ_UTIL_H
 
 #include <amount.h>
 #include <arith_uint256.h>
 #include <attributes.h>
+#include <coins.h>
 #include <script/script.h>
 #include <serialize.h>
 #include <streams.h>
 #include <uint256.h>
 #include <version.h>
 
 #include <test/fuzz/FuzzedDataProvider.h>
 #include <test/fuzz/fuzz.h>
 
 #include <cstdint>
 #include <string>
 #include <vector>
 
 NODISCARD inline std::vector<uint8_t>
 ConsumeRandomLengthByteVector(FuzzedDataProvider &fuzzed_data_provider,
                               const size_t max_length = 4096) noexcept {
     const std::string s =
         fuzzed_data_provider.ConsumeRandomLengthString(max_length);
     return {s.begin(), s.end()};
 }
 
 NODISCARD inline std::vector<std::string>
 ConsumeRandomLengthStringVector(FuzzedDataProvider &fuzzed_data_provider,
                                 const size_t max_vector_size = 16,
                                 const size_t max_string_length = 16) noexcept {
     const size_t n_elements =
         fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, max_vector_size);
     std::vector<std::string> r;
     for (size_t i = 0; i < n_elements; ++i) {
         r.push_back(
             fuzzed_data_provider.ConsumeRandomLengthString(max_string_length));
     }
     return r;
 }
 
 template <typename T>
 NODISCARD inline std::vector<T>
 ConsumeRandomLengthIntegralVector(FuzzedDataProvider &fuzzed_data_provider,
                                   const size_t max_vector_size = 16) noexcept {
     const size_t n_elements =
         fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, max_vector_size);
     std::vector<T> r;
     for (size_t i = 0; i < n_elements; ++i) {
         r.push_back(fuzzed_data_provider.ConsumeIntegral<T>());
     }
     return r;
 }
 
 template <typename T>
 NODISCARD inline std::optional<T>
 ConsumeDeserializable(FuzzedDataProvider &fuzzed_data_provider,
                       const size_t max_length = 4096) noexcept {
     const std::vector<uint8_t> buffer =
         ConsumeRandomLengthByteVector(fuzzed_data_provider, max_length);
     CDataStream ds{buffer, SER_NETWORK, INIT_PROTO_VERSION};
     T obj;
     try {
         ds >> obj;
     } catch (const std::ios_base::failure &) {
         return std::nullopt;
     }
     return obj;
 }
 
 NODISCARD inline opcodetype
 ConsumeOpcodeType(FuzzedDataProvider &fuzzed_data_provider) noexcept {
     return static_cast<opcodetype>(
         fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(0, MAX_OPCODE));
 }
 
 NODISCARD inline Amount
 ConsumeMoney(FuzzedDataProvider &fuzzed_data_provider) noexcept {
     // FIXME find a better way to avoid duplicating the MAX_MONEY definition
     int64_t maxMoneyAsInt = int64_t(21000000) * int64_t(100000000);
     return fuzzed_data_provider.ConsumeIntegralInRange<int64_t>(0,
                                                                 maxMoneyAsInt) *
            SATOSHI;
 }
 
 NODISCARD inline CScript
 ConsumeScript(FuzzedDataProvider &fuzzed_data_provider) noexcept {
     const std::vector<uint8_t> b =
         ConsumeRandomLengthByteVector(fuzzed_data_provider);
     return {b.begin(), b.end()};
 }
 
 NODISCARD inline CScriptNum
 ConsumeScriptNum(FuzzedDataProvider &fuzzed_data_provider) noexcept {
     return CScriptNum{fuzzed_data_provider.ConsumeIntegral<int64_t>()};
 }
 
 NODISCARD inline uint256
 ConsumeUInt256(FuzzedDataProvider &fuzzed_data_provider) noexcept {
     const std::vector<uint8_t> v256 =
         fuzzed_data_provider.ConsumeBytes<uint8_t>(sizeof(uint256));
     if (v256.size() != sizeof(uint256)) {
         return {};
     }
     return uint256{v256};
 }
 
 NODISCARD inline arith_uint256
 ConsumeArithUInt256(FuzzedDataProvider &fuzzed_data_provider) noexcept {
     return UintToArith256(ConsumeUInt256(fuzzed_data_provider));
 }
 
 template <typename T>
 NODISCARD bool MultiplicationOverflow(const T i, const T j) noexcept {
     static_assert(std::is_integral<T>::value, "Integral required.");
     if (std::numeric_limits<T>::is_signed) {
         if (i > 0) {
             if (j > 0) {
                 return i > (std::numeric_limits<T>::max() / j);
             } else {
                 return j < (std::numeric_limits<T>::min() / i);
             }
         } else {
             if (j > 0) {
                 return i < (std::numeric_limits<T>::min() / j);
             } else {
                 return i != 0 && (j < (std::numeric_limits<T>::max() / i));
             }
         }
     } else {
         return j != 0 && i > std::numeric_limits<T>::max() / j;
     }
 }
 
 template <class T>
 NODISCARD bool AdditionOverflow(const T i, const T j) noexcept {
     static_assert(std::is_integral<T>::value, "Integral required.");
     if (std::numeric_limits<T>::is_signed) {
         return (i > 0 && j > std::numeric_limits<T>::max() - i) ||
                (i < 0 && j < std::numeric_limits<T>::min() - i);
     }
     return std::numeric_limits<T>::max() - i < j;
 }
 
+NODISCARD inline bool
+ContainsSpentInput(const CTransaction &tx,
+                   const CCoinsViewCache &inputs) noexcept {
+    for (const CTxIn &tx_in : tx.vin) {
+        const Coin &coin = inputs.AccessCoin(tx_in.prevout);
+        if (coin.IsSpent()) {
+            return true;
+        }
+    }
+    return false;
+}
+
 #endif // BITCOIN_TEST_FUZZ_UTIL_H