diff --git a/tokens/token-fundraiser/anchor/package.json b/tokens/token-fundraiser/anchor/package.json index 0c225a54c..3ce7ba3d6 100644 --- a/tokens/token-fundraiser/anchor/package.json +++ b/tokens/token-fundraiser/anchor/package.json @@ -12,10 +12,12 @@ "devDependencies": { "@types/bn.js": "^5.1.0", "@types/chai": "^5.2.3", + "@types/chai-as-promised": "^8.0.2", "@types/mocha": "^10.0.10", "@types/node": "^26.1.0", "anchor-litesvm": "^0.2.1", "chai": "^6.2.2", + "chai-as-promised": "^8.0.2", "litesvm": "^0.8.0", "mocha": "^11.7.5", "prettier": "^2.6.2", diff --git a/tokens/token-fundraiser/anchor/pnpm-lock.yaml b/tokens/token-fundraiser/anchor/pnpm-lock.yaml index 7f7ea27e8..f5d97074b 100644 --- a/tokens/token-fundraiser/anchor/pnpm-lock.yaml +++ b/tokens/token-fundraiser/anchor/pnpm-lock.yaml @@ -27,6 +27,9 @@ importers: '@types/chai': specifier: ^5.2.3 version: 5.2.3 + '@types/chai-as-promised': + specifier: ^8.0.2 + version: 8.0.2 '@types/mocha': specifier: ^10.0.10 version: 10.0.10 @@ -39,6 +42,9 @@ importers: chai: specifier: ^6.2.2 version: 6.2.2 + chai-as-promised: + specifier: ^8.0.2 + version: 8.0.2(chai@6.2.2) litesvm: specifier: ^0.8.0 version: 0.8.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -346,6 +352,9 @@ packages: '@types/bn.js@5.1.5': resolution: {integrity: sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==} + '@types/chai-as-promised@8.0.2': + resolution: {integrity: sha512-meQ1wDr1K5KRCSvG2lX7n7/5wf70BeptTKst0axGvnN6zqaVpRqegoIbugiAPSqOW9K9aL8gDVrm7a2LXOtn2Q==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -453,6 +462,11 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + chai-as-promised@8.0.2: + resolution: {integrity: sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==} + peerDependencies: + chai: '>= 2.1.2 < 7' + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -469,6 +483,10 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1241,6 +1259,10 @@ snapshots: dependencies: '@types/node': 26.1.2 + '@types/chai-as-promised@8.0.2': + dependencies: + '@types/chai': 5.2.3 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -1350,6 +1372,11 @@ snapshots: camelcase@6.3.0: {} + chai-as-promised@8.0.2(chai@6.2.2): + dependencies: + chai: 6.2.2 + check-error: 2.1.3 + chai@6.2.2: {} chalk@4.1.2: @@ -1361,6 +1388,8 @@ snapshots: chalk@5.6.2: {} + check-error@2.1.3: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 diff --git a/tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs b/tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs index 4299ff73d..b1b3a1da6 100644 --- a/tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs +++ b/tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs @@ -24,6 +24,7 @@ pub struct CheckContributions<'info> { mut, seeds = [b"fundraiser".as_ref(), maker.key().as_ref()], bump = fundraiser.bump, + has_one = mint_to_raise, close = maker, )] pub fundraiser: Account<'info, Fundraiser>, diff --git a/tokens/token-fundraiser/anchor/tests/checker-mint-binding.test.ts b/tokens/token-fundraiser/anchor/tests/checker-mint-binding.test.ts new file mode 100644 index 000000000..1f2959563 --- /dev/null +++ b/tokens/token-fundraiser/anchor/tests/checker-mint-binding.test.ts @@ -0,0 +1,123 @@ +import * as anchor from '@anchor-lang/core'; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; +import { PublicKey } from '@solana/web3.js'; +import { LiteSVMProvider } from 'anchor-litesvm'; +import { assert, expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { LiteSVM } from 'litesvm'; +import IDL from '../target/idl/fundraiser.json'; +import type { Fundraiser } from '../target/types/fundraiser'; + +use(chaiAsPromised); + +const PROGRAM_ID = new PublicKey(IDL.address); + +describe('fundraiser checker mint binding', () => { + const client = new LiteSVM(); + client.addProgramFromFile(PROGRAM_ID, 'target/deploy/fundraiser.so'); + const provider = new LiteSVMProvider(client); + anchor.setProvider(provider); + const wallet = provider.wallet as anchor.Wallet; + const program = new anchor.Program(IDL, provider); + + const maker = anchor.web3.Keypair.generate(); + + const fundraiser = anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from('fundraiser'), maker.publicKey.toBuffer()], + program.programId, + )[0]; + + const AMOUNT_TO_RAISE = 3_000_000; // 3 tokens at 6 decimals + + let realMint: PublicKey; + let fakeMint: PublicKey; + + it('sets up a real campaign mint and a maker-controlled fake mint', async () => { + client.airdrop(maker.publicKey, BigInt(anchor.web3.LAMPORTS_PER_SOL)); + + const realMintKp = anchor.web3.Keypair.generate(); + realMint = realMintKp.publicKey; + const fakeMintKp = anchor.web3.Keypair.generate(); + fakeMint = fakeMintKp.publicKey; + + const lamports = await provider.connection.getMinimumBalanceForRentExemption(MINT_SIZE); + const setupTx = new anchor.web3.Transaction().add( + anchor.web3.SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: realMint, + space: MINT_SIZE, + lamports, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction(realMint, 6, provider.publicKey, provider.publicKey), + anchor.web3.SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: fakeMint, + space: MINT_SIZE, + lamports, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction(fakeMint, 6, maker.publicKey, maker.publicKey), + ); + await provider.sendAndConfirm(setupTx, [realMintKp, fakeMintKp]); + }); + + it('initializes the campaign against the real mint', async () => { + const vault = getAssociatedTokenAddressSync(realMint, fundraiser, true); + + await program.methods + .initialize(new anchor.BN(AMOUNT_TO_RAISE), 0) + .accountsPartial({ + maker: maker.publicKey, + fundraiser, + mintToRaise: realMint, + vault, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([maker]) + .rpc(); + + const state = await program.account.fundraiser.fetch(fundraiser); + assert.strictEqual(state.mintToRaise.toBase58(), realMint.toBase58()); + }); + + it('rejects check_contributions against a mint other than the one recorded', async () => { + const fakeVault = getAssociatedTokenAddressSync(fakeMint, fundraiser, true); + const makerFakeAta = getAssociatedTokenAddressSync(fakeMint, maker.publicKey); + + const fundTx = new anchor.web3.Transaction().add( + createAssociatedTokenAccountInstruction(maker.publicKey, fakeVault, fundraiser, fakeMint), + createAssociatedTokenAccountInstruction(maker.publicKey, makerFakeAta, maker.publicKey, fakeMint), + createMintToInstruction(fakeMint, fakeVault, maker.publicKey, AMOUNT_TO_RAISE), + ); + await provider.sendAndConfirm(fundTx, [maker]); + + const checkPromise = program.methods + .checkContributions() + .accountsPartial({ + maker: maker.publicKey, + mintToRaise: fakeMint, + fundraiser, + makerAta: makerFakeAta, + vault: fakeVault, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([maker]) + .rpc(); + + await expect(checkPromise).to.eventually.be.rejectedWith(anchor.AnchorError, 'ConstraintHasOne'); + + const state = await program.account.fundraiser.fetch(fundraiser); + assert.strictEqual(state.mintToRaise.toBase58(), realMint.toBase58()); + }); +});