You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The remainder of a drift sweep of this SDK against percolator-prog, percolator-nft and percolator-stake. The three items worth their own treatment are already filed — #376 (burn holder non-writable), #379 (EXPECTED_SLAB_VERSION), and the Reconcile companion in #378. This collects what is left so the signal stays on those.
Each item below was verified against program source, and against the live clusters where that was possible. Nothing here is a security issue.
1. Latent, and scheduled to fire: decodeStakePool is pinned to v3/392 while both programs are on v4/408
SDK: src/solana/stake.ts:1684STAKE_POOL_SIZE_V3 = 392, with STAKE_POOL_CURRENT_VERSION = 3 and an exact-version throw.
Not broken today: devnet GCHhcgw… currently holds 25 pools, all 392 bytes / version 3.
Failure mode on redeploy: a 408-byte pool satisfies data.length >= STAKE_POOL_SIZE_V3, so the decoder concludes v3, expects version 3, reads 4, and throws StakePool unsupported version: 4 !== 3. Every stake read dies at once.
1b. The same file reads two fields that v4 moved — and this one fails silently
stake.ts:1796-1797 reads pendingCooldownSlots at reservedStart + 10 and cooldownProposedAtSlot at reservedStart + 18. v4 promoted both to real struct fields at absolute 392 and 400 (percolator-stake/src/state.rs:162-180), leaving _reserved[10..26] as PERC-313 high-water-mark state exclusively.
So after the redeploy those two fields decode HWM state as cooldown state — no throw, just wrong numbers. The large "KNOWN BYTE-ALIASING BUG" block at stake.ts:1501-1520 and the _reserved map at :1568-1580 describe precisely the aliasing that v4 fixed, so they become actively misleading rather than merely stale.
Suggestion: schedule 1 and 1b together with the stake redeploy — they break as a pair, and one of them breaks quietly.
2. ACCOUNTS_CLOSE_SLAB cannot close a secondary-collateral market
src/abi/accounts.ts:422 defines six accounts. handle_close_slab reads accounts 6 and 7 — secondary_vault_token and secondary_dest_token, both expect_writable (v16_program.rs:11951-11954) — whenever cfg.secondary_collateral_mint != [0u8; 32].
Correct for the common case; NotEnoughAccountKeys on any market that has used UpdateBaseUnitMints (tag 60). There is no variant template and the doc block does not mention the conditional accounts.
3. ACCOUNTS_INIT_MARKET is still the v12 nine-account shape
src/abi/accounts.ts:25-37. handle_init_market reads only [0] admin (signer), [1] market (writable), [2] mint (v16_program.rs:7692-7694).
The first three are correctly ordered and Solana ignores surplus accounts, so a transaction still lands — but the template tells an integrator to create and pass a vault, a clock, a rent and a dummyAta that v17 never touches. The doc at :25 is verbatim v12 text ("9 accounts (Pyth Pull…)"), unlike every other v17 template in the file, which carries an accurate v16_program.rs-referenced block.
4. Stale doc comments — the encoder or template is right, the comment is not
Location
Comment says
Program
instructions.ts:180
DepositToLpVault = tag + u128 (17 B)
tag 75 reads amount u128 + domain u16 = 19 B (v16_program.rs:4597); encoder :3479 is correct
instructions.ts:190
ExecuteRedemption = tag (1 B)
tag 77 reads domain u16 = 3 B (:4602); encoder correct
instructions.ts:195
LpVaultCrankFees = tag (1 B)
tag 78 reads domain u16 = 3 B (:4605); encoder correct
instructions.ts:292
SetLpCollateralParams: 81 — "Not in v17"
tag 81 is live v17 CancelRedemption (:4610)
instructions.ts:294
AcceptAdmin: 82 — "Not in v17"
tag 82 is live v17 UnwrapEscrowedPortfolio (:4611) — the instruction the NFT burn path CPIs
instructions.ts:464
AuditCrank: 91 — "Not in v17"
tag 91 is live v17 RebalanceLpVaultBacking — which this file itself defines seven lines earlier at :457
instructions.ts:1158, :1441
TradeNoCpi / TradeCpi "= 28 bytes"
35 bytes (1+2+16+8+8); encodeTradeNoCpi asserts 35 at :1194
accounts.ts:203
"TradeNoCpi (tag 9)"
TradeNoCpi is tag 6; tag 9 is TopUpInsurance. Accounts themselves correct
The three "Not in v17" entries are the same class as the tag-64/65/73 notes that already carry explicit COLLIDES with v17 … warnings — 81/82/91 were simply never updated when they went live.
CrankAction is the one with a functional edge: action 2 (SettleB) is the permissionless bankrupt-settlement path a keeper needs, and the enum hides its existence.
5. Coverage gaps (missing rather than wrong)
Live wrapper tags with an encoder but no ACCOUNTS_* template: 57, 65, 66, 67, 72, 73, 76, 77, 79, 80. Note ACCOUNTS_TRANSFER_POSITION_OWNERSHIP (accounts.ts:1170) is the v12 tag-65 shape, not v17's tag-72 TransferPortfolioOwnership.
Live wrapper tags with neither: 37–40, 42–49, 53, 54, 56, 58–61, 64, 81, 82.
nft.ts lists tags 3 (GetPositionValue) and 6 (RepairExtraMetas) in its header but provides no encoder for either, and no account template for tags 2/3/6.
6. getStakeProgramId's mainnet behaviour contradicts its own doc
stake.ts:80-100 documents that it "throws a clear error on mainnet when no address is available", but STAKE_PROGRAM_IDS.mainnet (:58) is populated with DC5fov…, so it never throws. That address is a live executable mainnet program (RPC-confirmed), while percolator-stake/src/lib.rs:96-99 states flatly "There is deliberately NO mainnet id… Do not invent one", and a mainnet wrapper build has no STAKE_PROGRAM_ID at all — tag 87 fails closed.
The file already flags this at length as a known risk, so this is a doc/behaviour mismatch to reconcile rather than new drift — but the two repos currently disagree about whether a mainnet stake program exists.
Verified clean
Recording these so the absence of a finding is informative:
Program IDs — no repeat of [Med] ABI exports NFT_PROGRAM_ID pointing at a program that is not deployed #371. All seven hardcoded addresses resolve as executable BPF-upgradeable programs on exactly the cluster they claim, and are null on the other: devnet DhSkE7…, 4seJWjv3…, CNGBPZ…, GCHhcgw…; mainnet ESa89R5…, GDK8wx38…, FqhKJT9…. The devnet NFT default also agrees with the wrapper id pinned inside percolator-nft.
All ~50 live wrapper encoders match Instruction::decode field for field, including encodeInitMarket's 219 bytes and encodeInitMatcherCtx's 70. No live encoder points at a stale tag.
All 33 stake encoders and the whole STAKE_IX table match StakeInstruction::unpack, including the u128 payload on tag 26.
Remaining v17 account templates match their handlers, including every NFT-holder-auth trio base offset.
Summary
The remainder of a drift sweep of this SDK against
percolator-prog,percolator-nftandpercolator-stake. The three items worth their own treatment are already filed — #376 (burn holder non-writable), #379 (EXPECTED_SLAB_VERSION), and the Reconcile companion in #378. This collects what is left so the signal stays on those.Each item below was verified against program source, and against the live clusters where that was possible. Nothing here is a security issue.
1. Latent, and scheduled to fire:
decodeStakePoolis pinned to v3/392 while both programs are on v4/408src/solana/stake.ts:1684STAKE_POOL_SIZE_V3 = 392, withSTAKE_POOL_CURRENT_VERSION = 3and an exact-version throw.percolator-stakemain:src/state.rs:237assert!(STAKE_POOL_SIZE == 408),CURRENT_VERSION = 4.percolator-progorigin/main:v16_program.rs:356STAKE_POOL_LEN = 408,:380STAKE_POOL_VERSION = 4— landed as "fix(#441): pin StakePool v4/408".Not broken today: devnet
GCHhcgw…currently holds 25 pools, all 392 bytes / version 3.Failure mode on redeploy: a 408-byte pool satisfies
data.length >= STAKE_POOL_SIZE_V3, so the decoder concludes v3, expects version 3, reads 4, and throwsStakePool unsupported version: 4 !== 3. Every stake read dies at once.1b. The same file reads two fields that v4 moved — and this one fails silently
stake.ts:1796-1797readspendingCooldownSlotsatreservedStart + 10andcooldownProposedAtSlotatreservedStart + 18. v4 promoted both to real struct fields at absolute 392 and 400 (percolator-stake/src/state.rs:162-180), leaving_reserved[10..26]as PERC-313 high-water-mark state exclusively.So after the redeploy those two fields decode HWM state as cooldown state — no throw, just wrong numbers. The large "KNOWN BYTE-ALIASING BUG" block at
stake.ts:1501-1520and the_reservedmap at:1568-1580describe precisely the aliasing that v4 fixed, so they become actively misleading rather than merely stale.Suggestion: schedule 1 and 1b together with the stake redeploy — they break as a pair, and one of them breaks quietly.
2.
ACCOUNTS_CLOSE_SLABcannot close a secondary-collateral marketsrc/abi/accounts.ts:422defines six accounts.handle_close_slabreads accounts 6 and 7 —secondary_vault_tokenandsecondary_dest_token, bothexpect_writable(v16_program.rs:11951-11954) — whenevercfg.secondary_collateral_mint != [0u8; 32].Correct for the common case;
NotEnoughAccountKeyson any market that has usedUpdateBaseUnitMints(tag 60). There is no variant template and the doc block does not mention the conditional accounts.3.
ACCOUNTS_INIT_MARKETis still the v12 nine-account shapesrc/abi/accounts.ts:25-37.handle_init_marketreads only[0] admin (signer),[1] market (writable),[2] mint(v16_program.rs:7692-7694).The first three are correctly ordered and Solana ignores surplus accounts, so a transaction still lands — but the template tells an integrator to create and pass a
vault, aclock, arentand adummyAtathat v17 never touches. The doc at:25is verbatim v12 text ("9 accounts (Pyth Pull…)"), unlike every other v17 template in the file, which carries an accuratev16_program.rs-referenced block.4. Stale doc comments — the encoder or template is right, the comment is not
instructions.ts:180DepositToLpVault= tag + u128 (17 B)amount u128 + domain u16= 19 B (v16_program.rs:4597); encoder:3479is correctinstructions.ts:190ExecuteRedemption= tag (1 B)domain u16= 3 B (:4602); encoder correctinstructions.ts:195LpVaultCrankFees= tag (1 B)domain u16= 3 B (:4605); encoder correctinstructions.ts:292SetLpCollateralParams: 81— "Not in v17"CancelRedemption(:4610)instructions.ts:294AcceptAdmin: 82— "Not in v17"UnwrapEscrowedPortfolio(:4611) — the instruction the NFT burn path CPIsinstructions.ts:464AuditCrank: 91— "Not in v17"RebalanceLpVaultBacking— which this file itself defines seven lines earlier at:457instructions.ts:1158,:1441encodeTradeNoCpiasserts 35 at:1194accounts.ts:203instructions.ts:1065-1074CrankAction = {FeeSweep:0, Liquidate:1}0 => Refresh, 1 => Liquidate, 2 => SettleB(v16_program.rs:14358-14366)The three "Not in v17" entries are the same class as the tag-64/65/73 notes that already carry explicit
COLLIDES with v17 …warnings — 81/82/91 were simply never updated when they went live.CrankActionis the one with a functional edge: action 2 (SettleB) is the permissionless bankrupt-settlement path a keeper needs, and the enum hides its existence.5. Coverage gaps (missing rather than wrong)
ACCOUNTS_*template: 57, 65, 66, 67, 72, 73, 76, 77, 79, 80. NoteACCOUNTS_TRANSFER_POSITION_OWNERSHIP(accounts.ts:1170) is the v12 tag-65 shape, not v17's tag-72TransferPortfolioOwnership.nft.tslists tags 3 (GetPositionValue) and 6 (RepairExtraMetas) in its header but provides no encoder for either, and no account template for tags 2/3/6.6.
getStakeProgramId's mainnet behaviour contradicts its own docstake.ts:80-100documents that it "throws a clear error on mainnet when no address is available", butSTAKE_PROGRAM_IDS.mainnet(:58) is populated withDC5fov…, so it never throws. That address is a live executable mainnet program (RPC-confirmed), whilepercolator-stake/src/lib.rs:96-99states flatly "There is deliberately NO mainnet id… Do not invent one", and a mainnet wrapper build has noSTAKE_PROGRAM_IDat all — tag 87 fails closed.The file already flags this at length as a known risk, so this is a doc/behaviour mismatch to reconcile rather than new drift — but the two repos currently disagree about whether a mainnet stake program exists.
Verified clean
Recording these so the absence of a finding is informative:
nullon the other: devnetDhSkE7…,4seJWjv3…,CNGBPZ…,GCHhcgw…; mainnetESa89R5…,GDK8wx38…,FqhKJT9…. The devnet NFT default also agrees with the wrapper id pinned inside percolator-nft.Instruction::decodefield for field, includingencodeInitMarket's 219 bytes andencodeInitMatcherCtx's 70. No live encoder points at a stale tag.STAKE_IXtable matchStakeInstruction::unpack, including the u128 payload on tag 26.PositionNftV16offsets, the magic and version 2 are correct — with the[167..199]correction being addressed in feat(nft)!: ACCOUNTS_NFT_RECONCILE 7 -> 9 accounts (percolator-nft#182) — stacked on #377 #378.deriveNftPda,deriveMintAuthority,deriveExtraAccountMetasandderiveNftRegistryseeds all match program source.