Skip to content

Commit b0e83d6

Browse files
committed
test: assert the mint rejection with rejectedWith, not a bare try/catch
Per review: a try/catch that flags any error is too vague. Follow the token-2022/transfer-hook pattern — assert the call rejects with the specific AnchorError (ConstraintHasOne) via chai-as-promised.
1 parent 54fd7d6 commit b0e83d6

3 files changed

Lines changed: 51 additions & 42 deletions

File tree

tokens/token-fundraiser/anchor/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
"devDependencies": {
1313
"@types/bn.js": "^5.1.0",
1414
"@types/chai": "^5.2.3",
15+
"@types/chai-as-promised": "^8.0.2",
1516
"@types/mocha": "^10.0.10",
1617
"@types/node": "^26.1.0",
1718
"anchor-litesvm": "^0.2.1",
1819
"chai": "^6.2.2",
20+
"chai-as-promised": "^8.0.2",
1921
"litesvm": "^0.8.0",
2022
"mocha": "^11.7.5",
2123
"prettier": "^2.6.2",

tokens/token-fundraiser/anchor/pnpm-lock.yaml

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tokens/token-fundraiser/anchor/tests/checker-mint-binding.test.ts

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,16 @@ import {
1010
} from '@solana/spl-token';
1111
import { PublicKey } from '@solana/web3.js';
1212
import { LiteSVMProvider } from 'anchor-litesvm';
13-
import { assert } from 'chai';
13+
import { assert, expect, use } from 'chai';
14+
import chaiAsPromised from 'chai-as-promised';
1415
import { LiteSVM } from 'litesvm';
1516
import IDL from '../target/idl/fundraiser.json';
1617
import type { Fundraiser } from '../target/types/fundraiser';
1718

19+
use(chaiAsPromised);
20+
1821
const PROGRAM_ID = new PublicKey(IDL.address);
1922

20-
// Regression test for the missing mint binding on `checker.rs`.
21-
//
22-
// `contribute` and `refund` both carry `has_one = mint_to_raise` on the
23-
// `fundraiser` account, so the mint supplied in the transaction must equal the
24-
// one recorded at `initialize`. `check_contributions` omits that constraint, so
25-
// its `mint_to_raise` (and the vault derived from it) is whatever the caller
26-
// passes. A maker can therefore satisfy the goal check against a throwaway mint
27-
// they control and trigger `close = maker`, destroying the real campaign that
28-
// every contributor's refund depends on.
29-
//
30-
// With the constraint present, Anchor rejects the wrong mint before the handler
31-
// runs and the campaign account survives.
3223
describe('fundraiser checker mint binding', () => {
3324
const client = new LiteSVM();
3425
client.addProgramFromFile(PROGRAM_ID, 'target/deploy/fundraiser.so');
@@ -49,13 +40,11 @@ describe('fundraiser checker mint binding', () => {
4940
let realMint: PublicKey;
5041
let fakeMint: PublicKey;
5142

52-
it('sets up a real campaign and a maker-controlled fake mint', async () => {
43+
it('sets up a real campaign mint and a maker-controlled fake mint', async () => {
5344
client.airdrop(maker.publicKey, BigInt(anchor.web3.LAMPORTS_PER_SOL));
5445

55-
// The real campaign mint, authority held by the provider wallet.
5646
const realMintKp = anchor.web3.Keypair.generate();
5747
realMint = realMintKp.publicKey;
58-
// The fake mint, authority held by the maker — the whole point of the attack.
5948
const fakeMintKp = anchor.web3.Keypair.generate();
6049
fakeMint = fakeMintKp.publicKey;
6150

@@ -98,14 +87,11 @@ describe('fundraiser checker mint binding', () => {
9887
.signers([maker])
9988
.rpc();
10089

101-
// The campaign state exists and remembers the real mint.
10290
const state = await program.account.fundraiser.fetch(fundraiser);
10391
assert.strictEqual(state.mintToRaise.toBase58(), realMint.toBase58());
10492
});
10593

10694
it('rejects check_contributions against a mint other than the one recorded', async () => {
107-
// The maker funds the fundraiser's ATA *for the fake mint* to the goal.
108-
// Anyone may create an ATA on the PDA's behalf.
10995
const fakeVault = getAssociatedTokenAddressSync(fakeMint, fundraiser, true);
11096
const makerFakeAta = getAssociatedTokenAddressSync(fakeMint, maker.publicKey);
11197

@@ -116,29 +102,21 @@ describe('fundraiser checker mint binding', () => {
116102
);
117103
await provider.sendAndConfirm(fundTx, [maker]);
118104

119-
// Call the payout instruction with the fake mint and its funded vault.
120-
let rejected = false;
121-
try {
122-
await program.methods
123-
.checkContributions()
124-
.accountsPartial({
125-
maker: maker.publicKey,
126-
mintToRaise: fakeMint,
127-
fundraiser,
128-
makerAta: makerFakeAta,
129-
vault: fakeVault,
130-
tokenProgram: TOKEN_PROGRAM_ID,
131-
})
132-
.signers([maker])
133-
.rpc();
134-
} catch (_err) {
135-
rejected = true;
136-
}
137-
138-
assert.isTrue(rejected, 'check_contributions accepted a mint other than the one recorded at initialize');
139-
140-
// The real campaign must still be alive — a wrong-mint call must not
141-
// reach `close = maker`.
105+
const checkPromise = program.methods
106+
.checkContributions()
107+
.accountsPartial({
108+
maker: maker.publicKey,
109+
mintToRaise: fakeMint,
110+
fundraiser,
111+
makerAta: makerFakeAta,
112+
vault: fakeVault,
113+
tokenProgram: TOKEN_PROGRAM_ID,
114+
})
115+
.signers([maker])
116+
.rpc();
117+
118+
await expect(checkPromise).to.eventually.be.rejectedWith(anchor.AnchorError, 'ConstraintHasOne');
119+
142120
const state = await program.account.fundraiser.fetch(fundraiser);
143121
assert.strictEqual(state.mintToRaise.toBase58(), realMint.toBase58());
144122
});

0 commit comments

Comments
 (0)