I build my own MPL Candy Machine Guard and you can too.

We all have those days when we need features that the program being used doesn’t support. This happened to me this week when I need a ‘guard’ from the Metaplex Guard program that wasn’t supported.

In this guide, we’ll go over how to create your own custom candy guard and use it in your project.

This is just a rehashing from the official docs

Candy guard program

The Token Metadata candy machine is split into two programs.

The Why

We needed a guard that could dynamically change the price based on a predetermined formula determined by the number of items redeemed from the candy machine.

We needed the price of our mints to follow an exponential price curve.

$$ y = ae^{bx} + c $$

Starting from zero, as the demand to mint from our collection grew, we would gradually increase the price.

Solutions attempted

First attempt

Initially the first solution, involved making a CPI call into the candy guard mint_v1 instruction, but

  1. This instruction requires too many accounts. Seriously!!!! I thought the maximum accounts the #[derive(Accounts)] struct could hold was 30. This one takes a bajillion of them.

Look at all these lovely accounts!

  1. I couldn’t figure out what to pass into the mint_args and label parameters for the mint_v1 instruction.
pub fn mint_v2<'info>(
    ctx: Context<'_, '_, '_, 'info, MintV2<'info>>,
    mint_args: Vec<u8>,
    label: Option<String>,
) -> Result<()> {

// -- snip snip

Second attempt

Second solution, which was ruled out almost immediately was to try and combine the mint instructions from the candy guard program with the transfer instruction.

let mintTxBuilder = await transactionBuilder()
	.add(setComputeUnitLimit(umi, { units: 400_000 }))
	.add(
		mintV2(umi, {
			candyMachine: UMIPublicKey(candyMachine),
			nftMint: asset,
			collectionMint: UMIPublicKey(collection),
			collectionUpdateAuthority: umi.identity.publicKey,

			// -- snip snip
		})
	);

const mintIxs = mintTxBuilder
	.getInstructions()
	.map((ix) => toWeb3JsInstruction(ix));

const lamports = calculateMintPriceFn(growthRate);

const transferIx = SystemProgram.transfer({
	fromPubkey: fromPublicKey,
	toPubkey: toPublicKey,
	lamports,
});

const messageV0 = new TransactionMessage({
	payerKey: me.publicKey,
	recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
	instructions: [...mintIxs, transferIx],
}).compileToV0Message();

// sign and send tx

The glaring drawback with this solution was that the logic for calculating the mint price would have to be performed outside of any program and we’d basically be saying “Trust me bro, you got a fair price!”

My second worry was that we could have hit the transaction size limits with this further forcing us to overengineer some sort of queuing mechanism when a mint instruction came through.

Third attempt

Third solution was to try and use the solPayment guard. Since the solPayment only accepts a constant value for the mint price, would have set the initial price at zero and after every mint call the updateCandyGuard to set the new price of the mint.

On top of costing the payer for the update tx costs, like with the previously mentioned method, it would require us to be very careful and 1. make sure that no update is missed 2. make sure that the payer updating the candy guard is always credited with enough lamports to perform the operation.

Final attempt

The Fourth solution and final that we tried and that i’ll be explaining is forking the candy guard program and writing your own guard

Let’s chew some glass

Head on over to the Metaplex Candy machine repo and clone/fork the repo,

https://github.com/metaplex-foundation/mpl-candy-machine.git

Navigate to the candy guard program directory and open it in you preferred editor

# /programs/candy-guard
code .

Inspecting the Anchor.toml shows that the program is running on older anchor and solana CLI versions and as the programming adage goes if it works don’t touch it.

Lets downgrade/upgrade our tooling and try building the program

avm use 0.28.0

solana-install init 1.17.34

anchor build

If you get dependency issues while building, it might be because cargo id pulling the latest crate versions and you might want to set these versions directly.

The two that were misbehaving for me were the SPL crates,

cargo update -p [email protected] --precise 2.0.0
cargo update -p [email protected] --precise 0.7.0
cargo update -p [email protected] --precise 4.0.0

What we will build

We will be building a custom guard that calls the memo program upon every mint with the items that have been minted(“redeemed”) and the minter’s public key.

The first thing is to make sure that we avoid the programId mismatch error,

Let’s get the programId of the candy guard program and replace it inside lib.rs

# run this inside the candy guard directory

anchor keys list

# /program/lib.rs

declare_id!("UPDATE WITH THE ADDRESS GENERATED ABOVE");

We will conveniently call our guard memo. Inside the guards directory, create a new file memo.rs where out guard code will live.

touch memo.rs

We will also need to add the spl-memo crate. Run this inside the program directory i.e inside mpl-candy-machine/programs/candy-guard/program

cargo add [email protected] --features=no-entrypoint

As per our tradition run anchor build to make sure the new dep plays well with the others.

Should cargo go out of it’s way to resolve a newer package and build error out with


error: package `solana-program v2.0.9` cannot be built because it requires rustc 1.75.0 or newer, while the currently active rustc version is 1.68.0-dev
Either upgrade to rustc 1.75.0 or newer, or use
cargo update -p [email protected] --precise ver
where `ver` is the latest version of `solana-program` supporting rustc 1.68.0-dev

Find the version that cargo pulled using

cargo tree | grep spl-memo

In my case version [email protected] had been pulled and I downgraded it using

cargo update -p [email protected] --precise 4.0.0

Moa glass chewing

Back to building our memo guard in memo.rs.

Inside our file, let start by importing the required crates and defining the argument structure for our input.

use super::*;

use solana_program::program::invoke;
use spl_memo::build_memo;

use crate::{state::GuardType, utils::assert_keys_equal};

/// Guard that logs the items minted from our candy machine upon each mint and the minter address.
///
/// List of accounts required:
///
///   0. `[]` Account minting the NFT.
///
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct Memo {
    pub minter: Pubkey,
}

We are then going to bring our memo.rs module in scope, but declaring and exporting it from programs/candy-guard/program/src/guards/mod.rs

// -- snip
pub use gatekeeper::Gatekeeper;
pub use memo::Memo; // new
// -- snip

// -- snip
mod gatekeeper;
mod memo; // new
// -- snip

Let’s continue by adding our guard to the list of default Metaplex guards in programs/candy-guard/program/src/state/candy_guard.rs

/// The set of guards available.
#[derive(GuardSet, AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct GuardSet {
    // -- snip

    /// Token2022 payment guard (set the price for the mint in spl-token-2022 amount).
    pub token2022_payment: Option<Token2022Payment>,
    /// Logs items redeemed and the minter address
    pub memo: Option<Memo>,
}

