# Audit des sources Token-2022 résolues ## Ancrage reproductible L'interface Cargo résolue `spl-token-2022-interface 3.1.1` publie le commit `e18f9c6f9bf6044b934f48e3090e8e59e4820f02`, tagué `interface@v3.1.1` le 26 juin 2026. L'audit est effectué sur ce commit détaché, jamais sur la branche `main` mouvante. Le `Cargo.lock` du workspace opérateur, SHA-256 `248c02af8c78d3bb6ad747460fee0011f8141feef83364cf697e35e44bd4ceb9`, confirme que `kb_decoder_spl_token_2022 0.4.5` consomme directement les interfaces Token-2022 et registre ElGamal. Pour `spl-token-2022-interface 3.1.1`, il résout exactement : - `solana-zk-elgamal-proof-interface 0.1.3` ; - `spl-token-confidential-transfer-proof-extraction 0.6.1` ; - `spl-token-group-interface 0.7.2` ; - `spl-token-metadata-interface 1.0.1` ; - `spl-type-length-value 0.9.1`. La sortie `cargo tree -p spl-token-2022-interface -e features` prouve en particulier le chemin de consommation direct de Token Metadata `1.0.1`. Cette version diffère du `1.0.0` figé dans le `Cargo.lock` du processor source audité : les discriminants, layouts et builders devront donc être comparés différentiellement avant le décodeur, sans supposer leur égalité. La comparaison officielle clôt cette dérive : `interface@v1.0.1`, publiée le 29 juin 2026 au commit court `7038969`, contient le correctif fusionné `fc97c60` de la pull request `87`. Le diff de production ajoute 78 lignes dans `interface/src/instruction.rs` sans suppression. Les discriminants, structures de payload et builders existants ne changent pas ; `unpack()` valide désormais les longueurs Borsh `u32 LE` avant `try_from_slice` pour : - `Initialize` : `name`, `symbol`, `uri` ; - `UpdateField` : la chaîne de `Field::Key` lorsque le tag vaut `3`, puis `value` ; - `RemoveKey` : `key`, après l'octet booléen `idempotent`. `UpdateAuthority` et `Emit` ne transportent pas de chaîne et restent inchangés. Le décodeur devra appliquer la garde `1.0.1` avant toute allocation, même si le processor source figé résolvait encore `1.0.0`. Au même commit : - `program/Cargo.toml` déclare le processor `spl-token-2022 11.0.0` ; - sa dépendance path demande l'interface `3.1.0`, satisfaite par le package workspace `3.1.1` ; - le `Cargo.lock` source résout `spl-token-group-interface 0.7.2`, `spl-token-metadata-interface 1.0.0` et `spl-elgamal-registry-interface 0.2.1` ; - l'égalité du binaire déployé avec cette source reste non prouvée sur Localnet, Devnet et Mainnet. ## Dispatch réellement implémenté `Processor::_process_inner` applique cet ordre : 1. `PodTokenInstruction`, discriminant `u8` ; 2. si ce décodage échoue, `TokenMetadataInstruction`, discriminant SPL de huit octets ; 3. si ce décodage échoue, `TokenGroupInstruction`, discriminant SPL de huit octets ; 4. sinon `InvalidInstruction`. La surface routable contient donc : | Famille | Enveloppes ou formes | |------------------------------------------------------|---------------------:| | Tags Token-2022 `u8` | 48 | | Familles d'extensions parmi ces tags | 15 | | Sous-instructions de ces familles | 58 | | Formes Token-2022 hors enveloppes | 33 | | Instructions Token Metadata exécutées par Token-2022 | 5 | | Instructions Token Group exécutées par Token-2022 | 4 | | Formes feuilles routables par le processor | 100 | Les 48 tags ne constituent donc pas, seuls, une déclaration de couverture maximale. Les quinze tags d'extension sont des enveloppes et doivent être remplacés par leurs 58 sous-instructions dans le comptage des formes feuilles. ## Interfaces incorporées au processor Les neuf discriminants ci-dessous sont exécutés par Token-2022 lorsqu'ils ciblent le Program ID `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` : | Interface | Instruction | Discriminant hexadécimal | |------------------------|--------------------------|--------------------------| | Token Metadata `1.0.0` | `initialize` | `d2e11ea258b84d8d` | | Token Metadata `1.0.0` | `update_field` | `dde9312db5cadcc8` | | Token Metadata `1.0.0` | `remove_key` | `ea122038598d25b5` | | Token Metadata `1.0.0` | `update_authority` | `d7e4a6e45464567b` | | Token Metadata `1.0.0` | `emit` | `faa6b4fa0d0cb846` | | Token Group `0.7.2` | `initialize_group` | `79716c2736330004` | | Token Group `0.7.2` | `update_group_max_size` | `6c25ab8ff81e126e` | | Token Group `0.7.2` | `update_group_authority` | `a1695801edddd8cb` | | Token Group `0.7.2` | `initialize_member` | `9820deb0dfed7486` | Le propriétaire du décodage dépend toujours du Program ID exécutable : une CPI vers un programme tiers implémentant la même interface appartient au décodeur de ce programme tiers. Un TLV pointeur reste un lien et ne transfère pas la propriété du fait métier. ## Wire public des instructions hors enveloppes Les 33 tags qui ne sont pas des enveloppes d'extension ont maintenant une ligne machine-readable dans `baseInstructionWireAudit`. Les tailles incluent toujours le tag `u8`. L'interface `3.1.1` encode les options d'instruction de manière compacte, distincte du `COption` à quatre octets des états de compte : `0` tient sur un octet ; `1` est suivi de 32 octets pour une clé publique ou de huit octets pour un `u64`. Ce point donne notamment les tailles exactes suivantes : - `InitializeMint` et `InitializeMint2` : 35 octets sans freeze authority, 67 avec ; - `SetAuthority` : 3 octets sans nouvelle autorité, 35 avec ; - `InitializeMintCloseAuthority` : 2 octets sans autorité, 34 avec ; - `UnwrapLamports` : 2 octets sans montant, 10 avec. `GetAccountDataSize` et `Reallocate` consomment tout le suffixe par éléments `ExtensionType u16 LE` ; un suffixe impair ou un type inconnu est rejeté par l'unpacker d'interface. `UiAmountToAmount` consomme tout le suffixe comme UTF-8, y compris une chaîne vide au seul niveau de l'interface. `Batch` conserve tout le suffixe comme wire de records, dont les bornes et la validation processor restent à auditer. La distinction critique est conservée dans la matrice : `TokenInstruction::unpack()` appelle `unpack_with_rest()` puis ignore le reste retourné. Pour les formes à préfixe fixe ou sans payload, un succès de cet unpacker public ne prouve donc pas la consommation de tout le buffer, ni l'acceptation du suffixe par le runtime. Le futur décodeur devra préserver et diagnostiquer tout suffixe borné jusqu'à ce que le chemin exact du processor figé en établisse la règle variante par variante. ## Règles de suffixe du processor figé La comparaison a ensuite été menée sur les trois fichiers exacts du commit, avec leurs empreintes SHA-256 enregistrées dans la matrice : - `interface/src/instruction.rs` : `672b3890…b3a287` ; - `program/src/pod_instruction.rs` : `7b546580…49c7e5` ; - `program/src/processor.rs` : `9d11b834…3032d`. Les 33 formes hors enveloppes forment une partition complète et sans doublon : | Règle processor | Tags | Nombre | |------------------------------------------------|----------------------------------------|-------:| | longueur totale Pod exacte | `2,3,4,7,8,12,13,14,15,16,18,19,23,35` | 14 | | préfixe Pod + option compacte, suffixe accepté | `0,6,20,25,45` | 5 | | tag seul, suffixe non inspecté et accepté | `1,5,9,10,11,17,22,31,32,38` | 10 | | reste entier interprété comme payload | `21,24,29` | 3 | | flux de records `Batch` | `255` | 1 | `decode_instruction_data()` exige exactement `1 + size_of::()` octets. À l'inverse, les helpers privés des options compactes lisent la structure Pod, puis `0`, ou `1` suivi de la valeur, sans vérifier l'épuisement du buffer. Pour les dix branches sans données, le processor n'appelle aucun décodeur après la lecture du premier octet. Ces quinze formes acceptent donc réellement un suffixe, même si celui-ci ne porte aucune sémantique publiée. Le décodeur devra le préserver sous forme bornée et le diagnostiquer explicitement, sans créer de champ métier. Pour `GetAccountDataSize` et `Reallocate`, chaque morceau de deux octets doit former un `ExtensionType` connu ; un dernier morceau incomplet ou un type inconnu échoue. Pour `UiAmountToAmount`, tout le reste doit être UTF-8, la chaîne vide restant admise à ce niveau. `Batch` ne peut pas être vide : le plus petit wire total contient le tag externe, un header de deux octets et au moins un discriminant interne, soit quatre octets. Chaque record encode en `u8` son nombre de comptes et la longueur totale de son instruction interne. Un header ou payload tronqué échoue ; une longueur interne nulle échoue au dispatch ; un `Batch` interne est rejeté par `_process_inner`. Les fallbacks Token Metadata et Token Group restent en revanche joignables dans un record lorsque leur wire tient dans les 255 octets. Le processor n'impose pas de nombre maximal explicite de records au-delà des limites de transaction et de calcul : le décodeur devra donc poser sa propre borne. ## Comptes publiés par les builders de base La matrice contient désormais une ligne ordonnée pour chacune des 33 formes hors enveloppes. Elle enregistre les metas exactes des 32 helpers Rust publiés : rôle, `writable`, `signer`, compte optionnel et queue multisig. `Batch` reste la seule forme sans helper Rust dédié pour construire ses metas ; `TokenInstruction::pack()` ne produit que ses données. Les autorités simples et multisig partagent le contrat classique des builders : l'autorité est signataire lorsque la liste `signer_pubkeys` est vide ; sinon elle ne signe pas et chaque élément de la queue est ajouté en lecture seule avec le flag signataire. Le builder ne borne pas la taille de cette queue pour les opérations d'autorité et conserve ordre et doublons. Le runtime ne peut toutefois satisfaire qu'un multisig stockant de un à onze positions. `InitializeMultisig` et `InitializeMultisig2` ont un contrat différent : les clés de configuration ne signent pas. Le builder exige `m` et `n` dans `1..=11` ainsi que `m <= n`. Le processor consomme tous les comptes restants comme clés de configuration, valide séparément `m` et `n`, mais ne répète pas le contrôle `m <= n`. Une instruction manuelle `m > n` peut donc initialiser un état que le builder refuse. Le décodeur doit conserver ce fait et le diagnostiquer sans le normaliser. `SyncNative` publie deux helpers : le premier fournit seulement le compte natif modifiable ; le second ajoute Rent en lecture seule. Le processor traite effectivement le deuxième compte comme optionnel, mais ignore ceux qui suivraient. `Batch` découpe les comptes selon le compteur `u8` de chaque record ; les comptes externes restant après le dernier record ne sont pas rejetés. Ils devront rester visibles comme comptes non assignés. Le contrat builder de `Reallocate` est fixé — compte à agrandir modifiable, payer modifiable et signataire, System Program, propriétaire conditionnel puis queue multisig — mais la consommation runtime détaillée reste ouverte jusqu'à reconfirmation du module séparé `program/src/extension/reallocate.rs` au même commit. Cette limite empêche de déclarer l'audit processor des comptes entièrement clos. ## Sources auditées - dépôt `solana-program/token-2022`, commit figé ci-dessus ; - `interface/src/instruction.rs` et `interface/src/extension/**/instruction.rs` ; - `interface/src/extension/mod.rs` et les états d'extension ; - `program/src/processor.rs` et `program/src/extension/**/processor.rs` ; - dépôt `solana-program/token-metadata`, commit `552935e00710478541e3394a8b2624c9ba7f503f`, tag `interface@v1.0.0` ; - dépôt `solana-program/token-group`, commit `2645064953937118572735993ed0ac1d07db5f14`, tag `interface@v0.7.2`. ## Limites restantes Cette tranche confirme le wire public, les règles runtime de suffixe et les metas officielles des builders des 33 formes hors enveloppes. Elle ne clôt pas encore l'audit par variante : consommation processor complète des comptes, données de preuve, builders des extensions, statuts de construction et compatibilité cluster doivent encore être renseignés avant le décodeur. ## Première tranche verticale d'extensions Le décodeur structure désormais exactement douze feuilles issues de quatre enveloppes : les six instructions Transfer Fee, les deux Default Account State, les deux Memo Transfer et les deux CPI Guard. Les discriminants, champs numériques, options compactes et règles de longueur proviennent des interfaces figées au commit audité. Les onze autres enveloppes restent opaques et ne sont pas comptées comme décodées sémantiquement. Cette tranche ne déclare encore aucune capacité exécuteur d'extension. Conformément à l'ordre du jalon, les projections propriétaires et leurs tests de non-duplication doivent être validés avant d'activer les builders officiels correspondants. ## Dispatch incorporé Token Metadata et Token Group Le dispatch de production ne peut pas se limiter au premier octet de `TokenInstruction`. Après échec du décodage Token-2022 principal, le processor `11.0.0` audité tente successivement les interfaces Token Metadata puis Token Group. Les neuf discriminants déjà inventoriés dans la matrice sont donc désormais reconnus par le décodeur Token-2022 lorsque le Program ID réellement exécuté est `Tokenz...` : cinq feuilles Metadata et quatre feuilles Group. Les chaînes Borsh Metadata sont bornées avant allocation et décodage. Les layouts Pod Group sont traités avec une longueur exacte. Cette intégration ne décode pas un programme tiers implémentant les mêmes interfaces : une CPI vers un autre Program ID reste la propriété du décodeur de ce programme. L'égalité de surface à maintenir est désormais explicitement compilée et documentée : ```text 33 feuilles hors enveloppes + 58 feuilles d'extensions + 5 Metadata + 4 Group = 100 ``` ## Audit ElGamal restant La présence de `spl-elgamal-registry-interface 0.2.1`, de son Program ID `regVY...` et de la seed `elgamal-registry` ne suffit pas à revendiquer sa couverture. La tranche dédiée doit encore fixer les discriminants et le wire exacts, la dérivation PDA, la relation wallet/owner, les clés ElGamal, les références de preuve, le lifecycle create/update, les autorités et signataires, les builders actuels ou expérimentaux et les preuves de déploiement cluster. Toute référence au registre depuis une instruction confidentielle Token-2022 reste un lien inter-programme et non une instruction du registre.