Skip to content

Commit d869ae3

Browse files
authored
feat(spl): cheatcodes that model spl's account domain data (#806)
1 parent 14610fd commit d869ae3

File tree

5 files changed

+945
-0
lines changed

5 files changed

+945
-0
lines changed

kmir/src/kmir/kdist/mir-semantics/kmir.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ requires "cheatcodes.md"
99
requires "intrinsics.md"
1010
1111
requires "symbolic/p-token.md"
12+
requires "symbolic/spl-token.md"
1213
```
1314

1415
## Syntax of MIR in K
@@ -594,5 +595,6 @@ module KMIR
594595
imports KMIR-LEMMAS
595596
596597
imports KMIR-P-TOKEN // cheat codes
598+
imports KMIR-SPL-TOKEN // SPL-specific cheat codes
597599
endmodule
598600
```
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
**AccountInfo**
2+
- P-Token: `pinocchio::account_info::AccountInfo` stores its data in two layers:
3+
- `Account` (raw bytes, borrow state, Pubkey, etc.)
4+
- `AccountInfo`, which merely keeps a raw pointer to it
5+
- See `pinocchio-0.9.0/src/account_info.rs:18-120`.
6+
```rust
7+
#[repr(C)]
8+
pub(crate) struct Account {
9+
pub(crate) borrow_state: u8,
10+
is_signer: u8,
11+
is_writable: u8,
12+
executable: u8,
13+
resize_delta: i32,
14+
key: Pubkey,
15+
owner: Pubkey,
16+
lamports: u64,
17+
pub(crate) data_len: u64,
18+
}
19+
20+
#[repr(C)]
21+
#[derive(Clone, PartialEq, Eq)]
22+
pub struct AccountInfo {
23+
pub(crate) raw: *mut Account,
24+
}
25+
```
26+
Every `AccountInfo` method (`key()`, `owner()`, `lamports()`, etc.) directly dereferences with `unsafe { (*self.raw).... }`, so the Pinocchio pipeline’s `load::<T>` can reinterpret `AccountInfo.data` as a `#[repr(C)]` struct while remaining consistent with the borrow flags.
27+
- SPL Token: `solana_account_info::AccountInfo<'a>` exposes every field and wraps lamports/data inside `Rc<RefCell<...>>`, integrating with the runtime `next_account_info`/`Pack` borrow checks (see `solana-account-info-2.3.0/src/lib.rs:15-56`).
28+
```rust
29+
#[derive(Clone)]
30+
#[repr(C)]
31+
pub struct AccountInfo<'a> {
32+
pub key: &'a Pubkey,
33+
pub lamports: Rc<RefCell<&'a mut u64>>,
34+
pub data: Rc<RefCell<&'a mut [u8]>>,
35+
pub owner: &'a Pubkey,
36+
pub rent_epoch: u64,
37+
pub is_signer: bool,
38+
pub is_writable: bool,
39+
pub executable: bool,
40+
}
41+
```
42+
The Solana implementation relies on `RefCell::try_borrow(_mut)` to return `Ref`/`RefMut`, so processors receive runtime borrow errors whenever they call `AccountInfo.data.borrow_mut()` after `next_account_info`.
43+
44+
**Data parsing utilities (COption / Transmutable / Pack)**
45+
- P-Token: `p-interface/src/state/mod.rs:8-97` defines `COption<T> = ([u8; 4], T)`, `Transmutable`, `load/load_mut`, and `Initializable`. Length constants and byte-copy checks ensure that `AccountInfo` data matches the structure layout exactly, which removes the need for `Pack`.
46+
```rust
47+
// p-interface/src/state/mod.rs:8-97
48+
pub type COption<T> = ([u8; 4], T);
49+
pub unsafe trait Transmutable { const LEN: usize; }
50+
pub trait Initializable { fn is_initialized(&self) -> Result<bool, ProgramError>; }
51+
pub unsafe fn load<T: Initializable + Transmutable>(bytes: &[u8]) -> Result<&T, ProgramError> { … }
52+
pub unsafe fn load_mut<T: Initializable + Transmutable>(bytes: &mut [u8]) -> Result<&mut T, ProgramError> { … }
53+
```
54+
- SPL Token: `interface/src/state.rs:13-107` uses `solana_program_option::COption` together with the `Pack`/`IsInitialized` traits. `Pack::LEN`, `pack_into_slice`, and `unpack_from_slice` perform safe serialization on the `RefCell<[u8]>` inside `AccountInfo`.
55+
```rust
56+
// interface/src/state.rs:13-107
57+
use {
58+
arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs},
59+
solana_program_pack::{IsInitialized, Pack, Sealed},
60+
};
61+
```
62+
63+
**Account structure**
64+
- P-Token: `p-interface/src/state/account.rs:13-152` stores every numeric value inside `[u8; N]` fields, exposes getters/setters to interpret the slices as `u64` or `COption`, and implements `Transmutable + Initializable` so it can be passed to `load`.
65+
```rust
66+
// p-interface/src/state/account.rs:13-152
67+
#[repr(C)]
68+
pub struct Account {
69+
pub mint: Pubkey,
70+
pub owner: Pubkey,
71+
amount: [u8; 8],
72+
delegate: COption<Pubkey>,
73+
state: u8,
74+
is_native: [u8; 4],
75+
native_amount: [u8; 8],
76+
delegated_amount: [u8; 8],
77+
close_authority: COption<Pubkey>,
78+
}
79+
impl Account {
80+
pub fn amount(&self) -> u64 { … }
81+
pub fn delegate(&self) -> Option<&Pubkey> { … }
82+
pub fn native_amount(&self) -> Option<u64> { … }
83+
}
84+
```
85+
- SPL Token: `interface/src/state.rs:109-190` exposes `pub` fields and implements `Pack`. It uses the `arrayref` macros to read/write `u64` and `COption` at fixed offsets and also provides a default implementation of `GenericTokenAccount`.
86+
```rust
87+
// interface/src/state.rs:109-190
88+
#[repr(C)]
89+
pub struct Account {
90+
pub mint: Pubkey,
91+
pub owner: Pubkey,
92+
pub amount: u64,
93+
pub delegate: COption<Pubkey>,
94+
pub state: AccountState,
95+
pub is_native: COption<u64>,
96+
pub delegated_amount: u64,
97+
pub close_authority: COption<Pubkey>,
98+
}
99+
impl Pack for Account {
100+
const LEN: usize = 165;
101+
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> { … }
102+
fn pack_into_slice(&self, dst: &mut [u8]) { … }
103+
}
104+
```
105+
106+
**AccountState enum**
107+
- P-Token: `p-interface/src/state/account_state.rs:3-29` uses `#[repr(u8)]` and `TryFrom<u8>` to convert between raw bytes and semantic variants, and implements `Initializable` detection.
108+
```rust
109+
// p-interface/src/state/account_state.rs:3-29
110+
#[repr(u8)]
111+
pub enum AccountState { Uninitialized, Initialized, Frozen }
112+
impl TryFrom<u8> for AccountState { type Error = ProgramError; fn try_from(value: u8) -> Result<Self, ProgramError> { … } }
113+
```
114+
- SPL Token: `interface/src/state.rs:83-107` also declares `AccountState` as `#[repr(u8)]`, but implements `Pack`/`Sealed`’s `IsInitialized`, which `Account` relies on for serialization.
115+
```rust
116+
// interface/src/state.rs:83-107
117+
#[repr(u8)]
118+
pub enum AccountState { Uninitialized, Initialized, Frozen }
119+
impl IsInitialized for AccountState { fn is_initialized(&self) -> bool { matches!(self, Self::Initialized | Self::Frozen) } }
120+
```
121+
122+
**Mint structure**
123+
- P-Token: `p-interface/src/state/mint.rs:6-84` stores `supply`, `native_amount`, and similar fields as `[u8; 8]`, exposes methods that convert them to `u64`, and keeps `freeze_authority`/`mint_authority` as `COption<Pubkey>` to keep `Transmutable`’s length fixed.
124+
```rust
125+
// p-interface/src/state/mint.rs:6-84
126+
#[repr(C)]
127+
pub struct Mint {
128+
mint_authority: COption<Pubkey>,
129+
supply: [u8; 8],
130+
pub decimals: u8,
131+
is_initialized: u8,
132+
freeze_authority: COption<Pubkey>,
133+
}
134+
impl Mint { pub fn supply(&self) -> u64 { … } pub fn mint_authority(&self) -> Option<&Pubkey> { … } }
135+
```
136+
- SPL Token: `interface/src/state.rs:13-82` keeps fields such as `supply: u64` and `is_initialized: bool` as semantic types and writes them into a fixed-size array via `Pack`.
137+
```rust
138+
// interface/src/state.rs:13-82
139+
#[repr(C)]
140+
pub struct Mint {
141+
pub mint_authority: COption<Pubkey>,
142+
pub supply: u64,
143+
pub decimals: u8,
144+
pub is_initialized: bool,
145+
pub freeze_authority: COption<Pubkey>,
146+
}
147+
impl Pack for Mint { const LEN: usize = 82; fn pack_into_slice(&self, dst: &mut [u8]) { … } }
148+
```
149+
150+
**Multisig structure**
151+
- P-Token: `p-interface/src/state/multisig.rs:6-55` fixes `MAX_SIGNERS: u8 = 11`, stores `m/n` and `[Pubkey; MAX_SIGNERS as usize]` in a `#[repr(C)]` struct, and exposes helpers for validating signer indices.
152+
```rust
153+
// p-interface/src/state/multisig.rs:6-55
154+
pub const MAX_SIGNERS: u8 = 11;
155+
#[repr(C)]
156+
pub struct Multisig {
157+
pub m: u8,
158+
pub n: u8,
159+
is_initialized: u8,
160+
pub signers: [Pubkey; MAX_SIGNERS as usize],
161+
}
162+
impl Multisig { pub fn is_valid_signer_index(index: u8) -> bool { … } }
163+
```
164+
- SPL Token: `interface/src/state.rs:199-249` uses the same fields but relies on `Pack`, explicitly sets `LEN = 355`, and calls the `array_refs!` macro inside `pack/unpack` to slice the account data.
165+
```rust
166+
// interface/src/state.rs:199-249
167+
#[repr(C)]
168+
pub struct Multisig {
169+
pub m: u8,
170+
pub n: u8,
171+
pub is_initialized: bool,
172+
pub signers: [Pubkey; MAX_SIGNERS],
173+
}
174+
impl Pack for Multisig {
175+
const LEN: usize = 355;
176+
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> { … }
177+
fn pack_into_slice(&self, dst: &mut [u8]) { … }
178+
}
179+
```
180+
181+
**Helper readers / validators**
182+
- P-Token: `p-token/src/processor/mod.rs:89-138` defines `check_account_owner` and `validate_owner` that operate directly on `AccountInfo` buffers using pointer/slice arithmetic, reusing `load::<Multisig>` and related helpers to validate multisig signers.
183+
```rust
184+
// p-token/src/processor/mod.rs:89-138
185+
#[inline(always)]
186+
fn check_account_owner(account_info: &AccountInfo) -> ProgramResult { … }
187+
#[inline(always)]
188+
unsafe fn validate_owner(
189+
expected_owner: &Pubkey,
190+
owner_account_info: &AccountInfo,
191+
signers: &[AccountInfo],
192+
) -> ProgramResult { … }
193+
```
194+
- SPL Token: `interface/src/state.rs:252-357` supplies helpers such as `pack_coption_*`, `GenericTokenAccount`, and `ACCOUNT_INITIALIZED_INDEX`, so callers can validate `owner/mint` or initialization status via offsets without fully unpacking the account.
195+
```rust
196+
// interface/src/state.rs:294-357
197+
pub trait GenericTokenAccount {
198+
fn valid_account_data(account_data: &[u8]) -> bool;
199+
fn unpack_account_owner_unchecked(account_data: &[u8]) -> &Pubkey { … }
200+
fn unpack_account_mint_unchecked(account_data: &[u8]) -> &Pubkey { … }
201+
}
202+
pub const ACCOUNT_INITIALIZED_INDEX: usize = 108;
203+
pub fn is_initialized_account(account_data: &[u8]) -> bool { … }
204+
```
205+
206+
**Key differences**
207+
- `AccountInfo` usage: P-Token relies entirely on Pinocchio’s raw-pointer-based `AccountInfo`, while SPL uses the Solana SDK with `RefCell`, sysvars, and `Pack`.
208+
- Serialization strategy: P-Token enforces a one-to-one layout between structs and account bytes via `Transmutable`, whereas SPL declares size/offsets through `Pack`, allowing the `RefCell` buffer to copy data.
209+
- Optional fields: P-Token implements its own `[u8; 4]`-tagged `COption`, while SPL reuses `solana_program_option::COption` plus the `Pack` helpers.
210+
- Field visibility: P-Token prevents direct mutation of internal `[u8; N]` slices, whereas SPL makes the main fields `pub` and leans on the `Pack` implementation for consistency.
211+
- Metadata extraction: SPL adds helpers such as `GenericTokenAccount` and `ACCOUNT_INITIALIZED_INDEX` for CPI/clients to read owner/mint quickly, while P-Token depends on `load`-based reinterprets and custom validators.

0 commit comments

Comments
 (0)