/// Available guard types.
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub enum GuardType {
    // snip
    Token2022Payment,
    Memo,
}

It’s safe to ignore the trait bound error as it’ll go away once we implement our guard in the next section.

To write a Metaplex guard, you need to implement two traits for it.

  1. The Guard trait. Contains methods specific to the guard such as size which gets the size, load that loads a guard, is_enabled to check if a guard is active e.t.c.

  2. The Condition trait. Contains methods specific to the execution of the guard. The three methods are

    • validate verifies that the conditions specified for the guard are met.
    • pre_actions executed only after the guard conditions have been met in validate before the mint.
    • post_actions - executed after the mint occurs after the validation conditions have been met.

Let’s implement these for our guard.

// -- snip

impl Guard for Memo {
    fn size() -> usize {
        32 // minter
    }

    fn mask() -> u64 {
        GuardType::as_mask(GuardType::Memo)
    }
}

impl Condition for Memo {
    fn validate<'info>(
        &self,
        ctx: &mut EvaluationContext,
        _guard_set: &GuardSet,
        _mint_args: &[u8],
    ) -> Result<()> {
        Ok(())
    }

    fn post_actions<'info>(
        &self,
        ctx: &mut EvaluationContext,
        _guard_set: &GuardSet,
        _mint_args: &[u8],
    ) -> Result<()> {
        Ok(())
    }
}

We’ve defined the structure but not much is going on. Let’s start by writing the logic for the validate method.

We are going to start off by asserting that the account supplied in the arguments when calling the argument is the minter address.

