diff --git a/src/solana/stake.ts b/src/solana/stake.ts index 0a4cb60..fe53d41 100644 --- a/src/solana/stake.ts +++ b/src/solana/stake.ts @@ -71,10 +71,13 @@ export function getStakeProgramId(network?: 'devnet' | 'mainnet'): PublicKey { safeEnv('NETWORK')?.toLowerCase() ?? ''; if (n === 'mainnet' || n === 'mainnet-beta') return 'mainnet' as const; if (n === 'devnet') return 'devnet' as const; - // In browser bundles, process.env is empty (env vars aren't inlined into - // third-party SDK code). Default to mainnet to match the app's fail-closed - // behavior — devnet must be opted into explicitly. - if (typeof window !== 'undefined') return 'mainnet' as const; + // SECURITY: previously defaulted to 'mainnet' in browser bundles (where + // process.env is empty — env vars aren't inlined into third-party SDK code), + // which could silently target the live mainnet stake program for any + // frontend caller that didn't pass an explicit network. Mirrors the same + // class of bug fixed in program-ids.ts getCurrentNetwork() (PERC-697): fail + // open to devnet, not mainnet. Mainnet must be opted into explicitly via an + // explicit `network` argument or the NETWORK/NEXT_PUBLIC_DEFAULT_NETWORK env var. return 'devnet' as const; })(); diff --git a/test/drift-check.test.ts b/test/drift-check.test.ts index 71fe8b5..3b37ad9 100644 --- a/test/drift-check.test.ts +++ b/test/drift-check.test.ts @@ -608,6 +608,31 @@ describe("STAKE_PROGRAM_ID — address constants", () => { expect(STAKE_PROGRAM_IDS.devnet).toBe("6aJb1F9CDCVWCNYFwj8aQsVb696YnW6J1FznteHq4Q6k"); }); + it("getStakeProgramId() with no args, no env, in a browser context fails open to devnet (not mainnet)", () => { + // Mirrors the PERC-697 hardening in program-ids.ts getCurrentNetwork(): an + // unconfigured frontend caller must never be silently pointed at mainnet. + const savedStakeId = process.env.STAKE_PROGRAM_ID; + const savedNetwork = process.env.NETWORK; + const savedDefaultNetwork = process.env.NEXT_PUBLIC_DEFAULT_NETWORK; + delete process.env.STAKE_PROGRAM_ID; + delete process.env.NETWORK; + delete process.env.NEXT_PUBLIC_DEFAULT_NETWORK; + const hadWindow = "window" in globalThis; + const savedWindow = (globalThis as any).window; + (globalThis as any).window = {}; + try { + const pk = getStakeProgramId(); + expect(pk.toBase58()).toBe(STAKE_PROGRAM_IDS.devnet); + expect(pk.toBase58()).not.toBe(STAKE_PROGRAM_IDS.mainnet); + } finally { + if (hadWindow) (globalThis as any).window = savedWindow; + else delete (globalThis as any).window; + if (savedStakeId !== undefined) process.env.STAKE_PROGRAM_ID = savedStakeId; + if (savedNetwork !== undefined) process.env.NETWORK = savedNetwork; + if (savedDefaultNetwork !== undefined) process.env.NEXT_PUBLIC_DEFAULT_NETWORK = savedDefaultNetwork; + } + }); + it("mainnet and devnet addresses are different", () => { expect(STAKE_PROGRAM_IDS.mainnet).not.toBe(STAKE_PROGRAM_IDS.devnet); });