Skip to content

Commit

Permalink
Add program gate guard (#46)
Browse files Browse the repository at this point in the history
* Add program guard

* Update comments

* Add JS test

* Expected assertion

* Fix error assert

* Add docs

* Restore bot tax previous logic

* Clippy

* Typos

* Label in loop

* Update errors in IDL

* Add validation

* Fix import
  • Loading branch information
febo authored Oct 25, 2022
1 parent b43142d commit b3cda22
Show file tree
Hide file tree
Showing 18 changed files with 651 additions and 138 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ The Candy Guard program contains a set of core access control guards that can be
- `NftBurn`: restricts the mint to holders of a specified collection, requiring a burn of the NFT
- `NftGate`: restricts the mint to holders of a specified collection
- `NftPayment`: set the price of the mint as an NFT of a specified collection
- `ProgramGate`: restricts the programs that can be in a mint transaction
- `RedeemedAmount`: determines the end of the mint based on a total amount minted
- `SolPayment`: set the price of the mint in SOL
- `StartDate`: determines the start date of the mint
Expand Down Expand Up @@ -660,6 +661,16 @@ The `NftPayment` guard is a payment guard that charges another NFT (token) from

</details>

### `ProgramGate`

```rust
pub struct ProgramGate {
pub additional: Vec<Pubkey>,
}
```

The `ProgramGate` guard restricts the programs that can be in a mint transaction. The guard allows the necessary programs for the mint and any other program specified in the configuration.

### `RedeemedAmount`

```rust
Expand Down
47 changes: 45 additions & 2 deletions js/idl/candy_guard.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,9 @@
"name": "BotTax",
"docs": [
"Guard is used to:",
"* charge a penalty for invalid transactions.",
"* validate that the mint transaction is the last transaction.",
"* charge a penalty for invalid transactions",
"* validate that the mint transaction is the last transaction",
"* verify that only authorized programs have instructions",
"",
"The `bot_tax` is applied to any error that occurs during the",
"validation of the guards."
Expand Down Expand Up @@ -741,6 +742,24 @@
]
}
},
{
"name": "ProgramGate",
"docs": [
"Guard that restricts the programs that can be in a mint transaction. The guard allows the",
"necessary programs for the mint and any other program specified in the configuration."
],
"type": {
"kind": "struct",
"fields": [
{
"name": "additional",
"type": {
"vec": "publicKey"
}
}
]
}
},
{
"name": "RedeemedAmount",
"docs": [
Expand Down Expand Up @@ -1168,6 +1187,17 @@
"defined": "FreezeTokenPayment"
}
}
},
{
"name": "programGate",
"docs": [
"Program gate guard (restricts the programs that can be in a mint transaction)."
],
"type": {
"option": {
"defined": "ProgramGate"
}
}
}
]
}
Expand Down Expand Up @@ -1250,6 +1280,9 @@
},
{
"name": "FreezeTokenPayment"
},
{
"name": "ProgramGate"
}
]
}
Expand Down Expand Up @@ -1475,6 +1508,16 @@
"code": 6043,
"name": "DuplicatedMintLimitId",
"msg": "Duplicated mint limit id"
},
{
"code": 6044,
"name": "UnauthorizedProgramFound",
"msg": "An unauthorized program was found in the transaction"
},
{
"code": 6045,
"name": "ExceededProgramListSize",
"msg": "Exceeded the maximum number of programs in the additional list"
}
],
"metadata": {
Expand Down
43 changes: 43 additions & 0 deletions js/src/generated/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,49 @@ export class DuplicatedMintLimitIdError extends Error {
createErrorFromCodeLookup.set(0x179b, () => new DuplicatedMintLimitIdError());
createErrorFromNameLookup.set('DuplicatedMintLimitId', () => new DuplicatedMintLimitIdError());

/**
* UnauthorizedProgramFound: 'An unauthorized program was found in the transaction'
*
* @category Errors
* @category generated
*/
export class UnauthorizedProgramFoundError extends Error {
readonly code: number = 0x179c;
readonly name: string = 'UnauthorizedProgramFound';
constructor() {
super('An unauthorized program was found in the transaction');
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, UnauthorizedProgramFoundError);
}
}
}

createErrorFromCodeLookup.set(0x179c, () => new UnauthorizedProgramFoundError());
createErrorFromNameLookup.set(
'UnauthorizedProgramFound',
() => new UnauthorizedProgramFoundError(),
);

/**
* ExceededProgramListSize: 'Exceeded the maximum number of programs in the additional list'
*
* @category Errors
* @category generated
*/
export class ExceededProgramListSizeError extends Error {
readonly code: number = 0x179d;
readonly name: string = 'ExceededProgramListSize';
constructor() {
super('Exceeded the maximum number of programs in the additional list');
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, ExceededProgramListSizeError);
}
}
}

createErrorFromCodeLookup.set(0x179d, () => new ExceededProgramListSizeError());
createErrorFromNameLookup.set('ExceededProgramListSize', () => new ExceededProgramListSizeError());

/**
* Attempts to resolve a custom program error from the provided error code.
* @category Errors
Expand Down
3 changes: 3 additions & 0 deletions js/src/generated/types/GuardSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { NftBurn, nftBurnBeet } from './NftBurn';
import { TokenBurn, tokenBurnBeet } from './TokenBurn';
import { FreezeSolPayment, freezeSolPaymentBeet } from './FreezeSolPayment';
import { FreezeTokenPayment, freezeTokenPaymentBeet } from './FreezeTokenPayment';
import { ProgramGate, programGateBeet } from './ProgramGate';
export type GuardSet = {
botTax: beet.COption<BotTax>;
solPayment: beet.COption<SolPayment>;
Expand All @@ -43,6 +44,7 @@ export type GuardSet = {
tokenBurn: beet.COption<TokenBurn>;
freezeSolPayment: beet.COption<FreezeSolPayment>;
freezeTokenPayment: beet.COption<FreezeTokenPayment>;
programGate: beet.COption<ProgramGate>;
};

/**
Expand All @@ -69,6 +71,7 @@ export const guardSetBeet = new beet.FixableBeetArgsStruct<GuardSet>(
['tokenBurn', beet.coption(tokenBurnBeet)],
['freezeSolPayment', beet.coption(freezeSolPaymentBeet)],
['freezeTokenPayment', beet.coption(freezeTokenPaymentBeet)],
['programGate', beet.coption(programGateBeet)],
],
'GuardSet',
);
1 change: 1 addition & 0 deletions js/src/generated/types/GuardType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export enum GuardType {
TokenBurn,
FreezeSolPayment,
FreezeTokenPayment,
ProgramGate,
}

/**
Expand Down
22 changes: 22 additions & 0 deletions js/src/generated/types/ProgramGate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* This code was GENERATED using the solita package.
* Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality.
*
* See: https://github.com/metaplex-foundation/solita
*/

import * as web3 from '@solana/web3.js';
import * as beetSolana from '@metaplex-foundation/beet-solana';
import * as beet from '@metaplex-foundation/beet';
export type ProgramGate = {
additional: web3.PublicKey[];
};

/**
* @category userTypes
* @category generated
*/
export const programGateBeet = new beet.FixableBeetArgsStruct<ProgramGate>(
[['additional', beet.array(beetSolana.publicKey)]],
'ProgramGate',
);
1 change: 1 addition & 0 deletions js/src/generated/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './MintLimit';
export * from './NftBurn';
export * from './NftGate';
export * from './NftPayment';
export * from './ProgramGate';
export * from './RedeemedAmount';
export * from './RouteArgs';
export * from './SolPayment';
Expand Down
14 changes: 14 additions & 0 deletions js/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
GuardSet,
mintLimitBeet,
nftPaymentBeet,
programGateBeet,
startDateBeet,
thirdPartySignerBeet,
tokenGateBeet,
Expand Down Expand Up @@ -66,6 +67,8 @@ import { tokenBurnBeet } from './generated/types/TokenBurn';
* pub freeze_sol_payment: Option<FreezeSolPayment>,
* /// Freeze token payment guard (set the price for the mint in spl-token amount with a freeze period).
* pub freeze_token_payment: Option<FreezeTokenPayment>,
* /// Program gate guard (restricts the programs that can be in a mint transaction).
* pub program_gate: Option<ProgramGate>,
* }
* ```
*/
Expand All @@ -89,6 +92,7 @@ type Guards = {
/* 16 */ tokenBurnEnabled: boolean;
/* 17 */ freezeSolPaymentEnabled: boolean;
/* 18 */ freezeTokenPaymentEnabled: boolean;
/* 19 */ programGateEnabled: boolean;
};

const GUARDS_SIZE = {
Expand All @@ -110,6 +114,7 @@ const GUARDS_SIZE = {
/* 16 */ tokenBurn: 40,
/* 17 */ freezeSolPayment: 40,
/* 18 */ freezeTokenPayment: 72,
/* 19 */ programGate: 164,
};
const GUARDS_COUNT = 18;
const MAX_LABEL_LENGTH = 6;
Expand Down Expand Up @@ -141,6 +146,7 @@ function determineGuards(buffer: Buffer): Guards {
tokenBurnEnabled,
freezeSolPaymentEnabled,
freezeTokenPaymentEnabled,
programGateEnabled,
] = guards;

return {
Expand All @@ -162,6 +168,7 @@ function determineGuards(buffer: Buffer): Guards {
tokenBurnEnabled,
freezeSolPaymentEnabled,
freezeTokenPaymentEnabled,
programGateEnabled,
};
}

Expand Down Expand Up @@ -209,6 +216,7 @@ function parseGuardSet(buffer: Buffer): { guardSet: GuardSet; offset: number } {
tokenBurnEnabled,
freezeSolPaymentEnabled,
freezeTokenPaymentEnabled,
programGateEnabled,
} = guards;
logDebug('Guards: %O', guards);

Expand Down Expand Up @@ -325,6 +333,11 @@ function parseGuardSet(buffer: Buffer): { guardSet: GuardSet; offset: number } {
data.freezeTokenPayment = freezeTokenPayment;
cursor += GUARDS_SIZE.freezeTokenPayment;
}
if (programGateEnabled) {
const [programGate] = programGateBeet.deserialize(buffer, cursor);
data.programGate = programGate;
cursor += GUARDS_SIZE.programGate;
}

return {
guardSet: {
Expand All @@ -346,6 +359,7 @@ function parseGuardSet(buffer: Buffer): { guardSet: GuardSet; offset: number } {
tokenBurn: data.tokenBurn ?? null,
freezeSolPayment: data.freezeSolPayment ?? null,
freezeTokenPayment: data.freezeTokenPayment ?? null,
programGate: data.programGate ?? null,
},
offset: cursor,
};
Expand Down
39 changes: 38 additions & 1 deletion js/test/guards/bot-tax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ test('bot tax (transaction successful)', async (t) => {
await minterMintTx.assertSuccess(t);
});

test('bot tax (transaction failed)', async (t) => {
test('bot tax (invalid transaction)', async (t) => {
const { fstTxHandler, payerPair, connection } = await API.payer();

const data = newCandyGuardData();
Expand Down Expand Up @@ -112,3 +112,40 @@ test('bot tax (transaction failed)', async (t) => {
);
await minterMintTx.assertSuccess(t, [/Mint is not live/i, /Botting/i]);
});

test('bot tax (extra instruction)', async (t) => {
const { fstTxHandler, payerPair, connection } = await API.payer();

const data = newCandyGuardData();
data.default.botTax = {
lamports: 1000000000,
lastInstruction: true,
};

const { candyGuard, candyMachine } = await API.deploy(
t,
data,
payerPair,
fstTxHandler,
connection,
);

// mint (as a minter)

const {
fstTxHandler: minterHandler,
minterPair: minter,
connection: minterConnection,
} = await API.minter();
const [, mintForMinter] = await amman.genLabeledKeypair('Mint Account (minter)');
const { tx: minterMintTx } = await API.mintWithInvalidInstruction(
t,
candyGuard,
candyMachine,
minter,
mintForMinter,
minterHandler,
minterConnection,
);
await minterMintTx.assertSuccess(t, [/MintNotLastTransaction/i, /Botting/i]);
});
Loading

0 comments on commit b3cda22

Please sign in to comment.