Loss exceeds $88 million: Analysis of the Coldcard hardware wallet vulnerability and tracking of stolen funds. On July 31, approximately 500 Coldcard hardware wallets were compromised, resulting in the theft of 594 bitcoins worth approximately $38 million. Subsequently, Coinkite, the company responsible for manufacturing Coldcard wallets, confirmed a security vulnerability in the key generation process affecting multiple generations of the product, including Coldcard Mk2, Mk3, Mk4, Q, and Mk5.
The loss amount resulting from this vulnerability exploit has already exceeded $88 million, and the attack is still ongoing. Coldcard wallet users should transfer their funds to another address as soon as possible. Beosin’s analysis of this vulnerability and the tracking of the stolen funds are as follows.
I. Vulnerability Analysis
Analyzing the commit history of the Coldcard firmware reveals that, in the previous commit 37e4af5451c260c1e7d429fe8972c4cb5e68ee59, the development team updated several code sections related to MK4 configuration, including in mpconfigboard.h:
// We have our own version of this code.
#define MICROPY_HW_ENABLE_RNG (0)
On the MicroPython STM32 side, this macro controls the compilation path for the default hardware random number generator (RNG) binding and the generic random number implementation. Setting it to 0 prevents the default hardware RNG path from being used as a backend for the generic rng_get().
Its comments indicate that the developer will implement the RNG themselves; checking the custom rng.h file reveals only two MicroPython objects declared:
MP_DECLARE_CONST_FUN_OBJ_0(pyb_rng_get_obj); MP_DECLARE_CONST_FUN_OBJ_1(pyb_rng_get_bytes_obj);
The corresponding implementation is:
/// \function pyb_rng_get()///// Returns a 30-bit hardware-generated random number, or fails!//STATIC mp_obj_t pyb_rng_get(void){ // Retrieve and return the new random number return mp_obj_new_int(rng_get_or_fault() >> 2);}
/// \function rng_get_bytes()/// Fills a buffer with random bits; the caller must provide a buffer of appropriate size.STATIC mp_obj_t pyb_rng_get_bytes(mp_obj_t buffer_io) {
mp_buffer_info_t bufinfo; mp_get_buffer_raise(buffer_io, &bufinfo, MP_BUFFER_WRITE);
mp_uint_t count = bufinfo.len; if(count < 1) { mp_raise_ValueError(NULL); }
// Read 32-bit words and unpack into the provided buffer random_buffer(bufinfo.buf, count);
return mp_const_none;}
MP_DEFINE_CONST_FUN_OBJ_0(pyb_rng_get_obj, pyb_rng_get);MP_DEFINE_CONST_FUN_OBJ_1(pyb_rng_get_bytes_obj, pyb_rng_get_bytes);
rng_get_or_fault() reads the STM32 RNG peripheral directly:
static uint32_t rng_get_or_fault(void) {
// Enable the RNG peripheral if it's not already enabled
rng_init();
// Wait for a new random number to be ready, taking approximately 10 µs
uint32_t start = HAL_GetTick();
while (!(RNG->SR & RNG_SR_DRDY)) {
if (HAL_GetTick() - start >= RNG_TIMEOUT_MS) {
// Hardware failure... do not return anything!
mp_raise_OSError(MP_EFAULT);
}
}
// Retrieve and return the new random number
last_value = RNG->DR;
return last_value;
}
This indicates that the Coldcard custom code indeed intends to use the hardware RNG, but it only guarantees the use of the hardware function when calling pyb_rng_get* or its internal random_buffer().
During wallet creation, the following code is actually invoked, where the wallet creation function in shared/seed.py is:
async def make_new_wallet(nwords): # Select a new random seed. await ux_dramatic_pause('Generating...', 3) seed = generate_seed() words = await approve_word_list(seed, nwords) if words: await commit_new_words(words)
This call first enters the Coldcard's shared/random.py module. In previous versions, random.py explicitly depended on ngu.random and retained:
# random.py - subset of the random module, with no compatibility, using cryptographically secure RNG
# for bytes, use ngu.random.bytes(len)
# bytes = ngu.random.bytes
The wallet initialization uses random.bytes, not pyb.rng(); the custom pyb_rng_get_obj does not automatically override random.bytes.
Since MICROPY_HW_ENABLE_RNG is set to 0, the wallet generation did not use a hardware RNG but instead called pyb_rng_yasmarang from micropython/ports/stm32/rng.c:
#if MICROPY_HW_ENABLE_RNG
uint32_t rng_get(void) { // Use STM32 hardware RNG ...}
#else
// For MCUs without an RNG, we still need to provide an rng_get() function.// A pseudo-RNG is not ideal, but we use it for now.
// Yasmarang random number generatorstatic uint32_t pyb_rng_yasmarang(void) { static bool seeded = false; static uint32_t pad = 0, n = 0; ...}
uint32_t rng_get(void) { return pyb_rng_yasmarang();}
#endif
pyb_rng_yasmarang is a pseudorandom number generator and is highly insecure for generating hardware wallet seeds, as attackers can brute-force the key. Coinkite has now explicitly excluded stm32/rng.c in the Makefile:
Do not compile MicroPython's fallback PRNG. The board-specific rng.c provides rng_get(), and this empty object satisfies the upstream object list.
$(BUILD)/rng.o: CFLAGS += -Dpyb_rng_yasmarang=error-do-not-want-this
$(BUILD)/rng.o:
$(ECHO) "SKIP stm32/rng.c"
$(Q)$(CC) $(CFLAGS) -x c -c /dev/null -o $@
II. Tracking Stolen Funds
Funds from multiple victim wallets have already been transferred and aggregated into several addresses, without further laundering yet. Through threat intelligence and on-chain behavior analysis, Beosin Trace has identified the following aggregation addresses:
bc1qq85v2c926eg6pgxhwp6q7lf6cnsz80qs3fcu9r (562 BTC)
bc1qx76cae2706qd5q576feh7xq8rfcsjpf2htfhe3 (398.47 BTC)
Additionally, the following aggregation addresses exhibit a fund flow pattern similar to the one above and have not yet undergone any further transfers:
- bc1q8jy96fe5lf8vfugydnte3cguk92gpev7kwtp3q (89.62 BTC)
- bc1q0rvn88w08j75k4h48lf9fvhan7unjp7vjf5q6m (64.9 BTC)
- bc1qtfrwa4j6rmj9rsgspv6a0yjumkg39js2numu75 (45.9 BTC)
- bc1qmd5m5ktv7m5ffujxv4248fxv36myvdx79n8jp6 (30.18 BTC)
Attacks targeting the Coldcard wallet are still ongoing, and the Beosin team is continuously monitoring additional aggregated addresses and analyzing related fund movements.
III. Conclusion
This major security incident involving the Coldcard hardware wallet stemmed from an implementation error by the development team, which used a pseudorandom number generator during the critical wallet seed generation process. The development team should conduct ongoing, comprehensive testing and auditing of the code, and Coldcard wallet users should promptly transfer their assets and closely monitor Coinkite’s future security announcements.
Beosin is a leading blockchain security and regulatory compliance technology company specializing in pre-launch smart contract security audits, real-time security risk monitoring and blocking, asset recovery, virtual asset anti-money laundering (AML), and investigative tracing. Beosin has provided “one-stop” blockchain compliance products and security services to regulatory and law enforcement agencies in over 20 countries and regions, more than 200 virtual asset service providers, and 4,500+ Web3 projects.

