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 Candy-machine-core (not to be confused by core-candy-machine that uses the Core standard) program that is used to mint items and manage the “inventory” of items to be minted.
The candy guard program that offers “add-ons” to the candy machine so that minting only happens once certain criteria are met. e.g. you might want to charge fees (either in SPL tokens of SOL) for items being minted from your candy machine.
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
- 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.
- I couldn’t figure out what to pass into the
mint_args
andlabel
parameters for themint_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.
The
Guard
trait. Contains methods specific to the guard such assize
which gets the size,load
that loads a guard,is_enabled
to check if a guard is active e.t.c.The
Condition
trait. Contains methods specific to the execution of the guard. The three methods arevalidate
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
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
Why do we have to fork and deploy the candy guard program if we need a new guard for a program that already exists.
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.