Security
The threat model, the rules the code must always uphold, an attack that was actually tried, and what this does not protect against.
The threat model, in plain words
Pangu is called by another program (Meteora's token program) on every single movement of the sale token, so the biggest risks are the same shape as any program that gets handed accounts by a caller it does not fully trust: a look-alike account standing in for the real one, someone calling the hook directly instead of through a real transfer, many wallets or many token accounts used to get around the cap, and a price account that is stale, missing, or faked. The other big risk is the mirror image of all of that: a rule that misfires on a sell and traps a holder's money. Pangu treats a blocked sell as the worst possible failure and is built so nothing can cause one.
Attackers in this model can create unlimited wallets for free, choose which accounts to pass into a transaction, and control the timing of everything except what Pangu itself checks. The privileged parties are the issuer (who decides who is approved) and the holder of Pangu's upgrade key (the team, stated openly), who could in theory change the program's code while a sale is live.
The rules the code must always uphold
Twenty-five rules, C1 to C25, each with its proof beside it. The fourteen first rules on the program (C1 to C14) are each proven by a named test, and most also by a real, refused transaction on Solana devnet. The three first rules on the app (C15 to C17) are proven by recorded devnet runs or, for C17, by inspection of the code. The four program rules added in the rules v2 build (C18 to C21) are proven by named tests and by the sixth run, which opened both of its sales under them; C18's own refusal, BandNeedsDollarQuote, then landed on devnet in the eighth run. The other three refusals have not been sent on devnet, because every demo sale was set up to pass them. The three app rules added after the code review of 24 September 2026 (C22, C23 and C25) are proven by the app's review-fixes run, and the deploy tool's rule (C24) by the tool refusing binaries that are not the mainnet build, each given its own correct hash. The full table, with the file, the function, the test and the transaction for each rule, is on the rubric page.
| Rule | In plain words | Proven by |
|---|---|---|
| C1 | Calling the hook directly, outside a real transfer, changes nothing. | devnet transaction, NotTransferring |
| C2 | Every account the decision depends on is checked to be the genuine one, never a look-alike. | devnet transaction, BuyerRecordMissing |
| C3 | A wallet's net tokens bought never exceeds its cap, no matter how many token accounts it uses. | devnet transactions, OverCap, plus 6,000 randomized attack sequences with no break found |
| C4 | Tokens can only move between a wallet and the pool, never wallet to wallet, while the sale runs. | devnet transaction, WalletToWalletDuringSale |
| C5 | A holder can always sell back to the pool. Nothing can block an exit. | devnet transaction, a revoked wallet still sells successfully; 1,330 simulated exit sells in the randomized tests |
| C6 | With the approved-list on, only a currently approved wallet can buy. | devnet transaction, NotApproved |
| C7 | A sale's rules can only be created once, by the pool's own creator, in the same transaction as the pool. | tests confirming a second attempt and an outside caller are both refused |
| C8 | Rules that affect buyers cannot change invisibly; the cap can never change after the first trade. | there is no instruction that could change it, proven by the absence of one in the program's interface |
| C9 | A banded buy only succeeds with a fresh, correctly sourced price inside the ceiling; any doubt refuses the buy, never the sell. | devnet transactions, PriceStale and PriceOutsideBand |
| C10 | All the counters are checked; a sell larger than a wallet's recorded buys floors at zero rather than going negative. | test plus the randomized sequences |
| C11 | The sale's launch settings must pay fees in the paying token only, never the sale token. | on-chain check added after a code review, tested against real Meteora template bytes |
| C12 | The scripts never hold or print a private key, and every address paid is shown before signing and read back after. | script test suite |
| C13 | Tokens can only leave the pool into a token account whose owner can never change. | devnet transaction, ReceivingAccountOwnerCanChange |
| C14 | No sale token can be created outside the pool; a sale refuses to open on a token whose minting power is still live. | devnet transaction, MintAuthorityStillSet, found and fixed in a code review before this submission |
| C15 | The devnet demo key is the only key server code holds, used by two routes. The demo dollars button mints only the demo dollar, at most once a wallet an hour and ten times a minute. The price refresh posts only Pyth price updates for Pangu sales with a price band that the chain lists, at most one a minute and only when the stored price is over ten minutes old. The key never leaves the server. The limits count per server process, so on mainnet the refresher key's 0.05 SOL floor, not the counter, bounds what it can spend. | a race of 12 wallets on devnet: 10 let through the reservation, the other 2 refused; two price refresh calls in a row on devnet, one post then none |
| C16 | A transaction the app builds for a visitor moves value only between that visitor and the pool, or to an account the visitor owns, and every row states its cost before the wallet signs. Once the offering ends and a row's rule has lifted, that row does not send or restates its cost. | a devnet run of every row, each row's promise equal to the chain's answer |
| C17 | No public route lets an outsider spend the app's network budget: the price route answers only for the app's own sales, from a ten second cache, and the readout and price refresh answer only for sales on the chain's own list, which is read at most once every thirty seconds. These limits count per server process, so several instances multiply them. | by inspection of the code |
| C18 | A sale with a price ceiling is paid in a dollar the program lists for its network (USDC on mainnet; devnet USDC and the demo dollar on devnet), and every sale's paying token is checked and stored. | tests refusing a band on a sale priced in SOL; devnet transaction, BandNeedsDollarQuote, a ceiling on a token that looks like a dollar and is not on the list; the fork rehearsal refused a ceiling on the demo dollar under the mainnet build |
| C19 | The issuer cannot pick a paying token they are able to freeze, so they can never stop sellers being paid. | tests refusing a paying token the issuer can freeze; both sixth run sales opened past the check |
| C20 | The per-wallet cap sits below what the curve sells, so no wallet can buy the whole sale. | tests refusing a cap equal to the curve's supply; PBAND2's cap is a tenth of it, and held on devnet with OverCap |
| C21 | The offering period is fixed when the sale opens. While it runs every rule holds; when it ends every rule lifts. | five offering period tests and a forked-mainnet step where the rules lifted at the end; PBAND2 and PAAPLX opened with an end of 7 October 2026 and every rule held on them afterwards |
| C22 | A buy or a sell is refused on chain unless it returns at least what the page showed, less the 1 percent slippage the page states. | the app's review-fixes run |
| C23 | Every place a buy is offered shows the exact moment the offering ends and the time left, and flags an offering under 24 hours long: at that time every rule lifts. A display rule by decision, not a program floor. | the app's review-fixes run |
| C24 | The mainnet deploy tool checks by itself that the binary is the mainnet build (SBPF v3, mainnet USDC, neither devnet dollar); a typed hash is only a second check. | the tool refused the devnet build, the SBPF v0 unit-test build and a three byte file, each given its own correct hash, before any network call |
| C25 | Storing a launch's logo and details never takes more than 0.02 SOL from the issuer, and the price is shown before the wallet is asked. | the app's review-fixes run |
An attack that was actually tried
The price ceiling depends on a price account that anyone can refresh. Before trusting that account, four real attempts were made on Solana devnet to write a forged or stale price into it: signing with a key that was not a real price-oracle key, altering the price bytes after a real signature, redirecting a genuine price meant for a different stock onto this account, and replaying an old, genuine update after it had aged out. All four were refused by the oracle network's own program, none of them cost more than a transaction fee, and none of them changed the account. Two extra checks were then added to Pangu itself as a second line of defence, in case a future version of that oracle network's program were ever less careful. Full write-up: docs/security/attacks/forged-quote.md in the repository.
What a code review found and fixed
A full code review was run on the program and the SDK before this submission. It found eight issues; all were fixed or, where the fix was a product decision, brought to the founder to decide. The most serious: an issuer who still held the power to mint more of the sale token could have minted straight into any wallet, past the cap and past any approval, since minting is not a transfer and the hook never sees it. The decision: revoke the minting power at launch, and a sale now refuses to open on any token that still has it (C14 above). The other seven were smaller: an on-chain gap in the fee check (C11) that used to only be enforced by the client software, a preflight check that did not account for an expired or de-authorized credential, buyer records that could not be closed and their rent recovered until a sale fully ended, and a few housekeeping fixes. Full detail: docs/measurements/review-fixes.md in the repository.
What this does not protect against, stated plainly
- One person, many wallets. The cap is per wallet. Without a real identity check behind the approved-buyer list, a determined buyer can still split a purchase across wallets they control. The issuer-list mode has no identity check at all; the credential mode is only as good as whoever issues the credential.
- The issuer's own approval power. An issuer can approve their friends first. Every approval is public on chain, but Pangu does not judge who gets approved.
- The stock token issuer's own powers. A tokenised stock such as Apple's AAPLx carries a permanent delegate that can move the token out of any account, including the pool itself, and a pause switch that can stop every trade. Those powers belong to the stock token's own issuer, not to Pangu, and Pangu cannot defend against them. Any sale priced in a stock token discloses this.
- The upgrade key. Pangu's upgrade key is held by the team's wallet and stated openly. It could be used to change the program's code during a live sale; the team keeps it so a bug found during judging can be fixed. On mainnet the plan is to move it to a Squads v4 multisig, two of three, and freeze the program for good once an independent audit is done. The plan and its rehearsal are on the mainnet page.
- The dollar list is a list of addresses. A ceiling is allowed only on the exact dollar tokens the program lists. It does not check that a listed stablecoin still holds its peg.
- The freeze check covers the issuer's own key. A freeze authority held by another wallet the issuer controls cannot be told apart from a stablecoin issuer's.
- This has not been independently audited by a third party. It has been reviewed by an automated code-review pass and by the team building it, tested with 31 Rust tests, 141 unit tests on solana-bankrun, 38 steps against real cloned Meteora and attestation programs, 178 SDK tests, 126 script tests, and 6,000 randomized attack operations, rehearsed end to end on a forked copy of mainnet, and proven with real refused transactions on a live network. That is a strong bar for a hackathon submission, but it is not the same thing as a paid third-party security audit.