Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/solana/stake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
})();

Expand Down
25 changes: 25 additions & 0 deletions test/drift-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down