Skip to content

[Hardening] Drift sweep tail: stake v3/392 pin vs deployed v4/408, CloseSlab missing secondary-collateral accounts, v12-era InitMarket template, stale tag/size comments #381

Description

@0x-SquidSol

Summary

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:1684 STAKE_POOL_SIZE_V3 = 392, with STAKE_POOL_CURRENT_VERSION = 3 and an exact-version throw.
  • percolator-stake main: src/state.rs:237 assert!(STAKE_POOL_SIZE == 408), CURRENT_VERSION = 4.
  • percolator-prog origin/main: v16_program.rs:356 STAKE_POOL_LEN = 408, :380 STAKE_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 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 7secondary_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
instructions.ts:1065-1074 CrankAction = {FeeSweep:0, Liquidate:1} three actions: 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.

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.
  • All 12 PositionNftV16 offsets, 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, deriveExtraAccountMetas and deriveNftRegistry seeds all match program source.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions