On 1/4/22, nearly 4000 Solana NFT projects were drained of their funds due to a reinitialization bug present in the Candy Machine v1 smart contract on Solana. The account, cHfYkrVAwfEoe3Mr2GbvzpNQJboDL6AiBoFZDsf8dxj, converted 1,027 SOL into 155k USDC using Raydium, and then transferred the USDC into their FTX account. The vulnerability was patched while the attack was actively going on, at 6:20am on 1/4/22.
This investigation uncovered similar vulnerabilities in NFT exchanges, yet to be publicized.
Background
Metaplex's Candy Machine, a Solana program which handles the logistics of NFT issuance, just launched last September. You instantiate it with their CLI, feed it your images, and it handles the rest. It will deal with all the technically complex parts of putting the images on chain and creating the smart contracts to mint them to the buyers.
It’s extremely simple to launch an NFT sale with Metaplex; you choose the price you want to set, the timing of the collection drop and any other configs - it handles the rest and mints right to recipients wallets.
This simplicity greatly lowered the barrier to entry - you didn't need to have any Rust knowledge or Solana API experience to use it. When it first came out it led to a huge increase in NFT collections.
Since its inception, over 14,800 candy machines have been created, each corresponding to an NFT collection.

Impact
The goal of this research was to identify how the attacker exploited the vulnerability, trace the funds and their total dollar denominated value, and then to determine which projects were impacted.
The attacker targeted 4,410 of the 14,800 candy machines that were created at the time. I'm guessing they didn't target every vulnerable program because they had trouble pulling the historical candy machine creation records.
They fired off withdrawal transactions that took advantage of the reinitialization bug over the period of an hour.
The withdrawal transactions lasted between 5:57am and 6:49am EST on January 4th 2022. At 6:20am, the patched contract was deployed, causing every subsequent transaction by the attacker to fail.
Of the 4,410 candy machines targeted, 3,470 were completely drained. The vulnerability didn't give the attacker permanent control of the candy machines - only for the duration of that transaction, which means that the candy machines that were impacted are not currently vulnerable.
Some of the notable projects impacted by this vulnerability are SolSteads, Contrastive, and Degen Ape Society, with a full list below.
Vulnerability
The bug was subtle - the attacker was injecting pre-initialized accounts and the program was not checking if the account had already been initialized, meaning an attacker could populate their own address as the authority of the contract.
The fix itself was fairly straightforward.

The hack seems fairly unsophisticated - the damage this vulnerability could do was pretty high, as the bug effectively allowed any account to control the Candy Machine. The attacker submitted the transactions slowly, and would probably have been able to capture the entirety of the vulnerable set of candy machines had they submitted the transactions through their own RPC pool without rate limits.
What's also interesting about the fix is that it was actually fixed in code on December 31st for Candy Machine v2, but the CMv1 contract wasn't redeployed until it was actively being exploited.
Fund extraction
The attacker used Serum DEX and RaydiumSwapV2 to convert the SOL to USDC, then sent the USDC to a FTX address. It should be fairly easy to reverse their idea from FTXs end if they've KYC'd properly.

