diff --git a/anchor/programs/glam/src/instructions/investor.rs b/anchor/programs/glam/src/instructions/investor.rs index dc2e8276..1d799862 100644 --- a/anchor/programs/glam/src/instructions/investor.rs +++ b/anchor/programs/glam/src/instructions/investor.rs @@ -111,21 +111,40 @@ pub struct Subscribe<'info> { pub fn subscribe_handler<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, Subscribe<'info>>, amount: u64, - share_class_symbol: String, skip_state: bool, ) -> Result<()> { let fund = &ctx.accounts.fund; require!(fund.is_enabled(), InvestorError::FundNotActive); - let assets = fund.assets().unwrap(); - // msg!("assets: {:?}", assets); - if fund.share_classes.len() > 1 { // we need to define how to split the total amount into share classes panic!("not implemented") } require!(fund.share_classes.len() > 0, FundError::NoShareClassInFund); + if let Some(share_class_blocklist) = fund.share_class_blocklist(0) { + require!( + share_class_blocklist.len() == 0 + || !share_class_blocklist + .iter() + .any(|&k| k == ctx.accounts.signer.key()), + InvestorError::InvalidShareClass + ); + } + + if let Some(share_class_allowlist) = fund.share_class_allowlist(0) { + require!( + share_class_allowlist.len() == 0 + || share_class_allowlist + .iter() + .any(|&k| k == ctx.accounts.signer.key()), + InvestorError::InvalidShareClass + ); + } + + let assets = fund.assets().unwrap(); + // msg!("assets: {:?}", assets); + // msg!("fund.share_class[0]: {}", fund.share_classes[0]); // msg!("expected share class: {}", ctx.accounts.share_class.key()); diff --git a/anchor/programs/glam/src/lib.rs b/anchor/programs/glam/src/lib.rs index ab478b9c..d04e47e2 100644 --- a/anchor/programs/glam/src/lib.rs +++ b/anchor/programs/glam/src/lib.rs @@ -50,10 +50,9 @@ pub mod glam { pub fn subscribe<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, Subscribe<'info>>, amount: u64, - share_class_symbol: String, skip_state: bool, ) -> Result<()> { - investor::subscribe_handler(ctx, amount, share_class_symbol, skip_state) + investor::subscribe_handler(ctx, amount, skip_state) } pub fn redeem<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, Redeem<'info>>, diff --git a/anchor/target/idl/glam.json b/anchor/target/idl/glam.json index 7791e4f7..a74b4048 100644 --- a/anchor/target/idl/glam.json +++ b/anchor/target/idl/glam.json @@ -213,10 +213,6 @@ "name": "amount", "type": "u64" }, - { - "name": "shareClassSymbol", - "type": "string" - }, { "name": "skipState", "type": "bool" diff --git a/anchor/target/types/glam.ts b/anchor/target/types/glam.ts index dae817ae..9ba28198 100644 --- a/anchor/target/types/glam.ts +++ b/anchor/target/types/glam.ts @@ -213,10 +213,6 @@ export type Glam = { "name": "amount", "type": "u64" }, - { - "name": "shareClassSymbol", - "type": "string" - }, { "name": "skipState", "type": "bool" @@ -3273,10 +3269,6 @@ export const IDL: Glam = { "name": "amount", "type": "u64" }, - { - "name": "shareClassSymbol", - "type": "string" - }, { "name": "skipState", "type": "bool" diff --git a/anchor/tests/glam_investor.spec.ts b/anchor/tests/glam_investor.spec.ts index 14c78319..4b29e9cb 100644 --- a/anchor/tests/glam_investor.spec.ts +++ b/anchor/tests/glam_investor.spec.ts @@ -22,15 +22,13 @@ import { } from "@solana/spl-token"; import { fundTestExample, createFundForTest } from "./setup"; -import { Glam } from "../target/types/glam"; import { GlamClient } from "../src"; -import { getFundUri, getImageUri, getMetadataUri } from "../src/offchain"; describe("glam_investor", () => { const userKeypairs = [ - Keypair.generate(), // mock user 0 - Keypair.generate(), // ... - Keypair.generate() + Keypair.generate(), // alice + Keypair.generate(), // bob + Keypair.generate() // eve ]; const alice = userKeypairs[0]; const bob = userKeypairs[1]; @@ -47,25 +45,35 @@ describe("glam_investor", () => { const BTC_TOKEN_PROGRAM_ID = TOKEN_2022_PROGRAM_ID; const client = new GlamClient(); + const manager = (client.provider as anchor.AnchorProvider) + .wallet as anchor.Wallet; + + // overwrite share class acls + // alice and manager are allowed to subcribe + // bob and eve will be blocked + let fundTestExampleCopy = { ...fundTestExample }; + fundTestExampleCopy.shareClasses[0].allowlist = [ + alice.publicKey, + bob.publicKey, + manager.publicKey + ]; + fundTestExampleCopy.shareClasses[0].blocklist = [ + bob.publicKey, + eve.publicKey + ]; + const fundExample = { - ...fundTestExample, + ...fundTestExampleCopy, name: "Glam Investment", assets: [usdc.publicKey, btc.publicKey, eth.publicKey] } as any; - const shareClassSymbol = fundExample.shareClasses[0].symbol; const fundPDA = client.getFundPDA(fundExample); const treasuryPDA = client.getTreasuryPDA(fundPDA); const sharePDA = client.getShareClassPDA(fundPDA, 0); - // Configure the client to use the local cluster. - const provider = anchor.AnchorProvider.env(); - anchor.setProvider(provider); - const connection = provider.connection; - const program = anchor.workspace.Glam as Program; + const connection = client.provider.connection; const commitment = "confirmed"; - - const manager = provider.wallet as anchor.Wallet; - console.log("Manager:", manager.publicKey); + const program = client.program; const treasuryUsdcAta = getAssociatedTokenAddressSync( usdc.publicKey, @@ -192,7 +200,7 @@ describe("glam_investor", () => { // exec in parallel, but await before ending the test tokenKeypairs.map(async (token, idx) => { const mint = await createMint( - connection, + client.provider.connection, manager.payer, manager.publicKey, null, @@ -258,7 +266,7 @@ describe("glam_investor", () => { // // create fund // - const fundData = await createFundForTest(fundExample); + const fundData = await createFundForTest(client, fundExample); } catch (e) { console.error(e); throw e; @@ -332,7 +340,7 @@ describe("glam_investor", () => { const expectedShares = "3000"; // $10/share => 3k shares try { const txId = await program.methods - .subscribe(amount, shareClassSymbol, true) + .subscribe(amount, true) .accounts({ fund: fundPDA, shareClass: sharePDA, @@ -394,7 +402,7 @@ describe("glam_investor", () => { ASSOCIATED_TOKEN_PROGRAM_ID ); const txId = await program.methods - .subscribe(new BN(1 * 10 ** 9), shareClassSymbol, true) + .subscribe(new BN(1 * 10 ** 9), true) .accounts({ fund: fundPDA, shareClass: invalidShareClass, @@ -412,8 +420,6 @@ describe("glam_investor", () => { console.error(e); expect(e.message).toContain("A seeds constraint was violated"); expect(e.message).toContain("Error Code: ConstraintSeeds"); - // expect(e.message).toContain("Share class not allowed to subscribe"); - // expect(e.message).toContain("Error Code: InvalidShareClass"); } }); @@ -422,7 +428,7 @@ describe("glam_investor", () => { const expectedShares = "8100"; // 3,000 + 5,100 try { const txId = await program.methods - .subscribe(amount, shareClassSymbol, true) + .subscribe(amount, true) .accounts({ fund: fundPDA, shareClass: sharePDA, @@ -690,7 +696,7 @@ describe("glam_investor", () => { const amount = new BN(250 * 10 ** 6); // USDC has 6 decimals try { const txId = await program.methods - .subscribe(amount, shareClassSymbol, true) + .subscribe(amount, true) .accounts({ fund: fundPDA, shareClass: sharePDA, @@ -724,4 +730,82 @@ describe("glam_investor", () => { // in reality it will be less due to fees but toFixed(2) rounds it up expect((Number(shares.supply) / 1e9).toFixed(2)).toEqual("2.50"); }); + + it("Bob is not allowed to subscribe", async () => { + const bobUsdcAta = getAssociatedTokenAddressSync( + usdc.publicKey, + bob.publicKey, + false, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const amount = new BN(250 * 10 ** 6); // USDC has 6 decimals + try { + const txId = await program.methods + .subscribe(amount, true) + .accounts({ + fund: fundPDA, + shareClass: sharePDA, + signerShareAta: bobSharesAta, + asset: usdc.publicKey, + treasuryAta: treasuryUsdcAta, + signerAssetAta: bobUsdcAta, + signer: bob.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + token2022Program: TOKEN_2022_PROGRAM_ID + }) + .remainingAccounts(remainingAccountsSubscribe) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }) + ]) + .signers([bob]) + .rpc({ commitment }); + console.log("tx:", txId); + expect(txId).toBeUndefined(); + } catch (e) { + console.error(e); + expect(e.message).toContain("Share class not allowed to subscribe"); + expect(e.message).toContain("Error Code: InvalidShareClass"); + } + }); + + it("Eve is not allowed to subscribe", async () => { + const eveUsdcAta = getAssociatedTokenAddressSync( + usdc.publicKey, + eve.publicKey, + false, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const amount = new BN(250 * 10 ** 6); // USDC has 6 decimals + try { + const txId = await program.methods + .subscribe(amount, true) + .accounts({ + fund: fundPDA, + shareClass: sharePDA, + signerShareAta: eveSharesAta, + asset: usdc.publicKey, + treasuryAta: treasuryUsdcAta, + signerAssetAta: eveUsdcAta, + signer: eve.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + token2022Program: TOKEN_2022_PROGRAM_ID + }) + .remainingAccounts(remainingAccountsSubscribe) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }) + ]) + .signers([eve]) + .rpc({ commitment }); + console.log("tx:", txId); + expect(txId).toBeUndefined(); + } catch (e) { + console.error(e); + expect(e.message).toContain("Share class not allowed to subscribe"); + expect(e.message).toContain("Error Code: InvalidShareClass"); + } + }); }); diff --git a/web/src/app/glam/glam-data-access.tsx b/web/src/app/glam/glam-data-access.tsx index 989e47f2..6f57b787 100644 --- a/web/src/app/glam/glam-data-access.tsx +++ b/web/src/app/glam/glam-data-access.tsx @@ -262,7 +262,7 @@ export function useGlamProgramAccount({ fundKey }: { fundKey: PublicKey }) { const shareClassMetadata = await getTokenMetadata(connection, shareClass); return program.methods - .subscribe(amount, shareClassMetadata!.symbol, true) + .subscribe(amount, true) .accounts({ fund: fundKey, shareClass,