To get the account supplied when calling this guard, we use the account cursor which is an index that keeps track of remaining accounts supplied. Upon the use of an account we increment this cursor by the number of accounts we’ve consumed.

If you are familiar with anchor we can get the instruction accounts from the EvaluationContext that contains the required account for us to mint the NFT. We will get the minter account from this ctx and make sure that it matches the account supplied in the instruction call argument.

// -- snip

fn validate<'info>(
    &self,
    ctx: &mut EvaluationContext,
    _guard_set: &GuardSet,
    _mint_args: &[u8],
) -> Result<()> {
    // current rem accounts cursor
    let index = ctx.account_cursor;

    // validate minter acc is correct is similar to one in accs context
    // by getting it from the arguments supplied
    let minter = try_get_account_info(ctx.accounts.remaining, index)?;

    // assert that the keys are equal
    assert_keys_equal(minter.key, &self.minter)?;

    // consume the account index
    ctx.account_cursor += 1;

    // map consumed account to use in next ix
    ctx.indices.insert("minter_acc_index", index);

    Ok(())
}

// --snip

*Note:* This was noted after I’d written the guide, but this guard will only limit the minter address set during the candy machine to mint items. You can remove the minter arg to allow anyone to mint from the CM.

We are only making use of post_actions because we want to get the items_redeemed after the CPI call to mint the asset has been made and candy machine account updated.

fn post_actions<'info>(
    &self,
    ctx: &mut EvaluationContext,
    _guard_set: &GuardSet,
    _mint_args: &[u8],
) -> Result<()> {
    // get the minter acc using acc index we saved in validate
    let minter = try_get_account_info(ctx.accounts.remaining, ctx.indices["minter_acc_index"])?;

    // items redeemed from the candy machine
    let items_redeemed = ctx.accounts.candy_machine.items_redeemed;

    // build our memo instruction
    let redeemed_items_msg = format!("Items minted from CM -> {items_redeemed}");
    let minter_msg = format!("Latest items minted by -> {minter:?}");

    let redeemed_items_memo_ix = build_memo(redeemed_items_msg.as_bytes(), &[minter.key]);
    let minter_memo_ix = build_memo(minter_msg.as_bytes(), &[minter.key]);

    invoke(
        &redeemed_items_memo_ix,
        &[ctx.accounts.payer.to_account_info()],
    )?;
    invoke(&minter_memo_ix, &[ctx.accounts.payer.to_account_info()])?;

    Ok(())
}

You can check the code upto this point on this GitHub commit

We are ready to build and deploy our guard.

The configured upgrade and deployment authority is devnet.json keypair and since I assume like me you do not have that keypair update your Anchor.toml to use the default id.json

Also make sure that you have enough SOL. To deploy the candy guard program cost 14 SOL

# -- snip

[provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"

Let build and deploy it FR this time,

anchor build

anchor deploy

Screenshot of the program deployment tx hash on Solana Explorer!

Client side

At the root of the candy machine repo navigate to the /configs/shank.cjs file. Here we will need to change the programId for the candy guard to the ours

generateIdl({
	generator: "anchor",
	programName: "candy_guard",
	programId: "ueVvKsazojUQF3ytBmTsCV6C2diRr1GGziRknbw9sVb", // new
	idlDir,
	binaryInstallDir,
	programDir: path.join(programDir, "candy-guard", "program"),
	rustbin: {
		locked: true,
		versionRangeFallback: "0.27.0",
	},
});

At the root of the repo,

Install the required packages.

pnpm install

Generate the IDL and clients.

pnpm run generate

Open the /clients/js file in an editor,

Install required packages at the package root.

pnpm install

create a new file memo.ts inside src/defaultGuards directory

# src/defaultGuards/

touch memo.ts

We can start by copying the guard template in the Metaplex docs.

After refactoring it to house the functions we need, we are left with

import { PublicKey } from "@metaplex-foundation/umi";
import { Memo, MemoArgs, getMemoSerializer } from "../generated";
import { GuardManifest, noopParser } from "../guards";

export const memoGuardManifest: GuardManifest<
	MemoArgs,
	Memo,
	MemoGuardMintArgs
> = {
	name: "memo",
	serializer: getMemoSerializer,
	mintParser: (context, mintContext, args) => {
		return {
			data: new Uint8Array(),
			// Pass in any accounts needed for your custom guard from your mint args.
			// Your guard may or may not need remaining accounts.
			remainingAccounts: [{ publicKey: args.minter, isWritable: true }],
		};
	},
	routeParser: noopParser,
};

export type MemoGuardMintArgs = {
	minter: PublicKey;
};

Navigate to src/defaultGuards/index.ts and export the guard from there

// -- snip
export * from "./gatekeeper";
export * from "./memo"; // our guard
export * from "./mintLimit";
// -- snip

Navigate to src/defaultGuards/defaults.ts.ts and update the DefaultGuardSetArgs, DefaultGuardSet and DefaultGuardSetMintArgs to include the guard you’ve created

import { MemoArgs } from "../generated";

export type DefaultGuardSetArgs = GuardSetArgs & {
	// -- snip
	token2022Payment: OptionOrNullable<Token2022PaymentArgs>;
	memo: OptionOrNullable<MemoArgs>;
};

import { Memo } from "../generated";

export type DefaultGuardSet = GuardSet & {
	// -- snip
	token2022Payment: Option<Token2022Payment>;
	memo: Option<Memo>;
};

import { MemoGuardMintArgs } from "../generated";

export type DefaultGuardSetMintArgs = GuardSetMintArgs & {
	// -- snip
	token2022Payment: OptionOrNullable<Token2022PaymentMintArgs>;
	Memo: OptionOrNullable<MemoGuardMintArgs>;
};

We do not have any route setting for our guard so we can ignore that and more onto adding the guard name to defaultCandyGuardNames array

/** @internal */
export const defaultCandyGuardNames: string[] = [
	// -- snip
	"token2022Payment",
	"memo",
];

Let’s export our guard in the mplCandyMachine umi plugin. Head on over to /clients/js/src/plugin.ts and update the list with our new memo guard.

import { memoGuardManifest } from "./defaultGuards";

umi.guards.add(
	// -- snip
	token2022PaymentGuardManifest,
	memoGuardManifest
);

Finally, build and pack the plugin using npm

# clients/js

pnpm build

pnpm pack

Make sure you remember the path of this generated *.tgz package as we’ll make use of it in the next section.

You can check the code up to this point in this commit

Using the guard.

In a new directory separate from the candy-machine project, initialize a new empty npm project.

mkdir memo-cg-test

cd memo-cg-test

npm init -y  && tsc --init && git init .

mkdir src && touch src/main.ts src/helpers.ts

Install the required dependencies.

Let’s start with the one we packages in the previous step.

pnpm install ../mpl-candy-machine/clients/js/metaplex-foundation-mpl-candy-machine-6.0.1.tgz

Remaining required deps

pnpm install @metaplex-foundation/mpl-token-metadata @metaplex-foundation/mpl-toolbox @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/umi-web3js-adapters @solana/spl-token @solana/web3.js

Inside our main.ts will be the code for initializing UMI and helpers.ts will load the default system wallet from the id.json file

Fill your helpers.ts file with

import { Keypair } from "@solana/web3.js";
import { readFileSync } from "fs";
import { homedir } from "os";

const USER_KEYPAIR_PATH = homedir() + "/.config/solana/id.json";
export const UINT_USER_KEYPAIR = Buffer.from(
	JSON.parse(readFileSync(USER_KEYPAIR_PATH, "utf-8"))
);
export const signerKP = Keypair.fromSecretKey(UINT_USER_KEYPAIR);

Inside your main.ts, here’s how we will use the memo guard

// -- snip
mintArgs: {
    memo: some({
        minter: umi.identity.publicKey,
    }),
}
// - snip

I got tired so check out the full code for this in this repo

Check out the tests in the candy machine directory for more examples

inconveniences with this method

My main pain points with this methods is that

  1. Why do we have to fork and deploy the candy guard program if we need a new guard for a program that already exists.

  2. It becomes a pain point, if you mess with… erm modify an already existing guard and it results in undefined behaviour.

Proposed

A way to plug into the candy guard program with custom candy guards.

/solana/ /metaplex/