Candy Machine
Candy Machine v1 is now deprecated, and any new candy machines created should be v2s. From their docs:
The second iteration of the well-known Candy Machine, a fully on-chain generative NFT distribution program, provides many improvements over its predecessor. The new version also allows you to create a whole new set of distribution scenarios and offers protection from bot attacks, while providing the same easy-to-use experience.
Research
Querying for historical data on chain in Solana is a time consuming process. I tried doing research in jupyter notebook at first, but the volume of data made it hard to parse and query.
I ended up cloning the historical transactions into a local database, and indexing that for faster queries.
export class MongoClient {
init = async () => {
log.info("Connecting...");
await connect("mongodb://127.0.0.1:27017", {
keepAlive: true,
keepAliveInitialDelay: 300000,
dbName: "candymachine",
minPoolSize: 50,
maxPoolSize: 500,
});
log.info("Connected to mongo db");
};
saveHashes = async (hashes: object[]) => {
log.debug("Saving batch...");
try {
await Txhashes.insertMany(hashes, { ordered: false });
} catch (e: any) {
// ignore dup key errors
if (!e.message.includes("E11000")) {
log.error(e);
}
}
const count = await Txhashes.count();
log.debug(`Saved batch - ${count} total documents`);
};
getHashes = async (filter: FilterQuery<typeof Txhashes> = {}) => {
const docs = await Txhashes.find(filter).limit(25000);
return docs;
};
}
I first cloned all the transaction hashes into Mongo - I set up a connection pool of various RPCs to accomplish this, as there's no way of getting it from the Solana mainnet-beta RPC in a reasonable amount of time.
const history = await con.getSignaturesForAddress(new PublicKey(publicKey), options);
for (const c of chunk(history, 100000)) {
await mc.saveHashes(c);
log.info("Completed chunk");
}
Then, after fetching all the hashes, I would clone the parsed transaction details into Mongo
const run = async () => {
await mc.init();
log.info("Fetching hashes");
let hashesToFetch = await mc.getHashes({ tx: null });
log.info("Fetched hashes");
let i = 0;
while (hashesToFetch.length) {
let isNearEnd = hashesToFetch.length < 10000;
const chunkSize = isNearEnd ? 10 : 200;
if (isNearEnd) {
shuffle(hashesToFetch);
}
const chunkedHistory = chunk(hashesToFetch, chunkSize)
const processChunk = async (hashes: any[]) => {
let message = `Fetched txs ${i}`;
let savedTxMessage = `Saved txs ${i}`;
i++;
console.time(message);
const hashMap = {};
hashes.forEach((hash) => {
if (hash.signature) {
hashMap[hash.signature] = hash;
} else {
console.error("what");
}
});
try {
const { c, tx: txs } = await fetchTxsWithFallbackWithConnection(
hashes.map((p) => p.signature)
);
console.timeLog(message, c?._rpcEndpoint);
console.timeEnd(message);
let failureCount = 0;
txs.forEach((tx) => {
if (!tx) {
failureCount++;
return;
}
tx.transaction.signatures.forEach((s) => {
if (hashMap[s]) {
hashMap[s].tx = tx;
}
});
});
if (failureCount) {
console.error(`Invalid txs: ${failureCount}`);
}
console.time(savedTxMessage);
await Txhashes.bulkSave(hashes);
console.timeEnd(savedTxMessage);
} catch (e) {
return;
}
};
try {
const promises = chunkedHistory.map((h) => limit(() => processChunk(h)));
await Promise.all(promises);
} catch (e) {
console.error(e);
console.error(
`Chunk failed with total history length of ${hashesToFetch.length}`
);
}
console.log("Finished chunk");
log.info("Fetching hashes");
hashesToFetch = await mc.getHashes({ tx: null });
log.info("Fetched hashes");
}
};
I also pulled every transaction (legitimate and the attackers) that called the withdraw function on the candy machines.
Of the 14,800 candy machines, 11,848 have had the withdraw function executed on them. The top accounts associated with these functions are below.

Only cHfYkrVAwfEoe3Mr2GbvzpNQJboDL6AiBoFZDsf8dxj
seems to be doing this maliciously - the other accounts are all calling legitimate withdraw functions.
F9fER1Cb8hmjapWGZDukzcEYshAUDbSFpbXkj9QuBaQj
actually seems to have created over 2,000 candy machines, and then attempted to call withdraw on them, single handedly creating ~14% of all candy machines on Solana.
[Redacted Pending Vulnerability Disclosure]
[Redacted pending vulnerability disclosure of Solana exchange]
[Redacted Pending Vulnerability Disclosure]
[Redacted pending vulnerability disclosure of Solana exchange]
Identified Projects
I went through and fetched the public keys to all known affected projects and tried to map them back to the hacked machines. I identified 334 unique projects that were actively listed on Magic Eden, Solanart, DigitalEyez, and Solsea as being affected.
These projects can be found here.
Bug Bounty
In conjunction with this vulnerability research, Metaplex has launched a bug bounty program. You can read more on Twitter.
Timeline
- Dec 31st - Fix for CMv2 is landed
- Tue Jan 04 2022 05:57:11 GMT-0500 - First attacker transaction is executed
- Tue Jan 04 2022 06:20:29 GMT-0500 - First transaction that tries to interact with the newly updated contracted is executed
- Tue Jan 04 2022 06:49:27 GMT-0500 - Last transaction that tries to interact with the newly updated contracted is executed
- Tue Jan 06, 2022, 18:25 GMT-0500 - Fix for CMv1 is landed
- Tue Jan 15, 2022, 21:15 GMT-0500 - Metaplex is alerted to this specific vulnerability.
- Fri Mar 11, 2022 - Metaplex bug bounty program is launched in conjunction with this post
- Fri Mar 18, 2022 - Metaplex bug bounty for CMv1 is announced
Appendix
This vulnerability was discussed in Discord's and on Twitter but was not widely analyzed.
All the code for this research will be made public pending final vulnerability disclosures on various exchanges.